Compare commits
	
		
			63 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7b9d1a1159 | |||
| cdbe1f6161 | |||
| e43db2e065 | |||
| d1c74d2160 | |||
| f2119ce567 | |||
| 2c4dcb9cf0 | |||
| 93b8266821 | |||
| 443797d9b0 | |||
| a4365ca02c | |||
| 3750083667 | |||
| 66ef067ecf | |||
| b489b0e691 | |||
| f2154d9875 | |||
| 80a50f9bdb | |||
| dc8b89a6b9 | |||
| 8df55f22aa | |||
| f6c322be27 | |||
| a144552059 | |||
| 535d529193 | |||
| 6ed2e137a2 | |||
| 45bd63c720 | |||
| 736e13fc35 | |||
| 966fff008c | |||
| 64f15eadbd | |||
| 81b66ecdcd | |||
| 53e5cf7826 | |||
| 82654b3fd9 | |||
| 9b72c604dd | |||
| 5fb1b8044c | |||
| b8daab4377 | |||
| c5b91bdae8 | |||
| 39a208c55f | |||
| a5bfef9b6b | |||
| f1f4cbef9b | |||
| 8388120b06 | |||
| 2bf96828f1 | |||
| 22838e66fe | |||
| 484dd6de09 | |||
| b743736c26 | |||
| af91e2079b | |||
| cad1c17f14 | |||
| 120d32e4dc | |||
| 238b489e07 | |||
| 4daa70c894 | |||
| f8599438df | |||
| 155c9a4c3f | |||
| 8433b5e583 | |||
| dc5ba144f1 | |||
| 521a8b5356 | |||
| 3453077d7b | |||
| 70ede8581a | |||
| 6e9d297f02 | |||
| 6a7545fd43 | |||
| a8926cbd07 | |||
| 64d7b009ab | |||
| 2b5fddb7bf | |||
| b99d23c119 | |||
| 03905b74ff | |||
| 6b8a59cfbd | |||
| d6fdcd3ef9 | |||
| 53ebc551d2 | |||
| 3d4f43d6e3 | |||
| 074cde7cd5 | 
@ -1,5 +1,5 @@
 | 
			
		||||
[bumpversion]
 | 
			
		||||
current_version = 0.8.8-beta
 | 
			
		||||
current_version = 0.8.14-beta
 | 
			
		||||
tag = True
 | 
			
		||||
commit = True
 | 
			
		||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@ -15,14 +15,8 @@ jobs:
 | 
			
		||||
      - uses: actions/setup-python@v1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.8'
 | 
			
		||||
      - uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.local/share/virtualenvs/
 | 
			
		||||
          key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-pipenv-
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pip install -U pip pipenv && pipenv install --dev
 | 
			
		||||
        run: sudo pip install -U wheel pipenv && pipenv install --dev
 | 
			
		||||
      - name: Lint with pylint
 | 
			
		||||
        run: pipenv run pylint passbook
 | 
			
		||||
  black:
 | 
			
		||||
@ -32,14 +26,8 @@ jobs:
 | 
			
		||||
      - uses: actions/setup-python@v1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.8'
 | 
			
		||||
      - uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.local/share/virtualenvs/
 | 
			
		||||
          key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-pipenv-
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pip install -U pip pipenv && pipenv install --dev
 | 
			
		||||
        run: sudo pip install -U wheel pipenv && pipenv install --dev
 | 
			
		||||
      - name: Lint with black
 | 
			
		||||
        run: pipenv run black --check passbook
 | 
			
		||||
  prospector:
 | 
			
		||||
@ -49,14 +37,8 @@ jobs:
 | 
			
		||||
      - uses: actions/setup-python@v1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.8'
 | 
			
		||||
      - uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.local/share/virtualenvs/
 | 
			
		||||
          key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-pipenv-
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pip install -U pip pipenv && pipenv install --dev && pipenv install --dev prospector --skip-lock
 | 
			
		||||
        run: sudo pip install -U wheel pipenv && pipenv install --dev && pipenv install --dev prospector --skip-lock
 | 
			
		||||
      - name: Lint with prospector
 | 
			
		||||
        run: pipenv run prospector
 | 
			
		||||
  bandit:
 | 
			
		||||
@ -66,14 +48,8 @@ jobs:
 | 
			
		||||
      - uses: actions/setup-python@v1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.8'
 | 
			
		||||
      - uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.local/share/virtualenvs/
 | 
			
		||||
          key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-pipenv-
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pip install -U pip pipenv && pipenv install --dev
 | 
			
		||||
        run: sudo pip install -U wheel pipenv && pipenv install --dev
 | 
			
		||||
      - name: Lint with bandit
 | 
			
		||||
        run: pipenv run bandit -r passbook
 | 
			
		||||
  # Actual CI tests
 | 
			
		||||
@ -101,14 +77,8 @@ jobs:
 | 
			
		||||
      - uses: actions/setup-python@v1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.8'
 | 
			
		||||
      - uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.local/share/virtualenvs/
 | 
			
		||||
          key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-pipenv-
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pip install -U pip pipenv && pipenv install --dev
 | 
			
		||||
        run: sudo pip install -U wheel pipenv && pipenv install --dev
 | 
			
		||||
      - name: Run migrations
 | 
			
		||||
        run: pipenv run ./manage.py migrate
 | 
			
		||||
  coverage:
 | 
			
		||||
@ -135,14 +105,8 @@ jobs:
 | 
			
		||||
      - uses: actions/setup-python@v1
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.8'
 | 
			
		||||
      - uses: actions/cache@v1
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.local/share/virtualenvs/
 | 
			
		||||
          key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-pipenv-
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: pip install -U pip pipenv && pipenv install --dev
 | 
			
		||||
        run: sudo pip install -U wheel pipenv && pipenv install --dev
 | 
			
		||||
      - name: Run coverage
 | 
			
		||||
        run: pipenv run ./scripts/coverage.sh
 | 
			
		||||
  # Build
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@ -16,11 +16,11 @@ jobs:
 | 
			
		||||
      - name: Building Docker Image
 | 
			
		||||
        run: docker build
 | 
			
		||||
          --no-cache
 | 
			
		||||
          -t beryju/passbook:0.8.8-beta
 | 
			
		||||
          -t beryju/passbook:0.8.14-beta
 | 
			
		||||
          -t beryju/passbook:latest
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook:0.8.8-beta
 | 
			
		||||
        run: docker push beryju/passbook:0.8.14-beta
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook:latest
 | 
			
		||||
  build-gatekeeper:
 | 
			
		||||
@ -37,11 +37,11 @@ jobs:
 | 
			
		||||
          cd gatekeeper
 | 
			
		||||
          docker build \
 | 
			
		||||
          --no-cache \
 | 
			
		||||
          -t beryju/passbook-gatekeeper:0.8.8-beta \
 | 
			
		||||
          -t beryju/passbook-gatekeeper:0.8.14-beta \
 | 
			
		||||
          -t beryju/passbook-gatekeeper:latest \
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook-gatekeeper:0.8.8-beta
 | 
			
		||||
        run: docker push beryju/passbook-gatekeeper:0.8.14-beta
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook-gatekeeper:latest
 | 
			
		||||
  build-static:
 | 
			
		||||
@ -66,11 +66,11 @@ jobs:
 | 
			
		||||
        run: docker build
 | 
			
		||||
          --no-cache
 | 
			
		||||
          --network=$(docker network ls | grep github | awk '{print $1}')
 | 
			
		||||
          -t beryju/passbook-static:0.8.8-beta
 | 
			
		||||
          -t beryju/passbook-static:0.8.14-beta
 | 
			
		||||
          -t beryju/passbook-static:latest
 | 
			
		||||
          -f static.Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook-static:0.8.8-beta
 | 
			
		||||
        run: docker push beryju/passbook-static:0.8.14-beta
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook-static:latest
 | 
			
		||||
  test-release:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										6
									
								
								.isort.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.isort.cfg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
[settings]
 | 
			
		||||
multi_line_output=3
 | 
			
		||||
include_trailing_comma=True
 | 
			
		||||
force_grid_wrap=0
 | 
			
		||||
use_parentheses=True
 | 
			
		||||
line_length=88
 | 
			
		||||
@ -7,7 +7,3 @@ const-rgx=[a-zA-Z0-9_]{1,40}$
 | 
			
		||||
ignored-modules=django-otp
 | 
			
		||||
jobs=4
 | 
			
		||||
 | 
			
		||||
[SIMILARITIES]
 | 
			
		||||
 | 
			
		||||
# Minimum lines number of a similarity.
 | 
			
		||||
min-similarity-lines=20
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										587
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										587
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@ -25,10 +25,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "asgiref": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
 | 
			
		||||
                "sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
 | 
			
		||||
                "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
 | 
			
		||||
                "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.2.3"
 | 
			
		||||
            "version": "==3.2.7"
 | 
			
		||||
        },
 | 
			
		||||
        "asn1crypto": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -46,40 +46,40 @@
 | 
			
		||||
        },
 | 
			
		||||
        "billiard": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:26fd494dc3251f8ce1f5559744f18aeed427fdaf29a75d7baae26752a5d3816f",
 | 
			
		||||
                "sha256:f4e09366653aa3cb3ae8ed16423f9ba1665ff426f087bcdbbed86bf3664fe02c"
 | 
			
		||||
                "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede",
 | 
			
		||||
                "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.6.2.0"
 | 
			
		||||
            "version": "==3.6.3.0"
 | 
			
		||||
        },
 | 
			
		||||
        "boto3": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:33462a79d57c9c4a215e075472509537d03545f54566fc4f776fb0f4cfa616f6",
 | 
			
		||||
                "sha256:34f9a04f529dc849f0e427782d6f3c6b62f7fb734d8f4859b17e5dee0855323e"
 | 
			
		||||
                "sha256:970bd7b332e73d7b51077ed36772c634811b38c81b0cc6ed0f910e50d7ebadf8",
 | 
			
		||||
                "sha256:cdd79a3a7bbe1f33a365f0acfcc75c4405b482b3eb9ce3f4e6b16c418e201ac3"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.12.0"
 | 
			
		||||
            "version": "==1.12.39"
 | 
			
		||||
        },
 | 
			
		||||
        "botocore": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:055da4826f6c9158e4a61549d57a2ce449c27d44ce34ab4c96c7bb7b5c993efc",
 | 
			
		||||
                "sha256:1f7cecfcd38c7cac17b5386014eb04626d1c7559ee8d8ec1526058cd23f6d1d4"
 | 
			
		||||
                "sha256:94232b44e1540b7e043e220bd43f855400d0a243e926b26b3fb72994e971d518",
 | 
			
		||||
                "sha256:e20ba56476b1031ce5ac8e22b59dabc75bd0e03231f124ed6b9ff99fe0b0c96b"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.15.0"
 | 
			
		||||
            "version": "==1.15.39"
 | 
			
		||||
        },
 | 
			
		||||
        "celery": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7c544f37a84a5eadc44cab1aa8c9580dff94636bb81978cdf9bf8012d9ea7d8f",
 | 
			
		||||
                "sha256:d3363bb5df72d74420986a435449f3c3979285941dff57d5d97ecba352a0e3e2"
 | 
			
		||||
                "sha256:108a0bf9018a871620936c33a3ee9f6336a89f8ef0a0f567a9001f4aa361415f",
 | 
			
		||||
                "sha256:5b4b37e276033fe47575107a2775469f0b721646a08c96ec2c61531e4fe45f2a"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==4.4.0"
 | 
			
		||||
            "version": "==4.4.2"
 | 
			
		||||
        },
 | 
			
		||||
        "certifi": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
 | 
			
		||||
                "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
 | 
			
		||||
                "sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304",
 | 
			
		||||
                "sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2019.11.28"
 | 
			
		||||
            "version": "==2020.4.5.1"
 | 
			
		||||
        },
 | 
			
		||||
        "cffi": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -137,29 +137,27 @@
 | 
			
		||||
        },
 | 
			
		||||
        "cryptography": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c",
 | 
			
		||||
                "sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595",
 | 
			
		||||
                "sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad",
 | 
			
		||||
                "sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651",
 | 
			
		||||
                "sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2",
 | 
			
		||||
                "sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff",
 | 
			
		||||
                "sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d",
 | 
			
		||||
                "sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42",
 | 
			
		||||
                "sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d",
 | 
			
		||||
                "sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e",
 | 
			
		||||
                "sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912",
 | 
			
		||||
                "sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793",
 | 
			
		||||
                "sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13",
 | 
			
		||||
                "sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7",
 | 
			
		||||
                "sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0",
 | 
			
		||||
                "sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879",
 | 
			
		||||
                "sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f",
 | 
			
		||||
                "sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9",
 | 
			
		||||
                "sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2",
 | 
			
		||||
                "sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf",
 | 
			
		||||
                "sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"
 | 
			
		||||
                "sha256:0cacd3ef5c604b8e5f59bf2582c076c98a37fe206b31430d0cd08138aff0986e",
 | 
			
		||||
                "sha256:192ca04a36852a994ef21df13cca4d822adbbdc9d5009c0f96f1d2929e375d4f",
 | 
			
		||||
                "sha256:19ae795137682a9778892fb4390c07811828b173741bce91e30f899424b3934d",
 | 
			
		||||
                "sha256:1b9b535d6b55936a79dbe4990b64bb16048f48747c76c29713fea8c50eca2acf",
 | 
			
		||||
                "sha256:2a2ad24d43398d89f92209289f15265107928f22a8d10385f70def7a698d6a02",
 | 
			
		||||
                "sha256:3be7a5722d5bfe69894d3f7bbed15547b17619f3a88a318aab2e37f457524164",
 | 
			
		||||
                "sha256:49870684da168b90110bbaf86140d4681032c5e6a2461adc7afdd93be5634216",
 | 
			
		||||
                "sha256:587f98ce27ac4547177a0c6fe0986b8736058daffe9160dcf5f1bd411b7fbaa1",
 | 
			
		||||
                "sha256:5aca6f00b2f42546b9bdf11a69f248d1881212ce5b9e2618b04935b87f6f82a1",
 | 
			
		||||
                "sha256:6b744039b55988519cc183149cceb573189b3e46e16ccf6f8c46798bb767c9dc",
 | 
			
		||||
                "sha256:6b91cab3841b4c7cb70e4db1697c69f036c8bc0a253edc0baa6783154f1301e4",
 | 
			
		||||
                "sha256:7598974f6879a338c785c513e7c5a4329fbc58b9f6b9a6305035fca5b1076552",
 | 
			
		||||
                "sha256:7a279f33a081d436e90e91d1a7c338553c04e464de1c9302311a5e7e4b746088",
 | 
			
		||||
                "sha256:95e1296e0157361fe2f5f0ed307fd31f94b0ca13372e3673fa95095a627636a1",
 | 
			
		||||
                "sha256:9fc9da390e98cb6975eadf251b6e5fa088820141061bf041cd5c72deba1dc526",
 | 
			
		||||
                "sha256:cc20316e3f5a6b582fc3b029d8dc03aabeb645acfcb7fc1d9848841a33265748",
 | 
			
		||||
                "sha256:d1bf5a1a0d60c7f9a78e448adcb99aa101f3f9588b16708044638881be15d6bc",
 | 
			
		||||
                "sha256:ed1d0760c7e46436ec90834d6f10477ff09475c692ed1695329d324b2c5cd547",
 | 
			
		||||
                "sha256:ef9a55013676907df6c9d7dd943eb1770d014f68beaa7e73250fb43c759f4585"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.8"
 | 
			
		||||
            "version": "==2.9"
 | 
			
		||||
        },
 | 
			
		||||
        "defusedxml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -171,11 +169,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2f1ba1db8648484dd5c238fb62504777b7ad090c81c5f1fd8d5eb5ec21b5f283",
 | 
			
		||||
                "sha256:c91c91a7ad6ef67a874a4f76f58ba534f9208412692a840e1d125eb5c279cb0a"
 | 
			
		||||
                "sha256:642d8eceab321ca743ae71e0f985ff8fdca59f07aab3a9fb362c617d23e33a76",
 | 
			
		||||
                "sha256:d4666c2edefa38c5ede0ec1655424c56dc47ceb04b6d8d62a7eac09db89545c1"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.0.3"
 | 
			
		||||
            "version": "==3.0.5"
 | 
			
		||||
        },
 | 
			
		||||
        "django-cors-middleware": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -218,10 +216,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django-oauth-toolkit": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:ad1b76275950ebbff708222cec57bbdb879f89bac7df6b9dee0f4b9db485c264"
 | 
			
		||||
                "sha256:28508f83385ab4313936ddedfb310eaa8a1dcb737153d2956383ce47e75c2fab",
 | 
			
		||||
                "sha256:d5a1044af9419ddc048390c5974777ea97874e5b78e33c609e17eebb8423afb2"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.2.0"
 | 
			
		||||
            "version": "==1.3.2"
 | 
			
		||||
        },
 | 
			
		||||
        "django-oidc-provider": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -240,11 +239,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django-prometheus": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:362ea45e5ee26bdba85ce978aeb370659ca6bbc0d6bac69868a055179e053bd1",
 | 
			
		||||
                "sha256:facaa677386899303ea26c45552371cc43f476e42a81c081011a49cb5564af0b"
 | 
			
		||||
                "sha256:1a8cb752ae4181e38df00e7bd7d5f6495cde18b8b3ff697c22f9d8d2fe48bf28",
 | 
			
		||||
                "sha256:9f024af5495447c8e309f07e5289e7bc1100c5a380ac7cd0afe3a1b2a0b3b534"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.1.0.dev5"
 | 
			
		||||
            "version": "==2.1.0.dev14"
 | 
			
		||||
        },
 | 
			
		||||
        "django-recaptcha": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -322,16 +321,17 @@
 | 
			
		||||
        },
 | 
			
		||||
        "idna": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
 | 
			
		||||
                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
 | 
			
		||||
                "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
 | 
			
		||||
                "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.8"
 | 
			
		||||
            "version": "==2.9"
 | 
			
		||||
        },
 | 
			
		||||
        "inflection": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca"
 | 
			
		||||
                "sha256:32a5c3341d9583ec319548b9015b7fbdf8c429cbcb575d326c33ae3a0e90d52c",
 | 
			
		||||
                "sha256:9a15d3598f01220e93f2207c432cfede50daff53137ce660fb8be838ef1ca6cc"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.3.1"
 | 
			
		||||
            "version": "==0.4.0"
 | 
			
		||||
        },
 | 
			
		||||
        "itypes": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -349,10 +349,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "jmespath": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6",
 | 
			
		||||
                "sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c"
 | 
			
		||||
                "sha256:695cb76fa78a10663425d5b73ddc5714eb711157e52704d69be03b1a02ba4fec",
 | 
			
		||||
                "sha256:cca55c8d153173e21baa59983015ad0daf603f9cb799904ff057bfb8ff8dc2d9"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.9.4"
 | 
			
		||||
            "version": "==0.9.5"
 | 
			
		||||
        },
 | 
			
		||||
        "jsonschema": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -363,19 +363,19 @@
 | 
			
		||||
        },
 | 
			
		||||
        "kombu": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2a9e7adff14d046c9996752b2c48b6d9185d0b992106d5160e1a179907a5d4ac",
 | 
			
		||||
                "sha256:67b32ccb6fea030f8799f8fd50dd08e03a4b99464ebc4952d71d8747b1a52ad1"
 | 
			
		||||
                "sha256:2d1cda774126a044d91a7ff5fa6d09edf99f46924ab332a810760fe6740e9b76",
 | 
			
		||||
                "sha256:598e7e749d6ab54f646b74b2d2df67755dee13894f73ab02a2a9feb8870c7cb2"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==4.6.7"
 | 
			
		||||
            "version": "==4.6.8"
 | 
			
		||||
        },
 | 
			
		||||
        "ldap3": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:1898194d872539670a2f36d4b56fe5a35d4b9ead28103bec78f05a8993e8122f",
 | 
			
		||||
                "sha256:27cb673e7afcb539f6adcae5a3ecac4e74eb37ca0a2d50dc98f29a3829eee529"
 | 
			
		||||
                "sha256:17f04298b70bf7ecaa5db8a7d8622b5a962ef7fc2b245b2eea705ac1c24338c0",
 | 
			
		||||
                "sha256:81df4ac8b6df10fb1f05b17c18d0cb8c4c344d5a03083c382824960ed959cf5b"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.6.1"
 | 
			
		||||
            "version": "==2.7"
 | 
			
		||||
        },
 | 
			
		||||
        "lxml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -458,11 +458,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "packaging": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73",
 | 
			
		||||
                "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"
 | 
			
		||||
                "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
 | 
			
		||||
                "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==20.1"
 | 
			
		||||
            "version": "==20.3"
 | 
			
		||||
        },
 | 
			
		||||
        "prometheus-client": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -472,41 +472,39 @@
 | 
			
		||||
        },
 | 
			
		||||
        "psycopg2-binary": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:040234f8a4a8dfd692662a8308d78f63f31a97e1c42d2480e5e6810c48966a29",
 | 
			
		||||
                "sha256:086f7e89ec85a6704db51f68f0dcae432eff9300809723a6e8782c41c2f48e03",
 | 
			
		||||
                "sha256:18ca813fdb17bc1db73fe61b196b05dd1ca2165b884dd5ec5568877cabf9b039",
 | 
			
		||||
                "sha256:19dc39616850342a2a6db70559af55b22955f86667b5f652f40c0e99253d9881",
 | 
			
		||||
                "sha256:2166e770cb98f02ed5ee2b0b569d40db26788e0bf2ec3ae1a0d864ea6f1d8309",
 | 
			
		||||
                "sha256:3a2522b1d9178575acee4adf8fd9f979f9c0449b00b4164bb63c3475ea6528ed",
 | 
			
		||||
                "sha256:3aa773580f85a28ffdf6f862e59cb5a3cc7ef6885121f2de3fca8d6ada4dbf3b",
 | 
			
		||||
                "sha256:3b5deaa3ee7180585a296af33e14c9b18c218d148e735c7accf78130765a47e3",
 | 
			
		||||
                "sha256:407af6d7e46593415f216c7f56ba087a9a42bd6dc2ecb86028760aa45b802bd7",
 | 
			
		||||
                "sha256:4c3c09fb674401f630626310bcaf6cd6285daf0d5e4c26d6e55ca26a2734e39b",
 | 
			
		||||
                "sha256:4c6717962247445b4f9e21c962ea61d2e884fc17df5ddf5e35863b016f8a1f03",
 | 
			
		||||
                "sha256:50446fae5681fc99f87e505d4e77c9407e683ab60c555ec302f9ac9bffa61103",
 | 
			
		||||
                "sha256:5057669b6a66aa9ca118a2a860159f0ee3acf837eda937bdd2a64f3431361a2d",
 | 
			
		||||
                "sha256:5dd90c5438b4f935c9d01fcbad3620253da89d19c1f5fca9158646407ed7df35",
 | 
			
		||||
                "sha256:659c815b5b8e2a55193ede2795c1e2349b8011497310bb936da7d4745652823b",
 | 
			
		||||
                "sha256:69b13fdf12878b10dc6003acc8d0abf3ad93e79813fd5f3812497c1c9fb9be49",
 | 
			
		||||
                "sha256:7a1cb80e35e1ccea3e11a48afe65d38744a0e0bde88795cc56a4d05b6e4f9d70",
 | 
			
		||||
                "sha256:7e6e3c52e6732c219c07bd97fff6c088f8df4dae3b79752ee3a817e6f32e177e",
 | 
			
		||||
                "sha256:7f42a8490c4fe854325504ce7a6e4796b207960dabb2cbafe3c3959cb00d1d7e",
 | 
			
		||||
                "sha256:84156313f258eafff716b2961644a4483a9be44a5d43551d554844d15d4d224e",
 | 
			
		||||
                "sha256:8578d6b8192e4c805e85f187bc530d0f52ba86c39172e61cd51f68fddd648103",
 | 
			
		||||
                "sha256:890167d5091279a27e2505ff0e1fb273f8c48c41d35c5b92adbf4af80e6b2ed6",
 | 
			
		||||
                "sha256:98e10634792ac0e9e7a92a76b4991b44c2325d3e7798270a808407355e7bb0a1",
 | 
			
		||||
                "sha256:9aadff9032e967865f9778485571e93908d27dab21d0fdfdec0ca779bb6f8ad9",
 | 
			
		||||
                "sha256:9f24f383a298a0c0f9b3113b982e21751a8ecde6615494a3f1470eb4a9d70e9e",
 | 
			
		||||
                "sha256:a73021b44813b5c84eda4a3af5826dd72356a900bac9bd9dd1f0f81ee1c22c2f",
 | 
			
		||||
                "sha256:afd96845e12638d2c44d213d4810a08f4dc4a563f9a98204b7428e567014b1cd",
 | 
			
		||||
                "sha256:b73ddf033d8cd4cc9dfed6324b1ad2a89ba52c410ef6877998422fcb9c23e3a8",
 | 
			
		||||
                "sha256:b8f490f5fad1767a1331df1259763b3bad7d7af12a75b950c2843ba319b2415f",
 | 
			
		||||
                "sha256:dbc5cd56fff1a6152ca59445178652756f4e509f672e49ccdf3d79c1043113a4",
 | 
			
		||||
                "sha256:eac8a3499754790187bb00574ab980df13e754777d346f85e0ff6df929bcd964",
 | 
			
		||||
                "sha256:eaed1c65f461a959284649e37b5051224f4db6ebdc84e40b5e65f2986f101a08"
 | 
			
		||||
                "sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac",
 | 
			
		||||
                "sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a",
 | 
			
		||||
                "sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5",
 | 
			
		||||
                "sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04",
 | 
			
		||||
                "sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1",
 | 
			
		||||
                "sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5",
 | 
			
		||||
                "sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce",
 | 
			
		||||
                "sha256:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434",
 | 
			
		||||
                "sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9",
 | 
			
		||||
                "sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057",
 | 
			
		||||
                "sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98",
 | 
			
		||||
                "sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522",
 | 
			
		||||
                "sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505",
 | 
			
		||||
                "sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa",
 | 
			
		||||
                "sha256:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3",
 | 
			
		||||
                "sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f",
 | 
			
		||||
                "sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4",
 | 
			
		||||
                "sha256:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4",
 | 
			
		||||
                "sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266",
 | 
			
		||||
                "sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66",
 | 
			
		||||
                "sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38",
 | 
			
		||||
                "sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3",
 | 
			
		||||
                "sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389",
 | 
			
		||||
                "sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab",
 | 
			
		||||
                "sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb",
 | 
			
		||||
                "sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6",
 | 
			
		||||
                "sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d",
 | 
			
		||||
                "sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162",
 | 
			
		||||
                "sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e",
 | 
			
		||||
                "sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.8.4"
 | 
			
		||||
            "version": "==2.8.5"
 | 
			
		||||
        },
 | 
			
		||||
        "pyasn1": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -524,80 +522,81 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pycparser": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
 | 
			
		||||
                "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
 | 
			
		||||
                "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.19"
 | 
			
		||||
            "version": "==2.20"
 | 
			
		||||
        },
 | 
			
		||||
        "pycryptodome": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:012ca77c2105600e3c6aef43188101ac1d95052c633a4ae8fbebffab20c25f8a",
 | 
			
		||||
                "sha256:05b4d865710f9a6378d3ada28195ff78e52642d3ecffe6fa9d379d870b9bf29d",
 | 
			
		||||
                "sha256:07daddb98f98f771ba027f8f835bdb675aeb84effe41ed5221f520b267429354",
 | 
			
		||||
                "sha256:09bf05a489fe10f9280a5e0163f195e7b9630cafb15f7d72fb9c8f5eb2afa84f",
 | 
			
		||||
                "sha256:0a8d5f2dbb4bbe830ace54286b829bfa529f0853bedaab6225fcb2e6d1f7e356",
 | 
			
		||||
                "sha256:1259b8ca49662b8a941177357f08147d858595c0042e63ff81e9628e925b5c9d",
 | 
			
		||||
                "sha256:238d8b6dd27bd1a04816a68aa90a739e6dd23b192fcd83b50f9360958bff192a",
 | 
			
		||||
                "sha256:2a57daef18a2022a5e4b6f7376c9ddd0c2d946e4b1f1e59b837f5bf295be7380",
 | 
			
		||||
                "sha256:39e5ca2f66d1eac7abcba5ce1a03370d123dc6085620f1cd532dfee27e650178",
 | 
			
		||||
                "sha256:3d516df693c195b8da3795e381429bd420e87081b7e6c2871c62c9897c812cda",
 | 
			
		||||
                "sha256:3e486c5b7228e864665fc479e9f596b2547b5fe29c6f5c8ed3807784d06faed7",
 | 
			
		||||
                "sha256:5029c46b0d41dfb763c3981c0af68eab029f06fe2b94f2299112fc18cf9e8d6d",
 | 
			
		||||
                "sha256:5817c0b3c263025d851da96b90cbc7e95348008f88b990e90d10683dba376666",
 | 
			
		||||
                "sha256:79320f1fc5c9ca682869087c565bb29ca6f334692e940d7365771e9a94382e12",
 | 
			
		||||
                "sha256:887d08beca6368d3d70dc75126607ad76317a9fd07fe61323d8c3cb42add12b6",
 | 
			
		||||
                "sha256:9163fec630495c10c767991e3f8dab32f4427bfb2dfeaa59bb28fe3e52ba66f2",
 | 
			
		||||
                "sha256:95d324e603c5cec5d89e8595236bbf59ade5fe3a72d100ce61eebb323d598750",
 | 
			
		||||
                "sha256:9927aa8a8cb4af681279b6f28a1dcb14e0eb556c1aea8413a1e27608a8516e0c",
 | 
			
		||||
                "sha256:9948c2d5c5c0ee45ed44cee0e2eba2ce60a03be006ed3074521f3da3be162e72",
 | 
			
		||||
                "sha256:a719bd708207fa219fcbf4c8ebbcbc52846045f78179d00445b429fdabdbc1c4",
 | 
			
		||||
                "sha256:bc22ced26ebc46546798fa0141f4418f1db116dec517f0aeaecec87cf7b2416c",
 | 
			
		||||
                "sha256:c41b7e10b72cef00cd63410f31fe50e72dc3a40eafbd146e288384fbe4208064",
 | 
			
		||||
                "sha256:cdb0ad83a5d6bac986a37fcb7562bcbef0aabae8ea19505bab5cf83c4d18af12",
 | 
			
		||||
                "sha256:d8e480f65ac7105cbc288eec2417dc61eaac6ed6e75595aa15b8c7c77c53a68b",
 | 
			
		||||
                "sha256:da2d581da279bc7408d38e16ff77754f5448c4352f2acfe530a5d14d8fc6934a",
 | 
			
		||||
                "sha256:de61091dd68326b600422cf731eb4810c4c6363f18a65bccd6061784b7454f5b",
 | 
			
		||||
                "sha256:ec7d39589f9cfc2a8b83b1d2fc673441757c99d43283e97b2dd46e0e23730db8",
 | 
			
		||||
                "sha256:f3204006869ab037604b1d9f045c4e84882ddd365e4ee8caa5eb1ff47a59188e",
 | 
			
		||||
                "sha256:f4d2174e168d0eabd1fffaf88b4f62c2b6f30a67b8816f31024b8e48be3e2d75",
 | 
			
		||||
                "sha256:fcff8c9d88d58880f7eda2139c7c444552a38f98a9e77ba5970b6e78f54ac358"
 | 
			
		||||
                "sha256:07024fc364869eae8d6ac0d316e089956e6aeffe42dbdcf44fe1320d96becf7f",
 | 
			
		||||
                "sha256:09b6d6bcc01a4eb1a2b4deeff5aa602a108ec5aed8ac75ae554f97d1d7f0a5ad",
 | 
			
		||||
                "sha256:0e10f352ccbbcb5bb2dc4ecaf106564e65702a717d72ab260f9ac4c19753cfc2",
 | 
			
		||||
                "sha256:1f4752186298caf2e9ff5354f2e694d607ca7342aa313a62005235d46e28cf04",
 | 
			
		||||
                "sha256:2fbc472e0b567318fe2052281d5a8c0ae70099b446679815f655e9fbc18c3a65",
 | 
			
		||||
                "sha256:3ec3dc2f80f71fd0c955ce48b81bfaf8914c6f63a41a738f28885a1c4892968a",
 | 
			
		||||
                "sha256:426c188c83c10df71f053e04b4003b1437bae5cb37606440e498b00f160d71d0",
 | 
			
		||||
                "sha256:626c0a1d4d83ec6303f970a17158114f75c3ba1736f7f2983f7b40a265861bd8",
 | 
			
		||||
                "sha256:767ad0fb5d23efc36a4d5c2fc608ac603f3de028909bcf59abc943e0d0bc5a36",
 | 
			
		||||
                "sha256:7ac729d9091ed5478af2b4a4f44f5335a98febbc008af619e4569a59fe503e40",
 | 
			
		||||
                "sha256:83295a3fb5cf50c48631eb5b440cb5e9832d8c14d81d1d45f4497b67a9987de8",
 | 
			
		||||
                "sha256:8be56bde3312e022d9d1d6afa124556460ad5c844c2fc63642f6af723c098d35",
 | 
			
		||||
                "sha256:8f06556a8f7ea7b1e42eff39726bb0dca1c251205debae64e6eebea3cd7b438a",
 | 
			
		||||
                "sha256:9230fcb5d948c3fb40049bace4d33c5d254f8232c2c0bba05d2570aea3ba4520",
 | 
			
		||||
                "sha256:9378c309aec1f8cd8bad361ed0816a440151b97a2a3f6ffdaba1d1a1fb76873a",
 | 
			
		||||
                "sha256:9977086e0f93adb326379897437373871b80501e1d176fec63c7f46fb300c862",
 | 
			
		||||
                "sha256:9a94fca11fdc161460bd8659c15b6adef45c1b20da86402256eaf3addfaab324",
 | 
			
		||||
                "sha256:9c739b7795ccf2ef1fdad8d44e539a39ad300ee6786e804ea7f0c6a786eb5343",
 | 
			
		||||
                "sha256:b1e332587b3b195542e77681389c296e1837ca01240399d88803a075447d3557",
 | 
			
		||||
                "sha256:c109a26a21f21f695d369ff9b87f5d43e0d6c768d8384e10bc74142bed2e092e",
 | 
			
		||||
                "sha256:c818dc1f3eace93ee50c2b6b5c2becf7c418fa5dd1ba6fc0ef7db279ea21d5e4",
 | 
			
		||||
                "sha256:cff31f5a8977534f255f729d5d2467526f2b10563a30bbdade92223e0bf264bd",
 | 
			
		||||
                "sha256:d4f94368ce2d65873a87ad867eb3bf63f4ba81eb97a9ee66d38c2b71ce5a7439",
 | 
			
		||||
                "sha256:d61b012baa8c2b659e9890011358455c0019a4108536b811602d2f638c40802a",
 | 
			
		||||
                "sha256:d6e1bc5c94873bec742afe2dfadce0d20445b18e75c47afc0c115b19e5dd38dd",
 | 
			
		||||
                "sha256:ea83bcd9d6c03248ebd46e71ac313858e0afd5aa2fa81478c0e653242f3eb476",
 | 
			
		||||
                "sha256:ed5761b37615a1f222c5345bbf45272ae2cf8c7dff88a4f53a1e9f977cbb6d95",
 | 
			
		||||
                "sha256:f011cd0062e54658b7086a76f8cf0f4222812acc66e219e196ea2d0a8849d0ed",
 | 
			
		||||
                "sha256:f1add21b6d179179b3c177c33d18a2186a09cc0d3af41ff5ed3f377360b869f2",
 | 
			
		||||
                "sha256:f655addaaaa9974108d4808f4150652589cada96074c87115c52e575bfcd87d5"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.9.6"
 | 
			
		||||
            "version": "==3.9.7"
 | 
			
		||||
        },
 | 
			
		||||
        "pycryptodomex": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:04646e40ef5788bad6d415e52862ffcdf2ac2d888ba4a5c82d5cb44607a042f7",
 | 
			
		||||
                "sha256:132f1e5fa84921f25695a313a6d4988847dfaee7fb1fd0d1fbe03ef678836f58",
 | 
			
		||||
                "sha256:17ad1ebaa00806305d34550fe5d3c776e38a27b8a2678dfb7871ef0209d64e46",
 | 
			
		||||
                "sha256:27736fa02a2d3502e1ca4b150457e56ce3b98f132462f540073498884e5f8975",
 | 
			
		||||
                "sha256:38050b3fd86c74c6c79e40bbe824bec6431c3e4e36f6080ed544673ba2dc133a",
 | 
			
		||||
                "sha256:3b9306b360bddbc8e098b16eab7adacf49389d212db9c3739588ab840a1ca868",
 | 
			
		||||
                "sha256:466e36ba74a7e725625e717fad3f36e0b9293c247b7d0439c66528026ef2834f",
 | 
			
		||||
                "sha256:4f77360b23a21db32a4c35dacffac33dc30ac6a5a77162a34e99ab11ab631516",
 | 
			
		||||
                "sha256:5002388178845683c330a02f4faeddfe7cd477b87824987cca4718fa0c4f2085",
 | 
			
		||||
                "sha256:51be76756abfc1ddc97e1e2e3c38f4e62fb940161162368308ea9e5919e86c34",
 | 
			
		||||
                "sha256:544628ae67d61c31c28a60e621dadd738b303c5266492355d5ebdb6e7dd1e78f",
 | 
			
		||||
                "sha256:6ff9d4a06bc40211eee05cd88436740d698a01233f4aaff9eb70d8a90e578966",
 | 
			
		||||
                "sha256:718329c6ca60260f1c27b8392e372dd51e4e691f7dcb88adc53eb3b76af6363c",
 | 
			
		||||
                "sha256:918bc5a0170fe8ed7b72f202245b34f84a1997f5ca1520b9c7db71126e5acd62",
 | 
			
		||||
                "sha256:a8ea72adde0d010f89abece5f024b1be95a5c52472e9a57b3ac7d59aee3c8238",
 | 
			
		||||
                "sha256:a979d2c7bcc67282b7ec2600db384c63d37d74e250edb99168483605a380bf62",
 | 
			
		||||
                "sha256:b350f9ad09b692aed57e669fc3f8cf918557fae9f0229c6ce9286a6fe8c1b60f",
 | 
			
		||||
                "sha256:be838abc8557a21a60d453c5a4e64c738966b8a0b7d7f8f97eb8bb44041ca452",
 | 
			
		||||
                "sha256:bfa99692d3c8f994c5850cc8a894cba001abd76d34069a8bfaad173dd46387d6",
 | 
			
		||||
                "sha256:c021b66f5b3c4ea0c45422ec3241bfea4a16651e1ee5459a136639d0716ccb3c",
 | 
			
		||||
                "sha256:c7babb64484080057a24c74a82dbf7997904b1710b74caf62e261610f989b437",
 | 
			
		||||
                "sha256:c96b7762b601dc8a58d7712235c3c152868116f58a7ffa40dcd1c6f6cd97405e",
 | 
			
		||||
                "sha256:d67b6e0bae0777a2c6c83275fbd7cbf53cd5f23c2028f908b0f7d996466e5b15",
 | 
			
		||||
                "sha256:e15f39fcfb949cfd5536cc9647daba942b1a99b67e4d7211e3bdbcedbc2f823c",
 | 
			
		||||
                "sha256:e380448f1e39736f6230ec284cd6d771956ad802d6ce5bc56947a2481080cac1",
 | 
			
		||||
                "sha256:e5236f2171b21e704d1854fd809a7228eb22e29c894af31459e41986e6a53f87",
 | 
			
		||||
                "sha256:ea7b48ce8dbbc86ebadcfe56ebc10d413bdd12c9a5ff0b9147a41993f12b80b3",
 | 
			
		||||
                "sha256:f39f5b58d8fe348ed604bb44a89ca93b26130c275db2b249f718f1538cb70500",
 | 
			
		||||
                "sha256:f545f776e45f74c41329e4020463fdd4d0cd0a7501bdf9e50251dafe7bd959a9",
 | 
			
		||||
                "sha256:f667ac7ae29c19530f199854635f1a97e73d0bfd24163e0db6bdba7dba04eb9f"
 | 
			
		||||
                "sha256:1537d2d15b604b303aef56e7f440895a1c81adbee786b91f1f06eddc34da5314",
 | 
			
		||||
                "sha256:1d20ab8369b7558168fc014a0745c678613f9f486dae468cca2d68145196b8a4",
 | 
			
		||||
                "sha256:1ecc9db7409db67765eb008e558879d298406642d33ade43a6488224d23e8081",
 | 
			
		||||
                "sha256:37033976f72af829fe15f7fe5fe1dbed308cc43a98d9dd9d2a0a76de8ca5ee78",
 | 
			
		||||
                "sha256:3c3dd9d4c9c1e279d3945ae422895c901f98987333acc132dc094faf52afec35",
 | 
			
		||||
                "sha256:3c9b3fba037ea52c626060c5a87ee6de7e86c99e8a7c6ee07302539985d2bd64",
 | 
			
		||||
                "sha256:45ee555fc5e28c119a46d44ce373f5237e54a35c61b750fb3a94446b09855dbc",
 | 
			
		||||
                "sha256:4c93038ac011b36512cb0bf2ee3e2aec774e8bc81021d015917c89fe02bb0ee5",
 | 
			
		||||
                "sha256:50163324834edd0c9ce3e4512ded3e221c969086e10fdd5d3fdcaadac5e24a78",
 | 
			
		||||
                "sha256:59b0ea9cda5490f924771456912a225d8d9e678891f9f986661af718534719b2",
 | 
			
		||||
                "sha256:5cf306a17cccc327a33cdc3845629fa13f4573a4ec620ed607c79cf6785f2e27",
 | 
			
		||||
                "sha256:5fff8da399af16a1855f58771223acbbdac720b9969cd03fc5013d2e9a7bd9a4",
 | 
			
		||||
                "sha256:68650ce5b9f7152b8283302a4617269f821695a612692640dd247bd12ab21c0b",
 | 
			
		||||
                "sha256:6b3a9a562688996f760b5077714c3ab8b62ca56061b6e9ab7906841e43e19f91",
 | 
			
		||||
                "sha256:7e938ed51a59e29431ea86fab60423ada2757728db0f78952329fa02a789bd31",
 | 
			
		||||
                "sha256:87aa70daad6f039e814790a06422a3189311198b674b62f13933a2bdcb6b1bcc",
 | 
			
		||||
                "sha256:99be3a1df2b2b9f731ebe1c264a2c07c465e71cee68e35e1640b645b5213a755",
 | 
			
		||||
                "sha256:a3f2908666e6f74b8c4893f86dd02e16170f50e4a78ae7f3468b6208d54bc205",
 | 
			
		||||
                "sha256:ae3d44a639fd11dbdeca47e35e94febb1ee8bc15daf26673331add37146e0b85",
 | 
			
		||||
                "sha256:afb4c2fa3c6f492fd9a8b38d76e13f32d429b8e5e1e00238309391b5591cde0d",
 | 
			
		||||
                "sha256:b1515ce3a8a2c3fa537d137c5ca5f8b7a902044d04e07d7c3aa26c3e026120fb",
 | 
			
		||||
                "sha256:bf391b377413a197000b43ef2b74359974d8927d329a897c9f5ba7b63dca7b9c",
 | 
			
		||||
                "sha256:c436919117c23355740c669f89720673578b9aa4569bbfe105f6c10101fc1966",
 | 
			
		||||
                "sha256:d2c3c280975638e2a2c2fd9cb36ab111980219757fa163a2755594b9448e4138",
 | 
			
		||||
                "sha256:e585d530764c459cbd5d460aed0288807bb881f376ca9a20e653645217895961",
 | 
			
		||||
                "sha256:e76e6638ead4a7d93262a24218f0ff3ff74de6b6c823b7e19dccb31b6a481978",
 | 
			
		||||
                "sha256:ebfc2f885cafda076c31ae30fa0dd81e7e919ec34059a88d3018ed66e83fcce3",
 | 
			
		||||
                "sha256:f5797a39933a3d41526da60856735e6684b2b71a8ca99d5f79555ca121be2f4b",
 | 
			
		||||
                "sha256:f7e5fc5e124200b19a14be33fb0099e956e6ebb5e25d287b0829ef0a78ed76c7",
 | 
			
		||||
                "sha256:fb350e31e55211fec8ddc89fc0256f3b9bc3b44b68a8bde1cf44b3b4e80c0e42"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.9.6"
 | 
			
		||||
            "version": "==3.9.7"
 | 
			
		||||
        },
 | 
			
		||||
        "pyjwkest": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -614,16 +613,16 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pyparsing": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
 | 
			
		||||
                "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
 | 
			
		||||
                "sha256:67199f0c41a9c702154efb0e7a8cc08accf830eb003b4d9fa42c4059002e2492",
 | 
			
		||||
                "sha256:700d17888d441604b0bd51535908dcb297561b040819cccde647a92439db5a2a"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.4.6"
 | 
			
		||||
            "version": "==3.0.0a1"
 | 
			
		||||
        },
 | 
			
		||||
        "pyrsistent": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:cdc7b5e3ed77bed61270a47d35434a30617b9becdf2478af76ad2c6ade307280"
 | 
			
		||||
                "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.15.7"
 | 
			
		||||
            "version": "==0.16.0"
 | 
			
		||||
        },
 | 
			
		||||
        "python-dateutil": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -673,20 +672,20 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pyyaml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
 | 
			
		||||
                "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
 | 
			
		||||
                "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
 | 
			
		||||
                "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
 | 
			
		||||
                "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
 | 
			
		||||
                "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
 | 
			
		||||
                "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
 | 
			
		||||
                "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
 | 
			
		||||
                "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
 | 
			
		||||
                "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
 | 
			
		||||
                "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
 | 
			
		||||
                "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
 | 
			
		||||
                "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
 | 
			
		||||
                "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
 | 
			
		||||
                "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
 | 
			
		||||
                "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
 | 
			
		||||
                "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
 | 
			
		||||
                "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
 | 
			
		||||
                "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
 | 
			
		||||
                "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
 | 
			
		||||
                "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
 | 
			
		||||
                "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==5.3"
 | 
			
		||||
            "version": "==5.3.1"
 | 
			
		||||
        },
 | 
			
		||||
        "qrcode": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -705,10 +704,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "requests": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
 | 
			
		||||
                "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
 | 
			
		||||
                "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
 | 
			
		||||
                "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.22.0"
 | 
			
		||||
            "version": "==2.23.0"
 | 
			
		||||
        },
 | 
			
		||||
        "requests-oauthlib": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -759,11 +758,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "sentry-sdk": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:b06dd27391fd11fb32f84fe054e6a64736c469514a718a99fb5ce1dff95d6b28",
 | 
			
		||||
                "sha256:e023da07cfbead3868e1e2ba994160517885a32dfd994fc455b118e37989479b"
 | 
			
		||||
                "sha256:23808d571d2461a4ce3784ec12bbee5bdb8c026c143fe79d36cef8a6d653e71f",
 | 
			
		||||
                "sha256:bb90a4e19c7233a580715fc986cc44be2c48fc10b31e71580a2037e1c94b6950"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==0.14.1"
 | 
			
		||||
            "version": "==0.14.3"
 | 
			
		||||
        },
 | 
			
		||||
        "service-identity": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -790,10 +789,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "sqlparse": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
 | 
			
		||||
                "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"
 | 
			
		||||
                "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
 | 
			
		||||
                "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.3.0"
 | 
			
		||||
            "version": "==0.3.1"
 | 
			
		||||
        },
 | 
			
		||||
        "structlog": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -805,11 +804,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "swagger-spec-validator": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:57e29feb3aa921a9fb98bd70af148746b27c77d3207266f5571cebcce211e685",
 | 
			
		||||
                "sha256:62ef22eca3f429d93fddda5d793d2a1a9057d3732e7a14606e641805326ae4a6"
 | 
			
		||||
                "sha256:61f2d2a732b886cf33c2c24886565be9692e5814cacc17fd973e095b72b33e4f",
 | 
			
		||||
                "sha256:8eb82682871f8d63067b455e2e055c8dd953ca260e791635b58dfe0b73ba1f43"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.4.3"
 | 
			
		||||
            "version": "==2.5.0"
 | 
			
		||||
        },
 | 
			
		||||
        "uritemplate": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -848,10 +847,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "asgiref": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
 | 
			
		||||
                "sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
 | 
			
		||||
                "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
 | 
			
		||||
                "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.2.3"
 | 
			
		||||
            "version": "==3.2.7"
 | 
			
		||||
        },
 | 
			
		||||
        "astroid": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -869,10 +868,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "autopep8": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0f592a0447acea0c2b0a9602be1e4e3d86db52badd2e3c84f0193bfd89fd3a43"
 | 
			
		||||
                "sha256:cc6be1dfd46f2c7fa00e84a357f1a269683985b09eaffb47654ed551194399eb"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.5"
 | 
			
		||||
            "version": "==1.5.1"
 | 
			
		||||
        },
 | 
			
		||||
        "bandit": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -900,10 +899,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "click": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
 | 
			
		||||
                "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
 | 
			
		||||
                "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
 | 
			
		||||
                "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==7.0"
 | 
			
		||||
            "version": "==7.1.1"
 | 
			
		||||
        },
 | 
			
		||||
        "colorama": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -915,48 +914,48 @@
 | 
			
		||||
        },
 | 
			
		||||
        "coverage": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3",
 | 
			
		||||
                "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c",
 | 
			
		||||
                "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0",
 | 
			
		||||
                "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477",
 | 
			
		||||
                "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a",
 | 
			
		||||
                "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf",
 | 
			
		||||
                "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691",
 | 
			
		||||
                "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73",
 | 
			
		||||
                "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987",
 | 
			
		||||
                "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894",
 | 
			
		||||
                "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e",
 | 
			
		||||
                "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef",
 | 
			
		||||
                "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf",
 | 
			
		||||
                "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68",
 | 
			
		||||
                "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8",
 | 
			
		||||
                "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954",
 | 
			
		||||
                "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2",
 | 
			
		||||
                "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40",
 | 
			
		||||
                "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc",
 | 
			
		||||
                "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc",
 | 
			
		||||
                "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e",
 | 
			
		||||
                "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d",
 | 
			
		||||
                "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f",
 | 
			
		||||
                "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc",
 | 
			
		||||
                "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301",
 | 
			
		||||
                "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea",
 | 
			
		||||
                "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb",
 | 
			
		||||
                "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af",
 | 
			
		||||
                "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52",
 | 
			
		||||
                "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37",
 | 
			
		||||
                "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"
 | 
			
		||||
                "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0",
 | 
			
		||||
                "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30",
 | 
			
		||||
                "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b",
 | 
			
		||||
                "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0",
 | 
			
		||||
                "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823",
 | 
			
		||||
                "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe",
 | 
			
		||||
                "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037",
 | 
			
		||||
                "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6",
 | 
			
		||||
                "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31",
 | 
			
		||||
                "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd",
 | 
			
		||||
                "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892",
 | 
			
		||||
                "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1",
 | 
			
		||||
                "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78",
 | 
			
		||||
                "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac",
 | 
			
		||||
                "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006",
 | 
			
		||||
                "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014",
 | 
			
		||||
                "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2",
 | 
			
		||||
                "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7",
 | 
			
		||||
                "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8",
 | 
			
		||||
                "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7",
 | 
			
		||||
                "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9",
 | 
			
		||||
                "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1",
 | 
			
		||||
                "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307",
 | 
			
		||||
                "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a",
 | 
			
		||||
                "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435",
 | 
			
		||||
                "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0",
 | 
			
		||||
                "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5",
 | 
			
		||||
                "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441",
 | 
			
		||||
                "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732",
 | 
			
		||||
                "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de",
 | 
			
		||||
                "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==5.0.3"
 | 
			
		||||
            "version": "==5.0.4"
 | 
			
		||||
        },
 | 
			
		||||
        "django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2f1ba1db8648484dd5c238fb62504777b7ad090c81c5f1fd8d5eb5ec21b5f283",
 | 
			
		||||
                "sha256:c91c91a7ad6ef67a874a4f76f58ba534f9208412692a840e1d125eb5c279cb0a"
 | 
			
		||||
                "sha256:642d8eceab321ca743ae71e0f985ff8fdca59f07aab3a9fb362c617d23e33a76",
 | 
			
		||||
                "sha256:d4666c2edefa38c5ede0ec1655424c56dc47ceb04b6d8d62a7eac09db89545c1"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.0.3"
 | 
			
		||||
            "version": "==3.0.5"
 | 
			
		||||
        },
 | 
			
		||||
        "django-debug-toolbar": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -966,19 +965,19 @@
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.2"
 | 
			
		||||
        },
 | 
			
		||||
        "gitdb2": {
 | 
			
		||||
        "gitdb": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0375d983fd887d03c8942e81b1b0abc6c320cfb500cd3fe0d9c0eac87fbf2b52",
 | 
			
		||||
                "sha256:b2b3a67090c17dc61f8407ca485e79ae811225ab5ebcd98ac5ee01448e8987b5"
 | 
			
		||||
                "sha256:284a6a4554f954d6e737cddcff946404393e030b76a282c6640df8efd6b3da5e",
 | 
			
		||||
                "sha256:598e0096bb3175a0aab3a0b5aedaa18a9a25c6707e0eca0695ba1a0baf1b2150"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.0.2"
 | 
			
		||||
            "version": "==4.0.2"
 | 
			
		||||
        },
 | 
			
		||||
        "gitpython": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:620b3c729bbc143b498cfea77e302999deedc55faec5b1067086c9ef90e101bc",
 | 
			
		||||
                "sha256:a43a5d88a5bbc3cf32bb5223e4b4e68fd716db5e9996cad6e561bbfee6e5f4af"
 | 
			
		||||
                "sha256:43da89427bdf18bf07f1164c6d415750693b4d50e28fc9b68de706245147b9dd",
 | 
			
		||||
                "sha256:e426c3b587bd58c482f0b7fe6145ff4ac7ae6c82673fc656f489719abca6f4cb"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.0.8"
 | 
			
		||||
            "version": "==3.1.0"
 | 
			
		||||
        },
 | 
			
		||||
        "isort": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1022,17 +1021,17 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pathspec": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424",
 | 
			
		||||
                "sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"
 | 
			
		||||
                "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
 | 
			
		||||
                "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.7.0"
 | 
			
		||||
            "version": "==0.8.0"
 | 
			
		||||
        },
 | 
			
		||||
        "pbr": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:139d2625547dbfa5fb0b81daebb39601c478c21956dc57e2e07b74450a8c506b",
 | 
			
		||||
                "sha256:61aa52a0f18b71c5cc58232d2cf8f8d09cd67fcad60b742a60124cb8d6951488"
 | 
			
		||||
                "sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c",
 | 
			
		||||
                "sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==5.4.4"
 | 
			
		||||
            "version": "==5.4.5"
 | 
			
		||||
        },
 | 
			
		||||
        "pycodestyle": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1051,11 +1050,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pylint-django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:440beb814464928aedd2e21196bb6e47a83b63e2cbe886a701ba0f4a64206bbb",
 | 
			
		||||
                "sha256:d5d113605a64cf0e638b707d4cb42106e626f8851bc30a44d5b22bd698ad8483"
 | 
			
		||||
                "sha256:3a4cc19dd6301fc2d36c9fb6e15163001a6d12723c1f7f8c2249223c2a8c68f0",
 | 
			
		||||
                "sha256:c9bbcff6b87ee8466fae274fd7aae3d2d3d4c4d1ea20c48cbce673e837e36048"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.0.13"
 | 
			
		||||
            "version": "==2.0.14"
 | 
			
		||||
        },
 | 
			
		||||
        "pylint-plugin-utils": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1073,46 +1072,46 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pyyaml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
 | 
			
		||||
                "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
 | 
			
		||||
                "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
 | 
			
		||||
                "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
 | 
			
		||||
                "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
 | 
			
		||||
                "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
 | 
			
		||||
                "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
 | 
			
		||||
                "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
 | 
			
		||||
                "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
 | 
			
		||||
                "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
 | 
			
		||||
                "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
 | 
			
		||||
                "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
 | 
			
		||||
                "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
 | 
			
		||||
                "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
 | 
			
		||||
                "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
 | 
			
		||||
                "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
 | 
			
		||||
                "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
 | 
			
		||||
                "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
 | 
			
		||||
                "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
 | 
			
		||||
                "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
 | 
			
		||||
                "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
 | 
			
		||||
                "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==5.3"
 | 
			
		||||
            "version": "==5.3.1"
 | 
			
		||||
        },
 | 
			
		||||
        "regex": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:07b39bf943d3d2fe63d46281d8504f8df0ff3fe4c57e13d1656737950e53e525",
 | 
			
		||||
                "sha256:0932941cdfb3afcbc26cc3bcf7c3f3d73d5a9b9c56955d432dbf8bbc147d4c5b",
 | 
			
		||||
                "sha256:0e182d2f097ea8549a249040922fa2b92ae28be4be4895933e369a525ba36576",
 | 
			
		||||
                "sha256:10671601ee06cf4dc1bc0b4805309040bb34c9af423c12c379c83d7895622bb5",
 | 
			
		||||
                "sha256:23e2c2c0ff50f44877f64780b815b8fd2e003cda9ce817a7fd00dea5600c84a0",
 | 
			
		||||
                "sha256:26ff99c980f53b3191d8931b199b29d6787c059f2e029b2b0c694343b1708c35",
 | 
			
		||||
                "sha256:27429b8d74ba683484a06b260b7bb00f312e7c757792628ea251afdbf1434003",
 | 
			
		||||
                "sha256:3e77409b678b21a056415da3a56abfd7c3ad03da71f3051bbcdb68cf44d3c34d",
 | 
			
		||||
                "sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161",
 | 
			
		||||
                "sha256:4eae742636aec40cf7ab98171ab9400393360b97e8f9da67b1867a9ee0889b26",
 | 
			
		||||
                "sha256:6a6ae17bf8f2d82d1e8858a47757ce389b880083c4ff2498dba17c56e6c103b9",
 | 
			
		||||
                "sha256:6a6ba91b94427cd49cd27764679024b14a96874e0dc638ae6bdd4b1a3ce97be1",
 | 
			
		||||
                "sha256:7bcd322935377abcc79bfe5b63c44abd0b29387f267791d566bbb566edfdd146",
 | 
			
		||||
                "sha256:98b8ed7bb2155e2cbb8b76f627b2fd12cf4b22ab6e14873e8641f266e0fb6d8f",
 | 
			
		||||
                "sha256:bd25bb7980917e4e70ccccd7e3b5740614f1c408a642c245019cff9d7d1b6149",
 | 
			
		||||
                "sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351",
 | 
			
		||||
                "sha256:d58e4606da2a41659c84baeb3cfa2e4c87a74cec89a1e7c56bee4b956f9d7461",
 | 
			
		||||
                "sha256:e3cd21cc2840ca67de0bbe4071f79f031c81418deb544ceda93ad75ca1ee9f7b",
 | 
			
		||||
                "sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242",
 | 
			
		||||
                "sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c",
 | 
			
		||||
                "sha256:ecc6de77df3ef68fee966bb8cb4e067e84d4d1f397d0ef6fce46913663540d77"
 | 
			
		||||
                "sha256:08119f707f0ebf2da60d2f24c2f39ca616277bb67ef6c92b72cbf90cbe3a556b",
 | 
			
		||||
                "sha256:0ce9537396d8f556bcfc317c65b6a0705320701e5ce511f05fc04421ba05b8a8",
 | 
			
		||||
                "sha256:1cbe0fa0b7f673400eb29e9ef41d4f53638f65f9a2143854de6b1ce2899185c3",
 | 
			
		||||
                "sha256:2294f8b70e058a2553cd009df003a20802ef75b3c629506be20687df0908177e",
 | 
			
		||||
                "sha256:23069d9c07e115537f37270d1d5faea3e0bdded8279081c4d4d607a2ad393683",
 | 
			
		||||
                "sha256:24f4f4062eb16c5bbfff6a22312e8eab92c2c99c51a02e39b4eae54ce8255cd1",
 | 
			
		||||
                "sha256:295badf61a51add2d428a46b8580309c520d8b26e769868b922750cf3ce67142",
 | 
			
		||||
                "sha256:2a3bf8b48f8e37c3a40bb3f854bf0121c194e69a650b209628d951190b862de3",
 | 
			
		||||
                "sha256:4385f12aa289d79419fede43f979e372f527892ac44a541b5446617e4406c468",
 | 
			
		||||
                "sha256:5635cd1ed0a12b4c42cce18a8d2fb53ff13ff537f09de5fd791e97de27b6400e",
 | 
			
		||||
                "sha256:5bfed051dbff32fd8945eccca70f5e22b55e4148d2a8a45141a3b053d6455ae3",
 | 
			
		||||
                "sha256:7e1037073b1b7053ee74c3c6c0ada80f3501ec29d5f46e42669378eae6d4405a",
 | 
			
		||||
                "sha256:90742c6ff121a9c5b261b9b215cb476eea97df98ea82037ec8ac95d1be7a034f",
 | 
			
		||||
                "sha256:a58dd45cb865be0ce1d5ecc4cfc85cd8c6867bea66733623e54bd95131f473b6",
 | 
			
		||||
                "sha256:c087bff162158536387c53647411db09b6ee3f9603c334c90943e97b1052a156",
 | 
			
		||||
                "sha256:c162a21e0da33eb3d31a3ac17a51db5e634fc347f650d271f0305d96601dc15b",
 | 
			
		||||
                "sha256:c9423a150d3a4fc0f3f2aae897a59919acd293f4cb397429b120a5fcd96ea3db",
 | 
			
		||||
                "sha256:ccccdd84912875e34c5ad2d06e1989d890d43af6c2242c6fcfa51556997af6cd",
 | 
			
		||||
                "sha256:e91ba11da11cf770f389e47c3f5c30473e6d85e06d7fd9dcba0017d2867aab4a",
 | 
			
		||||
                "sha256:ea4adf02d23b437684cd388d557bf76e3afa72f7fed5bbc013482cc00c816948",
 | 
			
		||||
                "sha256:fb95debbd1a824b2c4376932f2216cc186912e389bdb0e27147778cf6acb3f89"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2020.1.8"
 | 
			
		||||
            "version": "==2020.4.4"
 | 
			
		||||
        },
 | 
			
		||||
        "six": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1121,19 +1120,19 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.14.0"
 | 
			
		||||
        },
 | 
			
		||||
        "smmap2": {
 | 
			
		||||
        "smmap": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0555a7bf4df71d1ef4218e4807bbf9b201f910174e6e08af2e138d4e517b4dde",
 | 
			
		||||
                "sha256:29a9ffa0497e7f2be94ca0ed1ca1aa3cd4cf25a1f6b4f5f87f74b46ed91d609a"
 | 
			
		||||
                "sha256:171484fe62793e3626c8b05dd752eb2ca01854b0c55a1efc0dc4210fccb65446",
 | 
			
		||||
                "sha256:5fead614cf2de17ee0707a8c6a5f2aa5a2fc6c698c70993ba42f515485ffda78"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.0.5"
 | 
			
		||||
            "version": "==3.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "sqlparse": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177",
 | 
			
		||||
                "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873"
 | 
			
		||||
                "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
 | 
			
		||||
                "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.3.0"
 | 
			
		||||
            "version": "==0.3.1"
 | 
			
		||||
        },
 | 
			
		||||
        "stevedore": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1177,11 +1176,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "unittest-xml-reporting": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:6584562cde8226fc79fa29e38903c669a02799074a563bb0b70fcd3a8e87829c",
 | 
			
		||||
                "sha256:dd8046a64dc62f3d30301523a54992e0be75a945194491e0a3b718130cb429e0"
 | 
			
		||||
                "sha256:74eaf7739a7957a74f52b8187c5616f61157372189bef0a32ba5c30bbc00e58a",
 | 
			
		||||
                "sha256:e09b8ae70cce9904cdd331f53bf929150962869a5324ab7ff3dd6c8b87e01f7d"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.0.1"
 | 
			
		||||
            "version": "==3.0.2"
 | 
			
		||||
        },
 | 
			
		||||
        "wrapt": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,8 @@ services:
 | 
			
		||||
  server:
 | 
			
		||||
    image: beryju/passbook:${SERVER_TAG:-latest}
 | 
			
		||||
    command:
 | 
			
		||||
      - ./manage.py
 | 
			
		||||
      - bootstrap
 | 
			
		||||
      - uwsgi
 | 
			
		||||
      - uwsgi.ini
 | 
			
		||||
    environment:
 | 
			
		||||
@ -42,6 +44,8 @@ services:
 | 
			
		||||
  worker:
 | 
			
		||||
    image: beryju/passbook:${SERVER_TAG:-latest}
 | 
			
		||||
    command:
 | 
			
		||||
      - ./manage.py
 | 
			
		||||
      - bootstrap
 | 
			
		||||
      - celery
 | 
			
		||||
      - worker
 | 
			
		||||
      - --autoscale=10,3
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
apiVersion: v1
 | 
			
		||||
appVersion: "0.8.8-beta"
 | 
			
		||||
appVersion: "0.8.14-beta"
 | 
			
		||||
description: A Helm chart for passbook.
 | 
			
		||||
name: passbook
 | 
			
		||||
version: "0.8.8-beta"
 | 
			
		||||
version: "0.8.14-beta"
 | 
			
		||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ spec:
 | 
			
		||||
      labels:
 | 
			
		||||
        app.kubernetes.io/name: {{ include "passbook.name" . }}
 | 
			
		||||
        app.kubernetes.io/instance: {{ .Release.Name }}
 | 
			
		||||
        passbook.io/component: web
 | 
			
		||||
        k8s.passbook.io/component: web
 | 
			
		||||
    spec:
 | 
			
		||||
      volumes:
 | 
			
		||||
        - name: config-volume
 | 
			
		||||
@ -27,9 +27,12 @@ spec:
 | 
			
		||||
      initContainers:
 | 
			
		||||
        - name: passbook-database-migrations
 | 
			
		||||
          image: "beryju/passbook:{{ .Values.image.tag }}"
 | 
			
		||||
          imagePullPolicy: Always
 | 
			
		||||
          command:
 | 
			
		||||
            - ./manage.py
 | 
			
		||||
          args:
 | 
			
		||||
            - bootstrap
 | 
			
		||||
            - ./manage.py
 | 
			
		||||
            - migrate
 | 
			
		||||
          volumeMounts:
 | 
			
		||||
            - mountPath: /etc/passbook
 | 
			
		||||
@ -57,10 +60,12 @@ spec:
 | 
			
		||||
      containers:
 | 
			
		||||
        - name: {{ .Chart.Name }}
 | 
			
		||||
          image: "beryju/passbook:{{ .Values.image.tag }}"
 | 
			
		||||
          imagePullPolicy: IfNotPresent
 | 
			
		||||
          imagePullPolicy: Always
 | 
			
		||||
          command:
 | 
			
		||||
            - uwsgi
 | 
			
		||||
            - ./manage.py
 | 
			
		||||
          args:
 | 
			
		||||
            - bootstrap
 | 
			
		||||
            - uwsgi
 | 
			
		||||
            - uwsgi.ini
 | 
			
		||||
          volumeMounts:
 | 
			
		||||
            - mountPath: /etc/passbook
 | 
			
		||||
 | 
			
		||||
@ -18,4 +18,4 @@ spec:
 | 
			
		||||
  selector:
 | 
			
		||||
    app.kubernetes.io/name: {{ include "passbook.name" . }}
 | 
			
		||||
    app.kubernetes.io/instance: {{ .Release.Name }}
 | 
			
		||||
    passbook.io/component: web
 | 
			
		||||
    k8s.passbook.io/component: web
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ spec:
 | 
			
		||||
      labels:
 | 
			
		||||
        app.kubernetes.io/name: {{ include "passbook.name" . }}
 | 
			
		||||
        app.kubernetes.io/instance: {{ .Release.Name }}
 | 
			
		||||
        passbook.io/component: worker
 | 
			
		||||
        k8s.passbook.io/component: worker
 | 
			
		||||
    spec:
 | 
			
		||||
      volumes:
 | 
			
		||||
        - name: config-volume
 | 
			
		||||
@ -29,8 +29,10 @@ spec:
 | 
			
		||||
          image: "beryju/passbook:{{ .Values.image.tag }}"
 | 
			
		||||
          imagePullPolicy: IfNotPresent
 | 
			
		||||
          command:
 | 
			
		||||
            - celery
 | 
			
		||||
            - ./manage.py
 | 
			
		||||
          args:
 | 
			
		||||
            - bootstrap
 | 
			
		||||
            - celery
 | 
			
		||||
            - worker
 | 
			
		||||
            - --autoscale=10,3
 | 
			
		||||
            - -E
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
# This is a YAML-formatted file.
 | 
			
		||||
# Declare variables to be passed into your templates.
 | 
			
		||||
image:
 | 
			
		||||
  tag: 0.8.8-beta
 | 
			
		||||
  tag: 0.8.14-beta
 | 
			
		||||
 | 
			
		||||
nameOverride: ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ nav:
 | 
			
		||||
          - Sentry: integrations/services/sentry/index.md
 | 
			
		||||
          - Ansible Tower/AWX: integrations/services/tower-awx/index.md
 | 
			
		||||
 | 
			
		||||
repo_name: "BeryJu.org/passbook"
 | 
			
		||||
repo_name: "BeryJu/passbook"
 | 
			
		||||
repo_url: https://github.com/BeryJu/passbook
 | 
			
		||||
theme:
 | 
			
		||||
  name: "material"
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook"""
 | 
			
		||||
__version__ = "0.8.8-beta"
 | 
			
		||||
__version__ = "0.8.14-beta"
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,7 @@ class YAMLField(forms.CharField):
 | 
			
		||||
    def prepare_value(self, value):
 | 
			
		||||
        if isinstance(value, InvalidYAMLInput):
 | 
			
		||||
            return value
 | 
			
		||||
        return yaml.dump(value, explicit_start=True)
 | 
			
		||||
        return yaml.dump(value, explicit_start=True, default_flow_style=False)
 | 
			
		||||
 | 
			
		||||
    def has_changed(self, initial, data):
 | 
			
		||||
        if super().has_changed(initial, data):
 | 
			
		||||
 | 
			
		||||
@ -1,40 +0,0 @@
 | 
			
		||||
"""passbook form helpers"""
 | 
			
		||||
from django import forms
 | 
			
		||||
 | 
			
		||||
from passbook.admin.fields import YAMLField
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TagModelForm(forms.ModelForm):
 | 
			
		||||
    """Base form for models that have attributes"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        # Check if we have an instance, load tags otherwise use an empty dict
 | 
			
		||||
        instance = kwargs.get("instance", None)
 | 
			
		||||
        tags = instance.tags if instance else {}
 | 
			
		||||
        # Make sure all predefined tags exist in tags, and set default if they don't
 | 
			
		||||
        predefined_tags = (
 | 
			
		||||
            self._meta.model().get_predefined_tags()  # pylint: disable=no-member
 | 
			
		||||
        )
 | 
			
		||||
        for key, value in predefined_tags.items():
 | 
			
		||||
            if key not in tags:
 | 
			
		||||
                tags[key] = value
 | 
			
		||||
        # Format JSON
 | 
			
		||||
        kwargs["initial"]["tags"] = tags
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def clean_tags(self):
 | 
			
		||||
        """Make sure all required tags are set"""
 | 
			
		||||
        if hasattr(self.instance, "get_required_keys") and hasattr(
 | 
			
		||||
            self.instance, "tags"
 | 
			
		||||
        ):
 | 
			
		||||
            for key in self.instance.get_required_keys():
 | 
			
		||||
                if key not in self.cleaned_data.get("tags"):
 | 
			
		||||
                    raise forms.ValidationError("Tag %s missing." % key)
 | 
			
		||||
        return self.cleaned_data.get("tags")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# pylint: disable=too-few-public-methods
 | 
			
		||||
class TagModelFormMeta:
 | 
			
		||||
    """Base Meta class that uses the YAMLField"""
 | 
			
		||||
 | 
			
		||||
    field_classes = {"tags": YAMLField}
 | 
			
		||||
@ -1,66 +1,68 @@
 | 
			
		||||
{% extends "administration/base.html" %}
 | 
			
		||||
{% extends "base/page.html" %}
 | 
			
		||||
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load 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-catalog"></i>
 | 
			
		||||
            {% trans 'Audit Log' %}
 | 
			
		||||
        </h1>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
 | 
			
		||||
    <div class="pf-c-card">
 | 
			
		||||
        <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
 | 
			
		||||
            {% include 'partials/pagination.html' %}
 | 
			
		||||
{% block page_content %}
 | 
			
		||||
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
 | 
			
		||||
    <section class="pf-c-page__main-section pf-m-light">
 | 
			
		||||
        <div class="pf-c-content">
 | 
			
		||||
            <h1>
 | 
			
		||||
                <i class="pf-icon pf-icon-catalog"></i>
 | 
			
		||||
                {% trans 'Audit Log' %}
 | 
			
		||||
            </h1>
 | 
			
		||||
        </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 'Action' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Context' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'User' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Creation Date' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Client IP' %}</th>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody role="rowgroup">
 | 
			
		||||
                {% for entry in object_list %}
 | 
			
		||||
                <tr role="row">
 | 
			
		||||
                    <th role="columnheader">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <div>{{ entry.action }}</div>
 | 
			
		||||
                            <small>{{ entry.app|default:'-' }}</small>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <code>{{ entry.context }}</code>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {{ entry.user }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {{ entry.created }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {{ entry.client_ip }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
        <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
 | 
			
		||||
            {% include 'partials/pagination.html' %}
 | 
			
		||||
    </section>
 | 
			
		||||
    <section class="pf-c-page__main-section pf-m-no-padding-mobile">
 | 
			
		||||
        <div class="pf-c-card">
 | 
			
		||||
            <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
 | 
			
		||||
                {% include 'partials/pagination.html' %}
 | 
			
		||||
            </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 'Action' %}</th>
 | 
			
		||||
                        <th role="columnheader" scope="col">{% trans 'Context' %}</th>
 | 
			
		||||
                        <th role="columnheader" scope="col">{% trans 'User' %}</th>
 | 
			
		||||
                        <th role="columnheader" scope="col">{% trans 'Creation Date' %}</th>
 | 
			
		||||
                        <th role="columnheader" scope="col">{% trans 'Client IP' %}</th>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                </thead>
 | 
			
		||||
                <tbody role="rowgroup">
 | 
			
		||||
                    {% for entry in object_list %}
 | 
			
		||||
                    <tr role="row">
 | 
			
		||||
                        <th role="columnheader">
 | 
			
		||||
                            <div>
 | 
			
		||||
                                <div>{{ entry.action }}</div>
 | 
			
		||||
                                <small>{{ entry.app|default:'-' }}</small>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </th>
 | 
			
		||||
                        <td role="cell">
 | 
			
		||||
                            <code>{{ entry.context }}</code>
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td role="cell">
 | 
			
		||||
                            <span>
 | 
			
		||||
                                {{ entry.user }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td role="cell">
 | 
			
		||||
                            <span>
 | 
			
		||||
                                {{ entry.created }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </td>
 | 
			
		||||
                        <td role="cell">
 | 
			
		||||
                            <span>
 | 
			
		||||
                                {{ entry.client_ip }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
            <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
 | 
			
		||||
                {% include 'partials/pagination.html' %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
    </section>
 | 
			
		||||
</main>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,10 @@
 | 
			
		||||
{% extends "overview/base.html" %}
 | 
			
		||||
{% extends "base/page.html" %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% load is_active %}
 | 
			
		||||
{% load utils %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
@ -12,3 +16,84 @@
 | 
			
		||||
<script src="{% static 'node_modules/codemirror/mode/yaml/yaml.js' %}"></script>
 | 
			
		||||
<script src="{% static 'node_modules/codemirror/mode/jinja2/jinja2.js' %}"></script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block page_content %}
 | 
			
		||||
<div class="pf-c-page__sidebar">
 | 
			
		||||
    <div class="pf-c-page__sidebar-body">
 | 
			
		||||
        <nav class="pf-c-nav" id="page-default-nav-example-primary-nav" aria-label="Global">
 | 
			
		||||
            <ul class="pf-c-nav__list">
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:overview' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active_url 'passbook_admin:overview' %}">
 | 
			
		||||
                        {% trans 'System Status' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:applications' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}">
 | 
			
		||||
                        {% trans 'Applications' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:sources' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-delete' %}">
 | 
			
		||||
                        {% trans 'Sources' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:providers' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
 | 
			
		||||
                        {% trans 'Providers' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:property-mappings' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
 | 
			
		||||
                        {% trans 'Property Mappings' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:factors' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
 | 
			
		||||
                        {% trans 'Factors' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:policies' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}">
 | 
			
		||||
                        {% trans 'Policies' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:certificate_key_pair' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:certificate_key_pair' 'passbook_admin:certificatekeypair-create' 'passbook_admin:certificatekeypair-update' 'passbook_admin:certificatekeypair-delete' %}">
 | 
			
		||||
                        {% trans 'Certificates' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:invitations' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
 | 
			
		||||
                        {% trans 'Invitations' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:users' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
 | 
			
		||||
                        {% trans 'Users' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:groups' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
 | 
			
		||||
                        {% trans 'Groups' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </nav>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
 | 
			
		||||
    {% block content %}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
</main>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,69 @@
 | 
			
		||||
{% extends "administration/base.html" %}
 | 
			
		||||
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load 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-key"></i>
 | 
			
		||||
            {% trans 'Certificate-Key Pairs' %}
 | 
			
		||||
        </h1>
 | 
			
		||||
        <p>{% trans "Import certificates of external providers or create certificates to sign requests with." %}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
 | 
			
		||||
    <div class="pf-c-card">
 | 
			
		||||
        <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-top">
 | 
			
		||||
            <div class="pf-c-toolbar__action-group">
 | 
			
		||||
                <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>
 | 
			
		||||
            {% include 'partials/pagination.html' %}
 | 
			
		||||
        </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 'Name' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Private Key available' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Fingerprint' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Provider Type' %}</th>
 | 
			
		||||
                    <th role="cell"></th>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody role="rowgroup">
 | 
			
		||||
                {% for kp in object_list %}
 | 
			
		||||
                <tr role="row">
 | 
			
		||||
                    <th role="columnheader">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            <div>{{ kp.name }}</div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {% if kp.key_data is not None %}
 | 
			
		||||
                            {% trans 'Yes' %}
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            {% trans 'No' %}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {{ kp.fingerprint }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:certificatekeypair-update' pk=kp.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
 | 
			
		||||
                        <a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:certificatekeypair-delete' pk=kp.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
        <div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
 | 
			
		||||
            {% include 'partials/pagination.html' %}
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -11,16 +11,22 @@
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block beneath_form %}
 | 
			
		||||
<p class="loading" style="display: none;">
 | 
			
		||||
  <span class="spinner spinner-xs spinner-inline"></span> {% trans 'Processing, please wait...' %}
 | 
			
		||||
</p>
 | 
			
		||||
<div class="pf-c-form__group pf-m-action" style="display: none;" id="loading">
 | 
			
		||||
    <div class="pf-c-form__horizontal-group">
 | 
			
		||||
        <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>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
<script>
 | 
			
		||||
  $('form').on('submit', function () {
 | 
			
		||||
    $('p.loading').show();
 | 
			
		||||
  })
 | 
			
		||||
document.querySelector("form").addEventListener("submit", (e) => {
 | 
			
		||||
    document.getElementById("loading").removeAttribute("style");
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ from django.urls import path
 | 
			
		||||
from passbook.admin.views import (
 | 
			
		||||
    applications,
 | 
			
		||||
    audit,
 | 
			
		||||
    certificate_key_pair,
 | 
			
		||||
    debug,
 | 
			
		||||
    factors,
 | 
			
		||||
    groups,
 | 
			
		||||
@ -148,6 +149,27 @@ urlpatterns = [
 | 
			
		||||
    path(
 | 
			
		||||
        "group/<uuid:pk>/delete/", groups.GroupDeleteView.as_view(), name="group-delete"
 | 
			
		||||
    ),
 | 
			
		||||
    # Certificate-Key Pairs
 | 
			
		||||
    path(
 | 
			
		||||
        "crypto/certificates/",
 | 
			
		||||
        certificate_key_pair.CertificateKeyPairListView.as_view(),
 | 
			
		||||
        name="certificate_key_pair",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "crypto/certificates/create/",
 | 
			
		||||
        certificate_key_pair.CertificateKeyPairCreateView.as_view(),
 | 
			
		||||
        name="certificatekeypair-create",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "crypto/certificates/<uuid:pk>/update/",
 | 
			
		||||
        certificate_key_pair.CertificateKeyPairUpdateView.as_view(),
 | 
			
		||||
        name="certificatekeypair-update",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "crypto/certificates/<uuid:pk>/delete/",
 | 
			
		||||
        certificate_key_pair.CertificateKeyPairDeleteView.as_view(),
 | 
			
		||||
        name="certificatekeypair-delete",
 | 
			
		||||
    ),
 | 
			
		||||
    # Audit Log
 | 
			
		||||
    path("audit/", audit.EventListView.as_view(), name="audit-log"),
 | 
			
		||||
    # Groups
 | 
			
		||||
 | 
			
		||||
@ -12,4 +12,4 @@ class EventListView(PermissionListMixin, ListView):
 | 
			
		||||
    template_name = "administration/audit/list.html"
 | 
			
		||||
    permission_required = "passbook_audit.view_event"
 | 
			
		||||
    ordering = "-created"
 | 
			
		||||
    paginate_by = 10
 | 
			
		||||
    paginate_by = 20
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										77
									
								
								passbook/admin/views/certificate_key_pair.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								passbook/admin/views/certificate_key_pair.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,77 @@
 | 
			
		||||
"""passbook CertificateKeyPair administration"""
 | 
			
		||||
from django.contrib import messages
 | 
			
		||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
			
		||||
from django.contrib.auth.mixins import (
 | 
			
		||||
    PermissionRequiredMixin as DjangoPermissionRequiredMixin,
 | 
			
		||||
)
 | 
			
		||||
from django.contrib.messages.views import SuccessMessageMixin
 | 
			
		||||
from django.urls import reverse_lazy
 | 
			
		||||
from django.utils.translation import ugettext as _
 | 
			
		||||
from django.views.generic import DeleteView, ListView, UpdateView
 | 
			
		||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
			
		||||
 | 
			
		||||
from passbook.crypto.forms import CertificateKeyPairForm
 | 
			
		||||
from passbook.crypto.models import CertificateKeyPair
 | 
			
		||||
from passbook.lib.views import CreateAssignPermView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CertificateKeyPairListView(LoginRequiredMixin, PermissionListMixin, ListView):
 | 
			
		||||
    """Show list of all keypairs"""
 | 
			
		||||
 | 
			
		||||
    model = CertificateKeyPair
 | 
			
		||||
    permission_required = "passbook_crypto.view_certificatekeypair"
 | 
			
		||||
    ordering = "name"
 | 
			
		||||
    paginate_by = 40
 | 
			
		||||
    template_name = "administration/certificatekeypair/list.html"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CertificateKeyPairCreateView(
 | 
			
		||||
    SuccessMessageMixin,
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    DjangoPermissionRequiredMixin,
 | 
			
		||||
    CreateAssignPermView,
 | 
			
		||||
):
 | 
			
		||||
    """Create new CertificateKeyPair"""
 | 
			
		||||
 | 
			
		||||
    model = CertificateKeyPair
 | 
			
		||||
    form_class = CertificateKeyPairForm
 | 
			
		||||
    permission_required = "passbook_crypto.add_certificatekeypair"
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/create.html"
 | 
			
		||||
    success_url = reverse_lazy("passbook_admin:certificate_key_pair")
 | 
			
		||||
    success_message = _("Successfully created CertificateKeyPair")
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        kwargs["type"] = "Certificate-Key Pair"
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CertificateKeyPairUpdateView(
 | 
			
		||||
    SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
 | 
			
		||||
):
 | 
			
		||||
    """Update certificatekeypair"""
 | 
			
		||||
 | 
			
		||||
    model = CertificateKeyPair
 | 
			
		||||
    form_class = CertificateKeyPairForm
 | 
			
		||||
    permission_required = "passbook_crypto.change_certificatekeypair"
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/update.html"
 | 
			
		||||
    success_url = reverse_lazy("passbook_admin:certificate_key_pair")
 | 
			
		||||
    success_message = _("Successfully updated Certificate-Key Pair")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CertificateKeyPairDeleteView(
 | 
			
		||||
    SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
 | 
			
		||||
):
 | 
			
		||||
    """Delete certificatekeypair"""
 | 
			
		||||
 | 
			
		||||
    model = CertificateKeyPair
 | 
			
		||||
    permission_required = "passbook_crypto.delete_certificatekeypair"
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/delete.html"
 | 
			
		||||
    success_url = reverse_lazy("passbook_admin:certificate_key_pair")
 | 
			
		||||
    success_message = _("Successfully deleted Certificate-Key Pair")
 | 
			
		||||
 | 
			
		||||
    def delete(self, request, *args, **kwargs):
 | 
			
		||||
        messages.success(self.request, self.success_message)
 | 
			
		||||
        return super().delete(request, *args, **kwargs)
 | 
			
		||||
@ -23,6 +23,8 @@ class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
 | 
			
		||||
    model = Invitation
 | 
			
		||||
    permission_required = "passbook_core.view_invitation"
 | 
			
		||||
    template_name = "administration/invitation/list.html"
 | 
			
		||||
    paginate_by = 10
 | 
			
		||||
    ordering = "-expires"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationCreateView(
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
 | 
			
		||||
 | 
			
		||||
    model = Policy
 | 
			
		||||
    permission_required = "passbook_core.view_policy"
 | 
			
		||||
    paginate_by = 10
 | 
			
		||||
    ordering = "order"
 | 
			
		||||
    template_name = "administration/policy/list.html"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,11 @@ from django.shortcuts import redirect
 | 
			
		||||
from django.urls import reverse, reverse_lazy
 | 
			
		||||
from django.utils.translation import ugettext as _
 | 
			
		||||
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
 | 
			
		||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
			
		||||
from guardian.mixins import (
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    PermissionRequiredMixin,
 | 
			
		||||
    get_anonymous_user,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from passbook.admin.forms.users import UserForm
 | 
			
		||||
from passbook.core.models import Nonce, User
 | 
			
		||||
@ -25,6 +29,9 @@ class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
 | 
			
		||||
    paginate_by = 40
 | 
			
		||||
    template_name = "administration/user/list.html"
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return super().get_queryset().exclude(pk=get_anonymous_user().pk)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserCreateView(
 | 
			
		||||
    SuccessMessageMixin,
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ from rest_framework import routers
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.api.permissions import CustomObjectPermissions
 | 
			
		||||
from passbook.audit.api.events import EventViewSet
 | 
			
		||||
from passbook.audit.api import EventViewSet
 | 
			
		||||
from passbook.core.api.applications import ApplicationViewSet
 | 
			
		||||
from passbook.core.api.factors import FactorViewSet
 | 
			
		||||
from passbook.core.api.groups import GroupViewSet
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ class EventSerializer(ModelSerializer):
 | 
			
		||||
            "date",
 | 
			
		||||
            "app",
 | 
			
		||||
            "context",
 | 
			
		||||
            "request_ip",
 | 
			
		||||
            "client_ip",
 | 
			
		||||
            "created",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
@ -33,11 +33,15 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
 | 
			
		||||
            source[key] = sanitize_dict(value)
 | 
			
		||||
        elif isinstance(value, models.Model):
 | 
			
		||||
            model_content_type = ContentType.objects.get_for_model(value)
 | 
			
		||||
            name = str(value)
 | 
			
		||||
            if hasattr(value, "name"):
 | 
			
		||||
                name = value.name
 | 
			
		||||
            source[key] = sanitize_dict(
 | 
			
		||||
                {
 | 
			
		||||
                    "app": model_content_type.app_label,
 | 
			
		||||
                    "name": model_content_type.model,
 | 
			
		||||
                    "model_name": model_content_type.model,
 | 
			
		||||
                    "pk": value.pk,
 | 
			
		||||
                    "name": name,
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        elif isinstance(value, UUID):
 | 
			
		||||
@ -133,6 +137,7 @@ class Event(UUIDModel):
 | 
			
		||||
            action=self.action,
 | 
			
		||||
            context=self.context,
 | 
			
		||||
            client_ip=self.client_ip,
 | 
			
		||||
            user=self.user,
 | 
			
		||||
        )
 | 
			
		||||
        return super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ class PropertyMappingSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = PropertyMapping
 | 
			
		||||
        fields = ["pk", "name", "__type__"]
 | 
			
		||||
        fields = ["pk", "name", "expression", "__type__"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PropertyMappingViewSet(ReadOnlyModelViewSet):
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,8 @@ class ApplicationForm(forms.ModelForm):
 | 
			
		||||
    """Application Form"""
 | 
			
		||||
 | 
			
		||||
    provider = forms.ModelChoiceField(
 | 
			
		||||
        queryset=Provider.objects.all().select_subclasses(), required=False
 | 
			
		||||
        queryset=Provider.objects.all().order_by("pk").select_subclasses(),
 | 
			
		||||
        required=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
@ -18,9 +18,11 @@ password_changed = Signal(providing_args=["user", "password"])
 | 
			
		||||
def invalidate_policy_cache(sender, instance, **_):
 | 
			
		||||
    """Invalidate Policy cache when policy is updated"""
 | 
			
		||||
    from passbook.core.models import Policy
 | 
			
		||||
    from passbook.policies.process import cache_key
 | 
			
		||||
 | 
			
		||||
    if isinstance(instance, Policy):
 | 
			
		||||
        LOGGER.debug("Invalidating policy cache", policy=instance)
 | 
			
		||||
        keys = cache.keys("%s#*" % instance.pk)
 | 
			
		||||
        prefix = cache_key(instance) + "*"
 | 
			
		||||
        keys = cache.keys(prefix)
 | 
			
		||||
        cache.delete_many(keys)
 | 
			
		||||
        LOGGER.debug("Deleted %d keys", len(keys))
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								passbook/core/templates/403_csrf.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								passbook/core/templates/403_csrf.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
{% extends 'login/base.html' %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load utils %}
 | 
			
		||||
 | 
			
		||||
{% block card_title %}
 | 
			
		||||
{{ title }} <span>(403)</span>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<form>
 | 
			
		||||
    <h3>{{ main }}</h3>
 | 
			
		||||
    {% if no_referer %}
 | 
			
		||||
    <p>{{ no_referer1 }}</p>
 | 
			
		||||
    <p>{{ no_referer2 }}</p>
 | 
			
		||||
    <p>{{ no_referer3 }}</p>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% if no_cookie %}
 | 
			
		||||
    <p>{{ no_cookie1 }}</p>
 | 
			
		||||
    <p>{{ no_cookie2 }}</p>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% if 'back' in request.GET %}
 | 
			
		||||
    <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										58
									
								
								passbook/core/templates/base/page.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								passbook/core/templates/base/page.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
			
		||||
{% extends "base/skeleton.html" %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% load is_active %}
 | 
			
		||||
{% load utils %}
 | 
			
		||||
 | 
			
		||||
{% block body %}
 | 
			
		||||
{% include 'partials/messages.html' %}
 | 
			
		||||
<div class="pf-c-page" id="page-default-nav-example">
 | 
			
		||||
    <a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">{% trans 'Skip to content' %}</a>
 | 
			
		||||
    <header role="banner" class="pf-c-page__header ws-page-header">
 | 
			
		||||
        <div class="pf-c-page__header-brand">
 | 
			
		||||
            <div class="pf-c-page__header-brand-toggle">
 | 
			
		||||
                <button class="pf-c-button pf-m-plain" type="button" id="page-default-nav-example-nav-toggle"
 | 
			
		||||
                    aria-label="Global navigation" aria-expanded="true"
 | 
			
		||||
                    aria-controls="page-default-nav-example-primary-nav">
 | 
			
		||||
                    <i class="fas fa-bars" aria-hidden="true"></i>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <a class="pf-c-page__header-brand-link">
 | 
			
		||||
                <img class="pf-c-brand" src="{% static 'passbook/logo.png' %}" alt="" />
 | 
			
		||||
                <img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" alt="passbook" />
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-page__header-nav">
 | 
			
		||||
            <nav class="pf-c-nav" aria-label="Nav">
 | 
			
		||||
                <ul class="pf-c-nav__horizontal-list ws-top-nav">
 | 
			
		||||
                    <li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_core:overview' %}"
 | 
			
		||||
                            href="{% url 'passbook_core:overview' %}">{% trans 'Access' %}</a></li>
 | 
			
		||||
                    {% if user.is_superuser %}
 | 
			
		||||
                    <li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_admin:overview' %}"
 | 
			
		||||
                            href="{% url 'passbook_admin:overview' %}">{% trans 'Administrate' %}</a></li>
 | 
			
		||||
                    <li class="pf-c-nav__item"><a class="pf-c-nav__link {% is_active_url 'passbook_admin:audit-log' %}"
 | 
			
		||||
                            href="{% url 'passbook_admin:audit-log' %}">{% trans 'Monitor' %}</a></li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </nav>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-page__header-tools">
 | 
			
		||||
            <div class="pf-c-page__header-tools-group pf-m-icons">
 | 
			
		||||
                <a href="{% url 'passbook_core:auth-logout' %}" class="pf-c-button pf-m-plain" type="button">
 | 
			
		||||
                    <i class="fas fa-sign-out-alt" aria-hidden="true"></i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-page__header-tools-group">
 | 
			
		||||
                <a href="{% url 'passbook_core:user-settings' %}" class="pf-c-button">
 | 
			
		||||
                    {{ user.username }}
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
 | 
			
		||||
        </div>
 | 
			
		||||
    </header>
 | 
			
		||||
    {% block page_content %}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
    <head>
 | 
			
		||||
        <meta charset="UTF-8">
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
 | 
			
		||||
        <title>{% block title %}{% title %}{% endblock %}</title>
 | 
			
		||||
        <title>{% block title %}{% trans title|default:"passbook" %}{% endblock %}</title>
 | 
			
		||||
        <link rel="icon" type="image/png" href="{% static 'passbook/logo.png' %}">
 | 
			
		||||
        <link rel="shortcut icon" type="image/png" href="{% static 'passbook/logo.png' %}">
 | 
			
		||||
        <link rel="stylesheet" type="text/css" href="{% static 'node_modules/@patternfly/patternfly/patternfly.css' %}">
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% config 'passbook.branding' as branding %}
 | 
			
		||||
<!-- HERO -->
 | 
			
		||||
<tr>
 | 
			
		||||
  <td bgcolor="#7c72dc" align="center" style="padding: 0px 10px 0px 10px;">
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
    <title>{% config passbook.branding %}</title>
 | 
			
		||||
    <title>passbook</title>
 | 
			
		||||
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
 | 
			
		||||
@ -118,7 +118,7 @@
 | 
			
		||||
                        <!-- ADDRESS -->
 | 
			
		||||
                        <tr>
 | 
			
		||||
                            <td bgcolor="#1b2a32" align="left" style="padding: 0px 30px 30px 30px; color: #E9ECEF; font-family: 'Metropolis', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: 400; line-height: 18px;">
 | 
			
		||||
                                <p style="margin: 0;"><a href="{% config 'passbook.branding' %}">{% config 'passbook.branding' %}</a></p>
 | 
			
		||||
                                <p style="margin: 0;"><a href="passbook">passbook</a></p>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </table>
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@
 | 
			
		||||
        <header class="pf-c-login__header">
 | 
			
		||||
            <img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;"
 | 
			
		||||
                alt="passbook icon" />
 | 
			
		||||
            <img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 80px;"
 | 
			
		||||
            <img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;"
 | 
			
		||||
                alt="passbook branding" />
 | 
			
		||||
        </header>
 | 
			
		||||
        <main class="pf-c-login__main">
 | 
			
		||||
@ -54,9 +54,10 @@
 | 
			
		||||
                        <a href="{{ source.url }}" class="pf-c-login__main-footer-links-item-link">
 | 
			
		||||
                            {% if source.icon_path %}
 | 
			
		||||
                            <img src="{% static source.icon_path %}" alt="{{ source.name }}">
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                            {% if source.icon_url %}
 | 
			
		||||
                            {% elif source.icon_url %}
 | 
			
		||||
                            <img src="icon_url" alt="{{ source.name }}">
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            <i class="pf-icon pf-icon-arrow" title="{{ source.name }}"></i>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
 | 
			
		||||
@ -4,27 +4,18 @@
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load utils %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
<style>
 | 
			
		||||
    .pf-icon {
 | 
			
		||||
        font-size: 48px;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
    }
 | 
			
		||||
</style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<header class="login-pf-header">
 | 
			
		||||
  <h1>{% trans title %}</h1>
 | 
			
		||||
</header>
 | 
			
		||||
<form method="POST">
 | 
			
		||||
  {% csrf_token %}
 | 
			
		||||
  {% include 'partials/form.html' %}
 | 
			
		||||
  <span class="pf-icon pficon-error-circle-o btn-block"></span>
 | 
			
		||||
  Access denied
 | 
			
		||||
  {% if 'back' in request.GET %}
 | 
			
		||||
  <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
 | 
			
		||||
  {% endif %}
 | 
			
		||||
<form method="POST" class="pf-c-form">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% include 'partials/form.html' %}
 | 
			
		||||
    <div class="pf-c-form__group">
 | 
			
		||||
        <p>
 | 
			
		||||
            <i class="pf-icon pf-icon-error-circle-o"></i>
 | 
			
		||||
            {% trans 'Access denied' %}
 | 
			
		||||
        </p>
 | 
			
		||||
    </div>
 | 
			
		||||
    {% if 'back' in request.GET %}
 | 
			
		||||
    <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@
 | 
			
		||||
{% load utils %}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
{% title title %}
 | 
			
		||||
{% trans title %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block head %}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
{% extends "base/skeleton.html" %}
 | 
			
		||||
{% extends "base/page.html" %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
@ -6,120 +6,9 @@
 | 
			
		||||
{% load is_active %}
 | 
			
		||||
{% load utils %}
 | 
			
		||||
 | 
			
		||||
{% block body %}
 | 
			
		||||
{% include 'partials/messages.html' %}
 | 
			
		||||
<div class="pf-c-page" id="page-default-nav-example">
 | 
			
		||||
    <a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content">{% trans 'Skip to content' %}</a>
 | 
			
		||||
    <header role="banner" class="pf-c-page__header">
 | 
			
		||||
        <div class="pf-c-page__header-brand">
 | 
			
		||||
            <div class="pf-c-page__header-brand-toggle">
 | 
			
		||||
                <button class="pf-c-button pf-m-plain" type="button" id="page-default-nav-example-nav-toggle"
 | 
			
		||||
                    aria-label="Global navigation" aria-expanded="true"
 | 
			
		||||
                    aria-controls="page-default-nav-example-primary-nav">
 | 
			
		||||
                    <i class="fas fa-bars" aria-hidden="true"></i>
 | 
			
		||||
                </button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <a class="pf-c-page__header-brand-link">
 | 
			
		||||
                <img class="pf-c-brand" src="{% static 'passbook/logo.png' %}" alt="" />
 | 
			
		||||
                <img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" alt="passbook" />
 | 
			
		||||
            </a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-page__header-tools">
 | 
			
		||||
            <div class="pf-c-page__header-tools-group pf-m-icons">
 | 
			
		||||
                <a href="{% url 'passbook_core:auth-logout' %}" class="pf-c-button pf-m-plain" type="button">
 | 
			
		||||
                    <i class="fas fa-sign-out-alt" aria-hidden="true"></i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-page__header-tools-group">
 | 
			
		||||
                <a href="{% url 'passbook_core:user-settings' %}" class="pf-c-button">
 | 
			
		||||
                    {{ user.username }}
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <img class="pf-c-avatar" src="{% gravatar user.email %}" alt="">
 | 
			
		||||
        </div>
 | 
			
		||||
    </header>
 | 
			
		||||
    <div class="pf-c-page__sidebar pf-m-dark">
 | 
			
		||||
        <div class="pf-c-page__sidebar-body">
 | 
			
		||||
            <nav class="pf-c-nav pf-m-dark" id="page-default-nav-example-primary-nav" aria-label="Global">
 | 
			
		||||
                <ul class="pf-c-nav__list">
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_core:overview' %}" class="pf-c-nav__link  {% is_active_url 'passbook_core:overview' %}">
 | 
			
		||||
                            {% trans 'Overview' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% if user.is_superuser %}
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:overview' %}" class="pf-c-nav__link {% is_active_url 'passbook_admin:overview' %}">
 | 
			
		||||
                            {% trans 'System Status' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:applications' %}" class="pf-c-nav__link {% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}">
 | 
			
		||||
                            {% trans 'Applications' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:sources' %}" class="pf-c-nav__link {% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-delete' %}">
 | 
			
		||||
                            {% trans 'Sources' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:providers' %}" class="pf-c-nav__link {% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
 | 
			
		||||
                            {% trans 'Providers' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:property-mappings' %}" class="pf-c-nav__link {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
 | 
			
		||||
                            {% trans 'Property Mappings' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:factors' %}" class="pf-c-nav__link {% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
 | 
			
		||||
                            {% trans 'Factors' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:policies' %}" class="pf-c-nav__link {% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}">
 | 
			
		||||
                            {% trans 'Policies' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:invitations' %}" class="pf-c-nav__link {% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
 | 
			
		||||
                            {% trans 'Invitations' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:users' %}" class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
 | 
			
		||||
                            {% trans 'Users' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:groups' %}" class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
 | 
			
		||||
                            {% trans 'Groups' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_admin:audit-log' %}" class="pf-c-nav__link {% is_active 'passbook_admin:audit-log' %}">
 | 
			
		||||
                            {% trans 'Audit Log' %}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </nav>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
 | 
			
		||||
        {% block content %}
 | 
			
		||||
            <section class="pf-c-page__main-section pf-m-light">
 | 
			
		||||
                <div class="pf-c-content">
 | 
			
		||||
                    <h1>Main title</h1>
 | 
			
		||||
                    <p>This is a demo of the Page component.</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </section>
 | 
			
		||||
            <section class="pf-c-page__main-section">
 | 
			
		||||
 | 
			
		||||
            </section>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
    </main>
 | 
			
		||||
</div>
 | 
			
		||||
{% block page_content %}
 | 
			
		||||
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
 | 
			
		||||
    {% block content %}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
</main>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,67 +1,69 @@
 | 
			
		||||
{% extends "overview/base.html" %}
 | 
			
		||||
{% extends "base/page.html" %}
 | 
			
		||||
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load is_active %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load passbook_user_settings %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<section class="pf-c-page__main-section">
 | 
			
		||||
    <div class="pf-l-split pf-m-gutter">
 | 
			
		||||
        <div class="pf-l-split__item">
 | 
			
		||||
            <div class="pf-c-card">
 | 
			
		||||
                <div class="pf-c-card__body">
 | 
			
		||||
                    <nav class="pf-c-nav" aria-label="Global">
 | 
			
		||||
                        <section class="pf-c-nav__section">
 | 
			
		||||
                            <h2 class="pf-c-nav__section-title">{% trans 'General Settings' %}</h2>
 | 
			
		||||
                            <ul class="pf-c-nav__list">
 | 
			
		||||
                                <li class="pf-c-nav__item">
 | 
			
		||||
                                    <a href="{% url 'passbook_core:user-settings' %}" class="pf-c-nav__link {% is_active 'passbook_core:user-settings' %}">{% trans 'User Details' %}</a>
 | 
			
		||||
                                </li>
 | 
			
		||||
                            </ul>
 | 
			
		||||
                        </section>
 | 
			
		||||
                        {% user_factors as user_factors_loc %}
 | 
			
		||||
                        {% if user_factors_loc %}
 | 
			
		||||
                        <section class="pf-c-nav__section">
 | 
			
		||||
                            <h2 class="pf-c-nav__section-title">{% trans 'Factors' %}</h2>
 | 
			
		||||
                            <ul class="pf-c-nav__list">
 | 
			
		||||
                                {% for factor in user_factors_loc %}
 | 
			
		||||
                                <li class="pf-c-nav__item">
 | 
			
		||||
                                    <a href="{% url factor.view_name %}" class="pf-c-nav__link {% is_active factor.view_name %}">
 | 
			
		||||
                                        <i class="{{ factor.icon }}"></i>
 | 
			
		||||
                                    {{ factor.name }}
 | 
			
		||||
                                    </a>
 | 
			
		||||
                                </li>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </ul>
 | 
			
		||||
                        </section>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        {% user_sources as user_sources_loc %}
 | 
			
		||||
                        {% if user_sources_loc %}
 | 
			
		||||
                        <section class="pf-c-nav__section">
 | 
			
		||||
                            <h2 class="pf-c-nav__section-title">{% trans 'Sources' %}</h2>
 | 
			
		||||
                            <ul class="pf-c-nav__list">
 | 
			
		||||
                                {% for source in user_sources_loc %}
 | 
			
		||||
                                <li class="pf-c-nav__item">
 | 
			
		||||
                                    <a href="{{ source.view_name }}" class="pf-c-nav__link {% if user_settings.view_name == request.get_full_path %} pf-m-current {% endif %}">
 | 
			
		||||
                                        <i class="{{ source.icon }}"></i>
 | 
			
		||||
                                    {{ source.name }}
 | 
			
		||||
                                    </a>
 | 
			
		||||
                                </li>
 | 
			
		||||
                                {% endfor %}
 | 
			
		||||
                            </ul>
 | 
			
		||||
                        </section>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </nav>
 | 
			
		||||
{% block page_content %}
 | 
			
		||||
<div class="pf-c-page__sidebar">
 | 
			
		||||
    <div class="pf-c-page__sidebar-body">
 | 
			
		||||
        <nav class="pf-c-nav" id="page-default-nav-example-primary-nav" aria-label="Global">
 | 
			
		||||
            <section class="pf-c-nav__section">
 | 
			
		||||
                <h2 class="pf-c-nav__section-title">{% trans 'General Settings' %}</h2>
 | 
			
		||||
                <ul class="pf-c-nav__list">
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url 'passbook_core:user-settings' %}"
 | 
			
		||||
                            class="pf-c-nav__link {% is_active 'passbook_core:user-settings' %}">{% trans 'User Details' %}</a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
            </section>
 | 
			
		||||
            {% user_factors as user_factors_loc %}
 | 
			
		||||
            {% if user_factors_loc %}
 | 
			
		||||
            <section class="pf-c-nav__section">
 | 
			
		||||
                <h2 class="pf-c-nav__section-title">{% trans 'Factors' %}</h2>
 | 
			
		||||
                <ul class="pf-c-nav__list">
 | 
			
		||||
                    {% for factor in user_factors_loc %}
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{% url factor.view_name %}" class="pf-c-nav__link {% is_active factor.view_name %}">
 | 
			
		||||
                            <i class="{{ factor.icon }}"></i>
 | 
			
		||||
                            {{ factor.name }}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </section>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            {% user_sources as user_sources_loc %}
 | 
			
		||||
            {% if user_sources_loc %}
 | 
			
		||||
            <section class="pf-c-nav__section">
 | 
			
		||||
                <h2 class="pf-c-nav__section-title">{% trans 'Sources' %}</h2>
 | 
			
		||||
                <ul class="pf-c-nav__list">
 | 
			
		||||
                    {% for source in user_sources_loc %}
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{{ source.view_name }}"
 | 
			
		||||
                            class="pf-c-nav__link {% if user_settings.view_name == request.get_full_path %} pf-m-current {% endif %}">
 | 
			
		||||
                            <i class="{{ source.icon }}"></i>
 | 
			
		||||
                            {{ source.name }}
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </section>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        </nav>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
<main role="main" class="pf-c-page__main" tabindex="-1" id="main-content">
 | 
			
		||||
    <section class="pf-c-page__main-section">
 | 
			
		||||
        <div class="pf-l-split pf-m-gutter">
 | 
			
		||||
            <div class="pf-l-split__item">
 | 
			
		||||
                <div class="pf-c-card">
 | 
			
		||||
                    {% block page %}
 | 
			
		||||
                    {% endblock %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-l-split__item">
 | 
			
		||||
            <div class="pf-c-card">
 | 
			
		||||
                {% block page %}
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
    </section>
 | 
			
		||||
</main>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -44,13 +44,13 @@ class LoginView(UserPassesTestMixin, FormView):
 | 
			
		||||
        kwargs["primary_action"] = _("Log in")
 | 
			
		||||
        kwargs["show_sign_up_notice"] = CONFIG.y("passbook.sign_up.enabled")
 | 
			
		||||
        kwargs["sources"] = []
 | 
			
		||||
        sources = Source.objects.filter(enabled=True).select_subclasses()
 | 
			
		||||
        sources = (
 | 
			
		||||
            Source.objects.filter(enabled=True).order_by("name").select_subclasses()
 | 
			
		||||
        )
 | 
			
		||||
        for source in sources:
 | 
			
		||||
            ui_login_button = source.ui_login_button
 | 
			
		||||
            if ui_login_button:
 | 
			
		||||
                kwargs["sources"].append(ui_login_button)
 | 
			
		||||
        # if kwargs["sources"]:
 | 
			
		||||
        #     self.template_name = "login/with_sources.html"
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_user(self, uid_value) -> Optional[User]:
 | 
			
		||||
@ -231,7 +231,6 @@ class PasswordResetView(View):
 | 
			
		||||
        login(request, nonce.user)
 | 
			
		||||
        nonce.delete()
 | 
			
		||||
        messages.success(
 | 
			
		||||
            request,
 | 
			
		||||
            _(("Temporarily authenticated with Nonce, " "please change your password")),
 | 
			
		||||
            request, _(("Temporarily authenticated, please change your password")),
 | 
			
		||||
        )
 | 
			
		||||
        return redirect("passbook_core:user-change-password")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								passbook/crypto/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								passbook/crypto/admin.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
"""passbook crypto model admin"""
 | 
			
		||||
 | 
			
		||||
from passbook.lib.admin import admin_autoregister
 | 
			
		||||
 | 
			
		||||
admin_autoregister("passbook_crypto")
 | 
			
		||||
							
								
								
									
										10
									
								
								passbook/crypto/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								passbook/crypto/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
"""passbook crypto app config"""
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PassbookCryptoConfig(AppConfig):
 | 
			
		||||
    """passbook crypto app config"""
 | 
			
		||||
 | 
			
		||||
    name = "passbook.crypto"
 | 
			
		||||
    label = "passbook_crypto"
 | 
			
		||||
    verbose_name = "passbook Crypto"
 | 
			
		||||
@ -36,8 +36,7 @@ class CertificateBuilder:
 | 
			
		||||
                x509.Name(
 | 
			
		||||
                    [
 | 
			
		||||
                        x509.NameAttribute(
 | 
			
		||||
                            NameOID.COMMON_NAME,
 | 
			
		||||
                            u"passbook Self-signed SAML Certificate",
 | 
			
		||||
                            NameOID.COMMON_NAME, u"passbook Self-signed Certificate",
 | 
			
		||||
                        ),
 | 
			
		||||
                        x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"passbook"),
 | 
			
		||||
                        x509.NameAttribute(
 | 
			
		||||
@ -50,8 +49,7 @@ class CertificateBuilder:
 | 
			
		||||
                x509.Name(
 | 
			
		||||
                    [
 | 
			
		||||
                        x509.NameAttribute(
 | 
			
		||||
                            NameOID.COMMON_NAME,
 | 
			
		||||
                            u"passbook Self-signed SAML Certificate",
 | 
			
		||||
                            NameOID.COMMON_NAME, u"passbook Self-signed Certificate",
 | 
			
		||||
                        ),
 | 
			
		||||
                    ]
 | 
			
		||||
                )
 | 
			
		||||
							
								
								
									
										27
									
								
								passbook/crypto/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								passbook/crypto/forms.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
"""passbook Crypto forms"""
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from passbook.crypto.models import CertificateKeyPair
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CertificateKeyPairForm(forms.ModelForm):
 | 
			
		||||
    """CertificateKeyPair Form"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = CertificateKeyPair
 | 
			
		||||
        fields = [
 | 
			
		||||
            "name",
 | 
			
		||||
            "certificate_data",
 | 
			
		||||
            "key_data",
 | 
			
		||||
        ]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput(),
 | 
			
		||||
            "certificate_data": forms.Textarea(attrs={"class": "monospaced"}),
 | 
			
		||||
            "key_data": forms.Textarea(attrs={"class": "monospaced"}),
 | 
			
		||||
        }
 | 
			
		||||
        labels = {
 | 
			
		||||
            "certificate_data": _("Certificate"),
 | 
			
		||||
            "key_data": _("Private Key"),
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										67
									
								
								passbook/crypto/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								passbook/crypto/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
# Generated by Django 3.0.3 on 2020-03-03 21:45
 | 
			
		||||
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_self_signed(apps, schema_editor):
 | 
			
		||||
    CertificateKeyPair = apps.get_model("passbook_crypto", "CertificateKeyPair")
 | 
			
		||||
    db_alias = schema_editor.connection.alias
 | 
			
		||||
    from passbook.crypto.builder import CertificateBuilder
 | 
			
		||||
 | 
			
		||||
    builder = CertificateBuilder()
 | 
			
		||||
    builder.build()
 | 
			
		||||
    CertificateKeyPair.objects.using(db_alias).create(
 | 
			
		||||
        name="passbook Self-signed Certificate",
 | 
			
		||||
        certificate_data=builder.certificate,
 | 
			
		||||
        key_data=builder.private_key,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    initial = True
 | 
			
		||||
 | 
			
		||||
    dependencies = []
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="CertificateKeyPair",
 | 
			
		||||
            fields=[
 | 
			
		||||
                ("created", models.DateTimeField(auto_now_add=True)),
 | 
			
		||||
                ("last_updated", models.DateTimeField(auto_now=True)),
 | 
			
		||||
                (
 | 
			
		||||
                    "uuid",
 | 
			
		||||
                    models.UUIDField(
 | 
			
		||||
                        default=uuid.uuid4,
 | 
			
		||||
                        editable=False,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("name", models.TextField()),
 | 
			
		||||
                ("certificate_data", models.TextField()),
 | 
			
		||||
                ("key_data", models.TextField(blank=True, default="")),
 | 
			
		||||
            ],
 | 
			
		||||
            options={
 | 
			
		||||
                "verbose_name": "Certificate-Key Pair",
 | 
			
		||||
                "verbose_name_plural": "Certificate-Key Pairs",
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RunPython(create_self_signed),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="certificatekeypair",
 | 
			
		||||
            name="certificate_data",
 | 
			
		||||
            field=models.TextField(help_text="PEM-encoded Certificate data"),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="certificatekeypair",
 | 
			
		||||
            name="key_data",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                default="",
 | 
			
		||||
                help_text="Optional Private Key. If this is set, you can use this keypair for encryption.",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
							
								
								
									
										0
									
								
								passbook/crypto/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/crypto/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										64
									
								
								passbook/crypto/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								passbook/crypto/models.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
			
		||||
"""passbook crypto models"""
 | 
			
		||||
from binascii import hexlify
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from cryptography.hazmat.backends import default_backend
 | 
			
		||||
from cryptography.hazmat.primitives import hashes
 | 
			
		||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
 | 
			
		||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
 | 
			
		||||
from cryptography.x509 import Certificate, load_pem_x509_certificate
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CertificateKeyPair(UUIDModel, CreatedUpdatedModel):
 | 
			
		||||
    """CertificateKeyPair that can be used for signing or encrypting if `key_data`
 | 
			
		||||
    is set, otherwise it can be used to verify remote data."""
 | 
			
		||||
 | 
			
		||||
    name = models.TextField()
 | 
			
		||||
    certificate_data = models.TextField(help_text=_("PEM-encoded Certificate data"))
 | 
			
		||||
    key_data = models.TextField(
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Optional Private Key. If this is set, you can use this keypair for encryption."
 | 
			
		||||
        ),
 | 
			
		||||
        blank=True,
 | 
			
		||||
        default="",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    _cert: Optional[Certificate] = None
 | 
			
		||||
    _key: Optional[RSAPrivateKey] = None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def certificate(self) -> Certificate:
 | 
			
		||||
        """Get python cryptography Certificate instance"""
 | 
			
		||||
        if not self._cert:
 | 
			
		||||
            self._cert = load_pem_x509_certificate(
 | 
			
		||||
                self.certificate_data.encode("utf-8"), default_backend()
 | 
			
		||||
            )
 | 
			
		||||
        return self._cert
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def private_key(self) -> Optional[RSAPrivateKey]:
 | 
			
		||||
        """Get python cryptography PrivateKey instance"""
 | 
			
		||||
        if not self._key:
 | 
			
		||||
            self._key = load_pem_private_key(
 | 
			
		||||
                str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])),
 | 
			
		||||
                password=None,
 | 
			
		||||
                backend=default_backend(),
 | 
			
		||||
            )
 | 
			
		||||
        return self._key
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def fingerprint(self) -> str:
 | 
			
		||||
        """Get SHA256 Fingerprint of certificate_data"""
 | 
			
		||||
        return hexlify(self.certificate.fingerprint(hashes.SHA256())).decode("utf-8")
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return f"Certificate-Key Pair {self.name} {self.fingerprint}"
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _("Certificate-Key Pair")
 | 
			
		||||
        verbose_name_plural = _("Certificate-Key Pairs")
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
from captcha.fields import ReCaptchaField
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from passbook.factors.captcha.models import CaptchaFactor
 | 
			
		||||
from passbook.factors.forms import GENERAL_FIELDS
 | 
			
		||||
@ -28,3 +28,8 @@ class CaptchaFactorForm(forms.ModelForm):
 | 
			
		||||
            "public_key": forms.TextInput(),
 | 
			
		||||
            "private_key": forms.TextInput(),
 | 
			
		||||
        }
 | 
			
		||||
        help_texts = {
 | 
			
		||||
            "policies": _(
 | 
			
		||||
                "Policies which determine if this factor applies to the current user."
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
"""passbook administration forms"""
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from passbook.factors.email.models import EmailFactor
 | 
			
		||||
from passbook.factors.forms import GENERAL_FIELDS
 | 
			
		||||
@ -41,3 +41,8 @@ class EmailFactorForm(forms.ModelForm):
 | 
			
		||||
            "ssl_keyfile": _("SSL Keyfile (optional)"),
 | 
			
		||||
            "ssl_certfile": _("SSL Certfile (optional)"),
 | 
			
		||||
        }
 | 
			
		||||
        help_texts = {
 | 
			
		||||
            "policies": _(
 | 
			
		||||
                "Policies which determine if this factor applies to the current user."
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ from django import forms
 | 
			
		||||
from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
			
		||||
from django.core.validators import RegexValidator
 | 
			
		||||
from django.utils.safestring import mark_safe
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from django_otp.models import Device
 | 
			
		||||
 | 
			
		||||
from passbook.factors.forms import GENERAL_FIELDS
 | 
			
		||||
@ -80,3 +80,8 @@ class OTPFactorForm(forms.ModelForm):
 | 
			
		||||
            "order": forms.NumberInput(),
 | 
			
		||||
            "policies": FilteredSelectMultiple(_("policies"), False),
 | 
			
		||||
        }
 | 
			
		||||
        help_texts = {
 | 
			
		||||
            "policies": _(
 | 
			
		||||
                "Policies which determine if this factor applies to the current user."
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from passbook.factors.forms import GENERAL_FIELDS
 | 
			
		||||
from passbook.factors.password.models import PasswordFactor
 | 
			
		||||
@ -49,3 +49,8 @@ class PasswordFactorForm(forms.ModelForm):
 | 
			
		||||
            "password_policies": FilteredSelectMultiple(_("password policies"), False),
 | 
			
		||||
            "reset_factors": FilteredSelectMultiple(_("reset factors"), False),
 | 
			
		||||
        }
 | 
			
		||||
        help_texts = {
 | 
			
		||||
            "policies": _(
 | 
			
		||||
                "Policies which determine if this factor applies to the current user."
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -25,18 +25,6 @@ passbook:
 | 
			
		||||
  password_reset:
 | 
			
		||||
    # Enable password reset, passwords are reset in internal Database and in LDAP if ldap.reset_password is true
 | 
			
		||||
    enabled: true
 | 
			
		||||
    # Verification the user has to provide in order to be able to reset passwords. Can be any combination of `email`, `2fa`, `security_questions`
 | 
			
		||||
    verification:
 | 
			
		||||
      - email
 | 
			
		||||
  # Text used in title, on login page and multiple other places
 | 
			
		||||
  branding: passbook
 | 
			
		||||
  login:
 | 
			
		||||
    # Override URL used for logo
 | 
			
		||||
    logo_url: null
 | 
			
		||||
    # Override URL used for Background on Login page
 | 
			
		||||
    bg_url: null
 | 
			
		||||
    # Optionally add a subtext, placed below logo on the login page
 | 
			
		||||
    subtext: null
 | 
			
		||||
  footer:
 | 
			
		||||
    links:
 | 
			
		||||
      # Optionally add links to the footer on the login page
 | 
			
		||||
@ -46,14 +34,3 @@ passbook:
 | 
			
		||||
  uid_fields:
 | 
			
		||||
    - username
 | 
			
		||||
    - email
 | 
			
		||||
# Provider-specific settings
 | 
			
		||||
ldap:
 | 
			
		||||
  # Which field from `uid_fields` maps to which LDAP Attribute
 | 
			
		||||
  login_field_map:
 | 
			
		||||
    username: sAMAccountName
 | 
			
		||||
    email: mail # or userPrincipalName
 | 
			
		||||
  user_attribute_map:
 | 
			
		||||
    active_directory:
 | 
			
		||||
      username: "%(sAMAccountName)s"
 | 
			
		||||
      email: "%(mail)s"
 | 
			
		||||
      name: "%(displayName)"
 | 
			
		||||
 | 
			
		||||
@ -1,48 +0,0 @@
 | 
			
		||||
"""passbook lib fields"""
 | 
			
		||||
from itertools import chain
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.contrib.postgres.utils import prefix_validation_error
 | 
			
		||||
 | 
			
		||||
from passbook.lib.widgets import DynamicArrayWidget
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DynamicArrayField(forms.Field):
 | 
			
		||||
    """Show array field as a dynamic amount of textboxes"""
 | 
			
		||||
 | 
			
		||||
    default_error_messages = {
 | 
			
		||||
        "item_invalid": "Item %(nth)s in the array did not validate: "
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def __init__(self, base_field, **kwargs):
 | 
			
		||||
        self.base_field = base_field
 | 
			
		||||
        self.max_length = kwargs.pop("max_length", None)
 | 
			
		||||
        kwargs.setdefault("widget", DynamicArrayWidget)
 | 
			
		||||
        super().__init__(**kwargs)
 | 
			
		||||
 | 
			
		||||
    def clean(self, value):
 | 
			
		||||
        cleaned_data = []
 | 
			
		||||
        errors = []
 | 
			
		||||
        value = [x for x in value if x]
 | 
			
		||||
        for index, item in enumerate(value):
 | 
			
		||||
            try:
 | 
			
		||||
                cleaned_data.append(self.base_field.clean(item))
 | 
			
		||||
            except forms.ValidationError as error:
 | 
			
		||||
                errors.append(
 | 
			
		||||
                    prefix_validation_error(
 | 
			
		||||
                        error,
 | 
			
		||||
                        self.error_messages["item_invalid"],
 | 
			
		||||
                        code="item_invalid",
 | 
			
		||||
                        params={"nth": index},
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
        if errors:
 | 
			
		||||
            raise forms.ValidationError(list(chain.from_iterable(errors)))
 | 
			
		||||
        if not cleaned_data and self.required:
 | 
			
		||||
            raise forms.ValidationError(self.error_messages["required"])
 | 
			
		||||
        return cleaned_data
 | 
			
		||||
 | 
			
		||||
    def has_changed(self, initial, data):
 | 
			
		||||
        if not data and not initial:
 | 
			
		||||
            return False
 | 
			
		||||
        return super().has_changed(initial, data)
 | 
			
		||||
							
								
								
									
										9
									
								
								passbook/lib/logging.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								passbook/lib/logging.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
"""logging helpers"""
 | 
			
		||||
from os import getpid
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
def add_process_id(logger, method_name, event_dict):
 | 
			
		||||
    """Add the current process ID"""
 | 
			
		||||
    event_dict["pid"] = getpid()
 | 
			
		||||
    return event_dict
 | 
			
		||||
							
								
								
									
										0
									
								
								passbook/lib/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/lib/management/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								passbook/lib/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/lib/management/commands/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										65
									
								
								passbook/lib/management/commands/bootstrap.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								passbook/lib/management/commands/bootstrap.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
			
		||||
"""passbook management command to bootstrap"""
 | 
			
		||||
from argparse import REMAINDER
 | 
			
		||||
from subprocess import Popen  # nosec
 | 
			
		||||
from sys import stderr, stdin, stdout
 | 
			
		||||
from sys import exit as _exit
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from django.core.management.base import BaseCommand
 | 
			
		||||
from django.db import connection
 | 
			
		||||
from django.db.utils import OperationalError
 | 
			
		||||
from django_redis import get_redis_connection
 | 
			
		||||
from redis.exceptions import ConnectionError as RedisConnectionError
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Command(BaseCommand):
 | 
			
		||||
    """Bootstrap passbook, ensure Database and Cache are
 | 
			
		||||
    reachable, and directories are writeable"""
 | 
			
		||||
 | 
			
		||||
    help = """Bootstrap passbook, ensure Database and Cache are
 | 
			
		||||
    reachable, and directories are writeable"""
 | 
			
		||||
 | 
			
		||||
    def add_arguments(self, parser):
 | 
			
		||||
        parser.add_argument("command", nargs=REMAINDER)
 | 
			
		||||
 | 
			
		||||
    def check_database(self) -> bool:
 | 
			
		||||
        """Return true if database is reachable, false otherwise"""
 | 
			
		||||
        try:
 | 
			
		||||
            connection.cursor()
 | 
			
		||||
            LOGGER.info("Database reachable")
 | 
			
		||||
            return True
 | 
			
		||||
        except OperationalError:
 | 
			
		||||
            LOGGER.info("Database unreachable")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def check_cache(self) -> bool:
 | 
			
		||||
        """Return true if cache is reachable, false otherwise"""
 | 
			
		||||
        try:
 | 
			
		||||
            con = get_redis_connection("default")
 | 
			
		||||
            con.ping()
 | 
			
		||||
            LOGGER.info("Cache reachable")
 | 
			
		||||
            return True
 | 
			
		||||
        except RedisConnectionError:
 | 
			
		||||
            LOGGER.info("Cache unreachable")
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def handle(self, *args, **options):
 | 
			
		||||
        LOGGER.info("passbook bootstrapping...")
 | 
			
		||||
        should_check = True
 | 
			
		||||
        while should_check:
 | 
			
		||||
            should_check = not (self.check_database() and self.check_cache())
 | 
			
		||||
            sleep(1)
 | 
			
		||||
        LOGGER.info("Dependencies are up, starting command...")
 | 
			
		||||
        commands: List[str] = options.get("command", ["exit", "1"])
 | 
			
		||||
        proc = Popen(args=commands, stdout=stdout, stderr=stderr, stdin=stdin)  # nosec
 | 
			
		||||
        try:
 | 
			
		||||
            proc.wait()
 | 
			
		||||
            _exit(proc.returncode)
 | 
			
		||||
        except KeyboardInterrupt:
 | 
			
		||||
            LOGGER.info("Killing process")
 | 
			
		||||
            proc.kill()
 | 
			
		||||
            _exit(254)
 | 
			
		||||
@ -1,29 +1,28 @@
 | 
			
		||||
"""passbook sentry integration"""
 | 
			
		||||
from billiard.exceptions import WorkerLostError
 | 
			
		||||
from botocore.client import ClientError
 | 
			
		||||
from django.core.exceptions import DisallowedHost, ValidationError
 | 
			
		||||
from django.db import InternalError, OperationalError, ProgrammingError
 | 
			
		||||
from django_redis.exceptions import ConnectionInterrupted
 | 
			
		||||
from redis.exceptions import RedisError
 | 
			
		||||
from rest_framework.exceptions import APIException
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SentryIgnoredException(Exception):
 | 
			
		||||
    """Base Class for all errors that are supressed, and not sent to sentry."""
 | 
			
		||||
    """Base Class for all errors that are suppressed, and not sent to sentry."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def before_send(event, hint):
 | 
			
		||||
    """Check if error is database error, and ignore if so"""
 | 
			
		||||
    from django_redis.exceptions import ConnectionInterrupted
 | 
			
		||||
    from django.db import OperationalError, InternalError
 | 
			
		||||
    from django.core.exceptions import ValidationError
 | 
			
		||||
    from rest_framework.exceptions import APIException
 | 
			
		||||
    from billiard.exceptions import WorkerLostError
 | 
			
		||||
    from django.core.exceptions import DisallowedHost
 | 
			
		||||
    from botocore.client import ClientError
 | 
			
		||||
    from redis.exceptions import RedisError
 | 
			
		||||
 | 
			
		||||
    ignored_classes = (
 | 
			
		||||
        OperationalError,
 | 
			
		||||
        InternalError,
 | 
			
		||||
        ProgrammingError,
 | 
			
		||||
        ConnectionInterrupted,
 | 
			
		||||
        APIException,
 | 
			
		||||
        InternalError,
 | 
			
		||||
        ConnectionResetError,
 | 
			
		||||
        WorkerLostError,
 | 
			
		||||
        DisallowedHost,
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,9 @@ from hashlib import md5
 | 
			
		||||
from urllib.parse import urlencode
 | 
			
		||||
 | 
			
		||||
from django import template
 | 
			
		||||
from django.apps import apps
 | 
			
		||||
from django.db.models import Model
 | 
			
		||||
from django.template import Context
 | 
			
		||||
from django.utils.html import escape
 | 
			
		||||
from django.utils.translation import ugettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.lib.config import CONFIG
 | 
			
		||||
from passbook.lib.utils.urls import is_url_absolute
 | 
			
		||||
@ -40,38 +38,6 @@ def fieldtype(field):
 | 
			
		||||
    return field.__class__.__name__
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(takes_context=True)
 | 
			
		||||
def title(context: Context, *title) -> str:
 | 
			
		||||
    """Return either just branding or title - branding"""
 | 
			
		||||
    branding = CONFIG.y("passbook.branding", "passbook")
 | 
			
		||||
    if not title:
 | 
			
		||||
        return branding
 | 
			
		||||
    if "request" not in context:
 | 
			
		||||
        return ""
 | 
			
		||||
    resolver_match = context.request.resolver_match
 | 
			
		||||
    if not resolver_match:
 | 
			
		||||
        return ""
 | 
			
		||||
    # Include App Title in title
 | 
			
		||||
    app = ""
 | 
			
		||||
    if resolver_match.namespace != "":
 | 
			
		||||
        dj_app = None
 | 
			
		||||
        namespace = context.request.resolver_match.namespace.split(":")[0]
 | 
			
		||||
        # New label (App URL Namespace == App Label)
 | 
			
		||||
        dj_app = apps.get_app_config(namespace)
 | 
			
		||||
        title_modifier = getattr(dj_app, "title_modifier", None)
 | 
			
		||||
        if title_modifier:
 | 
			
		||||
            app_title = dj_app.title_modifier(context.request)
 | 
			
		||||
            app = app_title + " -"
 | 
			
		||||
    return _(
 | 
			
		||||
        "%(title)s - %(app)s %(branding)s"
 | 
			
		||||
        % {
 | 
			
		||||
            "title": " - ".join([str(x) for x in title]),
 | 
			
		||||
            "branding": branding,
 | 
			
		||||
            "app": app,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag
 | 
			
		||||
def config(path, default=""):
 | 
			
		||||
    """Get a setting from the database. Returns default is setting doesn't exist."""
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Evaluator:
 | 
			
		||||
    """Validate and evaulate jinja2-based expressions"""
 | 
			
		||||
    """Validate and evaluate jinja2-based expressions"""
 | 
			
		||||
 | 
			
		||||
    _env: NativeEnvironment
 | 
			
		||||
 | 
			
		||||
@ -51,14 +51,15 @@ class Evaluator:
 | 
			
		||||
        """Return dictionary with additional global variables passed to expression"""
 | 
			
		||||
        # update passbook/policies/expression/templates/policy/expression/form.html
 | 
			
		||||
        # update docs/policies/expression/index.md
 | 
			
		||||
        kwargs["pb_is_sso_flow"] = request.http_request.session.get(
 | 
			
		||||
            AuthenticationView.SESSION_IS_SSO_LOGIN, False
 | 
			
		||||
        )
 | 
			
		||||
        kwargs["pb_is_group_member"] = Evaluator.jinja2_func_is_group_member
 | 
			
		||||
        kwargs["pb_logger"] = get_logger()
 | 
			
		||||
        kwargs["pb_client_ip"] = (
 | 
			
		||||
            get_client_ip(request.http_request) or "255.255.255.255"
 | 
			
		||||
        )
 | 
			
		||||
        if request.http_request:
 | 
			
		||||
            kwargs["pb_is_sso_flow"] = request.http_request.session.get(
 | 
			
		||||
                AuthenticationView.SESSION_IS_SSO_LOGIN, False
 | 
			
		||||
            )
 | 
			
		||||
            kwargs["pb_client_ip"] = (
 | 
			
		||||
                get_client_ip(request.http_request) or "255.255.255.255"
 | 
			
		||||
            )
 | 
			
		||||
        return kwargs
 | 
			
		||||
 | 
			
		||||
    def evaluate(self, expression_source: str, request: PolicyRequest) -> PolicyResult:
 | 
			
		||||
@ -81,7 +82,7 @@ class Evaluator:
 | 
			
		||||
                    req=request,
 | 
			
		||||
                )
 | 
			
		||||
                return PolicyResult(False)
 | 
			
		||||
            if isinstance(result, list) and len(result) == 2:
 | 
			
		||||
            if isinstance(result, (list, tuple)) and len(result) == 2:
 | 
			
		||||
                return PolicyResult(*result)
 | 
			
		||||
            if result:
 | 
			
		||||
                return PolicyResult(result)
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@
 | 
			
		||||
        <p>
 | 
			
		||||
            Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
 | 
			
		||||
        </p>
 | 
			
		||||
        <ul>
 | 
			
		||||
        <ul class="pf-c-list">
 | 
			
		||||
            <li><code>request.user</code>: Passbook User Object (<a href="https://beryju.github.io/passbook/property-mappings/reference/user-object/">Reference</a>)</li>
 | 
			
		||||
            <li><code>request.http_request</code>: Django HTTP Request Object (<a href="https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects">Reference</a>) </li>
 | 
			
		||||
            <li><code>request.obj</code>: Model the Policy is run against. </li>
 | 
			
		||||
@ -19,7 +19,7 @@
 | 
			
		||||
            <li><code>pb_client_ip</code>: Client's IP Address.</li>
 | 
			
		||||
        </ul>
 | 
			
		||||
        <p>Custom Filters:</p>
 | 
			
		||||
        <ul>
 | 
			
		||||
        <ul class="pf-c-list">
 | 
			
		||||
            <li><code>regex_match(regex)</code>: Checks if value matches <code>regex</code></li>
 | 
			
		||||
            <li><code>regex_replace(regex, repl)</code>: Replace string matched by <code>regex</code> with <code>repl</code></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								passbook/policies/expression/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/policies/expression/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										58
									
								
								passbook/policies/expression/tests/test_evaluator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								passbook/policies/expression/tests/test_evaluator.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
			
		||||
"""evaluator tests"""
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from guardian.shortcuts import get_anonymous_user
 | 
			
		||||
 | 
			
		||||
from passbook.policies.expression.evaluator import Evaluator
 | 
			
		||||
from passbook.policies.types import PolicyRequest
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestEvaluator(TestCase):
 | 
			
		||||
    """Evaluator tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.request = PolicyRequest(user=get_anonymous_user())
 | 
			
		||||
 | 
			
		||||
    def test_valid(self):
 | 
			
		||||
        """test simple value expression"""
 | 
			
		||||
        template = "True"
 | 
			
		||||
        evaluator = Evaluator()
 | 
			
		||||
        self.assertEqual(evaluator.evaluate(template, self.request).passing, True)
 | 
			
		||||
 | 
			
		||||
    def test_messages(self):
 | 
			
		||||
        """test expression with message return"""
 | 
			
		||||
        template = "False, 'some message'"
 | 
			
		||||
        evaluator = Evaluator()
 | 
			
		||||
        result = evaluator.evaluate(template, self.request)
 | 
			
		||||
        self.assertEqual(result.passing, False)
 | 
			
		||||
        self.assertEqual(result.messages, ("some message",))
 | 
			
		||||
 | 
			
		||||
    def test_invalid_syntax(self):
 | 
			
		||||
        """test invalid syntax"""
 | 
			
		||||
        template = "{%"
 | 
			
		||||
        evaluator = Evaluator()
 | 
			
		||||
        result = evaluator.evaluate(template, self.request)
 | 
			
		||||
        self.assertEqual(result.passing, False)
 | 
			
		||||
        self.assertEqual(result.messages, ("tag name expected",))
 | 
			
		||||
 | 
			
		||||
    def test_undefined(self):
 | 
			
		||||
        """test undefined result"""
 | 
			
		||||
        template = "{{ foo.bar }}"
 | 
			
		||||
        evaluator = Evaluator()
 | 
			
		||||
        result = evaluator.evaluate(template, self.request)
 | 
			
		||||
        self.assertEqual(result.passing, False)
 | 
			
		||||
        self.assertEqual(result.messages, ("'foo' is undefined",))
 | 
			
		||||
 | 
			
		||||
    def test_validate(self):
 | 
			
		||||
        """test validate"""
 | 
			
		||||
        template = "True"
 | 
			
		||||
        evaluator = Evaluator()
 | 
			
		||||
        result = evaluator.validate(template)
 | 
			
		||||
        self.assertEqual(result, True)
 | 
			
		||||
 | 
			
		||||
    def test_validate_invalid(self):
 | 
			
		||||
        """test validate"""
 | 
			
		||||
        template = "{%"
 | 
			
		||||
        evaluator = Evaluator()
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            evaluator.validate(template)
 | 
			
		||||
@ -5,16 +5,19 @@ from multiprocessing.connection import Connection
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Policy
 | 
			
		||||
from passbook.core.models import Policy, User
 | 
			
		||||
from passbook.policies.exceptions import PolicyException
 | 
			
		||||
from passbook.policies.types import PolicyRequest, PolicyResult
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cache_key(policy, user):
 | 
			
		||||
def cache_key(policy: Policy, user: User = None) -> str:
 | 
			
		||||
    """Generate Cache key for policy"""
 | 
			
		||||
    return f"policy_{policy.pk}#{user.pk}"
 | 
			
		||||
    prefix = f"policy_{policy.pk}"
 | 
			
		||||
    if user:
 | 
			
		||||
        prefix += f"#{user.pk}"
 | 
			
		||||
    return prefix
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyProcess(Process):
 | 
			
		||||
@ -33,7 +36,7 @@ class PolicyProcess(Process):
 | 
			
		||||
    def run(self):
 | 
			
		||||
        """Task wrapper to run policy checking"""
 | 
			
		||||
        LOGGER.debug(
 | 
			
		||||
            "Running policy",
 | 
			
		||||
            "P_ENG(proc): Running policy",
 | 
			
		||||
            policy=self.policy,
 | 
			
		||||
            user=self.request.user,
 | 
			
		||||
            process="PolicyProcess",
 | 
			
		||||
@ -41,13 +44,13 @@ class PolicyProcess(Process):
 | 
			
		||||
        try:
 | 
			
		||||
            policy_result = self.policy.passes(self.request)
 | 
			
		||||
        except PolicyException as exc:
 | 
			
		||||
            LOGGER.debug(exc)
 | 
			
		||||
            LOGGER.debug("P_ENG(proc): error", exc=exc)
 | 
			
		||||
            policy_result = PolicyResult(False, str(exc))
 | 
			
		||||
        # Invert result if policy.negate is set
 | 
			
		||||
        if self.policy.negate:
 | 
			
		||||
            policy_result.passing = not policy_result.passing
 | 
			
		||||
        LOGGER.debug(
 | 
			
		||||
            "Got result",
 | 
			
		||||
            "P_ENG(proc): Finished",
 | 
			
		||||
            policy=self.policy,
 | 
			
		||||
            result=policy_result,
 | 
			
		||||
            process="PolicyProcess",
 | 
			
		||||
@ -56,5 +59,5 @@ class PolicyProcess(Process):
 | 
			
		||||
        )
 | 
			
		||||
        key = cache_key(self.policy, self.request.user)
 | 
			
		||||
        cache.set(key, policy_result)
 | 
			
		||||
        LOGGER.debug("Cached policy evaluation", key=key)
 | 
			
		||||
        LOGGER.debug("P_ENG(proc): Cached policy evaluation", key=key)
 | 
			
		||||
        self.connection.send(policy_result)
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
"""policy structures"""
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
from typing import TYPE_CHECKING, Tuple
 | 
			
		||||
from typing import TYPE_CHECKING, Optional, Tuple
 | 
			
		||||
 | 
			
		||||
from django.db.models import Model
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
@ -14,11 +14,13 @@ class PolicyRequest:
 | 
			
		||||
    """Data-class to hold policy request data"""
 | 
			
		||||
 | 
			
		||||
    user: User
 | 
			
		||||
    http_request: HttpRequest
 | 
			
		||||
    obj: Model
 | 
			
		||||
    http_request: Optional[HttpRequest]
 | 
			
		||||
    obj: Optional[Model]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, user: User):
 | 
			
		||||
        self.user = user
 | 
			
		||||
        self.http_request = None
 | 
			
		||||
        self.obj = None
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"<PolicyRequest user={self.user}>"
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,7 @@ class ApplicationGatewayProviderSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = ApplicationGatewayProvider
 | 
			
		||||
        fields = ["pk", "name", "host", "client"]
 | 
			
		||||
        fields = ["pk", "name", "internal_host", "external_host", "client"]
 | 
			
		||||
        read_only_fields = ["client"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,15 +3,12 @@
 | 
			
		||||
{% load utils %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
{% title 'Authorize Application' %}
 | 
			
		||||
{% block card_title %}
 | 
			
		||||
{% trans 'Authorize Application' %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<header class="login-pf-header">
 | 
			
		||||
  <h1>{% trans 'Authorize Application' %}</h1>
 | 
			
		||||
</header>
 | 
			
		||||
<form method="POST">
 | 
			
		||||
<form method="POST" class="pf-c-form">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% if not error %}
 | 
			
		||||
        {% csrf_token %}
 | 
			
		||||
@ -20,32 +17,40 @@
 | 
			
		||||
                {{ field }}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
        <div class="pf-c-form__group">
 | 
			
		||||
            <p class="subtitle">
 | 
			
		||||
                {% blocktrans with remote=application.name %}
 | 
			
		||||
                You're about to sign into {{ remote }}
 | 
			
		||||
                You're about to sign into {{ remote }}.
 | 
			
		||||
                {% endblocktrans %}
 | 
			
		||||
            </p>
 | 
			
		||||
            <p>{% trans "Application requires following permissions" %}</p>
 | 
			
		||||
            <ul>
 | 
			
		||||
            <ul class="pf-c-list">
 | 
			
		||||
                {% for scope in scopes_descriptions %}
 | 
			
		||||
                <li>{{ scope }}</li>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </ul>
 | 
			
		||||
            {{ form.errors }}
 | 
			
		||||
            {{ form.non_field_errors }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-form__group">
 | 
			
		||||
            <p>
 | 
			
		||||
                {% blocktrans with user=user %}
 | 
			
		||||
                You are logged in as {{ user }}. Not you?
 | 
			
		||||
                {% endblocktrans %}
 | 
			
		||||
                <a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
 | 
			
		||||
            </p>
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <input type="submit" class="btn btn-success btn-disabled btn-lg click-spinner" name="allow" value="{% trans 'Continue' %}">
 | 
			
		||||
                <a href="{% back %}" class="btn btn-default btn-lg">{% trans "Cancel" %}</a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="form-group spinner-hidden hidden">
 | 
			
		||||
                <div class="spinner"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
            <input type="submit" class="pf-c-button pf-m-primary" name="allow" value="{% trans 'Continue' %}">
 | 
			
		||||
            <a href="{% back %}" class="pf-c-button pf-m-secondary">{% trans "Cancel" %}</a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-form__group" style="display: none;" id="loading">
 | 
			
		||||
            <div class="pf-c-form__horizontal-group">
 | 
			
		||||
                <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>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% else %}
 | 
			
		||||
@ -61,9 +66,8 @@
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    $('.click-spinner').on('click', function (e) {
 | 
			
		||||
        $('.spinner-hidden').removeClass('hidden');
 | 
			
		||||
        $(e.target).addClass('disabled');
 | 
			
		||||
    })
 | 
			
		||||
document.querySelector("form").addEventListener("submit", (e) => {
 | 
			
		||||
    document.getElementById("loading").removeAttribute("style");
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
"""passbook auth oidc provider app config"""
 | 
			
		||||
from importlib import import_module
 | 
			
		||||
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
from django.db.utils import InternalError, OperationalError, ProgrammingError
 | 
			
		||||
from django.urls import include, path
 | 
			
		||||
@ -34,3 +36,5 @@ class PassbookProviderOIDCConfig(AppConfig):
 | 
			
		||||
                include("oidc_provider.urls", namespace="oidc_provider"),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        import_module("passbook.providers.oidc.signals")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								passbook/providers/oidc/claims.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								passbook/providers/oidc/claims.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
"""passbook oidc claim helpers"""
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def userinfo(claims: Dict[str, Any], user: User) -> Dict[str, Any]:
 | 
			
		||||
    """Populate claims from userdata"""
 | 
			
		||||
    claims["name"] = user.name
 | 
			
		||||
    claims["given_name"] = user.name
 | 
			
		||||
    claims["family_name"] = user.name
 | 
			
		||||
    claims["email"] = user.email
 | 
			
		||||
 | 
			
		||||
    return claims
 | 
			
		||||
@ -19,6 +19,8 @@ class OIDCProviderForm(forms.ModelForm):
 | 
			
		||||
        self.fields["client_secret"].initial = generate_client_secret()
 | 
			
		||||
 | 
			
		||||
    def save(self, *args, **kwargs):
 | 
			
		||||
        self.instance.reuse_consent = False  # This is managed by passbook
 | 
			
		||||
        self.instance.require_consent = True  # This is managed by passbook
 | 
			
		||||
        response = super().save(*args, **kwargs)
 | 
			
		||||
        # Check if openidprovider class instance exists
 | 
			
		||||
        if not OpenIDProvider.objects.filter(oidc_client=self.instance).exists():
 | 
			
		||||
 | 
			
		||||
@ -4,5 +4,6 @@ INSTALLED_APPS = [
 | 
			
		||||
    "oidc_provider",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
OIDC_AFTER_USERLOGIN_HOOK = "passbook.providers.oidc.lib.check_permissions"
 | 
			
		||||
OIDC_AFTER_USERLOGIN_HOOK = "passbook.providers.oidc.auth.check_permissions"
 | 
			
		||||
OIDC_IDTOKEN_INCLUDE_CLAIMS = True
 | 
			
		||||
OIDC_USERINFO = "passbook.providers.oidc.claims.userinfo"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								passbook/providers/oidc/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								passbook/providers/oidc/signals.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
"""OIDC Provider signals"""
 | 
			
		||||
from django.db.models.signals import post_save
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Application
 | 
			
		||||
from passbook.providers.oidc.models import OpenIDProvider
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=Application)
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
def on_application_save(sender, instance: Application, **_):
 | 
			
		||||
    """Synchronize application's skip_authorization with oidc_client's require_consent"""
 | 
			
		||||
    if isinstance(instance.provider, OpenIDProvider):
 | 
			
		||||
        instance.provider.oidc_client.require_consent = not instance.skip_authorization
 | 
			
		||||
        instance.provider.oidc_client.save()
 | 
			
		||||
        print("updating skip_authz")
 | 
			
		||||
@ -3,15 +3,12 @@
 | 
			
		||||
{% load utils %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
{% title 'Authorize Application' %}
 | 
			
		||||
{% block card_title %}
 | 
			
		||||
{% trans 'Authorize Application' %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<header class="login-pf-header">
 | 
			
		||||
  <h1>{% trans 'Authorize Application' %}</h1>
 | 
			
		||||
</header>
 | 
			
		||||
<form method="POST">
 | 
			
		||||
<form method="POST" class="pf-c-form">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% if not error %}
 | 
			
		||||
        {% csrf_token %}
 | 
			
		||||
@ -20,14 +17,14 @@
 | 
			
		||||
                {{ field }}
 | 
			
		||||
            {% endif %}
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
        <div class="pf-c-form__group">
 | 
			
		||||
            <p class="subtitle">
 | 
			
		||||
                {% blocktrans with remote=client.name %}
 | 
			
		||||
                You're about to sign into {{ remote }}
 | 
			
		||||
                You're about to sign into {{ remote }}.
 | 
			
		||||
                {% endblocktrans %}
 | 
			
		||||
            </p>
 | 
			
		||||
            <p>{% trans "Application requires following permissions" %}</p>
 | 
			
		||||
            <ul>
 | 
			
		||||
            <ul class="pf-c-list">
 | 
			
		||||
                {% for scope in scopes %}
 | 
			
		||||
                <li>{{ scope.name }}</li>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
@ -35,18 +32,26 @@
 | 
			
		||||
            {{ hidden_inputs }}
 | 
			
		||||
            {{ form.errors }}
 | 
			
		||||
            {{ form.non_field_errors }}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-form__group">
 | 
			
		||||
            <p>
 | 
			
		||||
                {% blocktrans with user=user %}
 | 
			
		||||
                You are logged in as {{ user }}. Not you?
 | 
			
		||||
                {% endblocktrans %}
 | 
			
		||||
                <a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
 | 
			
		||||
            </p>
 | 
			
		||||
            <div class="form-group">
 | 
			
		||||
                <input type="submit" class="btn btn-success btn-disabled btn-lg click-spinner" name="allow" value="{% trans 'Continue' %}">
 | 
			
		||||
                <a href="{% back %}" class="btn btn-default btn-lg">{% trans "Cancel" %}</a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="form-group spinner-hidden hidden">
 | 
			
		||||
                <div class="spinner"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
            <input type="submit" class="pf-c-button pf-m-primary" name="allow" value="{% trans 'Continue' %}">
 | 
			
		||||
            <a href="{% back %}" class="pf-c-button pf-m-secondary">{% trans "Cancel" %}</a>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-form__group" style="display: none;" id="loading">
 | 
			
		||||
            <div class="pf-c-form__horizontal-group">
 | 
			
		||||
                <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>
 | 
			
		||||
        </div>
 | 
			
		||||
    {% else %}
 | 
			
		||||
@ -62,9 +67,8 @@
 | 
			
		||||
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
<script>
 | 
			
		||||
    $('.click-spinner').on('click', function (e) {
 | 
			
		||||
        $('.spinner-hidden').removeClass('hidden');
 | 
			
		||||
        $(e.target).addClass('disabled');
 | 
			
		||||
    })
 | 
			
		||||
document.querySelector("form").addEventListener("submit", (e) => {
 | 
			
		||||
    document.getElementById("loading").removeAttribute("style");
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								passbook/providers/oidc/templates/oidc_provider/error.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								passbook/providers/oidc/templates/oidc_provider/error.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
{% extends 'login/base.html' %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load utils %}
 | 
			
		||||
 | 
			
		||||
{% block card_title %}
 | 
			
		||||
{% trans error %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<form>
 | 
			
		||||
    <h3>{% trans description %}</h3>
 | 
			
		||||
    {% if 'back' in request.GET %}
 | 
			
		||||
    <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -24,9 +24,7 @@ class SAMLProviderSerializer(ModelSerializer):
 | 
			
		||||
            "property_mappings",
 | 
			
		||||
            "digest_algorithm",
 | 
			
		||||
            "signature_algorithm",
 | 
			
		||||
            "signing",
 | 
			
		||||
            "signing_cert",
 | 
			
		||||
            "signing_key",
 | 
			
		||||
            "signing_kp",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,6 @@ from passbook.providers.saml.models import (
 | 
			
		||||
    SAMLProvider,
 | 
			
		||||
    get_provider_choices,
 | 
			
		||||
)
 | 
			
		||||
from passbook.providers.saml.utils.cert import CertificateBuilder
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SAMLProviderForm(forms.ModelForm):
 | 
			
		||||
@ -19,13 +18,6 @@ class SAMLProviderForm(forms.ModelForm):
 | 
			
		||||
        choices=get_provider_choices(), label="Processor"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        builder = CertificateBuilder()
 | 
			
		||||
        builder.build()
 | 
			
		||||
        self.fields["signing_cert"].initial = builder.certificate
 | 
			
		||||
        self.fields["signing_key"].initial = builder.private_key
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = SAMLProvider
 | 
			
		||||
@ -41,9 +33,7 @@ class SAMLProviderForm(forms.ModelForm):
 | 
			
		||||
            "property_mappings",
 | 
			
		||||
            "digest_algorithm",
 | 
			
		||||
            "signature_algorithm",
 | 
			
		||||
            "signing",
 | 
			
		||||
            "signing_cert",
 | 
			
		||||
            "signing_key",
 | 
			
		||||
            "signing_kp",
 | 
			
		||||
        ]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput(),
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
# Generated by Django 3.0.3 on 2020-03-03 21:57
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_crypto", "0001_initial"),
 | 
			
		||||
        ("passbook_providers_saml", "0006_auto_20200217_2031"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(model_name="samlprovider", name="signing",),
 | 
			
		||||
        migrations.RemoveField(model_name="samlprovider", name="signing_cert",),
 | 
			
		||||
        migrations.RemoveField(model_name="samlprovider", name="signing_key",),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="samlprovider",
 | 
			
		||||
            name="singing_kp",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                default=None,
 | 
			
		||||
                help_text="Singing is enabled upon selection of a Key Pair.",
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_NULL,
 | 
			
		||||
                to="passbook_crypto.CertificateKeyPair",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -0,0 +1,16 @@
 | 
			
		||||
# Generated by Django 3.0.3 on 2020-03-05 16:06
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_providers_saml", "0007_auto_20200303_2157"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RenameField(
 | 
			
		||||
            model_name="samlprovider", old_name="singing_kp", new_name="signing_kp",
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import PropertyMapping, Provider
 | 
			
		||||
from passbook.crypto.models import CertificateKeyPair
 | 
			
		||||
from passbook.lib.utils.reflection import class_to_path, path_to_class
 | 
			
		||||
from passbook.lib.utils.template import render_to_string
 | 
			
		||||
from passbook.providers.saml.processors.base import Processor
 | 
			
		||||
@ -74,9 +75,13 @@ class SAMLProvider(Provider):
 | 
			
		||||
        default="rsa-sha256",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    signing = models.BooleanField(default=True)
 | 
			
		||||
    signing_cert = models.TextField(verbose_name=_("Singing Certificate"))
 | 
			
		||||
    signing_key = models.TextField()
 | 
			
		||||
    signing_kp = models.ForeignKey(
 | 
			
		||||
        CertificateKeyPair,
 | 
			
		||||
        default=None,
 | 
			
		||||
        null=True,
 | 
			
		||||
        help_text=_("Singing is enabled upon selection of a Key Pair."),
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    form = "passbook.providers.saml.forms.SAMLProviderForm"
 | 
			
		||||
    _processor = None
 | 
			
		||||
 | 
			
		||||
@ -184,7 +184,7 @@ class Processor:
 | 
			
		||||
        try:
 | 
			
		||||
            self._extract_saml_request()
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            raise CannotHandleAssertion(f"Couldn't find SAML request in user session:")
 | 
			
		||||
            raise CannotHandleAssertion(f"Couldn't find SAML request in user session")
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self._decode_and_parse_request()
 | 
			
		||||
 | 
			
		||||
@ -3,23 +3,17 @@
 | 
			
		||||
{% load utils %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
{% title 'Redirecting...' %}
 | 
			
		||||
{% block card_title %}
 | 
			
		||||
{% trans 'Redirecting...' %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<header class="login-pf-header">
 | 
			
		||||
    <h1>{% trans 'Redirecting...' %}</h1>
 | 
			
		||||
</header>
 | 
			
		||||
<form method="POST" action="{{ url }}">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% for key, value in attrs.items %}
 | 
			
		||||
    <input type="hidden" name="{{ key }}" value="{{ value }}">
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    <div class="login-group">
 | 
			
		||||
        <h3>
 | 
			
		||||
            {% trans "Redirecting..." %}
 | 
			
		||||
        </h3>
 | 
			
		||||
        <p>
 | 
			
		||||
            {% blocktrans with user=user %}
 | 
			
		||||
            You are logged in as {{ user }}.
 | 
			
		||||
@ -34,6 +28,6 @@
 | 
			
		||||
{% block scripts %}
 | 
			
		||||
{{ block.super }}
 | 
			
		||||
<script>
 | 
			
		||||
    $('form').submit();
 | 
			
		||||
document.querySelector("form").submit();
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -4,11 +4,8 @@
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<form method="POST" class="pf-c-form" action="{{ saml_params.acs_url }}">
 | 
			
		||||
<form method="POST" class="pf-c-form">
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    <input type="hidden" name="ACSUrl" value="{{ saml_params.acs_url }}">
 | 
			
		||||
    <input type="hidden" name="RelayState" value="{{ saml_params.relay_state }}" />
 | 
			
		||||
    <input type="hidden" name="SAMLResponse" value="{{ saml_params.saml_response }}" />
 | 
			
		||||
    <div class="pf-c-form__group">
 | 
			
		||||
        <h3>
 | 
			
		||||
            {% blocktrans with provider=provider.application.name %}
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
    <div class="c-form__horizontal-group">
 | 
			
		||||
        <p>
 | 
			
		||||
            Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
 | 
			
		||||
            <ul>
 | 
			
		||||
            <ul class="pf-c-list">
 | 
			
		||||
                <li><code>user</code>: Passbook User Object (<a href="https://beryju.github.io/passbook/reference/property-mappings/user-object/">Reference</a>)</li>
 | 
			
		||||
                <li><code>request</code>: Django HTTP Request Object (<a href="https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects">Reference</a>) </li>
 | 
			
		||||
                <li><code>provider</code>: Passbook SAML Provider Object (<a href="https://github.com/BeryJu/passbook/blob/master/passbook/providers/saml/models.py#L16">Reference</a>) </li>
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,5 @@
 | 
			
		||||
<saml:Subject>
 | 
			
		||||
    <saml:NameID Format="{{ SUBJECT_FORMAT }}">
 | 
			
		||||
    {{ SUBJECT }}
 | 
			
		||||
    </saml:NameID>
 | 
			
		||||
    <saml:NameID Format="{{ SUBJECT_FORMAT }}">{{ SUBJECT }}</saml:NameID>
 | 
			
		||||
    <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
 | 
			
		||||
        <saml:SubjectConfirmationData {{ IN_RESPONSE_TO|safe }} NotOnOrAfter="{{ NOT_ON_OR_AFTER }}" Recipient="{{ ACS_URL }}" />
 | 
			
		||||
    </saml:SubjectConfirmation>
 | 
			
		||||
 | 
			
		||||
@ -16,9 +16,9 @@ urlpatterns = [
 | 
			
		||||
        "<slug:application>/login/", views.LoginBeginView.as_view(), name="saml-login"
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "<slug:application>/login/process/",
 | 
			
		||||
        views.LoginProcessView.as_view(),
 | 
			
		||||
        name="saml-login-process",
 | 
			
		||||
        "<slug:application>/login/authorize/",
 | 
			
		||||
        views.AuthorizeView.as_view(),
 | 
			
		||||
        name="saml-login-authorize",
 | 
			
		||||
    ),
 | 
			
		||||
    path("<slug:application>/logout/", views.LogoutView.as_view(), name="saml-logout"),
 | 
			
		||||
    path(
 | 
			
		||||
 | 
			
		||||
@ -31,9 +31,12 @@ def sign_with_signxml(data: str, provider: "SAMLProvider", reference_uri=None) -
 | 
			
		||||
        digest_algorithm=provider.digest_algorithm,
 | 
			
		||||
    )
 | 
			
		||||
    signed = signer.sign(
 | 
			
		||||
        root, key=key, cert=[provider.signing_cert], reference_uri=reference_uri
 | 
			
		||||
        root,
 | 
			
		||||
        key=key,
 | 
			
		||||
        cert=[provider.signing_kp.certificate_data],
 | 
			
		||||
        reference_uri=reference_uri,
 | 
			
		||||
    )
 | 
			
		||||
    XMLVerifier().verify(signed, x509_cert=provider.signing_cert)
 | 
			
		||||
    XMLVerifier().verify(signed, x509_cert=provider.signing_kp.certificate_data)
 | 
			
		||||
    return etree.tostring(signed).decode("utf-8")  # nosec
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,12 +21,16 @@ from passbook.core.models import Application, Provider
 | 
			
		||||
from passbook.lib.utils.template import render_to_string
 | 
			
		||||
from passbook.lib.views import bad_request_message
 | 
			
		||||
from passbook.policies.engine import PolicyEngine
 | 
			
		||||
from passbook.providers.saml import exceptions
 | 
			
		||||
from passbook.providers.saml.exceptions import CannotHandleAssertion
 | 
			
		||||
from passbook.providers.saml.models import SAMLProvider
 | 
			
		||||
from passbook.providers.saml.processors.types import SAMLResponseParams
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
 | 
			
		||||
SESSION_KEY_SAML_REQUEST = "SAMLRequest"
 | 
			
		||||
SESSION_KEY_SAML_RESPONSE = "SAMLResponse"
 | 
			
		||||
SESSION_KEY_RELAY_STATE = "RelayState"
 | 
			
		||||
SESSION_KEY_PARAMS = "SAMLParams"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AccessRequiredView(AccessMixin, View):
 | 
			
		||||
@ -50,14 +54,18 @@ class AccessRequiredView(AccessMixin, View):
 | 
			
		||||
 | 
			
		||||
    def _has_access(self) -> bool:
 | 
			
		||||
        """Check if user has access to application"""
 | 
			
		||||
        LOGGER.debug(
 | 
			
		||||
            "_has_access", user=self.request.user, app=self.provider.application
 | 
			
		||||
        )
 | 
			
		||||
        policy_engine = PolicyEngine(
 | 
			
		||||
            self.provider.application.policies.all(), self.request.user, self.request
 | 
			
		||||
        )
 | 
			
		||||
        policy_engine.build()
 | 
			
		||||
        return policy_engine.passing
 | 
			
		||||
        passing = policy_engine.passing
 | 
			
		||||
        LOGGER.debug(
 | 
			
		||||
            "saml_has_access",
 | 
			
		||||
            user=self.request.user,
 | 
			
		||||
            app=self.provider.application,
 | 
			
		||||
            passing=passing,
 | 
			
		||||
        )
 | 
			
		||||
        return passing
 | 
			
		||||
 | 
			
		||||
    def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
			
		||||
        if not request.user.is_authenticated:
 | 
			
		||||
@ -75,80 +83,29 @@ class LoginBeginView(AccessRequiredView):
 | 
			
		||||
    """Receives a SAML 2.0 AuthnRequest from a Service Provider and
 | 
			
		||||
    stores it in the session prior to enforcing login."""
 | 
			
		||||
 | 
			
		||||
    @method_decorator(csrf_exempt)
 | 
			
		||||
    def dispatch(self, request: HttpRequest, application: str) -> HttpResponse:
 | 
			
		||||
        if request.method == "POST":
 | 
			
		||||
            source = request.POST
 | 
			
		||||
        else:
 | 
			
		||||
            source = request.GET
 | 
			
		||||
 | 
			
		||||
    def handler(self, source, application: str) -> HttpResponse:
 | 
			
		||||
        """Handle SAML Request whether its a POST or a Redirect binding"""
 | 
			
		||||
        # Store these values now, because Django's login cycle won't preserve them.
 | 
			
		||||
        try:
 | 
			
		||||
            request.session["SAMLRequest"] = source["SAMLRequest"]
 | 
			
		||||
            self.request.session[SESSION_KEY_SAML_REQUEST] = source[
 | 
			
		||||
                SESSION_KEY_SAML_REQUEST
 | 
			
		||||
            ]
 | 
			
		||||
        except (KeyError, MultiValueDictKeyError):
 | 
			
		||||
            return bad_request_message(request, "The SAML request payload is missing.")
 | 
			
		||||
 | 
			
		||||
        request.session["RelayState"] = source.get("RelayState", "")
 | 
			
		||||
        return redirect(
 | 
			
		||||
            reverse(
 | 
			
		||||
                "passbook_providers_saml:saml-login-process",
 | 
			
		||||
                kwargs={"application": application},
 | 
			
		||||
            return bad_request_message(
 | 
			
		||||
                self.request, "The SAML request payload is missing."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        self.request.session[SESSION_KEY_RELAY_STATE] = source.get(
 | 
			
		||||
            SESSION_KEY_RELAY_STATE, ""
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginProcessView(AccessRequiredView):
 | 
			
		||||
    """Processor-based login continuation.
 | 
			
		||||
    Presents a SAML 2.0 Assertion for POSTing back to the Service Provider."""
 | 
			
		||||
 | 
			
		||||
    def handle_redirect(
 | 
			
		||||
        self, params: SAMLResponseParams, skipped_authorization: bool
 | 
			
		||||
    ) -> HttpResponse:
 | 
			
		||||
        """Handle direct redirect to SP"""
 | 
			
		||||
        # Log Application Authorization
 | 
			
		||||
        Event.new(
 | 
			
		||||
            EventAction.AUTHORIZE_APPLICATION,
 | 
			
		||||
            authorized_application=self.provider.application,
 | 
			
		||||
            skipped_authorization=skipped_authorization,
 | 
			
		||||
        ).from_http(self.request)
 | 
			
		||||
        return render(
 | 
			
		||||
            self.request,
 | 
			
		||||
            "saml/idp/autosubmit_form.html",
 | 
			
		||||
            {
 | 
			
		||||
                "url": params.acs_url,
 | 
			
		||||
                "attrs": {
 | 
			
		||||
                    "SAMLResponse": params.saml_response,
 | 
			
		||||
                    "RelayState": params.relay_state,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get(self, request: HttpRequest, application: str) -> HttpResponse:
 | 
			
		||||
        """Handle get request, i.e. render form"""
 | 
			
		||||
        # User access gets checked in dispatch
 | 
			
		||||
 | 
			
		||||
        # Otherwise we generate the IdP initiated session
 | 
			
		||||
        try:
 | 
			
		||||
            # application.skip_authorization is set so we directly redirect the user
 | 
			
		||||
            if self.provider.application.skip_authorization:
 | 
			
		||||
                return self.post(request, application)
 | 
			
		||||
 | 
			
		||||
            self.provider.processor.init_deep_link(request)
 | 
			
		||||
            self.provider.processor.can_handle(self.request)
 | 
			
		||||
            params = self.provider.processor.generate_response()
 | 
			
		||||
 | 
			
		||||
            return render(
 | 
			
		||||
                request,
 | 
			
		||||
                "saml/idp/login.html",
 | 
			
		||||
                {
 | 
			
		||||
                    "saml_params": params,
 | 
			
		||||
                    "provider": self.provider,
 | 
			
		||||
                    "title": "Authorize Application",
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        except exceptions.CannotHandleAssertion as exc:
 | 
			
		||||
            LOGGER.error(exc)
 | 
			
		||||
            did_you_mean_link = request.build_absolute_uri(
 | 
			
		||||
            self.request.session[SESSION_KEY_PARAMS] = params
 | 
			
		||||
        except CannotHandleAssertion as exc:
 | 
			
		||||
            LOGGER.info(exc)
 | 
			
		||||
            did_you_mean_link = self.request.build_absolute_uri(
 | 
			
		||||
                reverse(
 | 
			
		||||
                    "passbook_providers_saml:saml-login-initiate",
 | 
			
		||||
                    kwargs={"application": application},
 | 
			
		||||
@ -158,19 +115,101 @@ class LoginProcessView(AccessRequiredView):
 | 
			
		||||
                f" Did you mean to go <a href='{did_you_mean_link}'>here</a>?"
 | 
			
		||||
            )
 | 
			
		||||
            return bad_request_message(
 | 
			
		||||
                request, mark_safe(str(exc) + did_you_mean_message)
 | 
			
		||||
                self.request, mark_safe(str(exc) + did_you_mean_message)
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return redirect(
 | 
			
		||||
            reverse(
 | 
			
		||||
                "passbook_providers_saml:saml-login-authorize",
 | 
			
		||||
                kwargs={"application": application},
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @method_decorator(csrf_exempt)
 | 
			
		||||
    def dispatch(self, *args, **kwargs):
 | 
			
		||||
        return super().dispatch(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @method_decorator(csrf_exempt)
 | 
			
		||||
    def get(self, request: HttpRequest, application: str) -> HttpResponse:
 | 
			
		||||
        """Handle REDIRECT bindings"""
 | 
			
		||||
        return self.handler(request.GET, application)
 | 
			
		||||
 | 
			
		||||
    @method_decorator(csrf_exempt)
 | 
			
		||||
    def post(self, request: HttpRequest, application: str) -> HttpResponse:
 | 
			
		||||
        """Handle POST Bindings"""
 | 
			
		||||
        return self.handler(request.POST, application)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InitiateLoginView(AccessRequiredView):
 | 
			
		||||
    """IdP-initiated Login"""
 | 
			
		||||
 | 
			
		||||
    def get(self, request: HttpRequest, application: str) -> HttpResponse:
 | 
			
		||||
        """Initiates an IdP-initiated link to a simple SP resource/target URL."""
 | 
			
		||||
        self.provider.processor.is_idp_initiated = True
 | 
			
		||||
        self.provider.processor.init_deep_link(request)
 | 
			
		||||
        params = self.provider.processor.generate_response()
 | 
			
		||||
        request.session[SESSION_KEY_PARAMS] = params
 | 
			
		||||
        return redirect(
 | 
			
		||||
            reverse(
 | 
			
		||||
                "passbook_providers_saml:saml-login-authorize",
 | 
			
		||||
                kwargs={"application": application},
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthorizeView(AccessRequiredView):
 | 
			
		||||
    """Ask the user for authorization to continue to the SP.
 | 
			
		||||
    Presents a SAML 2.0 Assertion for POSTing back to the Service Provider."""
 | 
			
		||||
 | 
			
		||||
    def get(self, request: HttpRequest, application: str) -> HttpResponse:
 | 
			
		||||
        """Handle get request, i.e. render form"""
 | 
			
		||||
        # User access gets checked in dispatch
 | 
			
		||||
 | 
			
		||||
        # Otherwise we generate the IdP initiated session
 | 
			
		||||
        try:
 | 
			
		||||
            # application.skip_authorization is set so we directly redirect the user
 | 
			
		||||
            if self.provider.application.skip_authorization:
 | 
			
		||||
                LOGGER.debug("skipping authz", application=self.provider.application)
 | 
			
		||||
                return self.post(request, application)
 | 
			
		||||
 | 
			
		||||
            return render(
 | 
			
		||||
                request,
 | 
			
		||||
                "saml/idp/login.html",
 | 
			
		||||
                {"provider": self.provider, "title": "Authorize Application",},
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            return bad_request_message(request, "Missing SAML Payload")
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=unused-argument
 | 
			
		||||
    def post(self, request: HttpRequest, application: str) -> HttpResponse:
 | 
			
		||||
        """Handle post request, return back to ACS"""
 | 
			
		||||
        # User access gets checked in dispatch
 | 
			
		||||
 | 
			
		||||
        # we get here when skip_authorization is False, and after the user accepted
 | 
			
		||||
        # we get here when skip_authorization is True, and after the user accepted
 | 
			
		||||
        # the authorization form
 | 
			
		||||
        self.provider.processor.can_handle(request)
 | 
			
		||||
        saml_params = self.provider.processor.generate_response()
 | 
			
		||||
        return self.handle_redirect(saml_params, True)
 | 
			
		||||
        # Log Application Authorization
 | 
			
		||||
        Event.new(
 | 
			
		||||
            EventAction.AUTHORIZE_APPLICATION,
 | 
			
		||||
            authorized_application=self.provider.application,
 | 
			
		||||
            skipped_authorization=self.provider.application.skip_authorization,
 | 
			
		||||
        ).from_http(self.request)
 | 
			
		||||
        self.request.session.pop(SESSION_KEY_SAML_REQUEST, None)
 | 
			
		||||
        self.request.session.pop(SESSION_KEY_SAML_RESPONSE, None)
 | 
			
		||||
        self.request.session.pop(SESSION_KEY_RELAY_STATE, None)
 | 
			
		||||
        response: SAMLResponseParams = self.request.session.pop(SESSION_KEY_PARAMS)
 | 
			
		||||
        return render(
 | 
			
		||||
            self.request,
 | 
			
		||||
            "saml/idp/autosubmit_form.html",
 | 
			
		||||
            {
 | 
			
		||||
                "url": response.acs_url,
 | 
			
		||||
                "attrs": {
 | 
			
		||||
                    "ACSUrl": response.acs_url,
 | 
			
		||||
                    SESSION_KEY_SAML_RESPONSE: response.saml_response,
 | 
			
		||||
                    SESSION_KEY_RELAY_STATE: response.relay_state,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@method_decorator(csrf_exempt, name="dispatch")
 | 
			
		||||
@ -204,7 +243,9 @@ class SLOLogout(AccessRequiredView):
 | 
			
		||||
    # pylint: disable=unused-argument
 | 
			
		||||
    def post(self, request: HttpRequest, application: str) -> HttpResponse:
 | 
			
		||||
        """Perform logout"""
 | 
			
		||||
        request.session["SAMLRequest"] = request.POST["SAMLRequest"]
 | 
			
		||||
        request.session[SESSION_KEY_SAML_REQUEST] = request.POST[
 | 
			
		||||
            SESSION_KEY_SAML_REQUEST
 | 
			
		||||
        ]
 | 
			
		||||
        # TODO: Parse SAML LogoutRequest from POST data, similar to login_process().
 | 
			
		||||
        # TODO: Modify the base processor to handle logouts?
 | 
			
		||||
        # TODO: Combine this with login_process(), since they are so very similar?
 | 
			
		||||
@ -233,9 +274,9 @@ class DescriptorDownloadView(AccessRequiredView):
 | 
			
		||||
                kwargs={"application": provider.application.slug},
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        pubkey = strip_pem_header(provider.signing_cert.replace("\r", "")).replace(
 | 
			
		||||
            "\n", ""
 | 
			
		||||
        )
 | 
			
		||||
        pubkey = strip_pem_header(
 | 
			
		||||
            provider.signing_kp.certificate_data.replace("\r", "")
 | 
			
		||||
        ).replace("\n", "")
 | 
			
		||||
        subject_format = provider.processor.subject_format
 | 
			
		||||
        ctx = {
 | 
			
		||||
            "entity_id": entity_id,
 | 
			
		||||
@ -259,54 +300,7 @@ class DescriptorDownloadView(AccessRequiredView):
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            response = HttpResponse(metadata, content_type="application/xml")
 | 
			
		||||
            response["Content-Disposition"] = (
 | 
			
		||||
                'attachment; filename="' '%s_passbook_meta.xml"' % self.provider.name
 | 
			
		||||
            )
 | 
			
		||||
            response[
 | 
			
		||||
                "Content-Disposition"
 | 
			
		||||
            ] = f'attachment; filename="{self.provider.name}_passbook_meta.xml"'
 | 
			
		||||
            return response
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InitiateLoginView(AccessRequiredView):
 | 
			
		||||
    """IdP-initiated Login"""
 | 
			
		||||
 | 
			
		||||
    def handle_redirect(
 | 
			
		||||
        self, params: SAMLResponseParams, skipped_authorization: bool
 | 
			
		||||
    ) -> HttpResponse:
 | 
			
		||||
        """Handle direct redirect to SP"""
 | 
			
		||||
        # Log Application Authorization
 | 
			
		||||
        Event.new(
 | 
			
		||||
            EventAction.AUTHORIZE_APPLICATION,
 | 
			
		||||
            authorized_application=self.provider.application,
 | 
			
		||||
            skipped_authorization=skipped_authorization,
 | 
			
		||||
        ).from_http(self.request)
 | 
			
		||||
        return render(
 | 
			
		||||
            self.request,
 | 
			
		||||
            "saml/idp/autosubmit_form.html",
 | 
			
		||||
            {
 | 
			
		||||
                "url": params.acs_url,
 | 
			
		||||
                "attrs": {
 | 
			
		||||
                    "SAMLResponse": params.saml_response,
 | 
			
		||||
                    "RelayState": params.relay_state,
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=unused-argument
 | 
			
		||||
    def get(self, request: HttpRequest, application: str) -> HttpResponse:
 | 
			
		||||
        """Initiates an IdP-initiated link to a simple SP resource/target URL."""
 | 
			
		||||
        self.provider.processor.is_idp_initiated = True
 | 
			
		||||
        self.provider.processor.init_deep_link(request)
 | 
			
		||||
        params = self.provider.processor.generate_response()
 | 
			
		||||
 | 
			
		||||
        # IdP-initiated Login Flow
 | 
			
		||||
        if self.provider.application.skip_authorization:
 | 
			
		||||
            return self.handle_redirect(params, True)
 | 
			
		||||
 | 
			
		||||
        return render(
 | 
			
		||||
            request,
 | 
			
		||||
            "saml/idp/login.html",
 | 
			
		||||
            {
 | 
			
		||||
                "saml_params": params,
 | 
			
		||||
                "provider": self.provider,
 | 
			
		||||
                "title": "Authorize Application",
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,7 @@ from sentry_sdk.integrations.django import DjangoIntegration
 | 
			
		||||
 | 
			
		||||
from passbook import __version__
 | 
			
		||||
from passbook.lib.config import CONFIG
 | 
			
		||||
from passbook.lib.logging import add_process_id
 | 
			
		||||
from passbook.lib.sentry import before_send
 | 
			
		||||
 | 
			
		||||
LOGGER = structlog.get_logger()
 | 
			
		||||
@ -53,6 +54,7 @@ if DEBUG:
 | 
			
		||||
    CSRF_COOKIE_NAME = "passbook_csrf_debug"
 | 
			
		||||
    LANGUAGE_COOKIE_NAME = "passbook_language_debug"
 | 
			
		||||
    SESSION_COOKIE_NAME = "passbook_session_debug"
 | 
			
		||||
    SESSION_COOKIE_SAMESITE = None
 | 
			
		||||
else:
 | 
			
		||||
    CSRF_COOKIE_NAME = "passbook_csrf"
 | 
			
		||||
    LANGUAGE_COOKIE_NAME = "passbook_language"
 | 
			
		||||
@ -83,6 +85,7 @@ INSTALLED_APPS = [
 | 
			
		||||
    "passbook.api.apps.PassbookAPIConfig",
 | 
			
		||||
    "passbook.lib.apps.PassbookLibConfig",
 | 
			
		||||
    "passbook.audit.apps.PassbookAuditConfig",
 | 
			
		||||
    "passbook.crypto.apps.PassbookCryptoConfig",
 | 
			
		||||
    "passbook.recovery.apps.PassbookRecoveryConfig",
 | 
			
		||||
    "passbook.sources.saml.apps.PassbookSourceSAMLConfig",
 | 
			
		||||
    "passbook.sources.ldap.apps.PassbookSourceLDAPConfig",
 | 
			
		||||
@ -278,6 +281,7 @@ structlog.configure_once(
 | 
			
		||||
    processors=[
 | 
			
		||||
        structlog.stdlib.add_log_level,
 | 
			
		||||
        structlog.stdlib.add_logger_name,
 | 
			
		||||
        add_process_id,
 | 
			
		||||
        structlog.stdlib.PositionalArgumentsFormatter(),
 | 
			
		||||
        structlog.processors.TimeStamper(),
 | 
			
		||||
        structlog.processors.StackInfoRenderer(),
 | 
			
		||||
@ -314,7 +318,7 @@ LOGGING = {
 | 
			
		||||
    },
 | 
			
		||||
    "handlers": {
 | 
			
		||||
        "console": {
 | 
			
		||||
            "level": DEBUG,
 | 
			
		||||
            "level": "DEBUG",
 | 
			
		||||
            "class": "logging.StreamHandler",
 | 
			
		||||
            "formatter": "colored" if DEBUG else "plain",
 | 
			
		||||
        },
 | 
			
		||||
@ -322,6 +326,7 @@ LOGGING = {
 | 
			
		||||
    "loggers": {},
 | 
			
		||||
}
 | 
			
		||||
_LOGGING_HANDLER_MAP = {
 | 
			
		||||
    "": "DEBUG",
 | 
			
		||||
    "passbook": "DEBUG",
 | 
			
		||||
    "django": "WARNING",
 | 
			
		||||
    "celery": "WARNING",
 | 
			
		||||
@ -334,7 +339,7 @@ for handler_name, level in _LOGGING_HANDLER_MAP.items():
 | 
			
		||||
    LOGGING["loggers"][handler_name] = {
 | 
			
		||||
        "handlers": ["console"],
 | 
			
		||||
        "level": level,
 | 
			
		||||
        "propagate": True,
 | 
			
		||||
        "propagate": False,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
TEST = False
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@
 | 
			
		||||
    <div class="c-form__horizontal-group">
 | 
			
		||||
        <p>
 | 
			
		||||
            Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
 | 
			
		||||
            <ul>
 | 
			
		||||
            <ul class="pf-c-list">
 | 
			
		||||
                <li><code>ldap</code>: A Dictionary of all values retrieved from LDAP.</li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </p>
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
"""OAuth Clients"""
 | 
			
		||||
import json
 | 
			
		||||
from typing import Dict
 | 
			
		||||
from typing import Dict, Optional
 | 
			
		||||
from urllib.parse import parse_qs, urlencode
 | 
			
		||||
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from django.utils.crypto import constant_time_compare, get_random_string
 | 
			
		||||
from django.utils.encoding import force_text
 | 
			
		||||
from requests import Session
 | 
			
		||||
@ -18,30 +19,26 @@ LOGGER = get_logger()
 | 
			
		||||
class BaseOAuthClient:
 | 
			
		||||
    """Base OAuth Client"""
 | 
			
		||||
 | 
			
		||||
    _session: Session = None
 | 
			
		||||
    session: Session = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, source, token=""):  # nosec
 | 
			
		||||
        self.source = source
 | 
			
		||||
        self.token = token
 | 
			
		||||
        self._session = Session()
 | 
			
		||||
        self._session.headers.update({"User-Agent": "passbook %s" % __version__})
 | 
			
		||||
        self.session = Session()
 | 
			
		||||
        self.session.headers.update({"User-Agent": "passbook %s" % __version__})
 | 
			
		||||
 | 
			
		||||
    def get_access_token(self, request, callback=None):
 | 
			
		||||
        "Fetch access token from callback request."
 | 
			
		||||
        raise NotImplementedError("Defined in a sub-class")  # pragma: no cover
 | 
			
		||||
 | 
			
		||||
    def get_profile_info(self, raw_token):
 | 
			
		||||
    def get_profile_info(self, token: Dict[str, str]):
 | 
			
		||||
        "Fetch user profile information."
 | 
			
		||||
        try:
 | 
			
		||||
            token = json.loads(raw_token)
 | 
			
		||||
            headers = {
 | 
			
		||||
                "Authorization": f"{token['token_type']} {token['access_token']}"
 | 
			
		||||
            }
 | 
			
		||||
            response = self.request(
 | 
			
		||||
                "get",
 | 
			
		||||
                self.source.profile_url,
 | 
			
		||||
                token=token["access_token"],
 | 
			
		||||
                headers=headers,
 | 
			
		||||
            response = self.session.request(
 | 
			
		||||
                "get", self.source.profile_url, headers=headers,
 | 
			
		||||
            )
 | 
			
		||||
            response.raise_for_status()
 | 
			
		||||
        except RequestException as exc:
 | 
			
		||||
@ -67,10 +64,6 @@ class BaseOAuthClient:
 | 
			
		||||
        "Parse token and secret from raw token response."
 | 
			
		||||
        raise NotImplementedError("Defined in a sub-class")  # pragma: no cover
 | 
			
		||||
 | 
			
		||||
    def request(self, method, url, **kwargs):
 | 
			
		||||
        "Build remote url request."
 | 
			
		||||
        return self._session.request(method, url, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def session_key(self):
 | 
			
		||||
        """Return Session Key"""
 | 
			
		||||
@ -80,36 +73,48 @@ class BaseOAuthClient:
 | 
			
		||||
class OAuthClient(BaseOAuthClient):
 | 
			
		||||
    """OAuth1 Client"""
 | 
			
		||||
 | 
			
		||||
    def get_access_token(self, request, callback=None):
 | 
			
		||||
    _default_headers = {
 | 
			
		||||
        "Accept": "application/json",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def get_access_token(
 | 
			
		||||
        self, request: HttpRequest, callback=None
 | 
			
		||||
    ) -> Optional[Dict[str, str]]:
 | 
			
		||||
        "Fetch access token from callback request."
 | 
			
		||||
        raw_token = request.session.get(self.session_key, None)
 | 
			
		||||
        verifier = request.GET.get("oauth_verifier", None)
 | 
			
		||||
        if raw_token is not None and verifier is not None:
 | 
			
		||||
            data = {"oauth_verifier": verifier}
 | 
			
		||||
            data = {
 | 
			
		||||
                "oauth_verifier": verifier,
 | 
			
		||||
                "oauth_callback": callback,
 | 
			
		||||
                "token": raw_token,
 | 
			
		||||
            }
 | 
			
		||||
            callback = request.build_absolute_uri(callback or request.path)
 | 
			
		||||
            callback = force_text(callback)
 | 
			
		||||
            try:
 | 
			
		||||
                response = self.request(
 | 
			
		||||
                response = self.session.request(
 | 
			
		||||
                    "post",
 | 
			
		||||
                    self.source.access_token_url,
 | 
			
		||||
                    token=raw_token,
 | 
			
		||||
                    data=data,
 | 
			
		||||
                    oauth_callback=callback,
 | 
			
		||||
                    headers=self._default_headers,
 | 
			
		||||
                )
 | 
			
		||||
                response.raise_for_status()
 | 
			
		||||
            except RequestException as exc:
 | 
			
		||||
                LOGGER.warning("Unable to fetch access token", exc=exc)
 | 
			
		||||
                return None
 | 
			
		||||
            else:
 | 
			
		||||
                return response.text
 | 
			
		||||
                return response.json()
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def get_request_token(self, request, callback):
 | 
			
		||||
        "Fetch the OAuth request token. Only required for OAuth 1.0."
 | 
			
		||||
        callback = force_text(request.build_absolute_uri(callback))
 | 
			
		||||
        try:
 | 
			
		||||
            response = self.request(
 | 
			
		||||
                "post", self.source.request_token_url, oauth_callback=callback
 | 
			
		||||
            response = self.session.request(
 | 
			
		||||
                "post",
 | 
			
		||||
                self.source.request_token_url,
 | 
			
		||||
                data={"oauth_callback": callback},
 | 
			
		||||
                headers=self._default_headers,
 | 
			
		||||
            )
 | 
			
		||||
            response.raise_for_status()
 | 
			
		||||
        except RequestException as exc:
 | 
			
		||||
@ -154,7 +159,7 @@ class OAuthClient(BaseOAuthClient):
 | 
			
		||||
            callback_uri=callback,
 | 
			
		||||
        )
 | 
			
		||||
        kwargs["auth"] = oauth
 | 
			
		||||
        return super(OAuthClient, self).request(method, url, **kwargs)
 | 
			
		||||
        return super(OAuthClient, self).session.request(method, url, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def session_key(self):
 | 
			
		||||
@ -164,6 +169,10 @@ class OAuthClient(BaseOAuthClient):
 | 
			
		||||
class OAuth2Client(BaseOAuthClient):
 | 
			
		||||
    """OAuth2 Client"""
 | 
			
		||||
 | 
			
		||||
    _default_headers = {
 | 
			
		||||
        "Accept": "application/json",
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=unused-argument
 | 
			
		||||
    def check_application_state(self, request, callback):
 | 
			
		||||
        "Check optional state parameter."
 | 
			
		||||
@ -197,15 +206,19 @@ class OAuth2Client(BaseOAuthClient):
 | 
			
		||||
            LOGGER.warning("No code returned by the source")
 | 
			
		||||
            return None
 | 
			
		||||
        try:
 | 
			
		||||
            response = self.request(
 | 
			
		||||
                "post", self.source.access_token_url, data=args, **request_kwargs
 | 
			
		||||
            response = self.session.request(
 | 
			
		||||
                "post",
 | 
			
		||||
                self.source.access_token_url,
 | 
			
		||||
                data=args,
 | 
			
		||||
                headers=self._default_headers,
 | 
			
		||||
                **request_kwargs,
 | 
			
		||||
            )
 | 
			
		||||
            response.raise_for_status()
 | 
			
		||||
        except RequestException as exc:
 | 
			
		||||
            LOGGER.warning("Unable to fetch access token", exc=exc)
 | 
			
		||||
            return None
 | 
			
		||||
        else:
 | 
			
		||||
            return response.text
 | 
			
		||||
            return response.json()
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=unused-argument
 | 
			
		||||
    def get_application_state(self, request, callback):
 | 
			
		||||
@ -247,7 +260,7 @@ class OAuth2Client(BaseOAuthClient):
 | 
			
		||||
            params = kwargs.get("params", {})
 | 
			
		||||
            params["access_token"] = token
 | 
			
		||||
            kwargs["params"] = params
 | 
			
		||||
        return super(OAuth2Client, self).request(method, url, **kwargs)
 | 
			
		||||
        return super(OAuth2Client, self).session.request(method, url, **kwargs)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def session_key(self):
 | 
			
		||||
 | 
			
		||||
@ -116,7 +116,7 @@ class AzureADOAuthSourceForm(OAuthSourceForm):
 | 
			
		||||
    class Meta(OAuthSourceForm.Meta):
 | 
			
		||||
 | 
			
		||||
        overrides = {
 | 
			
		||||
            "provider_type": "azure_ad",
 | 
			
		||||
            "provider_type": "azure-ad",
 | 
			
		||||
            "request_token_url": "",
 | 
			
		||||
            "authorization_url": "https://login.microsoftonline.com/common/oauth2/authorize",
 | 
			
		||||
            "access_token_url": "https://login.microsoftonline.com/common/oauth2/token",
 | 
			
		||||
 | 
			
		||||
@ -89,13 +89,15 @@ class OAuthCallback(OAuthClientMixin, View):
 | 
			
		||||
            client = self.get_client(self.source)
 | 
			
		||||
            callback = self.get_callback_url(self.source)
 | 
			
		||||
            # Fetch access token
 | 
			
		||||
            raw_token = client.get_access_token(self.request, callback=callback)
 | 
			
		||||
            if raw_token is None:
 | 
			
		||||
            token = client.get_access_token(self.request, callback=callback)
 | 
			
		||||
            if token is None:
 | 
			
		||||
                return self.handle_login_failure(
 | 
			
		||||
                    self.source, "Could not retrieve token."
 | 
			
		||||
                )
 | 
			
		||||
            if "error" in token:
 | 
			
		||||
                return self.handle_login_failure(self.source, token["error"])
 | 
			
		||||
            # Fetch profile info
 | 
			
		||||
            info = client.get_profile_info(raw_token)
 | 
			
		||||
            info = client.get_profile_info(token)
 | 
			
		||||
            if info is None:
 | 
			
		||||
                return self.handle_login_failure(
 | 
			
		||||
                    self.source, "Could not retrieve profile."
 | 
			
		||||
@ -105,7 +107,7 @@ class OAuthCallback(OAuthClientMixin, View):
 | 
			
		||||
                return self.handle_login_failure(self.source, "Could not determine id.")
 | 
			
		||||
            # Get or create access record
 | 
			
		||||
            defaults = {
 | 
			
		||||
                "access_token": raw_token,
 | 
			
		||||
                "access_token": token.get("access_token"),
 | 
			
		||||
            }
 | 
			
		||||
            existing = UserOAuthSourceConnection.objects.filter(
 | 
			
		||||
                source=self.source, identifier=identifier
 | 
			
		||||
@ -113,13 +115,15 @@ class OAuthCallback(OAuthClientMixin, View):
 | 
			
		||||
 | 
			
		||||
            if existing.exists():
 | 
			
		||||
                connection = existing.first()
 | 
			
		||||
                connection.access_token = raw_token
 | 
			
		||||
                connection.access_token = token.get("access_token")
 | 
			
		||||
                UserOAuthSourceConnection.objects.filter(pk=connection.pk).update(
 | 
			
		||||
                    **defaults
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                connection = UserOAuthSourceConnection(
 | 
			
		||||
                    source=self.source, identifier=identifier, access_token=raw_token
 | 
			
		||||
                    source=self.source,
 | 
			
		||||
                    identifier=identifier,
 | 
			
		||||
                    access_token=token.get("access_token"),
 | 
			
		||||
                )
 | 
			
		||||
            user = authenticate(
 | 
			
		||||
                source=self.source, identifier=identifier, request=request
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ class SAMLSourceSerializer(ModelSerializer):
 | 
			
		||||
            "idp_url",
 | 
			
		||||
            "idp_logout_url",
 | 
			
		||||
            "auto_logout",
 | 
			
		||||
            "signing_cert",
 | 
			
		||||
            "signing_kp",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,19 +5,12 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
 | 
			
		||||
from passbook.providers.saml.utils.cert import CertificateBuilder
 | 
			
		||||
from passbook.sources.saml.models import SAMLSource
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SAMLSourceForm(forms.ModelForm):
 | 
			
		||||
    """SAML Provider form"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
        builder = CertificateBuilder()
 | 
			
		||||
        builder.build()
 | 
			
		||||
        self.fields["signing_cert"].initial = builder.certificate
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = SAMLSource
 | 
			
		||||
@ -26,7 +19,7 @@ class SAMLSourceForm(forms.ModelForm):
 | 
			
		||||
            "idp_url",
 | 
			
		||||
            "idp_logout_url",
 | 
			
		||||
            "auto_logout",
 | 
			
		||||
            "signing_cert",
 | 
			
		||||
            "signing_kp",
 | 
			
		||||
        ]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput(),
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								passbook/sources/saml/migrations/0006_auto_20200303_2201.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								passbook/sources/saml/migrations/0006_auto_20200303_2201.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
# Generated by Django 3.0.3 on 2020-03-03 22:01
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_crypto", "0001_initial"),
 | 
			
		||||
        ("passbook_sources_saml", "0005_auto_20200220_1621"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RemoveField(model_name="samlsource", name="signing_cert",),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="samlsource",
 | 
			
		||||
            name="signing_kp",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                default=None,
 | 
			
		||||
                help_text="Certificate Key Pair of the IdP which Assertions are validated against.",
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_NULL,
 | 
			
		||||
                to="passbook_crypto.CertificateKeyPair",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user