Compare commits
	
		
			196 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 316ac78e49 | |||
| de2b67b111 | |||
| e1bbbe6671 | |||
| 8b3839343c | |||
| 7897ca4744 | |||
| 2fd00c6c9d | |||
| 80f7f82fa4 | |||
| 1a21012911 | |||
| d4a5269bf1 | |||
| fcf70a3cd4 | |||
| e9411d856c | |||
| 1a6dd00681 | |||
| 330bd0932b | |||
| 250e77f40f | |||
| ef71aba544 | |||
| 567a8f53da | |||
| 88c87aa205 | |||
| 90ac3d56ca | |||
| a298e9e2ca | |||
| abdf86d9c9 | |||
| c58658d820 | |||
| a9b5e6ea13 | |||
| ddb0fdee98 | |||
| 83205f1b49 | |||
| 7221800a16 | |||
| 4515cb6bbe | |||
| 7f9da11eba | |||
| da69d2611d | |||
| 3b4be5695a | |||
| 9d68c9550b | |||
| 3b2d469780 | |||
| ae629d1159 | |||
| 72a6f9cbe0 | |||
| 9793b7461b | |||
| 9c1a824dc4 | |||
| 738ced3327 | |||
| ed1ee1fa55 | |||
| 95776bbc56 | |||
| 62a4beb3d6 | |||
| 466a825f5b | |||
| 3ffed279d7 | |||
| 4b6b36b2d2 | |||
| 2a8f63bf86 | |||
| 3c12cf96a9 | |||
| d787caf0e4 | |||
| 0fc2f32d3d | |||
| 894d5da1d8 | |||
| 985d20d025 | |||
| 94f3e6d0c5 | |||
| 0a196608c7 | |||
| d33f0fb2cf | |||
| ffff69ada0 | |||
| 37a432267d | |||
| 88029a4335 | |||
| 4040eb9619 | |||
| c9663a08da | |||
| a3d92ebc0a | |||
| 6fa825e372 | |||
| 6aefd072c8 | |||
| ac2dd3611f | |||
| 74e628ce9c | |||
| d4ee18ee32 | |||
| 9ff3ee7c0c | |||
| 418b94a45a | |||
| 1393078fe6 | |||
| 50612991fa | |||
| 37b2400cdb | |||
| 05c3393669 | |||
| c60d1e1f9a | |||
| 2be7d3191f | |||
| aa692fdacb | |||
| c163637bfd | |||
| 5552aca079 | |||
| ff2456dcfa | |||
| 539264c396 | |||
| 1acfaf1562 | |||
| a81e277cfa | |||
| b4cb78f33f | |||
| 35c0a9532f | |||
| aff074420b | |||
| edbea9ccff | |||
| 6b26e10ea2 | |||
| a737335fdd | |||
| e15f7d7f28 | |||
| fbf9554a9e | |||
| 5f34b08433 | |||
| f67a03ad66 | |||
| 6095301337 | |||
| 4a774b5885 | |||
| aa8fac3a06 | |||
| b8407f5bf6 | |||
| 989c426211 | |||
| 9a888cfcf1 | |||
| 72ec871729 | |||
| 8d58842c9b | |||
| a90aa5e069 | |||
| 639020a2e1 | |||
| 8e6f915ec6 | |||
| 6631471566 | |||
| b452e751ea | |||
| a3baa100d4 | |||
| f7b9de1261 | |||
| 47ca566d06 | |||
| a943d060d2 | |||
| 1675dab314 | |||
| 996aa367d3 | |||
| be6f342e58 | |||
| 464b558a02 | |||
| d1151091cd | |||
| f8e5383ba2 | |||
| 06f73512df | |||
| 0ff4545bab | |||
| ff6e270886 | |||
| 8aa0b72b67 | |||
| 91766a2162 | |||
| a393097504 | |||
| 2056b86ce7 | |||
| 1b0c013d8e | |||
| 92a09be8c0 | |||
| 1e31cd03ed | |||
| dc863a6e87 | |||
| d74366f413 | |||
| 5bcf2aef8c | |||
| 8de3c4fbd6 | |||
| c191b62245 | |||
| 0babbde00e | |||
| b8af312ab1 | |||
| 38cabfb325 | |||
| 0a3528b5f4 | |||
| 30a672758a | |||
| 723a825085 | |||
| 40e794099a | |||
| 111b037512 | |||
| 52f66717d3 | |||
| 7ac4242a38 | |||
| 4caa4be476 | |||
| c6d8bae147 | |||
| c70310730a | |||
| 2d2b2d08f4 | |||
| 8fe6a5b62d | |||
| 5e6221deb8 | |||
| c3b493f7d4 | |||
| dbcb5b4f63 | |||
| f0640fcea9 | |||
| 64c47a59f8 | |||
| 3450b8f1fe | |||
| 9518cefdd7 | |||
| 32d5c26577 | |||
| ef2cdf27b3 | |||
| e58ac7ae90 | |||
| d786fa4b7c | |||
| 0e3e73989d | |||
| d831599608 | |||
| 1e57926603 | |||
| 1524880eec | |||
| 0bfb623f97 | |||
| 429627494c | |||
| 9feea155fe | |||
| 2717e02d93 | |||
| 18bd803b0d | |||
| c7f078ffcc | |||
| 571cb3d65f | |||
| 8c500c38b1 | |||
| 5644e57e6a | |||
| cfc181eed1 | |||
| 91bea38b8e | |||
| d95c5aa739 | |||
| 0b250b897e | |||
| c6880a0f16 | |||
| beb5ffcbdd | |||
| 0715cac39b | |||
| 41117d873d | |||
| 231e448b1a | |||
| b3b8cd807d | |||
| 9021bbd5de | |||
| 169475ab39 | |||
| c00e01626e | |||
| 05d4a9ef62 | |||
| 17a2ac73e7 | |||
| 6bc6f947dd | |||
| b048a1fb4f | |||
| 363940ee8d | |||
| a64e53479c | |||
| 14fdbe7720 | |||
| f56332c954 | |||
| 21c53c748f | |||
| b12182c1d1 | |||
| d8f27f595a | |||
| b25dc2aaa3 | |||
| 3ec3849e72 | |||
| 2dc1b65718 | |||
| af22f507f4 | |||
| 9958019bf3 | |||
| 02d65972cb | |||
| 24ad893350 | |||
| 9c5792b1e1 | 
@ -1,5 +1,5 @@
 | 
			
		||||
[bumpversion]
 | 
			
		||||
current_version = 0.9.0-pre5
 | 
			
		||||
current_version = 0.9.0-stable
 | 
			
		||||
tag = True
 | 
			
		||||
commit = True
 | 
			
		||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
 | 
			
		||||
@ -15,6 +15,10 @@ values =
 | 
			
		||||
	beta
 | 
			
		||||
	stable
 | 
			
		||||
 | 
			
		||||
[bumpversion:file:README.md]
 | 
			
		||||
 | 
			
		||||
[bumpversion:file:docs/installation/docker-compose.md]
 | 
			
		||||
 | 
			
		||||
[bumpversion:file:helm/values.yaml]
 | 
			
		||||
 | 
			
		||||
[bumpversion:file:helm/Chart.yaml]
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,6 @@ omit =
 | 
			
		||||
    manage.py
 | 
			
		||||
    */migrations/*
 | 
			
		||||
    */apps.py
 | 
			
		||||
    passbook/management/commands/web.py
 | 
			
		||||
    passbook/management/commands/worker.py
 | 
			
		||||
    docs/
 | 
			
		||||
 | 
			
		||||
[report]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										54
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
			
		||||
name: "CodeQL"
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [master, admin-more-info, ci-deploy-dev, gh-pages, provider-saml-v2]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    # The branches below must be a subset of the branches above
 | 
			
		||||
    branches: [master]
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '0 20 * * 2'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  analyse:
 | 
			
		||||
    name: Analyse
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Checkout repository
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
      with:
 | 
			
		||||
        # We must fetch at least the immediate parents so that if this is
 | 
			
		||||
        # a pull request then we can checkout the head.
 | 
			
		||||
        fetch-depth: 2
 | 
			
		||||
 | 
			
		||||
    # If this run was triggered by a pull request event, then checkout
 | 
			
		||||
    # the head of the pull request instead of the merge commit.
 | 
			
		||||
    - run: git checkout HEAD^2
 | 
			
		||||
      if: ${{ github.event_name == 'pull_request' }}
 | 
			
		||||
 | 
			
		||||
    # Initializes the CodeQL tools for scanning.
 | 
			
		||||
    - name: Initialize CodeQL
 | 
			
		||||
      uses: github/codeql-action/init@v1
 | 
			
		||||
      # Override language selection by uncommenting this and choosing your languages
 | 
			
		||||
      # with:
 | 
			
		||||
      #   languages: go, javascript, csharp, python, cpp, java
 | 
			
		||||
 | 
			
		||||
    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
 | 
			
		||||
    # If this step fails, then you should remove it and run the build manually (see below)
 | 
			
		||||
    - name: Autobuild
 | 
			
		||||
      uses: github/codeql-action/autobuild@v1
 | 
			
		||||
 | 
			
		||||
    # ℹ️ Command-line programs to run using the OS shell.
 | 
			
		||||
    # 📚 https://git.io/JvXDl
 | 
			
		||||
 | 
			
		||||
    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
 | 
			
		||||
    #    and modify them (or add more) to build your code if your project
 | 
			
		||||
    #    uses a compiled language
 | 
			
		||||
 | 
			
		||||
    #- run: |
 | 
			
		||||
    #   make bootstrap
 | 
			
		||||
    #   make release
 | 
			
		||||
 | 
			
		||||
    - name: Perform CodeQL Analysis
 | 
			
		||||
      uses: github/codeql-action/analyze@v1
 | 
			
		||||
							
								
								
									
										28
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@ -16,11 +16,11 @@ jobs:
 | 
			
		||||
      - name: Building Docker Image
 | 
			
		||||
        run: docker build
 | 
			
		||||
          --no-cache
 | 
			
		||||
          -t beryju/passbook:0.9.0-pre5
 | 
			
		||||
          -t beryju/passbook:0.9.0-stable
 | 
			
		||||
          -t beryju/passbook:latest
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook:0.9.0-pre5
 | 
			
		||||
        run: docker push beryju/passbook:0.9.0-stable
 | 
			
		||||
      - 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.9.0-pre5 \
 | 
			
		||||
          -t beryju/passbook-gatekeeper:0.9.0-stable \
 | 
			
		||||
          -t beryju/passbook-gatekeeper:latest \
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook-gatekeeper:0.9.0-pre5
 | 
			
		||||
        run: docker push beryju/passbook-gatekeeper:0.9.0-stable
 | 
			
		||||
      - 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.9.0-pre5
 | 
			
		||||
          -t beryju/passbook-static:0.9.0-stable
 | 
			
		||||
          -t beryju/passbook-static:latest
 | 
			
		||||
          -f static.Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook-static:0.9.0-pre5
 | 
			
		||||
        run: docker push beryju/passbook-static:0.9.0-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook-static:latest
 | 
			
		||||
  test-release:
 | 
			
		||||
@ -86,3 +86,19 @@ jobs:
 | 
			
		||||
          docker-compose up --no-start
 | 
			
		||||
          docker-compose start postgresql redis
 | 
			
		||||
          docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
 | 
			
		||||
  sentry-release:
 | 
			
		||||
    needs:
 | 
			
		||||
      - test-release
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v1
 | 
			
		||||
      - name: Create a Sentry.io release
 | 
			
		||||
        uses: tclindner/sentry-releases-action@v1.2.0
 | 
			
		||||
        env:
 | 
			
		||||
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
 | 
			
		||||
          SENTRY_ORG: beryjuorg
 | 
			
		||||
          SENTRY_PROJECT: passbook
 | 
			
		||||
          SENTRY_URL: https://sentry.beryju.org
 | 
			
		||||
        with:
 | 
			
		||||
          tagName: 0.9.0-stable
 | 
			
		||||
          environment: production
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							@ -41,6 +41,7 @@ structlog = "*"
 | 
			
		||||
swagger-spec-validator = "*"
 | 
			
		||||
urllib3 = {extras = ["secure"],version = "*"}
 | 
			
		||||
facebook-sdk = "*"
 | 
			
		||||
elastic-apm = "*"
 | 
			
		||||
 | 
			
		||||
[requires]
 | 
			
		||||
python_version = "3.8"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										382
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										382
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "_meta": {
 | 
			
		||||
        "hash": {
 | 
			
		||||
            "sha256": "fd0192b73c01aaffb90716ce7b6d4e5be9adb8788d3ebd58e54ccd6f85d9b71b"
 | 
			
		||||
            "sha256": "5c22d3a514247b663a07c6492cea09ab140346894a528db06bd805a4a3a4a320"
 | 
			
		||||
        },
 | 
			
		||||
        "pipfile-spec": 6,
 | 
			
		||||
        "requires": {
 | 
			
		||||
@ -18,10 +18,10 @@
 | 
			
		||||
    "default": {
 | 
			
		||||
        "amqp": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:24dbaff8ce4f30566bb88976b398e8c4e77637171af3af6f1b9650f48890e60b",
 | 
			
		||||
                "sha256:bb68f8d2bced8f93ccfd07d96c689b716b3227720add971be980accfc2952139"
 | 
			
		||||
                "sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
 | 
			
		||||
                "sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.6.0"
 | 
			
		||||
            "version": "==2.6.1"
 | 
			
		||||
        },
 | 
			
		||||
        "asgiref": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -46,18 +46,18 @@
 | 
			
		||||
        },
 | 
			
		||||
        "boto3": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:c2a223f4b48782e8b160b2130265e2a66081df111f630a5a384d6909e29a5aa9",
 | 
			
		||||
                "sha256:ce5a4ab6af9e993d1864209cbbb6f4812f65fbc57ad6b95e5967d8bf38b1dcfb"
 | 
			
		||||
                "sha256:35553b05b47fb6b3494bc447428342ca840348ede485e586d002399a32cae0a3",
 | 
			
		||||
                "sha256:e47537d530d523855e52367c2ff278c732651934cd6b33daf9487649dab8e674"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.14.16"
 | 
			
		||||
            "version": "==1.14.33"
 | 
			
		||||
        },
 | 
			
		||||
        "botocore": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:99d995ef99cf77458a661f3fc64e0c3a4ce77ca30facfdf0472f44b2953dd856",
 | 
			
		||||
                "sha256:fe0c4f7cd6b67eff3b7cb8dff6709a65d6fca10b7b7449a493b2036915e98b4c"
 | 
			
		||||
                "sha256:273dbd8e26d4faa568e4cd4ca3180890b59ff0e3e8df7fb352796796c6808527",
 | 
			
		||||
                "sha256:c12a0dc7021fca9d11c2bdbafdc44372e38180b56a1fab97c27b152f79455cd1"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.17.16"
 | 
			
		||||
            "version": "==1.17.33"
 | 
			
		||||
        },
 | 
			
		||||
        "celery": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -76,36 +76,36 @@
 | 
			
		||||
        },
 | 
			
		||||
        "cffi": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
 | 
			
		||||
                "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
 | 
			
		||||
                "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
 | 
			
		||||
                "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
 | 
			
		||||
                "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
 | 
			
		||||
                "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
 | 
			
		||||
                "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
 | 
			
		||||
                "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
 | 
			
		||||
                "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
 | 
			
		||||
                "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
 | 
			
		||||
                "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
 | 
			
		||||
                "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
 | 
			
		||||
                "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
 | 
			
		||||
                "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
 | 
			
		||||
                "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
 | 
			
		||||
                "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
 | 
			
		||||
                "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
 | 
			
		||||
                "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
 | 
			
		||||
                "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
 | 
			
		||||
                "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
 | 
			
		||||
                "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
 | 
			
		||||
                "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
 | 
			
		||||
                "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
 | 
			
		||||
                "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
 | 
			
		||||
                "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
 | 
			
		||||
                "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
 | 
			
		||||
                "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
 | 
			
		||||
                "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
 | 
			
		||||
                "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc",
 | 
			
		||||
                "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9",
 | 
			
		||||
                "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792",
 | 
			
		||||
                "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2",
 | 
			
		||||
                "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022",
 | 
			
		||||
                "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8",
 | 
			
		||||
                "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96",
 | 
			
		||||
                "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2",
 | 
			
		||||
                "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995",
 | 
			
		||||
                "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1",
 | 
			
		||||
                "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849",
 | 
			
		||||
                "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c",
 | 
			
		||||
                "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe",
 | 
			
		||||
                "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3",
 | 
			
		||||
                "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90",
 | 
			
		||||
                "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f",
 | 
			
		||||
                "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1",
 | 
			
		||||
                "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf",
 | 
			
		||||
                "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa",
 | 
			
		||||
                "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc",
 | 
			
		||||
                "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939",
 | 
			
		||||
                "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e",
 | 
			
		||||
                "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0",
 | 
			
		||||
                "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9",
 | 
			
		||||
                "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168",
 | 
			
		||||
                "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33",
 | 
			
		||||
                "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f",
 | 
			
		||||
                "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.14.0"
 | 
			
		||||
            "version": "==1.14.1"
 | 
			
		||||
        },
 | 
			
		||||
        "chardet": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -232,11 +232,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django-prometheus": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:054924b6aedd41f3f76940578127e7fac852a696805f956da39b7941c78bba91",
 | 
			
		||||
                "sha256:208e55e3a11ac9edd53a3b342b033e140ffe27914af1202f0b064edf99e83fc3"
 | 
			
		||||
                "sha256:441bd85531ecdeddacbe73c930f16de926c426869ce388fa1e8c8092f7ee5a1b",
 | 
			
		||||
                "sha256:6e824cd407b56c01810c69d2e296940d00afe609b58818794525f9760a9a5364"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.1.0.dev46"
 | 
			
		||||
            "version": "==2.1.0.dev52"
 | 
			
		||||
        },
 | 
			
		||||
        "django-recaptcha": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -306,6 +306,39 @@
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.0.0"
 | 
			
		||||
        },
 | 
			
		||||
        "elastic-apm": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0c766621a4d15ed4ff7dd195499df1af6d7eb8c13790a727bf05773de2952de0",
 | 
			
		||||
                "sha256:2187a0fd080cac7ed65dabfd64d7693ff187ae9b5ad4a810772387dca6877160",
 | 
			
		||||
                "sha256:2a0bb663d3f9388db233784356f218807b9cfe1f4d4fa4569f41b567c068b50f",
 | 
			
		||||
                "sha256:317e2a897b2a81d79bce42688975cfe0ccf6a3dc8025540c47093ea8ac5f1771",
 | 
			
		||||
                "sha256:3a91d2df89af564dbf0abccb3d370940083205247903fe6d708fa771b16fca38",
 | 
			
		||||
                "sha256:44fe2ce3ea57f97fce5fb32e747f6a9c9b361f5055608d59747c39ae06d1c526",
 | 
			
		||||
                "sha256:4ca9f42d4b841ce598819f2f3a4d516c549cd5c02ab43c8283ca406c3b92a2db",
 | 
			
		||||
                "sha256:56b34b30420aebf9566eeee3ffd633131ce51d1e2a4da6061f143a2b547d1980",
 | 
			
		||||
                "sha256:5a56d20734771a4f7823ec12492fcd17a15dac761ecf1452d034a9b9b8b83388",
 | 
			
		||||
                "sha256:6279cc28bd2f2bc2da478cebd5ace711b52549f736d138f950ebe0fa8f706a6f",
 | 
			
		||||
                "sha256:69bcac2cee8f16a093f57000128caab7d1d3d8ac1474e24ce45190264ffc5ebe",
 | 
			
		||||
                "sha256:7021b931210140e02540f3e56fdc8be07542eed10de82c9e5464dbe449a4c9aa",
 | 
			
		||||
                "sha256:70237e1242ae461500ed455f47a5518abdbdc565e47265eddf3ca1dad530a541",
 | 
			
		||||
                "sha256:7545f27703151ce71d73271a95662735cffb537189c214f778195a6fdab58533",
 | 
			
		||||
                "sha256:8525ba800fbd955b65af667c43889df2358c22b1ef66ee92a846f5f4bc8d7286",
 | 
			
		||||
                "sha256:8ba4239862f0b043d191a19e021637a25c3490f677cb8b1dd752bc425bb382e0",
 | 
			
		||||
                "sha256:8c98625cb825c404954763ca5a6f82e06b833a6e6a9e2035065dc9894b4dc6dc",
 | 
			
		||||
                "sha256:b02394f4d55af4f39086aee7bacf8652fde703f7226c5a564cdae9f7e2bf3f71",
 | 
			
		||||
                "sha256:b3b1815765638ce01f9dbd136822d79e887d8d09cd10bc8770d4cc1d530bb853",
 | 
			
		||||
                "sha256:b7bce10060abd98198d8a96e7f3e2e0e169dbd860c76e2c09e6a8874384eebb7",
 | 
			
		||||
                "sha256:b8f849202dffe97512843dd366c4104d07d3b319e42916e3e031cff3db7475db",
 | 
			
		||||
                "sha256:bc677614c198486ca4ef1026bde0c4efd74b936598ff9d64ea109f978a6381bb",
 | 
			
		||||
                "sha256:d19fe00915c60ceabee42ae8c0aa76c6a48c2ffa67c5ba7f0d0fbb856ac36c09",
 | 
			
		||||
                "sha256:d5561eb57eaa43c721258797dfab67b13938fdc94b7daec7a6ccb56dc524fe02",
 | 
			
		||||
                "sha256:dc04aa32c7a3a17c688e3cc4c6293f2176be2482d67efccc651ff1fbb5c00ed6",
 | 
			
		||||
                "sha256:e0d2c3463061b0e50ca53530bd5317498517d208618d90cf6e9933e93f9c727e",
 | 
			
		||||
                "sha256:e9a416418cb2f6deb7a18b68bd75dad0552b4fd85d3e72e59ae4add0e8739b1c"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==5.8.1"
 | 
			
		||||
        },
 | 
			
		||||
        "facebook-sdk": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e",
 | 
			
		||||
@ -380,36 +413,40 @@
 | 
			
		||||
        },
 | 
			
		||||
        "lxml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:06748c7192eab0f48e3d35a7adae609a329c6257495d5e53878003660dc0fec6",
 | 
			
		||||
                "sha256:0790ddca3f825dd914978c94c2545dbea5f56f008b050e835403714babe62a5f",
 | 
			
		||||
                "sha256:1aa7a6197c1cdd65d974f3e4953764eee3d9c7b67e3966616b41fab7f8f516b7",
 | 
			
		||||
                "sha256:22c6d34fdb0e65d5f782a4d1a1edb52e0a8365858dafb1c08cb1d16546cf0786",
 | 
			
		||||
                "sha256:2754d4406438c83144f9ffd3628bbe2dcc6d62b20dbc5c1ec4bc4385e5d44b42",
 | 
			
		||||
                "sha256:27ee0faf8077c7c1a589573b1450743011117f1aa1a91d5ae776bbc5ca6070f2",
 | 
			
		||||
                "sha256:2b02c106709466a93ed424454ce4c970791c486d5fcdf52b0d822a7e29789626",
 | 
			
		||||
                "sha256:2d1ddce96cf15f1254a68dba6935e6e0f1fe39247de631c115e84dd404a6f031",
 | 
			
		||||
                "sha256:4f282737d187ae723b2633856085c31ae5d4d432968b7f3f478a48a54835f5c4",
 | 
			
		||||
                "sha256:51bb4edeb36d24ec97eb3e6a6007be128b720114f9a875d6b370317d62ac80b9",
 | 
			
		||||
                "sha256:7eee37c1b9815e6505847aa5e68f192e8a1b730c5c7ead39ff317fde9ce29448",
 | 
			
		||||
                "sha256:7fd88cb91a470b383aafad554c3fe1ccf6dfb2456ff0e84b95335d582a799804",
 | 
			
		||||
                "sha256:9144ce36ca0824b29ebc2e02ca186e54040ebb224292072250467190fb613b96",
 | 
			
		||||
                "sha256:925baf6ff1ef2c45169f548cc85204433e061360bfa7d01e1be7ae38bef73194",
 | 
			
		||||
                "sha256:a636346c6c0e1092ffc202d97ec1843a75937d8c98aaf6771348ad6422e44bb0",
 | 
			
		||||
                "sha256:a87dbee7ad9dce3aaefada2081843caf08a44a8f52e03e0a4cc5819f8398f2f4",
 | 
			
		||||
                "sha256:a9e3b8011388e7e373565daa5e92f6c9cb844790dc18e43073212bb3e76f7007",
 | 
			
		||||
                "sha256:afb53edf1046599991fb4a7d03e601ab5f5422a5435c47ee6ba91ec3b61416a6",
 | 
			
		||||
                "sha256:b26719890c79a1dae7d53acac5f089d66fd8cc68a81f4e4bd355e45470dc25e1",
 | 
			
		||||
                "sha256:b7462cdab6fffcda853338e1741ce99706cdf880d921b5a769202ea7b94e8528",
 | 
			
		||||
                "sha256:b77975465234ff49fdad871c08aa747aae06f5e5be62866595057c43f8d2f62c",
 | 
			
		||||
                "sha256:c47a8a5d00060122ca5908909478abce7bbf62d812e3fc35c6c802df8fb01fe7",
 | 
			
		||||
                "sha256:c79e5debbe092e3c93ca4aee44c9a7631bdd407b2871cb541b979fd350bbbc29",
 | 
			
		||||
                "sha256:d8d40e0121ca1606aa9e78c28a3a7d88a05c06b3ca61630242cded87d8ce55fa",
 | 
			
		||||
                "sha256:ee2be8b8f72a2772e72ab926a3bccebf47bb727bda41ae070dc91d1fb759b726",
 | 
			
		||||
                "sha256:f95d28193c3863132b1f55c1056036bf580b5a488d908f7d22a04ace8935a3a9",
 | 
			
		||||
                "sha256:fadd2a63a2bfd7fb604508e553d1cf68eca250b2fbdbd81213b5f6f2fbf23529"
 | 
			
		||||
                "sha256:05a444b207901a68a6526948c7cc8f9fe6d6f24c70781488e32fd74ff5996e3f",
 | 
			
		||||
                "sha256:08fc93257dcfe9542c0a6883a25ba4971d78297f63d7a5a26ffa34861ca78730",
 | 
			
		||||
                "sha256:107781b213cf7201ec3806555657ccda67b1fccc4261fb889ef7fc56976db81f",
 | 
			
		||||
                "sha256:121b665b04083a1e85ff1f5243d4a93aa1aaba281bc12ea334d5a187278ceaf1",
 | 
			
		||||
                "sha256:1fa21263c3aba2b76fd7c45713d4428dbcc7644d73dcf0650e9d344e433741b3",
 | 
			
		||||
                "sha256:2b30aa2bcff8e958cd85d907d5109820b01ac511eae5b460803430a7404e34d7",
 | 
			
		||||
                "sha256:4b4a111bcf4b9c948e020fd207f915c24a6de3f1adc7682a2d92660eb4e84f1a",
 | 
			
		||||
                "sha256:5591c4164755778e29e69b86e425880f852464a21c7bb53c7ea453bbe2633bbe",
 | 
			
		||||
                "sha256:59daa84aef650b11bccd18f99f64bfe44b9f14a08a28259959d33676554065a1",
 | 
			
		||||
                "sha256:5a9c8d11aa2c8f8b6043d845927a51eb9102eb558e3f936df494e96393f5fd3e",
 | 
			
		||||
                "sha256:5dd20538a60c4cc9a077d3b715bb42307239fcd25ef1ca7286775f95e9e9a46d",
 | 
			
		||||
                "sha256:74f48ec98430e06c1fa8949b49ebdd8d27ceb9df8d3d1c92e1fdc2773f003f20",
 | 
			
		||||
                "sha256:786aad2aa20de3dbff21aab86b2fb6a7be68064cbbc0219bde414d3a30aa47ae",
 | 
			
		||||
                "sha256:7ad7906e098ccd30d8f7068030a0b16668ab8aa5cda6fcd5146d8d20cbaa71b5",
 | 
			
		||||
                "sha256:80a38b188d20c0524fe8959c8ce770a8fdf0e617c6912d23fc97c68301bb9aba",
 | 
			
		||||
                "sha256:8f0ec6b9b3832e0bd1d57af41f9238ea7709bbd7271f639024f2fc9d3bb01293",
 | 
			
		||||
                "sha256:92282c83547a9add85ad658143c76a64a8d339028926d7dc1998ca029c88ea6a",
 | 
			
		||||
                "sha256:94150231f1e90c9595ccc80d7d2006c61f90a5995db82bccbca7944fd457f0f6",
 | 
			
		||||
                "sha256:9dc9006dcc47e00a8a6a029eb035c8f696ad38e40a27d073a003d7d1443f5d88",
 | 
			
		||||
                "sha256:a76979f728dd845655026ab991df25d26379a1a8fc1e9e68e25c7eda43004bed",
 | 
			
		||||
                "sha256:aa8eba3db3d8761db161003e2d0586608092e217151d7458206e243be5a43843",
 | 
			
		||||
                "sha256:bea760a63ce9bba566c23f726d72b3c0250e2fa2569909e2d83cda1534c79443",
 | 
			
		||||
                "sha256:c3f511a3c58676147c277eff0224c061dd5a6a8e1373572ac817ac6324f1b1e0",
 | 
			
		||||
                "sha256:c9d317efde4bafbc1561509bfa8a23c5cab66c44d49ab5b63ff690f5159b2304",
 | 
			
		||||
                "sha256:cc411ad324a4486b142c41d9b2b6a722c534096963688d879ea6fa8a35028258",
 | 
			
		||||
                "sha256:cdc13a1682b2a6241080745b1953719e7fe0850b40a5c71ca574f090a1391df6",
 | 
			
		||||
                "sha256:cfd7c5dd3c35c19cec59c63df9571c67c6d6e5c92e0fe63517920e97f61106d1",
 | 
			
		||||
                "sha256:e1cacf4796b20865789083252186ce9dc6cc59eca0c2e79cca332bdff24ac481",
 | 
			
		||||
                "sha256:e70d4e467e243455492f5de463b72151cc400710ac03a0678206a5f27e79ddef",
 | 
			
		||||
                "sha256:ecc930ae559ea8a43377e8b60ca6f8d61ac532fc57efb915d899de4a67928efd",
 | 
			
		||||
                "sha256:f161af26f596131b63b236372e4ce40f3167c1b5b5d459b29d2514bd8c9dc9ee"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==4.5.1"
 | 
			
		||||
            "version": "==4.5.2"
 | 
			
		||||
        },
 | 
			
		||||
        "markupsafe": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -735,11 +772,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "sentry-sdk": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:da06bc3641e81ec2c942f87a0676cd9180044fa3d1697524a0005345997542e2",
 | 
			
		||||
                "sha256:e80d61af85d99a1222c1a3e2a24023618374cd50a99673aa7fa3cf920e7d813b"
 | 
			
		||||
                "sha256:2de15b13836fa3522815a933bd9c887c77f4868071043349f94f1b896c1bcfb8",
 | 
			
		||||
                "sha256:38bb09d0277117f76507c8728d9a5156f09a47ac5175bb8072513859d19a593b"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==0.16.0"
 | 
			
		||||
            "version": "==0.16.2"
 | 
			
		||||
        },
 | 
			
		||||
        "service-identity": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -799,12 +836,12 @@
 | 
			
		||||
                "secure"
 | 
			
		||||
            ],
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
 | 
			
		||||
                "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
 | 
			
		||||
                "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
 | 
			
		||||
                "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "markers": null,
 | 
			
		||||
            "version": "==1.25.9"
 | 
			
		||||
            "version": "==1.25.10"
 | 
			
		||||
        },
 | 
			
		||||
        "vine": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -890,36 +927,36 @@
 | 
			
		||||
        },
 | 
			
		||||
        "cffi": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff",
 | 
			
		||||
                "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b",
 | 
			
		||||
                "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac",
 | 
			
		||||
                "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0",
 | 
			
		||||
                "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384",
 | 
			
		||||
                "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26",
 | 
			
		||||
                "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6",
 | 
			
		||||
                "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b",
 | 
			
		||||
                "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e",
 | 
			
		||||
                "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd",
 | 
			
		||||
                "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2",
 | 
			
		||||
                "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66",
 | 
			
		||||
                "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc",
 | 
			
		||||
                "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8",
 | 
			
		||||
                "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55",
 | 
			
		||||
                "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4",
 | 
			
		||||
                "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5",
 | 
			
		||||
                "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d",
 | 
			
		||||
                "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78",
 | 
			
		||||
                "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa",
 | 
			
		||||
                "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793",
 | 
			
		||||
                "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f",
 | 
			
		||||
                "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a",
 | 
			
		||||
                "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f",
 | 
			
		||||
                "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30",
 | 
			
		||||
                "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f",
 | 
			
		||||
                "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3",
 | 
			
		||||
                "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"
 | 
			
		||||
                "sha256:267adcf6e68d77ba154334a3e4fc921b8e63cbb38ca00d33d40655d4228502bc",
 | 
			
		||||
                "sha256:26f33e8f6a70c255767e3c3f957ccafc7f1f706b966e110b855bfe944511f1f9",
 | 
			
		||||
                "sha256:3cd2c044517f38d1b577f05927fb9729d3396f1d44d0c659a445599e79519792",
 | 
			
		||||
                "sha256:4a03416915b82b81af5502459a8a9dd62a3c299b295dcdf470877cb948d655f2",
 | 
			
		||||
                "sha256:4ce1e995aeecf7cc32380bc11598bfdfa017d592259d5da00fc7ded11e61d022",
 | 
			
		||||
                "sha256:4f53e4128c81ca3212ff4cf097c797ab44646a40b42ec02a891155cd7a2ba4d8",
 | 
			
		||||
                "sha256:4fa72a52a906425416f41738728268072d5acfd48cbe7796af07a923236bcf96",
 | 
			
		||||
                "sha256:66dd45eb9530e3dde8f7c009f84568bc7cac489b93d04ac86e3111fb46e470c2",
 | 
			
		||||
                "sha256:6923d077d9ae9e8bacbdb1c07ae78405a9306c8fd1af13bfa06ca891095eb995",
 | 
			
		||||
                "sha256:833401b15de1bb92791d7b6fb353d4af60dc688eaa521bd97203dcd2d124a7c1",
 | 
			
		||||
                "sha256:8416ed88ddc057bab0526d4e4e9f3660f614ac2394b5e019a628cdfff3733849",
 | 
			
		||||
                "sha256:892daa86384994fdf4856cb43c93f40cbe80f7f95bb5da94971b39c7f54b3a9c",
 | 
			
		||||
                "sha256:98be759efdb5e5fa161e46d404f4e0ce388e72fbf7d9baf010aff16689e22abe",
 | 
			
		||||
                "sha256:a6d28e7f14ecf3b2ad67c4f106841218c8ab12a0683b1528534a6c87d2307af3",
 | 
			
		||||
                "sha256:b1d6ebc891607e71fd9da71688fcf332a6630b7f5b7f5549e6e631821c0e5d90",
 | 
			
		||||
                "sha256:b2a2b0d276a136146e012154baefaea2758ef1f56ae9f4e01c612b0831e0bd2f",
 | 
			
		||||
                "sha256:b87dfa9f10a470eee7f24234a37d1d5f51e5f5fa9eeffda7c282e2b8f5162eb1",
 | 
			
		||||
                "sha256:bac0d6f7728a9cc3c1e06d4fcbac12aaa70e9379b3025b27ec1226f0e2d404cf",
 | 
			
		||||
                "sha256:c991112622baee0ae4d55c008380c32ecfd0ad417bcd0417ba432e6ba7328caa",
 | 
			
		||||
                "sha256:cda422d54ee7905bfc53ee6915ab68fe7b230cacf581110df4272ee10462aadc",
 | 
			
		||||
                "sha256:d3148b6ba3923c5850ea197a91a42683f946dba7e8eb82dfa211ab7e708de939",
 | 
			
		||||
                "sha256:d6033b4ffa34ef70f0b8086fd4c3df4bf801fee485a8a7d4519399818351aa8e",
 | 
			
		||||
                "sha256:ddff0b2bd7edcc8c82d1adde6dbbf5e60d57ce985402541cd2985c27f7bec2a0",
 | 
			
		||||
                "sha256:e23cb7f1d8e0f93addf0cae3c5b6f00324cccb4a7949ee558d7b6ca973ab8ae9",
 | 
			
		||||
                "sha256:effd2ba52cee4ceff1a77f20d2a9f9bf8d50353c854a282b8760ac15b9833168",
 | 
			
		||||
                "sha256:f90c2267101010de42f7273c94a1f026e56cbc043f9330acd8a80e64300aba33",
 | 
			
		||||
                "sha256:f960375e9823ae6a07072ff7f8a85954e5a6434f97869f50d0e41649a1c8144f",
 | 
			
		||||
                "sha256:fcf32bf76dc25e30ed793145a57426064520890d7c02866eb93d3e4abe516948"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.14.0"
 | 
			
		||||
            "version": "==1.14.1"
 | 
			
		||||
        },
 | 
			
		||||
        "chardet": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -945,40 +982,43 @@
 | 
			
		||||
        },
 | 
			
		||||
        "coverage": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
 | 
			
		||||
                "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
 | 
			
		||||
                "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
 | 
			
		||||
                "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
 | 
			
		||||
                "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
 | 
			
		||||
                "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
 | 
			
		||||
                "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
 | 
			
		||||
                "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
 | 
			
		||||
                "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
 | 
			
		||||
                "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
 | 
			
		||||
                "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
 | 
			
		||||
                "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
 | 
			
		||||
                "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
 | 
			
		||||
                "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
 | 
			
		||||
                "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
 | 
			
		||||
                "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
 | 
			
		||||
                "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
 | 
			
		||||
                "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
 | 
			
		||||
                "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
 | 
			
		||||
                "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
 | 
			
		||||
                "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
 | 
			
		||||
                "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
 | 
			
		||||
                "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
 | 
			
		||||
                "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
 | 
			
		||||
                "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
 | 
			
		||||
                "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
 | 
			
		||||
                "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
 | 
			
		||||
                "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
 | 
			
		||||
                "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
 | 
			
		||||
                "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
 | 
			
		||||
                "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
 | 
			
		||||
                "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb",
 | 
			
		||||
                "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3",
 | 
			
		||||
                "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716",
 | 
			
		||||
                "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034",
 | 
			
		||||
                "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3",
 | 
			
		||||
                "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8",
 | 
			
		||||
                "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0",
 | 
			
		||||
                "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f",
 | 
			
		||||
                "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4",
 | 
			
		||||
                "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962",
 | 
			
		||||
                "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d",
 | 
			
		||||
                "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b",
 | 
			
		||||
                "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4",
 | 
			
		||||
                "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3",
 | 
			
		||||
                "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258",
 | 
			
		||||
                "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59",
 | 
			
		||||
                "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01",
 | 
			
		||||
                "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd",
 | 
			
		||||
                "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b",
 | 
			
		||||
                "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d",
 | 
			
		||||
                "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89",
 | 
			
		||||
                "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd",
 | 
			
		||||
                "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b",
 | 
			
		||||
                "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d",
 | 
			
		||||
                "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46",
 | 
			
		||||
                "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546",
 | 
			
		||||
                "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082",
 | 
			
		||||
                "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b",
 | 
			
		||||
                "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4",
 | 
			
		||||
                "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8",
 | 
			
		||||
                "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811",
 | 
			
		||||
                "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd",
 | 
			
		||||
                "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651",
 | 
			
		||||
                "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==5.1"
 | 
			
		||||
            "version": "==5.2.1"
 | 
			
		||||
        },
 | 
			
		||||
        "cryptography": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1037,10 +1077,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "gitpython": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:e107af4d873daed64648b4f4beb89f89f0cfbe3ef558fc7821ed2331c2f8da1a",
 | 
			
		||||
                "sha256:ef1d60b01b5ce0040ad3ec20bc64f783362d41fa0822a2742d3586e1f49bb8ac"
 | 
			
		||||
                "sha256:2db287d71a284e22e5c2846042d0602465c7434d910406990d5b74df4afb0858",
 | 
			
		||||
                "sha256:fa3b92da728a457dd75d62bb5f3eb2816d99a7fe6c67398e260637a40e3fafb5"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.1.3"
 | 
			
		||||
            "version": "==3.1.7"
 | 
			
		||||
        },
 | 
			
		||||
        "idna": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1127,11 +1167,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pylint-django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:06a64331c498a3f049ba669dc0c174b92209e164198d43e589b1096ee616d5f8",
 | 
			
		||||
                "sha256:3d3436ba8d0fae576ae2db160e33a8f2746a101fda4463f2b3ff3a8b6fccec38"
 | 
			
		||||
                "sha256:20e4d5f3987e96d29ce51ef24f13187f0d23f37a0558b6eed9b5571487ba3f4c",
 | 
			
		||||
                "sha256:d47f278f2ef9244decc006a7412d0ea6bebe1594e6b5402703febbac036ba401"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.0.15"
 | 
			
		||||
            "version": "==2.2.0"
 | 
			
		||||
        },
 | 
			
		||||
        "pylint-plugin-utils": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1173,29 +1213,29 @@
 | 
			
		||||
        },
 | 
			
		||||
        "regex": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:08997a37b221a3e27d68ffb601e45abfb0093d39ee770e4257bd2f5115e8cb0a",
 | 
			
		||||
                "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938",
 | 
			
		||||
                "sha256:1700419d8a18c26ff396b3b06ace315b5f2a6e780dad387e4c48717a12a22c29",
 | 
			
		||||
                "sha256:2f6f211633ee8d3f7706953e9d3edc7ce63a1d6aad0be5dcee1ece127eea13ae",
 | 
			
		||||
                "sha256:52e1b4bef02f4040b2fd547357a170fc1146e60ab310cdbdd098db86e929b387",
 | 
			
		||||
                "sha256:55b4c25cbb3b29f8d5e63aeed27b49fa0f8476b0d4e1b3171d85db891938cc3a",
 | 
			
		||||
                "sha256:5aaa5928b039ae440d775acea11d01e42ff26e1561c0ffcd3d805750973c6baf",
 | 
			
		||||
                "sha256:654cb773b2792e50151f0e22be0f2b6e1c3a04c5328ff1d9d59c0398d37ef610",
 | 
			
		||||
                "sha256:690f858d9a94d903cf5cada62ce069b5d93b313d7d05456dbcd99420856562d9",
 | 
			
		||||
                "sha256:6ad8663c17db4c5ef438141f99e291c4d4edfeaacc0ce28b5bba2b0bf273d9b5",
 | 
			
		||||
                "sha256:89cda1a5d3e33ec9e231ece7307afc101b5217523d55ef4dc7fb2abd6de71ba3",
 | 
			
		||||
                "sha256:92d8a043a4241a710c1cf7593f5577fbb832cf6c3a00ff3fc1ff2052aff5dd89",
 | 
			
		||||
                "sha256:95fa7726d073c87141f7bbfb04c284901f8328e2d430eeb71b8ffdd5742a5ded",
 | 
			
		||||
                "sha256:97712e0d0af05febd8ab63d2ef0ab2d0cd9deddf4476f7aa153f76feef4b2754",
 | 
			
		||||
                "sha256:b2ba0f78b3ef375114856cbdaa30559914d081c416b431f2437f83ce4f8b7f2f",
 | 
			
		||||
                "sha256:bae83f2a56ab30d5353b47f9b2a33e4aac4de9401fb582b55c42b132a8ac3868",
 | 
			
		||||
                "sha256:c78e66a922de1c95a208e4ec02e2e5cf0bb83a36ceececc10a72841e53fbf2bd",
 | 
			
		||||
                "sha256:cf59bbf282b627130f5ba68b7fa3abdb96372b24b66bdf72a4920e8153fc7910",
 | 
			
		||||
                "sha256:e3cdc9423808f7e1bb9c2e0bdb1c9dc37b0607b30d646ff6faf0d4e41ee8fee3",
 | 
			
		||||
                "sha256:e9b64e609d37438f7d6e68c2546d2cb8062f3adb27e6336bc129b51be20773ac",
 | 
			
		||||
                "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"
 | 
			
		||||
                "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204",
 | 
			
		||||
                "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162",
 | 
			
		||||
                "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f",
 | 
			
		||||
                "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb",
 | 
			
		||||
                "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6",
 | 
			
		||||
                "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7",
 | 
			
		||||
                "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88",
 | 
			
		||||
                "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99",
 | 
			
		||||
                "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644",
 | 
			
		||||
                "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a",
 | 
			
		||||
                "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840",
 | 
			
		||||
                "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067",
 | 
			
		||||
                "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd",
 | 
			
		||||
                "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4",
 | 
			
		||||
                "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e",
 | 
			
		||||
                "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89",
 | 
			
		||||
                "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e",
 | 
			
		||||
                "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc",
 | 
			
		||||
                "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf",
 | 
			
		||||
                "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341",
 | 
			
		||||
                "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2020.6.8"
 | 
			
		||||
            "version": "==2020.7.14"
 | 
			
		||||
        },
 | 
			
		||||
        "requests": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1235,10 +1275,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "stevedore": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:609912b87df5ad338ff8e44d13eaad4f4170a65b79ae9cb0aa5632598994a1b7",
 | 
			
		||||
                "sha256:c4724f8d7b8f6be42130663855d01a9c2414d6046055b5a65ab58a0e38637688"
 | 
			
		||||
                "sha256:38791aa5bed922b0a844513c5f9ed37774b68edc609e5ab8ab8d8fe0ce4315e5",
 | 
			
		||||
                "sha256:c8f4f0ebbc394e52ddf49de8bcc3cf8ad2b4425ebac494106bbc5e3661ac7633"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.0.1"
 | 
			
		||||
            "version": "==3.2.0"
 | 
			
		||||
        },
 | 
			
		||||
        "toml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1286,12 +1326,12 @@
 | 
			
		||||
                "secure"
 | 
			
		||||
            ],
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
 | 
			
		||||
                "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
 | 
			
		||||
                "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
 | 
			
		||||
                "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "markers": null,
 | 
			
		||||
            "version": "==1.25.9"
 | 
			
		||||
            "version": "==1.25.10"
 | 
			
		||||
        },
 | 
			
		||||
        "websocket-client": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,12 @@
 | 
			
		||||
<img src="passbook/static/static/passbook/logo.svg" height="50" alt="passbook logo"><img src="passbook/static/static/passbook/brand_inverted.svg" height="50" alt="passbook">
 | 
			
		||||
 | 
			
		||||
=======
 | 
			
		||||

 | 
			
		||||
[](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
 | 
			
		||||

 | 
			
		||||
[](https://codecov.io/gh/BeryJu/passbook)
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## What is passbook?
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
 | 
			
		||||
# Optionally enable Error-reporting
 | 
			
		||||
# export PASSBOOK_ERROR_REPORTING=true
 | 
			
		||||
# Optionally deploy a different version
 | 
			
		||||
# export PASSBOOK_TAG=0.8.15-beta
 | 
			
		||||
# export PASSBOOK_TAG=0.9.0-stable
 | 
			
		||||
# If this is a productive installation, set a different PostgreSQL Password
 | 
			
		||||
# export PG_PASS=$(pwgen 40 1)
 | 
			
		||||
docker-compose pull
 | 
			
		||||
 | 
			
		||||
@ -106,7 +106,7 @@ stages:
 | 
			
		||||
          - task: DockerCompose@0
 | 
			
		||||
            displayName: Run services
 | 
			
		||||
            inputs:
 | 
			
		||||
              dockerComposeFile: 'scripts/docker-compose.yml'
 | 
			
		||||
              dockerComposeFile: 'scripts/ci.docker-compose.yml'
 | 
			
		||||
              action: 'Run services'
 | 
			
		||||
              buildImages: false
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
@ -117,7 +117,7 @@ stages:
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: pipenv run ./manage.py migrate
 | 
			
		||||
      - job: coverage
 | 
			
		||||
      - job: coverage_unittest
 | 
			
		||||
        pool:
 | 
			
		||||
          vmImage: 'ubuntu-latest'
 | 
			
		||||
        steps:
 | 
			
		||||
@ -127,7 +127,38 @@ stages:
 | 
			
		||||
          - task: DockerCompose@0
 | 
			
		||||
            displayName: Run services
 | 
			
		||||
            inputs:
 | 
			
		||||
              dockerComposeFile: 'scripts/docker-compose.yml'
 | 
			
		||||
              dockerComposeFile: 'scripts/ci.docker-compose.yml'
 | 
			
		||||
              action: 'Run services'
 | 
			
		||||
              buildImages: false
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
                sudo pip install -U wheel pipenv
 | 
			
		||||
                pipenv install --dev
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            displayName: Run full test suite
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
                pipenv run coverage run ./manage.py test passbook
 | 
			
		||||
                mkdir output-unittest
 | 
			
		||||
                mv unittest.xml output-unittest/unittest.xml
 | 
			
		||||
                mv .coverage output-unittest/coverage
 | 
			
		||||
          - task: PublishPipelineArtifact@1
 | 
			
		||||
            inputs:
 | 
			
		||||
              targetPath: 'output-unittest/'
 | 
			
		||||
              artifact: 'coverage-unittest'
 | 
			
		||||
              publishLocation: 'pipeline'
 | 
			
		||||
      - job: coverage_e2e
 | 
			
		||||
        pool:
 | 
			
		||||
          vmImage: 'ubuntu-latest'
 | 
			
		||||
        steps:
 | 
			
		||||
          - task: UsePythonVersion@0
 | 
			
		||||
            inputs:
 | 
			
		||||
              versionSpec: '3.8'
 | 
			
		||||
          - task: DockerCompose@0
 | 
			
		||||
            displayName: Run services
 | 
			
		||||
            inputs:
 | 
			
		||||
              dockerComposeFile: 'scripts/ci.docker-compose.yml'
 | 
			
		||||
              action: 'Run services'
 | 
			
		||||
              buildImages: false
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
@ -138,7 +169,7 @@ stages:
 | 
			
		||||
          - task: DockerCompose@0
 | 
			
		||||
            displayName: Run ChromeDriver
 | 
			
		||||
            inputs:
 | 
			
		||||
              dockerComposeFile: 'e2e/docker-compose.yml'
 | 
			
		||||
              dockerComposeFile: 'e2e/ci.docker-compose.yml'
 | 
			
		||||
              action: 'Run a specific service'
 | 
			
		||||
              serviceName: 'chrome'
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
@ -150,28 +181,68 @@ stages:
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            displayName: Run full test suite
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: pipenv run coverage run ./manage.py test --failfast
 | 
			
		||||
          - task: PublishBuildArtifacts@1
 | 
			
		||||
              script: pipenv run coverage run ./manage.py test e2e
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            displayName: Prepare unittests and coverage for upload
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
                mkdir output-e2e
 | 
			
		||||
                mv unittest.xml output-e2e/unittest.xml
 | 
			
		||||
                mv .coverage output-e2e/coverage
 | 
			
		||||
          - task: PublishPipelineArtifact@1
 | 
			
		||||
            condition: failed()
 | 
			
		||||
            displayName: Upload screenshots if selenium tests fail
 | 
			
		||||
            inputs:
 | 
			
		||||
              PathtoPublish: 'selenium_screenshots/'
 | 
			
		||||
              ArtifactName: 'drop'
 | 
			
		||||
              publishLocation: 'Container'
 | 
			
		||||
              targetPath: 'selenium_screenshots/'
 | 
			
		||||
              artifact: 'selenium screenshots'
 | 
			
		||||
              publishLocation: 'pipeline'
 | 
			
		||||
          - task: PublishPipelineArtifact@1
 | 
			
		||||
            inputs:
 | 
			
		||||
              targetPath: 'output-e2e/'
 | 
			
		||||
              artifact: 'coverage-e2e'
 | 
			
		||||
              publishLocation: 'pipeline'
 | 
			
		||||
  - stage: test_combine
 | 
			
		||||
    jobs:
 | 
			
		||||
      - job: test_coverage_combine
 | 
			
		||||
        pool:
 | 
			
		||||
          vmImage: 'ubuntu-latest'
 | 
			
		||||
        steps:
 | 
			
		||||
          - task: DownloadPipelineArtifact@2
 | 
			
		||||
            inputs:
 | 
			
		||||
              buildType: 'current'
 | 
			
		||||
              artifactName: 'coverage-e2e'
 | 
			
		||||
              path: "coverage-e2e/"
 | 
			
		||||
          - task: DownloadPipelineArtifact@2
 | 
			
		||||
            inputs:
 | 
			
		||||
              buildType: 'current'
 | 
			
		||||
              artifactName: 'coverage-unittest'
 | 
			
		||||
              path: "coverage-unittest/"
 | 
			
		||||
          - task: UsePythonVersion@0
 | 
			
		||||
            inputs:
 | 
			
		||||
              versionSpec: '3.8'
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            inputs:
 | 
			
		||||
              script: |
 | 
			
		||||
                sudo pip install -U wheel pipenv
 | 
			
		||||
                pipenv install --dev
 | 
			
		||||
                find .
 | 
			
		||||
                pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
 | 
			
		||||
                pipenv run coverage xml
 | 
			
		||||
                pipenv run coverage html
 | 
			
		||||
                find .
 | 
			
		||||
          - task: PublishCodeCoverageResults@1
 | 
			
		||||
            inputs:
 | 
			
		||||
              codeCoverageTool: Cobertura
 | 
			
		||||
              codeCoverageTool: 'Cobertura'
 | 
			
		||||
              summaryFileLocation: 'coverage.xml'
 | 
			
		||||
              pathToSources: '$(System.DefaultWorkingDirectory)'
 | 
			
		||||
          - task: PublishTestResults@2
 | 
			
		||||
            condition: succeededOrFailed()
 | 
			
		||||
            inputs:
 | 
			
		||||
              testRunTitle: 'Publish test results for Python $(python.version)'
 | 
			
		||||
              testResultsFiles: 'unittest.xml'
 | 
			
		||||
              testResultsFormat: 'JUnit'
 | 
			
		||||
              testResultsFiles: |
 | 
			
		||||
                coverage-e2e/unittest.xml
 | 
			
		||||
                coverage-unittest/unittest.xml
 | 
			
		||||
              mergeTestResults: true
 | 
			
		||||
          - task: CmdLine@2
 | 
			
		||||
            env:
 | 
			
		||||
              CODECOV_TOKEN: $(CODECOV_TOKEN)
 | 
			
		||||
@ -194,15 +265,13 @@ stages:
 | 
			
		||||
        pool:
 | 
			
		||||
          vmImage: 'ubuntu-latest'
 | 
			
		||||
        steps:
 | 
			
		||||
        - task: CmdLine@2
 | 
			
		||||
          inputs:
 | 
			
		||||
            script: cd gatekeeper
 | 
			
		||||
        - task: Docker@2
 | 
			
		||||
          inputs:
 | 
			
		||||
            containerRegistry: 'dockerhub'
 | 
			
		||||
            repository: 'beryju/passbook-gatekeeper'
 | 
			
		||||
            command: 'buildAndPush'
 | 
			
		||||
            Dockerfile: 'Dockerfile'
 | 
			
		||||
            Dockerfile: 'gatekeeper/Dockerfile'
 | 
			
		||||
            buildContext: 'gatekeeper/'
 | 
			
		||||
            tags: 'gh-$(Build.SourceBranchName)'
 | 
			
		||||
      - job: build_static
 | 
			
		||||
        pool:
 | 
			
		||||
@ -211,7 +280,7 @@ stages:
 | 
			
		||||
        - task: DockerCompose@0
 | 
			
		||||
          displayName: Run services
 | 
			
		||||
          inputs:
 | 
			
		||||
            dockerComposeFile: 'scripts/docker-compose.yml'
 | 
			
		||||
            dockerComposeFile: 'scripts/ci.docker-compose.yml'
 | 
			
		||||
            action: 'Run services'
 | 
			
		||||
            buildImages: false
 | 
			
		||||
        - task: Docker@2
 | 
			
		||||
 | 
			
		||||
@ -62,7 +62,7 @@ services:
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
    labels:
 | 
			
		||||
      - traefik.frontend.rule=PathPrefix:/static, /robots.txt
 | 
			
		||||
      - traefik.frontend.rule=PathPrefix:/static, /robots.txt, /favicon.ico
 | 
			
		||||
      - traefik.port=80
 | 
			
		||||
      - traefik.docker.network=internal
 | 
			
		||||
  traefik:
 | 
			
		||||
 | 
			
		||||
@ -53,3 +53,14 @@ Example:
 | 
			
		||||
```python
 | 
			
		||||
other_user = pb_user_by(username="other_user")
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Comparing IP Addresses
 | 
			
		||||
 | 
			
		||||
To compare IP Addresses or check if an IP Address is within a given subnet, you can use the functions `ip_address('192.0.2.1')` and `ip_network('192.0.2.0/24')`. With these objects you can do [arithmetic operations](https://docs.python.org/3/library/ipaddress.html#operators).
 | 
			
		||||
 | 
			
		||||
You can also check if an IP Address is within a subnet by writing the following:
 | 
			
		||||
 | 
			
		||||
```python
 | 
			
		||||
ip_address('192.0.2.1') in ip_network('192.0.2.0/24')
 | 
			
		||||
# evaluates to True
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 253 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 338 KiB  | 
@ -16,7 +16,7 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
 | 
			
		||||
# Optionally enable Error-reporting
 | 
			
		||||
# export PASSBOOK_ERROR_REPORTING=true
 | 
			
		||||
# Optionally deploy a different version
 | 
			
		||||
# export PASSBOOK_TAG=0.8.15-beta
 | 
			
		||||
# export PASSBOOK_TAG=0.9.0-stable
 | 
			
		||||
# If this is a productive installation, set a different PostgreSQL Password
 | 
			
		||||
# export PG_PASS=$(pwgen 40 1)
 | 
			
		||||
docker-compose pull
 | 
			
		||||
 | 
			
		||||
@ -22,6 +22,12 @@ config:
 | 
			
		||||
  # Log level used by web and worker
 | 
			
		||||
  # Can be either debug, info, warning, error
 | 
			
		||||
  log_level: warning
 | 
			
		||||
  # Optionally enable Elastic APM Support
 | 
			
		||||
  apm:
 | 
			
		||||
    enabled: false
 | 
			
		||||
    server_url: ""
 | 
			
		||||
    secret_token: ""
 | 
			
		||||
    verify_server_cert: true
 | 
			
		||||
 | 
			
		||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
 | 
			
		||||
# This requires the CoreOS Prometheus Operator.
 | 
			
		||||
 | 
			
		||||
@ -16,6 +16,7 @@ Create an application in passbook and note the slug, as this will be used later.
 | 
			
		||||
-   ACS URL: `https://signin.aws.amazon.com/saml`
 | 
			
		||||
-   Audience: `urn:amazon:webservices`
 | 
			
		||||
-   Issuer: `passbook`
 | 
			
		||||
-   Binding: `Post`
 | 
			
		||||
 | 
			
		||||
You can of course use a custom signing certificate, and adjust durations.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -26,5 +26,5 @@ return False
 | 
			
		||||
    - `request.obj`: A Django Model instance. This is only set if the policy is ran against an object.
 | 
			
		||||
    - `request.context`: A dictionary with dynamic data. This depends on the origin of the execution.
 | 
			
		||||
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider.
 | 
			
		||||
- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted.
 | 
			
		||||
- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted. Can be [compared](../expressions/index.md#comparing-ip-addresses)
 | 
			
		||||
- `pb_flow_plan`: Current Plan if Policy is called from the Flow Planner.
 | 
			
		||||
 | 
			
		||||
@ -6,13 +6,13 @@ To export data from your old instance, run this command:
 | 
			
		||||
 | 
			
		||||
- docker-compose
 | 
			
		||||
```
 | 
			
		||||
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
 | 
			
		||||
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
 | 
			
		||||
docker cp passbook_server_1:/tmp/passbook_dump.json passbook_dump.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
- kubernetes
 | 
			
		||||
```
 | 
			
		||||
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
 | 
			
		||||
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
 | 
			
		||||
kubectl cp passbook-web-...:/tmp/passbook_dump.json passbook_dump.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								e2e/ci.docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								e2e/ci.docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
version: '3.7'
 | 
			
		||||
 | 
			
		||||
services:
 | 
			
		||||
  chrome:
 | 
			
		||||
    image: selenium/standalone-chrome
 | 
			
		||||
    volumes:
 | 
			
		||||
      - /dev/shm:/dev/shm
 | 
			
		||||
    network_mode: host
 | 
			
		||||
@ -6,15 +6,3 @@ services:
 | 
			
		||||
    volumes:
 | 
			
		||||
      - /dev/shm:/dev/shm
 | 
			
		||||
    network_mode: host
 | 
			
		||||
 | 
			
		||||
  postgresql:
 | 
			
		||||
    image: postgres:11
 | 
			
		||||
    restart: always
 | 
			
		||||
    environment:
 | 
			
		||||
      POSTGRES_HOST_AUTH_METHOD: trust
 | 
			
		||||
      POSTGRES_DB: passbook
 | 
			
		||||
    network_mode: host
 | 
			
		||||
  redis:
 | 
			
		||||
    image: redis
 | 
			
		||||
    restart: always
 | 
			
		||||
    network_mode: host
 | 
			
		||||
 | 
			
		||||
@ -1,498 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "id": "7d9b2407-1520-4c04-b040-68e8ada9aecc",
 | 
			
		||||
  "version": "2.0",
 | 
			
		||||
  "name": "passbook",
 | 
			
		||||
  "url": "http://localhost:8000",
 | 
			
		||||
  "tests": [{
 | 
			
		||||
    "id": "94b39863-74ec-4b7d-98c5-2b380b6d2c55",
 | 
			
		||||
    "name": "passbook login simple",
 | 
			
		||||
    "commands": [{
 | 
			
		||||
      "id": "e60e4382-4f96-44c3-ba06-5e18609c9c2b",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "open",
 | 
			
		||||
      "target": "/flows/default-authentication-flow/?next=%2F",
 | 
			
		||||
      "targets": [],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "b2652f24-931e-45b0-b01d-2f0ac0f74db8",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "id=id_uid_field",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_uid_field", "id"],
 | 
			
		||||
        ["name=uid_field", "name"],
 | 
			
		||||
        ["css=#id_uid_field", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "f1930f8a-984a-4076-a925-20937bb2f8d3",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_uid_field",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_uid_field", "id"],
 | 
			
		||||
        ["name=uid_field", "name"],
 | 
			
		||||
        ["css=#id_uid_field", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "admin@example.tld"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "0b568ee3-1bed-4821-a3bc-f6b960dbed9d",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "sendKeys",
 | 
			
		||||
      "target": "id=id_uid_field",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_uid_field", "id"],
 | 
			
		||||
        ["name=uid_field", "name"],
 | 
			
		||||
        ["css=#id_uid_field", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "${KEY_ENTER}"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "6d98e479-2825-484d-996a-ccf350d2761f",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_password",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password", "id"],
 | 
			
		||||
        ["name=password", "name"],
 | 
			
		||||
        ["css=#id_password", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[2]/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "pbadmin"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "6f7abec6-ff44-4eb5-ae23-520c1c29a706",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "sendKeys",
 | 
			
		||||
      "target": "id=id_password",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password", "id"],
 | 
			
		||||
        ["name=password", "name"],
 | 
			
		||||
        ["css=#id_password", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[2]/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "${KEY_ENTER}"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "04c5876f-1405-4077-a98b-e911f09113d7",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "assertText",
 | 
			
		||||
      "target": "xpath=//a[contains(@href, '/-/user/')]",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["linkText=pbadmin", "linkText"],
 | 
			
		||||
        ["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
 | 
			
		||||
        ["xpath=//a[contains(text(),'pbadmin')]", "xpath:link"],
 | 
			
		||||
        ["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
 | 
			
		||||
        ["xpath=//div[2]/a", "xpath:position"],
 | 
			
		||||
        ["xpath=//a[contains(.,'pbadmin')]", "xpath:innerText"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "pbadmin"
 | 
			
		||||
    }]
 | 
			
		||||
  }, {
 | 
			
		||||
    "id": "61948b3c-3012-4f97-aa52-bc8f34fec333",
 | 
			
		||||
    "name": "passbook enroll simple",
 | 
			
		||||
    "commands": [{
 | 
			
		||||
      "id": "0f4884b3-4891-41bc-956d-1fa433e892e9",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "open",
 | 
			
		||||
      "target": "/flows/default-authentication-flow/?next=%2F",
 | 
			
		||||
      "targets": [],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "84d3861f-a60c-4650-8689-535f82b39577",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "linkText=Sign up.",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["linkText=Sign up.", "linkText"],
 | 
			
		||||
        ["css=.pf-c-login__main-footer-band-item > a", "css:finder"],
 | 
			
		||||
        ["xpath=//a[contains(text(),'Sign up.')]", "xpath:link"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/footer/div/p/a", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//a[contains(@href, '/flows/default-enrollment-flow/')]", "xpath:href"],
 | 
			
		||||
        ["xpath=//a", "xpath:position"],
 | 
			
		||||
        ["xpath=//a[contains(.,'Sign up.')]", "xpath:innerText"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "a32435ca-d84a-41e7-a915-fcbbc5f88341",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_username",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_username", "id"],
 | 
			
		||||
        ["name=username", "name"],
 | 
			
		||||
        ["css=#id_username", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_username']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "foo"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "3b5dcf53-8297-46c5-88b7-11c2eb25f34f",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_password",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password", "id"],
 | 
			
		||||
        ["name=password", "name"],
 | 
			
		||||
        ["css=#id_password", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[2]/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "pbadmin"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "e948d61c-dae6-4994-b56f-ff130892b342",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_password_repeat",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password_repeat", "id"],
 | 
			
		||||
        ["name=password_repeat", "name"],
 | 
			
		||||
        ["css=#id_password_repeat", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password_repeat']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[3]/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[3]/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "pbadmin"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "e7527bfc-ec74-4d96-86f0-5a3a55a59025",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "css=.pf-c-button",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["css=.pf-c-button", "css:finder"],
 | 
			
		||||
        ["xpath=//button[@type='submit']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[4]/button", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//button", "xpath:position"],
 | 
			
		||||
        ["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "434b842c-a659-4ff5-aca8-06a6a3489597",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_name",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_name", "id"],
 | 
			
		||||
        ["name=name", "name"],
 | 
			
		||||
        ["css=#id_name", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_name']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "some name"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "cbc43a1b-2cfe-46e2-85bc-476fb32c6cb1",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_email",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_email", "id"],
 | 
			
		||||
        ["name=email", "name"],
 | 
			
		||||
        ["css=#id_email", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_email']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[2]/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "foo@bar.baz"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "e74389a0-228b-4312-9677-e9add6358de3",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "css=.pf-c-button",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["css=.pf-c-button", "css:finder"],
 | 
			
		||||
        ["xpath=//button[@type='submit']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[3]/button", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//button", "xpath:position"],
 | 
			
		||||
        ["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "3e22f9c2-5ebd-49c2-81b1-340fa0435bbc",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "linkText=foo",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["linkText=foo", "linkText"],
 | 
			
		||||
        ["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
 | 
			
		||||
        ["xpath=//a[contains(text(),'foo')]", "xpath:link"],
 | 
			
		||||
        ["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
 | 
			
		||||
        ["xpath=//div[2]/a", "xpath:position"],
 | 
			
		||||
        ["xpath=//a[contains(.,'foo')]", "xpath:innerText"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "60124cfd-f11c-4d7f-8b01-bef54c8cbd73",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "assertText",
 | 
			
		||||
      "target": "xpath=//a[contains(@href, '/-/user/')]",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["linkText=foo", "linkText"],
 | 
			
		||||
        ["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
 | 
			
		||||
        ["xpath=//a[contains(text(),'foo')]", "xpath:link"],
 | 
			
		||||
        ["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
 | 
			
		||||
        ["xpath=//div[2]/a", "xpath:position"],
 | 
			
		||||
        ["xpath=//a[contains(.,'foo')]", "xpath:innerText"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "foo"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "429ee61b-9991-4919-8131-55f8e1bd9a0d",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "assertValue",
 | 
			
		||||
      "target": "id=id_username",
 | 
			
		||||
      "targets": [],
 | 
			
		||||
      "value": "foo"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "f6c50760-52ed-4c1d-b232-30f8afe144eb",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "assertText",
 | 
			
		||||
      "target": "id=id_name",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_name", "id"],
 | 
			
		||||
        ["name=name", "name"],
 | 
			
		||||
        ["css=#id_name", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_name']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[2]/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[2]/div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "some name"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "b26905b5-89b5-4b41-abf5-a9f848f08622",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "assertText",
 | 
			
		||||
      "target": "id=id_email",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_email", "id"],
 | 
			
		||||
        ["name=email", "name"],
 | 
			
		||||
        ["css=#id_email", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_email']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='main-content']/section/div/div/div/div[2]/form/div[3]/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[3]/div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "foo@bar.baz"
 | 
			
		||||
    }]
 | 
			
		||||
  }, {
 | 
			
		||||
    "id": "1a3172e0-ac23-4781-9367-19afccee4f4a",
 | 
			
		||||
    "name": "flows stage setup password",
 | 
			
		||||
    "commands": [{
 | 
			
		||||
      "id": "77784f77-d840-4b3d-a42f-7928f02fb7e1",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "open",
 | 
			
		||||
      "target": "/flows/default-authentication-flow/?next=%2F",
 | 
			
		||||
      "targets": [],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "783aa9a6-81e5-49c6-8789-2f360a5750b1",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "setWindowSize",
 | 
			
		||||
      "target": "1699x1417",
 | 
			
		||||
      "targets": [],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "cb0cd63e-30e9-4443-af59-5345fe26dc88",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "id=id_uid_field",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_uid_field", "id"],
 | 
			
		||||
        ["name=uid_field", "name"],
 | 
			
		||||
        ["css=#id_uid_field", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "8466ded1-c5f6-451c-b63f-0889da38503a",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_uid_field",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_uid_field", "id"],
 | 
			
		||||
        ["name=uid_field", "name"],
 | 
			
		||||
        ["css=#id_uid_field", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "pbadmin"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "27383093-d01a-4416-8fc6-9caad4926cd3",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "sendKeys",
 | 
			
		||||
      "target": "id=id_uid_field",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_uid_field", "id"],
 | 
			
		||||
        ["name=uid_field", "name"],
 | 
			
		||||
        ["css=#id_uid_field", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_uid_field']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "${KEY_ENTER}"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "4602745a-0ebb-4425-a841-a1ed4899659d",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_password",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password", "id"],
 | 
			
		||||
        ["name=password", "name"],
 | 
			
		||||
        ["css=#id_password", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[2]/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "pbadmin"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "d1ff4f81-d8f9-45dc-ad5d-f99b54c0cd18",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "sendKeys",
 | 
			
		||||
      "target": "id=id_password",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password", "id"],
 | 
			
		||||
        ["name=password", "name"],
 | 
			
		||||
        ["css=#id_password", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[2]/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "${KEY_ENTER}"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "014c8f57-7ef2-469c-b700-efa94ba81b66",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "css=.pf-c-page__header",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["css=.pf-c-page__header", "css:finder"],
 | 
			
		||||
        ["xpath=//div[@id='page-default-nav-example']/header", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//header", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "14e86b6f-6add-4bcc-913a-42b1e7322c79",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "linkText=pbadmin",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["linkText=pbadmin", "linkText"],
 | 
			
		||||
        ["css=.pf-c-page__header-tools-group:nth-child(2) > .pf-c-button", "css:finder"],
 | 
			
		||||
        ["xpath=//a[contains(text(),'pbadmin')]", "xpath:link"],
 | 
			
		||||
        ["xpath=//div[@id='page-default-nav-example']/header/div[3]/div[2]/a", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//a[contains(@href, '/-/user/')]", "xpath:href"],
 | 
			
		||||
        ["xpath=//div[2]/a", "xpath:position"],
 | 
			
		||||
        ["xpath=//a[contains(.,'pbadmin')]", "xpath:innerText"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "8280da13-632e-4cba-9e18-ecae0d57d052",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "linkText=Change password",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["linkText=Change password", "linkText"],
 | 
			
		||||
        ["css=.pf-c-nav__section:nth-child(2) .pf-c-nav__link", "css:finder"],
 | 
			
		||||
        ["xpath=//a[contains(text(),'Change password')]", "xpath:link"],
 | 
			
		||||
        ["xpath=//nav[@id='page-default-nav-example-primary-nav']/section[2]/ul/li/a", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//a[contains(@href, '/-/user/stage/password/b929b529-e384-4409-8d40-ac4a195fcab2/change/?next=%2F-%2Fuser%2F')]", "xpath:href"],
 | 
			
		||||
        ["xpath=//section[2]/ul/li/a", "xpath:position"],
 | 
			
		||||
        ["xpath=//a[contains(.,'Change password')]", "xpath:innerText"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "716d7e0c-79dc-469b-a31f-dceaa0765e9c",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "id=id_password",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password", "id"],
 | 
			
		||||
        ["name=password", "name"],
 | 
			
		||||
        ["css=#id_password", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "77005d70-adf0-4add-8329-b092d43f829a",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_password",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password", "id"],
 | 
			
		||||
        ["name=password", "name"],
 | 
			
		||||
        ["css=#id_password", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "test"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "965ca365-99f4-45d1-97c3-c944269341b9",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "id=id_password_repeat",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password_repeat", "id"],
 | 
			
		||||
        ["name=password_repeat", "name"],
 | 
			
		||||
        ["css=#id_password_repeat", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password_repeat']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[2]/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "9b421468-c65e-4943-b6b1-1e80410a6b87",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "type",
 | 
			
		||||
      "target": "id=id_password_repeat",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["id=id_password_repeat", "id"],
 | 
			
		||||
        ["name=password_repeat", "name"],
 | 
			
		||||
        ["css=#id_password_repeat", "css:finder"],
 | 
			
		||||
        ["xpath=//input[@id='id_password_repeat']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[2]/input", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//div[2]/input", "xpath:position"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": "test"
 | 
			
		||||
    }, {
 | 
			
		||||
      "id": "572c1400-a0f2-499f-808a-18c1f56bf13f",
 | 
			
		||||
      "comment": "",
 | 
			
		||||
      "command": "click",
 | 
			
		||||
      "target": "css=.pf-c-button",
 | 
			
		||||
      "targets": [
 | 
			
		||||
        ["css=.pf-c-button", "css:finder"],
 | 
			
		||||
        ["xpath=//button[@type='submit']", "xpath:attributes"],
 | 
			
		||||
        ["xpath=//main[@id='flow-body']/div/form/div[3]/button", "xpath:idRelative"],
 | 
			
		||||
        ["xpath=//button", "xpath:position"],
 | 
			
		||||
        ["xpath=//button[contains(.,'Continue')]", "xpath:innerText"]
 | 
			
		||||
      ],
 | 
			
		||||
      "value": ""
 | 
			
		||||
    }]
 | 
			
		||||
  }],
 | 
			
		||||
  "suites": [{
 | 
			
		||||
    "id": "495657fb-3f5e-4431-877c-4d0b248c0841",
 | 
			
		||||
    "name": "Default Suite",
 | 
			
		||||
    "persistSession": false,
 | 
			
		||||
    "parallel": false,
 | 
			
		||||
    "timeout": 300,
 | 
			
		||||
    "tests": ["94b39863-74ec-4b7d-98c5-2b380b6d2c55"]
 | 
			
		||||
  }],
 | 
			
		||||
  "urls": ["http://localhost:8000/"],
 | 
			
		||||
  "plugins": []
 | 
			
		||||
}
 | 
			
		||||
@ -23,8 +23,8 @@ class TestFlowsEnroll(SeleniumTestCase):
 | 
			
		||||
    """Test Enroll flow"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.container = self.setup_client()
 | 
			
		||||
        super().setUp()
 | 
			
		||||
 | 
			
		||||
    def setup_client(self) -> Container:
 | 
			
		||||
        """Setup test IdP container"""
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,8 @@ from selenium.webdriver.common.keys import Keys
 | 
			
		||||
 | 
			
		||||
from e2e.utils import USER, SeleniumTestCase
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
from passbook.flows.models import Flow, FlowDesignation
 | 
			
		||||
from passbook.stages.password.models import PasswordStage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestFlowsStageSetup(SeleniumTestCase):
 | 
			
		||||
@ -15,6 +17,16 @@ class TestFlowsStageSetup(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def test_password_change(self):
 | 
			
		||||
        """test password change flow"""
 | 
			
		||||
        # Ensure that password stage has change_flow set
 | 
			
		||||
        flow = Flow.objects.get(
 | 
			
		||||
            slug="default-password-change", designation=FlowDesignation.STAGE_SETUP,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        stages = PasswordStage.objects.filter(name="default-authentication-password")
 | 
			
		||||
        stage = stages.first()
 | 
			
		||||
        stage.change_flow = flow
 | 
			
		||||
        stage.save()
 | 
			
		||||
 | 
			
		||||
        new_password = "".join(
 | 
			
		||||
            SystemRandom().choice(string.ascii_uppercase + string.digits)
 | 
			
		||||
            for _ in range(8)
 | 
			
		||||
@ -29,6 +41,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
 | 
			
		||||
        self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
 | 
			
		||||
        self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
 | 
			
		||||
        self.wait_for_url(self.url("passbook_core:user-settings"))
 | 
			
		||||
        self.driver.find_element(By.LINK_TEXT, "Change password").click()
 | 
			
		||||
        self.driver.find_element(By.ID, "id_password").send_keys(new_password)
 | 
			
		||||
        self.driver.find_element(By.ID, "id_password_repeat").click()
 | 
			
		||||
 | 
			
		||||
@ -20,16 +20,16 @@ class TestProviderOAuth(SeleniumTestCase):
 | 
			
		||||
    """test OAuth Provider flow"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.client_id = generate_client_id()
 | 
			
		||||
        self.client_secret = generate_client_secret()
 | 
			
		||||
        self.container = self.setup_client()
 | 
			
		||||
        super().setUp()
 | 
			
		||||
 | 
			
		||||
    def setup_client(self) -> Container:
 | 
			
		||||
        """Setup client grafana container which we test OAuth against"""
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="grafana/grafana:latest",
 | 
			
		||||
            image="grafana/grafana:7.1.0",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            auto_remove=True,
 | 
			
		||||
@ -103,23 +103,20 @@ class TestProviderOAuth(SeleniumTestCase):
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
 | 
			
		||||
                "value"
 | 
			
		||||
            ),
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=email]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=login]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
@ -165,6 +162,7 @@ class TestProviderOAuth(SeleniumTestCase):
 | 
			
		||||
                By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
 | 
			
		||||
            ).text,
 | 
			
		||||
        )
 | 
			
		||||
        sleep(1)
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
 | 
			
		||||
 | 
			
		||||
        self.wait_for_url("http://localhost:3000/?orgId=1")
 | 
			
		||||
@ -174,23 +172,20 @@ class TestProviderOAuth(SeleniumTestCase):
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
 | 
			
		||||
                "value"
 | 
			
		||||
            ),
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=email]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=login]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().username,
 | 
			
		||||
        )
 | 
			
		||||
@ -230,6 +225,6 @@ class TestProviderOAuth(SeleniumTestCase):
 | 
			
		||||
        self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
 | 
			
		||||
        self.wait_for_url(self.url("passbook_flows:denied"))
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "#flow-body > header > h1").text,
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
 | 
			
		||||
            "Permission denied",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -23,16 +23,16 @@ class TestProviderOIDC(SeleniumTestCase):
 | 
			
		||||
    """test OpenID Provider flow"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.client_id = generate_client_id()
 | 
			
		||||
        self.client_secret = generate_client_secret()
 | 
			
		||||
        self.container = self.setup_client()
 | 
			
		||||
        super().setUp()
 | 
			
		||||
 | 
			
		||||
    def setup_client(self) -> Container:
 | 
			
		||||
        """Setup client grafana container which we test OIDC against"""
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="grafana/grafana:latest",
 | 
			
		||||
            image="grafana/grafana:7.1.0",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            auto_remove=True,
 | 
			
		||||
@ -153,23 +153,20 @@ class TestProviderOIDC(SeleniumTestCase):
 | 
			
		||||
            USER().name,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
 | 
			
		||||
                "value"
 | 
			
		||||
            ),
 | 
			
		||||
            USER().name,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=email]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=login]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
@ -221,6 +218,7 @@ class TestProviderOIDC(SeleniumTestCase):
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
 | 
			
		||||
        )
 | 
			
		||||
        sleep(1)
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
 | 
			
		||||
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
@ -234,23 +232,20 @@ class TestProviderOIDC(SeleniumTestCase):
 | 
			
		||||
            USER().name,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
 | 
			
		||||
                "value"
 | 
			
		||||
            ),
 | 
			
		||||
            USER().name,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=email]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(
 | 
			
		||||
                By.XPATH,
 | 
			
		||||
                "/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
 | 
			
		||||
                By.CSS_SELECTOR, "input[name=login]"
 | 
			
		||||
            ).get_attribute("value"),
 | 
			
		||||
            USER().email,
 | 
			
		||||
        )
 | 
			
		||||
@ -298,6 +293,6 @@ class TestProviderOIDC(SeleniumTestCase):
 | 
			
		||||
        self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
 | 
			
		||||
        self.wait_for_url(self.url("passbook_flows:denied"))
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "#flow-body > header > h1").text,
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
 | 
			
		||||
            "Permission denied",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,6 @@ from e2e.utils import USER, SeleniumTestCase
 | 
			
		||||
from passbook.core.models import Application
 | 
			
		||||
from passbook.crypto.models import CertificateKeyPair
 | 
			
		||||
from passbook.flows.models import Flow
 | 
			
		||||
from passbook.lib.utils.reflection import class_to_path
 | 
			
		||||
from passbook.policies.expression.models import ExpressionPolicy
 | 
			
		||||
from passbook.policies.models import PolicyBinding
 | 
			
		||||
from passbook.providers.saml.models import (
 | 
			
		||||
@ -19,7 +18,6 @@ from passbook.providers.saml.models import (
 | 
			
		||||
    SAMLPropertyMapping,
 | 
			
		||||
    SAMLProvider,
 | 
			
		||||
)
 | 
			
		||||
from passbook.providers.saml.processors.generic import GenericProcessor
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
@ -70,7 +68,6 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider: SAMLProvider = SAMLProvider.objects.create(
 | 
			
		||||
            name="saml-test",
 | 
			
		||||
            processor_path=class_to_path(GenericProcessor),
 | 
			
		||||
            acs_url="http://localhost:9009/saml/acs",
 | 
			
		||||
            audience="passbook-e2e",
 | 
			
		||||
            issuer="passbook-e2e",
 | 
			
		||||
@ -104,7 +101,6 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider: SAMLProvider = SAMLProvider.objects.create(
 | 
			
		||||
            name="saml-test",
 | 
			
		||||
            processor_path=class_to_path(GenericProcessor),
 | 
			
		||||
            acs_url="http://localhost:9009/saml/acs",
 | 
			
		||||
            audience="passbook-e2e",
 | 
			
		||||
            issuer="passbook-e2e",
 | 
			
		||||
@ -146,7 +142,6 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider: SAMLProvider = SAMLProvider.objects.create(
 | 
			
		||||
            name="saml-test",
 | 
			
		||||
            processor_path=class_to_path(GenericProcessor),
 | 
			
		||||
            acs_url="http://localhost:9009/saml/acs",
 | 
			
		||||
            audience="passbook-e2e",
 | 
			
		||||
            issuer="passbook-e2e",
 | 
			
		||||
@ -188,7 +183,6 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        )
 | 
			
		||||
        provider: SAMLProvider = SAMLProvider.objects.create(
 | 
			
		||||
            name="saml-test",
 | 
			
		||||
            processor_path=class_to_path(GenericProcessor),
 | 
			
		||||
            acs_url="http://localhost:9009/saml/acs",
 | 
			
		||||
            audience="passbook-e2e",
 | 
			
		||||
            issuer="passbook-e2e",
 | 
			
		||||
@ -211,6 +205,6 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
 | 
			
		||||
        self.wait_for_url(self.url("passbook_flows:denied"))
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "#flow-body > header > h1").text,
 | 
			
		||||
            self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
 | 
			
		||||
            "Permission denied",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -35,13 +35,42 @@ NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG
 | 
			
		||||
aQ==
 | 
			
		||||
-----END CERTIFICATE-----"""
 | 
			
		||||
 | 
			
		||||
IDP_KEY = """-----BEGIN PRIVATE KEY-----
 | 
			
		||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDNQIWjOA1vWHUz
 | 
			
		||||
SPM1FIKOE4GdH65VtWlpZ9dghH4CFYN0R7mvJj4KBq86Dxt8vJvLMV16GVh0NGCR
 | 
			
		||||
50QH8aMbxonDTqXSoXiMM4DDSQTKBYK7aZwftc7FG35gAfdNUdr8e7VbdaPOShuq
 | 
			
		||||
qotDyCQpZYzbt86ABnoaJ5okE3pUFIwxw97LcdYsGZz5Ngma/V1to7aMeEqHyl8r
 | 
			
		||||
DRbXZUzw/U8g7yC/g+G7+64liJ4FYqLEETLLSUePKLFgUJHXbF2HgIDjur3nxlEa
 | 
			
		||||
ecNQYVUTVCGBFpwkI5n1t3m32avwotpUFhMImjkRETyPKZpvl0+p7mop8mwJmKpa
 | 
			
		||||
CVuNSj23AgMBAAECggEABn4I/B20xxXcNzASiVZJvua9DdRHtmxTlkLznBj0x2oY
 | 
			
		||||
y1/Nbs3d3oFRn5uEuhBZOTcphsgwdRSHDXZsP3gUObew+d2N/zieUIj8hLDVlvJP
 | 
			
		||||
rU/s4U/l53Q0LiNByE9ThvL+zJLPCKJtd5uHZjB5fFm69+Q7gu8xg4xHIub+0pP5
 | 
			
		||||
PHanmHCDrbgNN/oqlar4FZ2MXTgekW6Amyc/koE9hIn4Baa2Ke/B/AUGY4pMRLqp
 | 
			
		||||
TArt+GTVeWeoFY9QACUpaHpJhGb/Piou6tlU57e42cLoki1f0+SARsBBKyXA7BB1
 | 
			
		||||
1fMH10KQYFA68dTYWlKzQau/K4xaqg4FKmtwF66GQQKBgQD9OpNUS7oRxMHVJaBR
 | 
			
		||||
TNWW+V1FXycqojekFpDijPb2X5CWV16oeWgaXp0nOHFdy9EWs3GtGpfZasaRVHsX
 | 
			
		||||
SHtPh4Nb8JqHdGE0/CD6t0+4Dns8Bn9cSqtdQB7R3Jn7IMXi9X/U8LDKo+A18/Jq
 | 
			
		||||
V8VgUngMny9YjMkQIbK8TRWkYQKBgQDPf4nxO6ju+tOHHORQty3bYDD0+OV3I0+L
 | 
			
		||||
0yz0uPreryBVi9nY43KakH52D7UZEwwsBjjGXD+WH8xEsmBWsGNXJu025PvzIJoz
 | 
			
		||||
lAEiXvMp/NmYp+tY4rDmO8RhyVocBqWHzh38m0IFOd4ByFD5nLEDrA3pDVo0aNgY
 | 
			
		||||
n0GwRysZFwKBgQDkCj3m6ZMUsUWEty+aR0EJhmKyODBDOnY09IVhH2S/FexVFzUN
 | 
			
		||||
LtfK9206hp/Awez3Ln2uT4Zzqq5K7fMzUniJdBWdVB004l8voeXpIe9OZuwfcBJ9
 | 
			
		||||
gFi1zypx/uFDv421BzQpBN+QfOdKbvbdQVFjnqCxbSDr80yVlGMrI5fbwQKBgG09
 | 
			
		||||
oRrepO7EIO8GN/GCruLK/ptKGkyhy3Q6xnVEmdb47hX7ncJA5IoZPmrblCVSUNsw
 | 
			
		||||
n11XHabksL8OBgg9rt8oQEThQv/aDzTOW9aDlJNragejiBTwq99aYeZ1gjo1CZq4
 | 
			
		||||
2jKubpCfyZC4rGDtrIfZYi1q+S2UcQhtd8DdhwQbAoGAAM4EpDA4yHB5yiek1p/o
 | 
			
		||||
CbqRCta/Dx6Eyo0KlNAyPuFPAshupG4NBx7mT2ASfL+2VBHoi6mHSri+BDX5ryYF
 | 
			
		||||
fMYvp7URYoq7w7qivRlvvEg5yoYrK13F2+Gj6xJ4jEN9m0KdM/g3mJGq0HBTIQrp
 | 
			
		||||
Sm75WXsflOxuTn08LbgGc4s=
 | 
			
		||||
-----END PRIVATE KEY-----"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
    """test SAML Source flow"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.container = self.setup_client()
 | 
			
		||||
        super().setUp()
 | 
			
		||||
 | 
			
		||||
    def setup_client(self) -> Container:
 | 
			
		||||
        """Setup test IdP container"""
 | 
			
		||||
@ -81,7 +110,7 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
        authentication_flow = Flow.objects.get(slug="default-source-authentication")
 | 
			
		||||
        enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
 | 
			
		||||
        keypair = CertificateKeyPair.objects.create(
 | 
			
		||||
            name="test-idp-cert", certificate_data=IDP_CERT
 | 
			
		||||
            name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        SAMLSource.objects.create(
 | 
			
		||||
@ -125,3 +154,109 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
        self.assertNotEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_idp_post(self):
 | 
			
		||||
        """test SAML Source With post binding"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
        authentication_flow = Flow.objects.get(slug="default-source-authentication")
 | 
			
		||||
        enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
 | 
			
		||||
        keypair = CertificateKeyPair.objects.create(
 | 
			
		||||
            name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        SAMLSource.objects.create(
 | 
			
		||||
            name="saml-idp-test",
 | 
			
		||||
            slug="saml-idp-test",
 | 
			
		||||
            authentication_flow=authentication_flow,
 | 
			
		||||
            enrollment_flow=enrollment_flow,
 | 
			
		||||
            issuer="entity-id",
 | 
			
		||||
            sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
 | 
			
		||||
            binding_type=SAMLBindingTypes.POST,
 | 
			
		||||
            signing_kp=keypair,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get(self.live_server_url)
 | 
			
		||||
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located(
 | 
			
		||||
                (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(
 | 
			
		||||
            By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
 | 
			
		||||
        ).click()
 | 
			
		||||
        sleep(1)
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
 | 
			
		||||
 | 
			
		||||
        # Now we should be at the IDP, wait for the username field
 | 
			
		||||
        self.wait.until(ec.presence_of_element_located((By.ID, "username")))
 | 
			
		||||
        self.driver.find_element(By.ID, "username").send_keys("user1")
 | 
			
		||||
        self.driver.find_element(By.ID, "password").send_keys("user1pass")
 | 
			
		||||
        self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
 | 
			
		||||
 | 
			
		||||
        # Wait until we're logged in
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located(
 | 
			
		||||
                (By.XPATH, "//a[contains(@href, '/-/user/')]")
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
 | 
			
		||||
 | 
			
		||||
        # Wait until we've loaded the user info page
 | 
			
		||||
        self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
 | 
			
		||||
        self.assertNotEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_idp_post_auto(self):
 | 
			
		||||
        """test SAML Source With post binding (auto redirect)"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
        authentication_flow = Flow.objects.get(slug="default-source-authentication")
 | 
			
		||||
        enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
 | 
			
		||||
        keypair = CertificateKeyPair.objects.create(
 | 
			
		||||
            name="test-idp-cert", certificate_data=IDP_CERT, key_data=IDP_KEY,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        SAMLSource.objects.create(
 | 
			
		||||
            name="saml-idp-test",
 | 
			
		||||
            slug="saml-idp-test",
 | 
			
		||||
            authentication_flow=authentication_flow,
 | 
			
		||||
            enrollment_flow=enrollment_flow,
 | 
			
		||||
            issuer="entity-id",
 | 
			
		||||
            sso_url="http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
 | 
			
		||||
            binding_type=SAMLBindingTypes.POST_AUTO,
 | 
			
		||||
            signing_kp=keypair,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.driver.get(self.live_server_url)
 | 
			
		||||
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located(
 | 
			
		||||
                (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(
 | 
			
		||||
            By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
 | 
			
		||||
        ).click()
 | 
			
		||||
 | 
			
		||||
        # Now we should be at the IDP, wait for the username field
 | 
			
		||||
        self.wait.until(ec.presence_of_element_located((By.ID, "username")))
 | 
			
		||||
        self.driver.find_element(By.ID, "username").send_keys("user1")
 | 
			
		||||
        self.driver.find_element(By.ID, "password").send_keys("user1pass")
 | 
			
		||||
        self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
 | 
			
		||||
 | 
			
		||||
        # Wait until we're logged in
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located(
 | 
			
		||||
                (By.XPATH, "//a[contains(@href, '/-/user/')]")
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
 | 
			
		||||
 | 
			
		||||
        # Wait until we've loaded the user info page
 | 
			
		||||
        self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
 | 
			
		||||
        self.assertNotEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), ""
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										212
									
								
								e2e/test_sources_oauth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								e2e/test_sources_oauth.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,212 @@
 | 
			
		||||
"""test OAuth Source"""
 | 
			
		||||
from os.path import abspath
 | 
			
		||||
from time import sleep
 | 
			
		||||
 | 
			
		||||
from oauth2_provider.generators import generate_client_secret
 | 
			
		||||
from selenium.webdriver.common.by import By
 | 
			
		||||
from selenium.webdriver.common.keys import Keys
 | 
			
		||||
from selenium.webdriver.support import expected_conditions as ec
 | 
			
		||||
from yaml import safe_dump
 | 
			
		||||
 | 
			
		||||
from docker import DockerClient, from_env
 | 
			
		||||
from docker.models.containers import Container
 | 
			
		||||
from docker.types import Healthcheck
 | 
			
		||||
from e2e.utils import SeleniumTestCase
 | 
			
		||||
from passbook.flows.models import Flow
 | 
			
		||||
from passbook.sources.oauth.models import OAuthSource
 | 
			
		||||
 | 
			
		||||
TOKEN_URL = "http://127.0.0.1:5556/dex/token"
 | 
			
		||||
CONFIG_PATH = "/tmp/dex.yml"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSourceOAuth(SeleniumTestCase):
 | 
			
		||||
    """test OAuth Source flow"""
 | 
			
		||||
 | 
			
		||||
    container: Container
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.client_secret = generate_client_secret()
 | 
			
		||||
        self.container = self.setup_client()
 | 
			
		||||
        super().setUp()
 | 
			
		||||
 | 
			
		||||
    def prepare_dex_config(self):
 | 
			
		||||
        """Since Dex does not document which environment
 | 
			
		||||
        variables can be used to configure clients"""
 | 
			
		||||
        config = {
 | 
			
		||||
            "enablePasswordDB": True,
 | 
			
		||||
            "issuer": "http://127.0.0.1:5556/dex",
 | 
			
		||||
            "logger": {"level": "debug"},
 | 
			
		||||
            "staticClients": [
 | 
			
		||||
                {
 | 
			
		||||
                    "id": "example-app",
 | 
			
		||||
                    "name": "Example App",
 | 
			
		||||
                    "redirectURIs": [
 | 
			
		||||
                        self.url(
 | 
			
		||||
                            "passbook_sources_oauth:oauth-client-callback",
 | 
			
		||||
                            source_slug="dex",
 | 
			
		||||
                        )
 | 
			
		||||
                    ],
 | 
			
		||||
                    "secret": self.client_secret,
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "staticPasswords": [
 | 
			
		||||
                {
 | 
			
		||||
                    "email": "admin@example.com",
 | 
			
		||||
                    # hash for password
 | 
			
		||||
                    "hash": "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W",
 | 
			
		||||
                    "userID": "08a8684b-db88-4b73-90a9-3cd1661f5466",
 | 
			
		||||
                    "username": "admin",
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            "storage": {"config": {"file": "/tmp/dex.db"}, "type": "sqlite3"},
 | 
			
		||||
            "web": {"http": "0.0.0.0:5556"},
 | 
			
		||||
        }
 | 
			
		||||
        with open(CONFIG_PATH, "w+") as _file:
 | 
			
		||||
            safe_dump(config, _file)
 | 
			
		||||
 | 
			
		||||
    def setup_client(self) -> Container:
 | 
			
		||||
        """Setup test Dex container"""
 | 
			
		||||
        self.prepare_dex_config()
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="quay.io/dexidp/dex:v2.24.0",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            auto_remove=True,
 | 
			
		||||
            command="serve /config.yml",
 | 
			
		||||
            healthcheck=Healthcheck(
 | 
			
		||||
                test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
 | 
			
		||||
                interval=5 * 100 * 1000000,
 | 
			
		||||
                start_period=1 * 100 * 1000000,
 | 
			
		||||
            ),
 | 
			
		||||
            volumes={abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro",}},
 | 
			
		||||
        )
 | 
			
		||||
        while True:
 | 
			
		||||
            container.reload()
 | 
			
		||||
            status = container.attrs.get("State", {}).get("Health", {}).get("Status")
 | 
			
		||||
            if status == "healthy":
 | 
			
		||||
                return container
 | 
			
		||||
            sleep(1)
 | 
			
		||||
 | 
			
		||||
    def create_objects(self):
 | 
			
		||||
        """Create required objects"""
 | 
			
		||||
        sleep(1)
 | 
			
		||||
        # Bootstrap all needed objects
 | 
			
		||||
        authentication_flow = Flow.objects.get(slug="default-source-authentication")
 | 
			
		||||
        enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
 | 
			
		||||
 | 
			
		||||
        OAuthSource.objects.create(
 | 
			
		||||
            name="dex",
 | 
			
		||||
            slug="dex",
 | 
			
		||||
            authentication_flow=authentication_flow,
 | 
			
		||||
            enrollment_flow=enrollment_flow,
 | 
			
		||||
            provider_type="openid-connect",
 | 
			
		||||
            authorization_url="http://127.0.0.1:5556/dex/auth",
 | 
			
		||||
            access_token_url=TOKEN_URL,
 | 
			
		||||
            profile_url="http://127.0.0.1:5556/dex/userinfo",
 | 
			
		||||
            consumer_key="example-app",
 | 
			
		||||
            consumer_secret=self.client_secret,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        self.container.kill()
 | 
			
		||||
        super().tearDown()
 | 
			
		||||
 | 
			
		||||
    def test_oauth_enroll(self):
 | 
			
		||||
        """test OAuth Source With With OIDC"""
 | 
			
		||||
        self.create_objects()
 | 
			
		||||
        self.driver.get(self.live_server_url)
 | 
			
		||||
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located(
 | 
			
		||||
                (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(
 | 
			
		||||
            By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
 | 
			
		||||
        ).click()
 | 
			
		||||
 | 
			
		||||
        # Now we should be at the IDP, wait for the login field
 | 
			
		||||
        self.wait.until(ec.presence_of_element_located((By.ID, "login")))
 | 
			
		||||
        self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
 | 
			
		||||
        self.driver.find_element(By.ID, "password").send_keys("password")
 | 
			
		||||
        self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
 | 
			
		||||
 | 
			
		||||
        # Wait until we're logged in
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
 | 
			
		||||
 | 
			
		||||
        # At this point we've been redirected back
 | 
			
		||||
        # and we're asked for the username
 | 
			
		||||
        self.driver.find_element(By.NAME, "username").click()
 | 
			
		||||
        self.driver.find_element(By.NAME, "username").send_keys("foo")
 | 
			
		||||
        self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
 | 
			
		||||
 | 
			
		||||
        # Wait until we've loaded the user info page
 | 
			
		||||
        self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
 | 
			
		||||
        self.driver.find_element(By.LINK_TEXT, "foo").click()
 | 
			
		||||
 | 
			
		||||
        self.wait_for_url(self.url("passbook_core:user-settings"))
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
 | 
			
		||||
            "foo",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_email").get_attribute("value"),
 | 
			
		||||
            "admin@example.com",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_oauth_enroll_auth(self):
 | 
			
		||||
        """test OAuth Source With With OIDC (enroll and authenticate again)"""
 | 
			
		||||
        self.test_oauth_enroll()
 | 
			
		||||
        # We're logged in at the end of this, log out and re-login
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, "[aria-label=logout]").click()
 | 
			
		||||
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located(
 | 
			
		||||
                (By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(
 | 
			
		||||
            By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
 | 
			
		||||
        ).click()
 | 
			
		||||
 | 
			
		||||
        # Now we should be at the IDP, wait for the login field
 | 
			
		||||
        self.wait.until(ec.presence_of_element_located((By.ID, "login")))
 | 
			
		||||
        self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
 | 
			
		||||
        self.driver.find_element(By.ID, "password").send_keys("password")
 | 
			
		||||
        self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
 | 
			
		||||
 | 
			
		||||
        # Wait until we're logged in
 | 
			
		||||
        self.wait.until(
 | 
			
		||||
            ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
 | 
			
		||||
        )
 | 
			
		||||
        self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
 | 
			
		||||
 | 
			
		||||
        # Wait until we've loaded the user info page
 | 
			
		||||
        self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
 | 
			
		||||
        self.driver.find_element(By.LINK_TEXT, "foo").click()
 | 
			
		||||
 | 
			
		||||
        self.wait_for_url(self.url("passbook_core:user-settings"))
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
 | 
			
		||||
            "foo",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_name").get_attribute("value"), "admin",
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(
 | 
			
		||||
            self.driver.find_element(By.ID, "id_email").get_attribute("value"),
 | 
			
		||||
            "admin@example.com",
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										15
									
								
								e2e/utils.py
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								e2e/utils.py
									
									
									
									
									
								
							@ -3,7 +3,7 @@ from functools import lru_cache
 | 
			
		||||
from glob import glob
 | 
			
		||||
from importlib.util import module_from_spec, spec_from_file_location
 | 
			
		||||
from inspect import getmembers, isfunction
 | 
			
		||||
from os import makedirs
 | 
			
		||||
from os import environ, makedirs
 | 
			
		||||
from time import time
 | 
			
		||||
 | 
			
		||||
from Cryptodome.PublicKey import RSA
 | 
			
		||||
@ -46,8 +46,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
 | 
			
		||||
        makedirs("selenium_screenshots/", exist_ok=True)
 | 
			
		||||
        self.driver = self._get_driver()
 | 
			
		||||
        self.driver.maximize_window()
 | 
			
		||||
        self.driver.implicitly_wait(300)
 | 
			
		||||
        self.wait = WebDriverWait(self.driver, 500)
 | 
			
		||||
        self.driver.implicitly_wait(30)
 | 
			
		||||
        self.wait = WebDriverWait(self.driver, 50)
 | 
			
		||||
        self.apply_default_data()
 | 
			
		||||
        self.logger = get_logger()
 | 
			
		||||
 | 
			
		||||
@ -58,9 +58,12 @@ class SeleniumTestCase(StaticLiveServerTestCase):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        self.driver.save_screenshot(
 | 
			
		||||
            f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
 | 
			
		||||
        )
 | 
			
		||||
        if "TF_BUILD" in environ:
 | 
			
		||||
            screenshot_file = (
 | 
			
		||||
                f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
 | 
			
		||||
            )
 | 
			
		||||
            self.driver.save_screenshot(screenshot_file)
 | 
			
		||||
            self.logger.warning("Saved screenshot", file=screenshot_file)
 | 
			
		||||
        for line in self.driver.get_log("browser"):
 | 
			
		||||
            self.logger.warning(
 | 
			
		||||
                line["message"], source=line["source"], level=line["level"]
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
FROM quay.io/oauth2-proxy/oauth2-proxy
 | 
			
		||||
 | 
			
		||||
COPY templates /templates
 | 
			
		||||
 | 
			
		||||
ENV OAUTH2_PROXY_EMAIL_DOMAINS=*
 | 
			
		||||
ENV OAUTH2_PROXY_PROVIDER=oidc
 | 
			
		||||
ENV OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR=/templates
 | 
			
		||||
ENV OAUTH2_PROXY_HTTP_ADDRESS=:4180
 | 
			
		||||
# TODO: If service is access over HTTPS, this needs to be set to true (default), otherwise needs to be false
 | 
			
		||||
# ENV OAUTH2_PROXY_COOKIE_SECURE=true
 | 
			
		||||
ENV OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true
 | 
			
		||||
 | 
			
		||||
@ -1,18 +0,0 @@
 | 
			
		||||
{{define "error.html"}}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en" charset="utf-8">
 | 
			
		||||
 | 
			
		||||
<head>
 | 
			
		||||
    <title>{{.Title}}</title>
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
    <h2>{{.Title}}</h2>
 | 
			
		||||
    <p>{{.Message}}</p>
 | 
			
		||||
    <hr>
 | 
			
		||||
    <p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
{{end}}
 | 
			
		||||
@ -1,119 +0,0 @@
 | 
			
		||||
{{define "sign_in.html"}}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en" charset="utf-8">
 | 
			
		||||
<head>
 | 
			
		||||
    <title>Sign In with passbook</title>
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
 | 
			
		||||
    <style>
 | 
			
		||||
        body {
 | 
			
		||||
            font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
            line-height: 1.42857143;
 | 
			
		||||
            color: #333;
 | 
			
		||||
            background: #f0f0f0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .signin {
 | 
			
		||||
            display: block;
 | 
			
		||||
            margin: 20px auto;
 | 
			
		||||
            max-width: 400px;
 | 
			
		||||
            background: #fff;
 | 
			
		||||
            border: 1px solid #ccc;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .center {
 | 
			
		||||
            text-align: center;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .btn {
 | 
			
		||||
            color: #fff;
 | 
			
		||||
            background-color: #428bca;
 | 
			
		||||
            border: 1px solid #357ebd;
 | 
			
		||||
            -webkit-border-radius: 4;
 | 
			
		||||
            -moz-border-radius: 4;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
            padding: 6px 12px;
 | 
			
		||||
            text-decoration: none;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .btn:hover {
 | 
			
		||||
            background-color: #3071a9;
 | 
			
		||||
            border-color: #285e8e;
 | 
			
		||||
            text-decoration: none;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        label {
 | 
			
		||||
            display: inline-block;
 | 
			
		||||
            max-width: 100%;
 | 
			
		||||
            margin-bottom: 5px;
 | 
			
		||||
            font-weight: 700;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        input {
 | 
			
		||||
            display: block;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 34px;
 | 
			
		||||
            padding: 6px 12px;
 | 
			
		||||
            font-size: 14px;
 | 
			
		||||
            line-height: 1.42857143;
 | 
			
		||||
            color: #555;
 | 
			
		||||
            background-color: #fff;
 | 
			
		||||
            background-image: none;
 | 
			
		||||
            border: 1px solid #ccc;
 | 
			
		||||
            border-radius: 4px;
 | 
			
		||||
            -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
 | 
			
		||||
            box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
 | 
			
		||||
            -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
 | 
			
		||||
            -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
 | 
			
		||||
            transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            box-sizing: border-box;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        footer {
 | 
			
		||||
            display: block;
 | 
			
		||||
            font-size: 10px;
 | 
			
		||||
            color: #aaa;
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            margin-bottom: 10px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        footer a {
 | 
			
		||||
            display: inline-block;
 | 
			
		||||
            height: 25px;
 | 
			
		||||
            line-height: 25px;
 | 
			
		||||
            color: #aaa;
 | 
			
		||||
            text-decoration: underline;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        footer a:hover {
 | 
			
		||||
            color: #aaa;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="signin center">
 | 
			
		||||
        <form method="GET" action="{{.ProxyPrefix}}/start">
 | 
			
		||||
            <input type="hidden" name="rd" value="{{.Redirect}}">
 | 
			
		||||
            <button type="submit" class="btn">Sign in with passbook</button><br />
 | 
			
		||||
        </form>
 | 
			
		||||
    </div>
 | 
			
		||||
    <script>
 | 
			
		||||
        if (window.location.hash) {
 | 
			
		||||
            (function () {
 | 
			
		||||
                var inputs = document.getElementsByName('rd');
 | 
			
		||||
                for (var i = 0; i < inputs.length; i++) {
 | 
			
		||||
                    inputs[i].value += window.location.hash;
 | 
			
		||||
                }
 | 
			
		||||
            })();
 | 
			
		||||
        }
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
 | 
			
		||||
</html>
 | 
			
		||||
{{end}}
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
apiVersion: v1
 | 
			
		||||
appVersion: "0.9.0-pre5"
 | 
			
		||||
appVersion: "0.9.0-stable"
 | 
			
		||||
description: A Helm chart for passbook.
 | 
			
		||||
name: passbook
 | 
			
		||||
version: "0.9.0-pre5"
 | 
			
		||||
version: "0.9.0-stable"
 | 
			
		||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
 | 
			
		||||
 | 
			
		||||
@ -21,3 +21,8 @@ data:
 | 
			
		||||
      message_queue_db: 1
 | 
			
		||||
    error_reporting: {{ .Values.config.error_reporting }}
 | 
			
		||||
    log_level: "{{ .Values.config.log_level }}"
 | 
			
		||||
    apm:
 | 
			
		||||
      enabled: {{ .Values.config.apm.enabled }}
 | 
			
		||||
      server_url: "{{ .Values.config.apm.server_url }}"
 | 
			
		||||
      secret_token: "{{ .Values.config.apm.server_token }}"
 | 
			
		||||
      verify_server_cert: {{ .Values.config.apm.verify_server_cert }}
 | 
			
		||||
 | 
			
		||||
@ -41,5 +41,9 @@ spec:
 | 
			
		||||
            backend:
 | 
			
		||||
              serviceName: {{ $fullName }}-static
 | 
			
		||||
              servicePort: http
 | 
			
		||||
          - path: /favicon.ico
 | 
			
		||||
            backend:
 | 
			
		||||
              serviceName: {{ $fullName }}-static
 | 
			
		||||
              servicePort: http
 | 
			
		||||
  {{- end }}
 | 
			
		||||
{{- end }}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
# This is a YAML-formatted file.
 | 
			
		||||
# Declare variables to be passed into your templates.
 | 
			
		||||
image:
 | 
			
		||||
  tag: 0.9.0-pre5
 | 
			
		||||
  tag: 0.9.0-stable
 | 
			
		||||
 | 
			
		||||
nameOverride: ""
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,12 @@ config:
 | 
			
		||||
  # Log level used by web and worker
 | 
			
		||||
  # Can be either debug, info, warning, error
 | 
			
		||||
  log_level: warning
 | 
			
		||||
  # Optionally enable Elastic APM Support
 | 
			
		||||
  apm:
 | 
			
		||||
    enabled: false
 | 
			
		||||
    server_url: ""
 | 
			
		||||
    secret_token: ""
 | 
			
		||||
    verify_server_cert: true
 | 
			
		||||
 | 
			
		||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
 | 
			
		||||
# This requires the CoreOS Prometheus Operator.
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook"""
 | 
			
		||||
__version__ = "0.9.0-pre5"
 | 
			
		||||
__version__ = "0.9.0-stable"
 | 
			
		||||
 | 
			
		||||
@ -10,29 +10,33 @@
 | 
			
		||||
</section>
 | 
			
		||||
<section class="pf-c-page__main-section">
 | 
			
		||||
    <div class="pf-l-gallery pf-m-gutter">
 | 
			
		||||
        <a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ application_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ application_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ source_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ source_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
 | 
			
		||||
@ -40,15 +44,19 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if providers_without_application.exists %}
 | 
			
		||||
                <i class="pf-icon pf-icon-warning-triangle"></i> {{ provider_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ provider_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ provider_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %}
 | 
			
		||||
@ -56,26 +64,32 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if stage_count < 1 %}
 | 
			
		||||
                <i class="pficon-error-circle-o"></i> {{ stage_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="pficon-error-circle-o"></i> {{ stage_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'No Stages configured. No Users will be able to login.' %}"></p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ stage_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ stage_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ flow_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ flow_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
 | 
			
		||||
@ -83,58 +97,71 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if policies_without_binding %}
 | 
			
		||||
                <i class="pf-icon pf-icon-warning-triangle"></i> {{ policy_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'Policies without binding exist.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ policy_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ policy_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ invitation_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ invitation_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ user_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ user_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    {% if version >= version_latest %}
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ version }}
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ version }}
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% if version >= version_latest %}
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i>
 | 
			
		||||
                    {% blocktrans with version=version %}
 | 
			
		||||
                    {{ version }} (Up-to-date!)
 | 
			
		||||
                    {% blocktrans %}
 | 
			
		||||
                    Up-to-date!
 | 
			
		||||
                    {% endblocktrans %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <i class="pf-icon pf-icon-warning-triangle"></i>
 | 
			
		||||
                    {% blocktrans with version=version latest=version_latest %}
 | 
			
		||||
                    {{ version }} ({{ latest }} is available!)
 | 
			
		||||
                    {% blocktrans with latest=version_latest %}
 | 
			
		||||
                    {{ latest }} is available!
 | 
			
		||||
                    {% endblocktrans %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-m-hoverable pf-m-compact">
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
 | 
			
		||||
@ -142,15 +169,19 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if worker_count < 1 %}
 | 
			
		||||
                <i class="pf-icon pf-icon-warning-triangle"></i> {{ worker_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ worker_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'No workers connected.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ worker_count }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ worker_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <a class="pf-c-card pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
 | 
			
		||||
        <a class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
 | 
			
		||||
@ -158,13 +189,37 @@
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if cached_policies < 1 %}
 | 
			
		||||
                <i class="pf-icon pf-icon-warning-triangle"></i> {{ cached_policies }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <i class="pf-icon pf-icon-ok"></i> {{ cached_policies }}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ cached_policies }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </a>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if cached_flows < 1 %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'No flows cached.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="aggregate-status">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ cached_flows }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </section>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="pf-c-backdrop" id="clearCacheModalRoot" hidden>
 | 
			
		||||
@ -173,7 +228,9 @@
 | 
			
		||||
            <button data-modal-close class="pf-c-button pf-m-plain" type="button" aria-label="Close dialog">
 | 
			
		||||
                <i class="fas fa-times" aria-hidden="true"></i>
 | 
			
		||||
            </button>
 | 
			
		||||
            <h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
 | 
			
		||||
            <div class="pf-c-modal-box__header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-modal-box__body" id="modal-description">
 | 
			
		||||
                <form method="post" id="clearForm">
 | 
			
		||||
                    {% csrf_token %}
 | 
			
		||||
 | 
			
		||||
@ -69,12 +69,11 @@
 | 
			
		||||
            <div class="pf-c-empty-state__content">
 | 
			
		||||
                <i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-lg">
 | 
			
		||||
                    {% trans 'No Applications.' %}
 | 
			
		||||
                    {% trans 'No Tokens.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <div class="pf-c-empty-state__body">
 | 
			
		||||
                    {% trans 'Currently no applications exist. Click the button below to create one.' %}
 | 
			
		||||
                    {% trans 'Currently no tokens exist.' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a href="{% url 'passbook_admin:application-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block action %}
 | 
			
		||||
{% blocktrans with type=form|form_verbose_name|title %}
 | 
			
		||||
{% blocktrans with type=form|form_verbose_name %}
 | 
			
		||||
Update {{ type }}
 | 
			
		||||
{% endblocktrans %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,4 @@
 | 
			
		||||
"""passbook administration overview"""
 | 
			
		||||
from functools import lru_cache
 | 
			
		||||
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.shortcuts import redirect, reverse
 | 
			
		||||
from django.views.generic import TemplateView
 | 
			
		||||
@ -15,18 +13,21 @@ from passbook.policies.models import Policy
 | 
			
		||||
from passbook.root.celery import CELERY_APP
 | 
			
		||||
from passbook.stages.invitation.models import Invitation
 | 
			
		||||
 | 
			
		||||
VERSION_CACHE_KEY = "passbook_latest_version"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@lru_cache
 | 
			
		||||
def latest_version() -> Version:
 | 
			
		||||
    """Get latest release from GitHub, cached"""
 | 
			
		||||
    try:
 | 
			
		||||
        data = get(
 | 
			
		||||
            "https://api.github.com/repos/beryju/passbook/releases/latest"
 | 
			
		||||
        ).json()
 | 
			
		||||
        tag_name = data.get("tag_name")
 | 
			
		||||
        return parse(tag_name.split("/")[1])
 | 
			
		||||
    except RequestException:
 | 
			
		||||
        return parse("0.0.0")
 | 
			
		||||
    if not cache.get(VERSION_CACHE_KEY):
 | 
			
		||||
        try:
 | 
			
		||||
            data = get(
 | 
			
		||||
                "https://api.github.com/repos/beryju/passbook/releases/latest"
 | 
			
		||||
            ).json()
 | 
			
		||||
            tag_name = data.get("tag_name")
 | 
			
		||||
            cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], 30)
 | 
			
		||||
        except (RequestException, IndexError):
 | 
			
		||||
            cache.set(VERSION_CACHE_KEY, "0.0.0", 30)
 | 
			
		||||
    return parse(cache.get(VERSION_CACHE_KEY))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
 | 
			
		||||
@ -60,4 +61,5 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
 | 
			
		||||
            Policy.objects.filter(bindings__isnull=True)
 | 
			
		||||
        )
 | 
			
		||||
        kwargs["cached_policies"] = len(cache.keys("policy_*"))
 | 
			
		||||
        kwargs["cached_flows"] = len(cache.keys("flow_*"))
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
@ -11,10 +11,10 @@ from django.views.generic import ListView
 | 
			
		||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
			
		||||
 | 
			
		||||
from passbook.admin.views.utils import DeleteMessageView
 | 
			
		||||
from passbook.core.signals import invitation_created
 | 
			
		||||
from passbook.lib.views import CreateAssignPermView
 | 
			
		||||
from passbook.stages.invitation.forms import InvitationForm
 | 
			
		||||
from passbook.stages.invitation.models import Invitation
 | 
			
		||||
from passbook.stages.invitation.signals import invitation_created
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ from django.contrib.messages.views import SuccessMessageMixin
 | 
			
		||||
from django.http import Http404
 | 
			
		||||
from django.views.generic import DeleteView, ListView, UpdateView
 | 
			
		||||
 | 
			
		||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
 | 
			
		||||
from passbook.lib.utils.reflection import all_subclasses
 | 
			
		||||
from passbook.lib.views import CreateAssignPermView
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@ class InheritanceCreateView(CreateAssignPermView):
 | 
			
		||||
            )
 | 
			
		||||
        except StopIteration as exc:
 | 
			
		||||
            raise Http404 from exc
 | 
			
		||||
        return path_to_class(model.form)
 | 
			
		||||
        return model.form(model)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
 | 
			
		||||
        kwargs = super().get_context_data(**kwargs)
 | 
			
		||||
@ -61,9 +61,7 @@ class InheritanceUpdateView(UpdateView):
 | 
			
		||||
        return kwargs
 | 
			
		||||
 | 
			
		||||
    def get_form_class(self):
 | 
			
		||||
        form_class_path = self.get_object().form
 | 
			
		||||
        form_class = path_to_class(form_class_path)
 | 
			
		||||
        return form_class
 | 
			
		||||
        return self.get_object().form()
 | 
			
		||||
 | 
			
		||||
    def get_object(self, queryset=None):
 | 
			
		||||
        return (
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
"""passbook audit signal listener"""
 | 
			
		||||
from typing import Dict
 | 
			
		||||
from threading import Thread
 | 
			
		||||
from typing import Any, Dict, Optional
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.signals import (
 | 
			
		||||
    user_logged_in,
 | 
			
		||||
@ -11,21 +12,54 @@ from django.http import HttpRequest
 | 
			
		||||
 | 
			
		||||
from passbook.audit.models import Event, EventAction
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
from passbook.core.signals import invitation_created, invitation_used, user_signed_up
 | 
			
		||||
from passbook.stages.invitation.models import Invitation
 | 
			
		||||
from passbook.stages.invitation.signals import invitation_created, invitation_used
 | 
			
		||||
from passbook.stages.user_write.signals import user_write
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EventNewThread(Thread):
 | 
			
		||||
    """Create Event in background thread"""
 | 
			
		||||
 | 
			
		||||
    action: EventAction
 | 
			
		||||
    request: HttpRequest
 | 
			
		||||
    kwargs: Dict[str, Any]
 | 
			
		||||
    user: Optional[User] = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, action: EventAction, request: HttpRequest, **kwargs):
 | 
			
		||||
        super().__init__()
 | 
			
		||||
        self.action = action
 | 
			
		||||
        self.request = request
 | 
			
		||||
        self.kwargs = kwargs
 | 
			
		||||
 | 
			
		||||
    def run(self):
 | 
			
		||||
        Event.new(self.action, **self.kwargs).from_http(self.request, user=self.user)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(user_logged_in)
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
 | 
			
		||||
    """Log successful login"""
 | 
			
		||||
    Event.new(EventAction.LOGIN).from_http(request)
 | 
			
		||||
    thread = EventNewThread(EventAction.LOGIN, request)
 | 
			
		||||
    thread.user = user
 | 
			
		||||
    thread.run()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(user_logged_out)
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
 | 
			
		||||
    """Log successfully logout"""
 | 
			
		||||
    Event.new(EventAction.LOGOUT).from_http(request)
 | 
			
		||||
    thread = EventNewThread(EventAction.LOGOUT, request)
 | 
			
		||||
    thread.user = user
 | 
			
		||||
    thread.run()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(user_write)
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_):
 | 
			
		||||
    """Log User write"""
 | 
			
		||||
    thread = EventNewThread(EventAction.CUSTOM, request, **data)
 | 
			
		||||
    thread.user = user
 | 
			
		||||
    thread.run()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(user_login_failed)
 | 
			
		||||
@ -34,29 +68,25 @@ def on_user_login_failed(
 | 
			
		||||
    sender, credentials: Dict[str, str], request: HttpRequest, **_
 | 
			
		||||
):
 | 
			
		||||
    """Failed Login"""
 | 
			
		||||
    Event.new(EventAction.LOGIN_FAILED, **credentials).from_http(request)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(user_signed_up)
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
def on_user_signed_up(sender, request: HttpRequest, user: User, **_):
 | 
			
		||||
    """Log successfully signed up"""
 | 
			
		||||
    Event.new(EventAction.SIGN_UP).from_http(request)
 | 
			
		||||
    thread = EventNewThread(EventAction.LOGIN_FAILED, request, **credentials)
 | 
			
		||||
    thread.run()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(invitation_created)
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
def on_invitation_created(sender, request: HttpRequest, invitation, **_):
 | 
			
		||||
def on_invitation_created(sender, request: HttpRequest, invitation: Invitation, **_):
 | 
			
		||||
    """Log Invitation creation"""
 | 
			
		||||
    Event.new(
 | 
			
		||||
        EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex
 | 
			
		||||
    ).from_http(request)
 | 
			
		||||
    thread = EventNewThread(
 | 
			
		||||
        EventAction.INVITE_CREATED, request, invitation_uuid=invitation.invite_uuid.hex
 | 
			
		||||
    )
 | 
			
		||||
    thread.run()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(invitation_used)
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
def on_invitation_used(sender, request: HttpRequest, invitation, **_):
 | 
			
		||||
def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_):
 | 
			
		||||
    """Log Invitation usage"""
 | 
			
		||||
    Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(
 | 
			
		||||
        request
 | 
			
		||||
    thread = EventNewThread(
 | 
			
		||||
        EventAction.INVITE_USED, request, invitation_uuid=invitation.invite_uuid.hex
 | 
			
		||||
    )
 | 
			
		||||
    thread.run()
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,12 @@
 | 
			
		||||
"""passbook Event administration"""
 | 
			
		||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
			
		||||
from django.views.generic import ListView
 | 
			
		||||
from guardian.mixins import PermissionListMixin
 | 
			
		||||
 | 
			
		||||
from passbook.audit.models import Event
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EventListView(PermissionListMixin, ListView):
 | 
			
		||||
class EventListView(PermissionListMixin, LoginRequiredMixin, ListView):
 | 
			
		||||
    """Show list of all invitations"""
 | 
			
		||||
 | 
			
		||||
    model = Event
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								passbook/core/migrations/0006_auto_20200709_1608.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								passbook/core/migrations/0006_auto_20200709_1608.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
# Generated by Django 3.0.8 on 2020-07-09 16:08
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_core", "0005_token_intent"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="source",
 | 
			
		||||
            name="slug",
 | 
			
		||||
            field=models.SlugField(
 | 
			
		||||
                help_text="Internal source name, used in URLs.", unique=True
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,12 +1,13 @@
 | 
			
		||||
"""passbook core models"""
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from typing import Any, Optional
 | 
			
		||||
from typing import Any, Optional, Type
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.models import AbstractUser
 | 
			
		||||
from django.contrib.postgres.fields import JSONField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Q, QuerySet
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
@ -92,6 +93,10 @@ class Provider(models.Model):
 | 
			
		||||
 | 
			
		||||
    objects = InheritanceManager()
 | 
			
		||||
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        """Return Form class used to edit this object"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    # This class defines no field for easier inheritance
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        if hasattr(self, "name"):
 | 
			
		||||
@ -134,7 +139,9 @@ class Source(PolicyBindingModel):
 | 
			
		||||
    """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
 | 
			
		||||
 | 
			
		||||
    name = models.TextField(help_text=_("Source's display Name."))
 | 
			
		||||
    slug = models.SlugField(help_text=_("Internal source name, used in URLs."))
 | 
			
		||||
    slug = models.SlugField(
 | 
			
		||||
        help_text=_("Internal source name, used in URLs."), unique=True
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    enabled = models.BooleanField(default=True)
 | 
			
		||||
    property_mappings = models.ManyToManyField(
 | 
			
		||||
@ -160,10 +167,12 @@ class Source(PolicyBindingModel):
 | 
			
		||||
        related_name="source_enrollment",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    form = ""  # ModelForm-based class ued to create/edit instance
 | 
			
		||||
 | 
			
		||||
    objects = InheritanceManager()
 | 
			
		||||
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        """Return Form class used to edit this object"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_login_button(self) -> Optional[UILoginButton]:
 | 
			
		||||
        """If source uses a http-based flow, return UI Information about the login
 | 
			
		||||
@ -196,6 +205,31 @@ class UserSourceConnection(CreatedUpdatedModel):
 | 
			
		||||
        unique_together = (("user", "source"),)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExpiringModel(models.Model):
 | 
			
		||||
    """Base Model which can expire, and is automatically cleaned up."""
 | 
			
		||||
 | 
			
		||||
    expires = models.DateTimeField(default=default_token_duration)
 | 
			
		||||
    expiring = models.BooleanField(default=True)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def filter_not_expired(cls, **kwargs) -> QuerySet:
 | 
			
		||||
        """Filer for tokens which are not expired yet or are not expiring,
 | 
			
		||||
        and match filters in `kwargs`"""
 | 
			
		||||
        query = Q(**kwargs)
 | 
			
		||||
        query_not_expired_yet = Q(expires__lt=now(), expiring=True)
 | 
			
		||||
        query_not_expiring = Q(expiring=False)
 | 
			
		||||
        return cls.objects.filter(query & (query_not_expired_yet | query_not_expiring))
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_expired(self) -> bool:
 | 
			
		||||
        """Check if token is expired yet."""
 | 
			
		||||
        return now() > self.expires
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        abstract = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TokenIntents(models.TextChoices):
 | 
			
		||||
    """Intents a Token can be created for."""
 | 
			
		||||
 | 
			
		||||
@ -206,34 +240,16 @@ class TokenIntents(models.TextChoices):
 | 
			
		||||
    INTENT_API = "api"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Token(models.Model):
 | 
			
		||||
class Token(ExpiringModel):
 | 
			
		||||
    """Token used to authenticate the User for API Access or confirm another Stage like Email."""
 | 
			
		||||
 | 
			
		||||
    token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
			
		||||
    intent = models.TextField(
 | 
			
		||||
        choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION
 | 
			
		||||
    )
 | 
			
		||||
    expires = models.DateTimeField(default=default_token_duration)
 | 
			
		||||
    user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
 | 
			
		||||
    expiring = models.BooleanField(default=True)
 | 
			
		||||
    description = models.TextField(default="", blank=True)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def filter_not_expired(**kwargs) -> QuerySet:
 | 
			
		||||
        """Filer for tokens which are not expired yet or are not expiring,
 | 
			
		||||
        and match filters in `kwargs`"""
 | 
			
		||||
        query = Q(**kwargs)
 | 
			
		||||
        query_not_expired_yet = Q(expires__lt=now(), expiring=True)
 | 
			
		||||
        query_not_expiring = Q(expiring=False)
 | 
			
		||||
        return Token.objects.filter(
 | 
			
		||||
            query & (query_not_expired_yet | query_not_expiring)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def is_expired(self) -> bool:
 | 
			
		||||
        """Check if token is expired yet."""
 | 
			
		||||
        return now() > self.expires
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return (
 | 
			
		||||
            f"Token {self.token_uuid.hex} {self.description} (expires={self.expires})"
 | 
			
		||||
@ -252,9 +268,12 @@ class PropertyMapping(models.Model):
 | 
			
		||||
    name = models.TextField()
 | 
			
		||||
    expression = models.TextField()
 | 
			
		||||
 | 
			
		||||
    form = ""
 | 
			
		||||
    objects = InheritanceManager()
 | 
			
		||||
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        """Return Form class used to edit this object"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def evaluate(
 | 
			
		||||
        self, user: Optional[User], request: Optional[HttpRequest], **kwargs
 | 
			
		||||
    ) -> Any:
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,4 @@
 | 
			
		||||
"""passbook core signals"""
 | 
			
		||||
from django.core.signals import Signal
 | 
			
		||||
 | 
			
		||||
user_signed_up = Signal(providing_args=["request", "user"])
 | 
			
		||||
invitation_created = Signal(providing_args=["request", "invitation"])
 | 
			
		||||
invitation_used = Signal(providing_args=["request", "invitation", "user"])
 | 
			
		||||
password_changed = Signal(providing_args=["user", "password"])
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,16 @@
 | 
			
		||||
"""passbook core tasks"""
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Token
 | 
			
		||||
from passbook.core.models import ExpiringModel
 | 
			
		||||
from passbook.root.celery import CELERY_APP
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task()
 | 
			
		||||
def clean_tokens():
 | 
			
		||||
    """Remove expired tokens"""
 | 
			
		||||
    amount, _ = Token.objects.filter(expires__lt=now(), expiring=True).delete()
 | 
			
		||||
    LOGGER.debug("Deleted expired tokens", amount=amount)
 | 
			
		||||
def clean_expired_models():
 | 
			
		||||
    """Remove expired objects"""
 | 
			
		||||
    for cls in ExpiringModel.__subclasses__():
 | 
			
		||||
        cls: ExpiringModel
 | 
			
		||||
        amount, _ = cls.filter_not_expired().delete()
 | 
			
		||||
        LOGGER.debug("Deleted expired models", model=cls, amount=amount)
 | 
			
		||||
 | 
			
		||||
@ -1,57 +1,20 @@
 | 
			
		||||
{% extends 'base/skeleton.html' %}
 | 
			
		||||
{% extends 'login/base_full.html' %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load passbook_utils %}
 | 
			
		||||
 | 
			
		||||
{% block body %}
 | 
			
		||||
<div class="pf-c-background-image">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
 | 
			
		||||
        <filter id="image_overlay">
 | 
			
		||||
            <feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
 | 
			
		||||
            <feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
 | 
			
		||||
                <feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
 | 
			
		||||
                <feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
 | 
			
		||||
                <feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
 | 
			
		||||
                <feFuncA type="table" tableValues="0 1"></feFuncA>
 | 
			
		||||
            </feComponentTransfer>
 | 
			
		||||
        </filter>
 | 
			
		||||
    </svg>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="pf-c-login">
 | 
			
		||||
    <div class="pf-c-login__container">
 | 
			
		||||
        <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: 60px;" alt="passbook branding" />
 | 
			
		||||
        </header>
 | 
			
		||||
        <main class="pf-c-login__main" id="flow-body">
 | 
			
		||||
            <header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    {% trans 'Bad Request' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                {% block card %}
 | 
			
		||||
                <form method="POST" class="pf-c-form">
 | 
			
		||||
                    {% if message %}
 | 
			
		||||
                    <h3>{% trans message %}</h3>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if 'back' in request.GET %}
 | 
			
		||||
                    <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </form>
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </main>
 | 
			
		||||
        <footer class="pf-c-login__footer">
 | 
			
		||||
            <p></p>
 | 
			
		||||
            <ul class="pf-c-list pf-m-inline">
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <!-- TODO:load config.passbook.footer.links -->
 | 
			
		||||
            </ul>
 | 
			
		||||
        </footer>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% block title %}
 | 
			
		||||
{% trans 'Bad Request' %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<form method="POST" class="pf-c-form">
 | 
			
		||||
    {% if message %}
 | 
			
		||||
    <h3>{% trans message %}</h3>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
    {% if 'back' in request.GET %}
 | 
			
		||||
    <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
 | 
			
		||||
    {% endif %}
 | 
			
		||||
</form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								passbook/core/templates/generic/autosubmit_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								passbook/core/templates/generic/autosubmit_form.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
{% extends "login/base.html" %}
 | 
			
		||||
 | 
			
		||||
{% load passbook_utils %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
{{ title }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<form method="POST" action="{{ url }}" autosubmit>
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% for key, value in attrs.items %}
 | 
			
		||||
    <input type="hidden" name="{{ key }}" value="{{ value }}">
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    <div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
 | 
			
		||||
        <div class="pf-c-form__group-control">
 | 
			
		||||
            <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>
 | 
			
		||||
    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
        <div class="pf-c-form__actions">
 | 
			
		||||
            <button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										34
									
								
								passbook/core/templates/generic/autosubmit_form_full.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								passbook/core/templates/generic/autosubmit_form_full.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
{% extends "login/base_full.html" %}
 | 
			
		||||
 | 
			
		||||
{% load passbook_utils %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% block title %}
 | 
			
		||||
{{ title }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
<form method="POST" action="{{ url }}" autosubmit>
 | 
			
		||||
    {% csrf_token %}
 | 
			
		||||
    {% for key, value in attrs.items %}
 | 
			
		||||
    <input type="hidden" name="{{ key }}" value="{{ value }}">
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    <div class="pf-c-form__group pf-u-display-flex pf-u-justify-content-center">
 | 
			
		||||
        <div class="pf-c-form__group-control">
 | 
			
		||||
            <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>
 | 
			
		||||
    <div class="pf-c-form__group pf-m-action">
 | 
			
		||||
        <div class="pf-c-form__actions">
 | 
			
		||||
            <button class="pf-c-button pf-m-primary pf-m-block" type="submit">{% trans 'Continue' %}</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</form>
 | 
			
		||||
<script>
 | 
			
		||||
document.querySelector("form").submit();
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										54
									
								
								passbook/core/templates/login/base_full.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								passbook/core/templates/login/base_full.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
			
		||||
{% extends 'base/skeleton.html' %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load passbook_utils %}
 | 
			
		||||
 | 
			
		||||
{% block body %}
 | 
			
		||||
<div class="pf-c-background-image">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
 | 
			
		||||
        <filter id="image_overlay">
 | 
			
		||||
            <feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
 | 
			
		||||
            <feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
 | 
			
		||||
                <feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
 | 
			
		||||
                <feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
 | 
			
		||||
                <feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
 | 
			
		||||
                <feFuncA type="table" tableValues="0 1"></feFuncA>
 | 
			
		||||
            </feComponentTransfer>
 | 
			
		||||
        </filter>
 | 
			
		||||
    </svg>
 | 
			
		||||
</div>
 | 
			
		||||
{% include 'partials/messages.html' %}
 | 
			
		||||
<div class="pf-c-login">
 | 
			
		||||
    <div class="pf-c-login__container">
 | 
			
		||||
        <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: 60px;" alt="passbook branding" />
 | 
			
		||||
        </header>
 | 
			
		||||
        {% block main_container %}
 | 
			
		||||
        <main class="pf-c-login__main">
 | 
			
		||||
            <header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    {% block title %}
 | 
			
		||||
                    {% endblock %}
 | 
			
		||||
                </h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                {% block card %}
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </main>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
        <footer class="pf-c-login__footer">
 | 
			
		||||
            <p></p>
 | 
			
		||||
            <ul class="pf-c-list pf-m-inline">
 | 
			
		||||
                {% for link in config.passbook.footer_links %}
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="{{ link.href }}">{{ link.name }}</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </footer>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -1,62 +1,25 @@
 | 
			
		||||
{% extends 'base/skeleton.html' %}
 | 
			
		||||
{% extends 'login/base_full.html' %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load passbook_utils %}
 | 
			
		||||
 | 
			
		||||
{% block body %}
 | 
			
		||||
<div class="pf-c-background-image">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
 | 
			
		||||
        <filter id="image_overlay">
 | 
			
		||||
            <feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
 | 
			
		||||
            <feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
 | 
			
		||||
                <feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
 | 
			
		||||
                <feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
 | 
			
		||||
                <feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
 | 
			
		||||
                <feFuncA type="table" tableValues="0 1"></feFuncA>
 | 
			
		||||
            </feComponentTransfer>
 | 
			
		||||
        </filter>
 | 
			
		||||
    </svg>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="pf-c-login">
 | 
			
		||||
    <div class="pf-c-login__container">
 | 
			
		||||
        <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: 60px;" alt="passbook branding" />
 | 
			
		||||
        </header>
 | 
			
		||||
        <main class="pf-c-login__main" id="flow-body">
 | 
			
		||||
            <header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    {% trans 'Permission denied' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                {% block card %}
 | 
			
		||||
                    <form method="POST" class="pf-c-form">
 | 
			
		||||
                        {% csrf_token %}
 | 
			
		||||
                        {% include 'partials/form.html' %}
 | 
			
		||||
                        <div class="pf-c-form__group">
 | 
			
		||||
                            <p>
 | 
			
		||||
                                <i class="pf-icon pf-icon-error-circle-o"></i>
 | 
			
		||||
                                {% trans 'Access denied' %}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% if 'back' in request.GET %}
 | 
			
		||||
                        <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                    </form>
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </main>
 | 
			
		||||
        <footer class="pf-c-login__footer">
 | 
			
		||||
            <p></p>
 | 
			
		||||
            <ul class="pf-c-list pf-m-inline">
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <!-- TODO: load config.passbook.footer.links -->
 | 
			
		||||
            </ul>
 | 
			
		||||
        </footer>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% block title %}
 | 
			
		||||
{% trans 'Permission denied' %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block card %}
 | 
			
		||||
    <form method="POST" class="pf-c-form">
 | 
			
		||||
        {% csrf_token %}
 | 
			
		||||
        {% include 'partials/form.html' %}
 | 
			
		||||
        <div class="pf-c-form__group">
 | 
			
		||||
            <p>
 | 
			
		||||
                <i class="pf-icon pf-icon-error-circle-o"></i>
 | 
			
		||||
                {% trans 'Access denied' %}
 | 
			
		||||
            </p>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% if 'back' in request.GET %}
 | 
			
		||||
        <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </form>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
{% if messages %}
 | 
			
		||||
<ul class="pf-c-alert-group pf-m-toast">
 | 
			
		||||
    {% for msg in messages %}
 | 
			
		||||
    <li class="pf-c-alert-group__item">
 | 
			
		||||
@ -21,4 +20,3 @@
 | 
			
		||||
    </li>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
</ul>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
@ -40,7 +40,7 @@
 | 
			
		||||
                <ul class="pf-c-nav__list">
 | 
			
		||||
                    {% for source in user_sources_loc %}
 | 
			
		||||
                    <li class="pf-c-nav__item">
 | 
			
		||||
                        <a href="{{ source.view_name }}"
 | 
			
		||||
                        <a href="{{ source.url }}"
 | 
			
		||||
                            class="pf-c-nav__link {% if source.url == request.get_full_path %} pf-m-current {% endif %}">
 | 
			
		||||
                            {{ source.name }}
 | 
			
		||||
                        </a>
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
"""passbook user view tests"""
 | 
			
		||||
"""passbook core task tests"""
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from guardian.shortcuts import get_anonymous_user
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Token
 | 
			
		||||
from passbook.core.tasks import clean_tokens
 | 
			
		||||
from passbook.core.tasks import clean_expired_models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTasks(TestCase):
 | 
			
		||||
@ -14,5 +14,5 @@ class TestTasks(TestCase):
 | 
			
		||||
        """Test Token cleanup task"""
 | 
			
		||||
        Token.objects.create(expires=now(), user=get_anonymous_user())
 | 
			
		||||
        self.assertEqual(Token.objects.all().count(), 1)
 | 
			
		||||
        clean_tokens()
 | 
			
		||||
        clean_expired_models()
 | 
			
		||||
        self.assertEqual(Token.objects.all().count(), 0)
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,9 @@ class CertificateKeyPair(CreatedUpdatedModel):
 | 
			
		||||
    @property
 | 
			
		||||
    def fingerprint(self) -> str:
 | 
			
		||||
        """Get SHA256 Fingerprint of certificate_data"""
 | 
			
		||||
        return hexlify(self.certificate.fingerprint(hashes.SHA256())).decode("utf-8")
 | 
			
		||||
        return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
 | 
			
		||||
            "utf-8"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return f"Certificate-Key Pair {self.name} {self.fingerprint}"
 | 
			
		||||
 | 
			
		||||
@ -10,9 +10,9 @@ from passbook.stages.prompt.models import FieldTypes
 | 
			
		||||
FLOW_POLICY_EXPRESSION = """# This policy ensures that this flow can only be used when the user
 | 
			
		||||
# is in a SSO Flow (meaning they come from an external IdP)
 | 
			
		||||
return pb_is_sso_flow"""
 | 
			
		||||
PROMPT_POLICY_EXPRESSION = """# Check if we've been given a username by the external IdP
 | 
			
		||||
PROMPT_POLICY_EXPRESSION = """# Check if we've not been given a username by the external IdP
 | 
			
		||||
# and trigger the enrollment flow
 | 
			
		||||
return 'username' in pb_flow_plan.context.get('prompt_data', {})"""
 | 
			
		||||
return 'username' not in pb_flow_plan.context.get('prompt_data', {})"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_default_source_enrollment_flow(
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,20 @@
 | 
			
		||||
"""Flow models"""
 | 
			
		||||
from typing import Callable, Optional
 | 
			
		||||
from typing import TYPE_CHECKING, Optional, Type
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from model_utils.managers import InheritanceManager
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.types import UIUserSettings
 | 
			
		||||
from passbook.lib.utils.reflection import class_to_path
 | 
			
		||||
from passbook.policies.models import PolicyBindingModel
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from passbook.flows.stage import StageView
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -44,8 +47,17 @@ class Stage(models.Model):
 | 
			
		||||
    name = models.TextField()
 | 
			
		||||
 | 
			
		||||
    objects = InheritanceManager()
 | 
			
		||||
    type = ""
 | 
			
		||||
    form = ""
 | 
			
		||||
 | 
			
		||||
    def type(self) -> Type["StageView"]:
 | 
			
		||||
        """Return StageView class that implements logic for this stage"""
 | 
			
		||||
        # This is a bit of a workaround, since we can't set class methods with setattr
 | 
			
		||||
        if hasattr(self, "__in_memory_type"):
 | 
			
		||||
            return getattr(self, "__in_memory_type")
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        """Return Form class used to edit this object"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UIUserSettings]:
 | 
			
		||||
@ -57,11 +69,13 @@ class Stage(models.Model):
 | 
			
		||||
        return f"Stage {self.name}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def in_memory_stage(_type: Callable) -> Stage:
 | 
			
		||||
def in_memory_stage(view: Type["StageView"]) -> Stage:
 | 
			
		||||
    """Creates an in-memory stage instance, based on a `_type` as view."""
 | 
			
		||||
    class_path = class_to_path(_type)
 | 
			
		||||
    stage = Stage()
 | 
			
		||||
    stage.type = class_path
 | 
			
		||||
    # Because we can't pickle a locally generated function,
 | 
			
		||||
    # we set the view as a separate property and reference a generic function
 | 
			
		||||
    # that returns that member
 | 
			
		||||
    setattr(stage, "__in_memory_type", view)
 | 
			
		||||
    return stage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
 | 
			
		||||
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from elasticapm import capture_span
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
@ -51,7 +52,8 @@ class FlowPlan:
 | 
			
		||||
        stage = self.stages[0]
 | 
			
		||||
        marker = self.markers[0]
 | 
			
		||||
 | 
			
		||||
        LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
 | 
			
		||||
        if marker.__class__ is not StageMarker:
 | 
			
		||||
            LOGGER.debug("f(plan_inst): stage has marker", stage=stage, marker=marker)
 | 
			
		||||
        marked_stage = marker.process(self, stage)
 | 
			
		||||
        if not marked_stage:
 | 
			
		||||
            LOGGER.debug("f(plan_inst): marker returned none, next stage", stage=stage)
 | 
			
		||||
@ -88,6 +90,7 @@ class FlowPlanner:
 | 
			
		||||
        self.allow_empty_flows = False
 | 
			
		||||
        self.flow = flow
 | 
			
		||||
 | 
			
		||||
    @capture_span(name="FlowPlanner", span_type="flow.planner.plan")
 | 
			
		||||
    def plan(
 | 
			
		||||
        self, request: HttpRequest, default_context: Optional[Dict[str, Any]] = None
 | 
			
		||||
    ) -> FlowPlan:
 | 
			
		||||
@ -127,6 +130,7 @@ class FlowPlanner:
 | 
			
		||||
            raise EmptyFlowException()
 | 
			
		||||
        return plan
 | 
			
		||||
 | 
			
		||||
    @capture_span(name="FlowPlanner", span_type="flow.planner.build_plan")
 | 
			
		||||
    def _build_plan(
 | 
			
		||||
        self,
 | 
			
		||||
        user: User,
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
{% extends 'base/skeleton.html' %}
 | 
			
		||||
{% extends 'login/base_full.html' %}
 | 
			
		||||
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
@ -20,50 +20,16 @@
 | 
			
		||||
</style>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block body %}
 | 
			
		||||
<div class="pf-c-background-image">
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
 | 
			
		||||
        <filter id="image_overlay">
 | 
			
		||||
            <feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
 | 
			
		||||
            <feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
 | 
			
		||||
                <feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
 | 
			
		||||
                <feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
 | 
			
		||||
                <feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
 | 
			
		||||
                <feFuncA type="table" tableValues="0 1"></feFuncA>
 | 
			
		||||
            </feComponentTransfer>
 | 
			
		||||
        </filter>
 | 
			
		||||
    </svg>
 | 
			
		||||
</div>
 | 
			
		||||
<ul class="pf-c-alert-group pf-m-toast">
 | 
			
		||||
</ul>
 | 
			
		||||
<div class="pf-c-login">
 | 
			
		||||
    <div class="pf-c-login__container">
 | 
			
		||||
        <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: 60px;"
 | 
			
		||||
                alt="passbook branding" />
 | 
			
		||||
        </header>
 | 
			
		||||
        <main class="pf-c-login__main" id="flow-body">
 | 
			
		||||
            <div class="pf-c-login__main-body pb-loading">
 | 
			
		||||
                <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>
 | 
			
		||||
        </main>
 | 
			
		||||
        <footer class="pf-c-login__footer">
 | 
			
		||||
            <p></p>
 | 
			
		||||
            <ul class="pf-c-list pf-m-inline">
 | 
			
		||||
                <li>
 | 
			
		||||
                    <a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <!-- TODO:load config.passbook.footer.links -->
 | 
			
		||||
            </ul>
 | 
			
		||||
        </footer>
 | 
			
		||||
{% block main_container %}
 | 
			
		||||
<main class="pf-c-login__main" id="flow-body">
 | 
			
		||||
    <div class="pf-c-login__main-body pb-loading">
 | 
			
		||||
        <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>
 | 
			
		||||
</main>
 | 
			
		||||
<script>
 | 
			
		||||
const flowBodyUrl = "{{ exec_url }}";
 | 
			
		||||
const messagesUrl = "{{ msg_url }}";
 | 
			
		||||
@ -171,6 +137,7 @@ const setFormSubmitHandlers = () => {
 | 
			
		||||
        form.addEventListener('submit', (e) => {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            let formData = new FormData(form);
 | 
			
		||||
            showSpinner();
 | 
			
		||||
            fetch(flowBodyUrl, {
 | 
			
		||||
                method: 'post',
 | 
			
		||||
                body: formData,
 | 
			
		||||
 | 
			
		||||
@ -21,14 +21,15 @@ from passbook.core.views.utils import PermissionDeniedView
 | 
			
		||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
 | 
			
		||||
from passbook.flows.models import Flow, FlowDesignation, Stage
 | 
			
		||||
from passbook.flows.planner import FlowPlan, FlowPlanner
 | 
			
		||||
from passbook.lib.utils.reflection import class_to_path, path_to_class
 | 
			
		||||
from passbook.lib.utils.urls import redirect_with_qs
 | 
			
		||||
from passbook.lib.utils.reflection import class_to_path
 | 
			
		||||
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
 | 
			
		||||
from passbook.lib.views import bad_request_message
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
# Argument used to redirect user after login
 | 
			
		||||
NEXT_ARG_NAME = "next"
 | 
			
		||||
SESSION_KEY_PLAN = "passbook_flows_plan"
 | 
			
		||||
SESSION_KEY_APPLICATION_PRE = "passbook_flows_application_pre"
 | 
			
		||||
SESSION_KEY_GET = "passbook_flows_get"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -49,8 +50,9 @@ class FlowExecutorView(View):
 | 
			
		||||
    def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
 | 
			
		||||
        """When a flow is non-applicable check if user is on the correct domain"""
 | 
			
		||||
        if NEXT_ARG_NAME in self.request.GET:
 | 
			
		||||
            LOGGER.debug("f(exec): Redirecting to next on fail")
 | 
			
		||||
            return redirect(self.request.GET.get(NEXT_ARG_NAME))
 | 
			
		||||
            if not is_url_absolute(self.request.GET.get(NEXT_ARG_NAME)):
 | 
			
		||||
                LOGGER.debug("f(exec): Redirecting to next on fail")
 | 
			
		||||
                return redirect(self.request.GET.get(NEXT_ARG_NAME))
 | 
			
		||||
        message = exc.__doc__ if exc.__doc__ else str(exc)
 | 
			
		||||
        return bad_request_message(self.request, message)
 | 
			
		||||
 | 
			
		||||
@ -92,7 +94,7 @@ class FlowExecutorView(View):
 | 
			
		||||
        if not self.current_stage:
 | 
			
		||||
            LOGGER.debug("f(exec): no more stages, flow is done.")
 | 
			
		||||
            return self._flow_done()
 | 
			
		||||
        stage_cls = path_to_class(self.current_stage.type)
 | 
			
		||||
        stage_cls = self.current_stage.type()
 | 
			
		||||
        self.current_stage_view = stage_cls(self)
 | 
			
		||||
        self.current_stage_view.args = self.args
 | 
			
		||||
        self.current_stage_view.kwargs = self.kwargs
 | 
			
		||||
@ -151,11 +153,12 @@ class FlowExecutorView(View):
 | 
			
		||||
 | 
			
		||||
    def _flow_done(self) -> HttpResponse:
 | 
			
		||||
        """User Successfully passed all stages"""
 | 
			
		||||
        self.cancel()
 | 
			
		||||
        # Since this is wrapped by the ExecutorShell, the next argument is saved in the session
 | 
			
		||||
        # extract the next param before cancel as that cleans it
 | 
			
		||||
        next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
 | 
			
		||||
            NEXT_ARG_NAME, "passbook_core:overview"
 | 
			
		||||
        )
 | 
			
		||||
        self.cancel()
 | 
			
		||||
        return redirect_with_qs(next_param)
 | 
			
		||||
 | 
			
		||||
    def stage_ok(self) -> HttpResponse:
 | 
			
		||||
@ -198,8 +201,14 @@ class FlowExecutorView(View):
 | 
			
		||||
 | 
			
		||||
    def cancel(self):
 | 
			
		||||
        """Cancel current execution and return a redirect"""
 | 
			
		||||
        if SESSION_KEY_PLAN in self.request.session:
 | 
			
		||||
            del self.request.session[SESSION_KEY_PLAN]
 | 
			
		||||
        keys_to_delete = [
 | 
			
		||||
            SESSION_KEY_APPLICATION_PRE,
 | 
			
		||||
            SESSION_KEY_PLAN,
 | 
			
		||||
            SESSION_KEY_GET,
 | 
			
		||||
        ]
 | 
			
		||||
        for key in keys_to_delete:
 | 
			
		||||
            if key in self.request.session:
 | 
			
		||||
                del self.request.session[key]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FlowPermissionDeniedView(PermissionDeniedView):
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,12 @@ import os
 | 
			
		||||
from collections.abc import Mapping
 | 
			
		||||
from contextlib import contextmanager
 | 
			
		||||
from glob import glob
 | 
			
		||||
from typing import Any
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
import yaml
 | 
			
		||||
from django.conf import ImproperlyConfigured
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", ""] + glob(
 | 
			
		||||
@ -18,6 +19,12 @@ ENV_PREFIX = "PASSBOOK"
 | 
			
		||||
ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def context_processor(request: HttpRequest) -> Dict[str, Any]:
 | 
			
		||||
    """Context Processor that injects config object into every template"""
 | 
			
		||||
    kwargs = {"config": CONFIG.raw}
 | 
			
		||||
    return kwargs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConfigLoader:
 | 
			
		||||
    """Search through SEARCH_PATHS and load configuration. Environment variables starting with
 | 
			
		||||
    `ENV_PREFIX` are also applied.
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,9 @@ log_level: warning
 | 
			
		||||
error_reporting: false
 | 
			
		||||
 | 
			
		||||
passbook:
 | 
			
		||||
  # Optionally add links to the footer on the login page
 | 
			
		||||
  footer_links:
 | 
			
		||||
    # Optionally add links to the footer on the login page
 | 
			
		||||
    - name: Documentation
 | 
			
		||||
      href: https://passbook.beryju.org/
 | 
			
		||||
    #  - name: test
 | 
			
		||||
    #    href: https://test
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ from textwrap import indent
 | 
			
		||||
from typing import Any, Dict, Iterable, Optional
 | 
			
		||||
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from elasticapm import capture_span
 | 
			
		||||
from requests import Session
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
@ -63,11 +64,14 @@ class BaseEvaluator:
 | 
			
		||||
    def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
 | 
			
		||||
        """Wrap expression in a function, call it, and save the result as `result`"""
 | 
			
		||||
        handler_signature = ",".join(params)
 | 
			
		||||
        full_expression = f"def handler({handler_signature}):\n"
 | 
			
		||||
        full_expression = ""
 | 
			
		||||
        full_expression += "from ipaddress import ip_address, ip_network\n"
 | 
			
		||||
        full_expression += f"def handler({handler_signature}):\n"
 | 
			
		||||
        full_expression += indent(expression, "    ")
 | 
			
		||||
        full_expression += f"\nresult = handler({handler_signature})"
 | 
			
		||||
        return full_expression
 | 
			
		||||
 | 
			
		||||
    @capture_span(name="BaseEvaluator", span_type="lib.evaluator.evaluate")
 | 
			
		||||
    def evaluate(self, expression_source: str) -> Any:
 | 
			
		||||
        """Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
 | 
			
		||||
        If any exception is raised during execution, it is raised.
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ 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 elasticapm.transport.http import TransportException
 | 
			
		||||
from redis.exceptions import RedisError
 | 
			
		||||
from rest_framework.exceptions import APIException
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
@ -33,9 +34,10 @@ def before_send(event, hint):
 | 
			
		||||
        OSError,
 | 
			
		||||
        RedisError,
 | 
			
		||||
        SentryIgnoredException,
 | 
			
		||||
        TransportException,
 | 
			
		||||
    )
 | 
			
		||||
    if "exc_info" in hint:
 | 
			
		||||
        _exc_type, exc_value, _ = hint["exc_info"]
 | 
			
		||||
        _, exc_value, _ = hint["exc_info"]
 | 
			
		||||
        if isinstance(exc_value, ignored_classes):
 | 
			
		||||
            LOGGER.info("Supressing error %r", exc_value)
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ def back(context: Context) -> str:
 | 
			
		||||
def fieldtype(field):
 | 
			
		||||
    """Return classname"""
 | 
			
		||||
    if isinstance(field.__class__, Model) or issubclass(field.__class__, Model):
 | 
			
		||||
        return field._meta.verbose_name
 | 
			
		||||
        return verbose_name(field)
 | 
			
		||||
    return field.__class__.__name__
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -84,6 +84,9 @@ def verbose_name(obj) -> str:
 | 
			
		||||
    """Return Object's Verbose Name"""
 | 
			
		||||
    if not obj:
 | 
			
		||||
        return ""
 | 
			
		||||
    if hasattr(obj, "verbose_name"):
 | 
			
		||||
        print(obj.verbose_name)
 | 
			
		||||
        return obj.verbose_name
 | 
			
		||||
    return obj._meta.verbose_name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -92,7 +95,7 @@ def form_verbose_name(obj) -> str:
 | 
			
		||||
    """Return ModelForm's Object's Verbose Name"""
 | 
			
		||||
    if not obj:
 | 
			
		||||
        return ""
 | 
			
		||||
    return obj._meta.model._meta.verbose_name
 | 
			
		||||
    return verbose_name(obj._meta.model)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.filter
 | 
			
		||||
 | 
			
		||||
@ -23,4 +23,4 @@ def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]:
 | 
			
		||||
    Returns none if no IP Could be found"""
 | 
			
		||||
    if request:
 | 
			
		||||
        return _get_client_ip_from_meta(request.META)
 | 
			
		||||
    return ""
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										38
									
								
								passbook/lib/utils/time.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								passbook/lib/utils/time.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
"""Time utilities"""
 | 
			
		||||
import datetime
 | 
			
		||||
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
ALLOWED_KEYS = (
 | 
			
		||||
    "days",
 | 
			
		||||
    "seconds",
 | 
			
		||||
    "microseconds",
 | 
			
		||||
    "milliseconds",
 | 
			
		||||
    "minutes",
 | 
			
		||||
    "hours",
 | 
			
		||||
    "weeks",
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def timedelta_string_validator(value: str):
 | 
			
		||||
    """Validator for Django that checks if value can be parsed with `timedelta_from_string`"""
 | 
			
		||||
    try:
 | 
			
		||||
        timedelta_from_string(value)
 | 
			
		||||
    except ValueError as exc:
 | 
			
		||||
        raise ValidationError(
 | 
			
		||||
            _("%(value)s is not in the correct format of 'hours=3;minutes=1'."),
 | 
			
		||||
            params={"value": value},
 | 
			
		||||
        ) from exc
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def timedelta_from_string(expr: str) -> datetime.timedelta:
 | 
			
		||||
    """Convert a string with the format of 'hours=1;minute=3;seconds=5' to a
 | 
			
		||||
    `datetime.timedelta` Object with hours = 1, minutes = 3, seconds = 5"""
 | 
			
		||||
    kwargs = {}
 | 
			
		||||
    for duration_pair in expr.split(";"):
 | 
			
		||||
        key, value = duration_pair.split("=")
 | 
			
		||||
        if key.lower() not in ALLOWED_KEYS:
 | 
			
		||||
            continue
 | 
			
		||||
        kwargs[key.lower()] = float(value)
 | 
			
		||||
    return datetime.timedelta(**kwargs)
 | 
			
		||||
@ -1,8 +1,10 @@
 | 
			
		||||
"""Dummy policy"""
 | 
			
		||||
from random import SystemRandom
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Type
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
@ -22,7 +24,10 @@ class DummyPolicy(Policy):
 | 
			
		||||
    wait_min = models.IntegerField(default=5)
 | 
			
		||||
    wait_max = models.IntegerField(default=30)
 | 
			
		||||
 | 
			
		||||
    form = "passbook.policies.dummy.forms.DummyPolicyForm"
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from passbook.policies.dummy.forms import DummyPolicyForm
 | 
			
		||||
 | 
			
		||||
        return DummyPolicyForm
 | 
			
		||||
 | 
			
		||||
    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
			
		||||
        """Wait random time then return result"""
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ from typing import List, Optional
 | 
			
		||||
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from elasticapm import capture_span
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
@ -69,6 +70,7 @@ class PolicyEngine:
 | 
			
		||||
        if policy.__class__ == Policy:
 | 
			
		||||
            raise TypeError(f"Policy '{policy}' is root type")
 | 
			
		||||
 | 
			
		||||
    @capture_span(name="PolicyEngine", span_type="policy.engine.build")
 | 
			
		||||
    def build(self) -> "PolicyEngine":
 | 
			
		||||
        """Build task group"""
 | 
			
		||||
        for binding in self._iter_bindings():
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,9 @@
 | 
			
		||||
"""passbook password_expiry_policy Models"""
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from typing import Type
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
@ -19,7 +21,10 @@ class PasswordExpiryPolicy(Policy):
 | 
			
		||||
    deny_only = models.BooleanField(default=False)
 | 
			
		||||
    days = models.IntegerField()
 | 
			
		||||
 | 
			
		||||
    form = "passbook.policies.expiry.forms.PasswordExpiryPolicyForm"
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from passbook.policies.expiry.forms import PasswordExpiryPolicyForm
 | 
			
		||||
 | 
			
		||||
        return PasswordExpiryPolicyForm
 | 
			
		||||
 | 
			
		||||
    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
			
		||||
        """If password change date is more than x days in the past, call set_unusable_password
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
"""passbook expression policy evaluator"""
 | 
			
		||||
from ipaddress import ip_address
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
@ -41,7 +42,9 @@ class PolicyEvaluator(BaseEvaluator):
 | 
			
		||||
        """Update context based on http request"""
 | 
			
		||||
        # update passbook/policies/expression/templates/policy/expression/form.html
 | 
			
		||||
        # update docs/policies/expression/index.md
 | 
			
		||||
        self._context["pb_client_ip"] = get_client_ip(request) or "255.255.255.255"
 | 
			
		||||
        self._context["pb_client_ip"] = ip_address(
 | 
			
		||||
            get_client_ip(request) or "255.255.255.255"
 | 
			
		||||
        )
 | 
			
		||||
        self._context["request"] = request
 | 
			
		||||
        if SESSION_KEY_PLAN in request.session:
 | 
			
		||||
            self._context["pb_flow_plan"] = request.session[SESSION_KEY_PLAN]
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,8 @@
 | 
			
		||||
"""passbook expression Policy Models"""
 | 
			
		||||
from typing import Type
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.policies.expression.evaluator import PolicyEvaluator
 | 
			
		||||
@ -12,7 +15,10 @@ class ExpressionPolicy(Policy):
 | 
			
		||||
 | 
			
		||||
    expression = models.TextField()
 | 
			
		||||
 | 
			
		||||
    form = "passbook.policies.expression.forms.ExpressionPolicyForm"
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from passbook.policies.expression.forms import ExpressionPolicyForm
 | 
			
		||||
 | 
			
		||||
        return ExpressionPolicyForm
 | 
			
		||||
 | 
			
		||||
    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
			
		||||
        """Evaluate and render expression. Returns PolicyResult(false) on error."""
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ class TestEvaluator(TestCase):
 | 
			
		||||
        evaluator.set_policy_request(self.request)
 | 
			
		||||
        result = evaluator.evaluate(template)
 | 
			
		||||
        self.assertEqual(result.passing, False)
 | 
			
		||||
        self.assertEqual(result.messages, ("invalid syntax (test, line 2)",))
 | 
			
		||||
        self.assertEqual(result.messages, ("invalid syntax (test, line 3)",))
 | 
			
		||||
 | 
			
		||||
    def test_undefined(self):
 | 
			
		||||
        """test undefined result"""
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,8 @@
 | 
			
		||||
"""user field matcher models"""
 | 
			
		||||
from typing import Type
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Group
 | 
			
		||||
@ -12,7 +15,10 @@ class GroupMembershipPolicy(Policy):
 | 
			
		||||
 | 
			
		||||
    group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL)
 | 
			
		||||
 | 
			
		||||
    form = "passbook.policies.group_membership.forms.GroupMembershipPolicyForm"
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from passbook.policies.group_membership.forms import GroupMembershipPolicyForm
 | 
			
		||||
 | 
			
		||||
        return GroupMembershipPolicyForm
 | 
			
		||||
 | 
			
		||||
    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
			
		||||
        return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists())
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ class HaveIBeenPwendPolicySerializer(ModelSerializer):
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = HaveIBeenPwendPolicy
 | 
			
		||||
        fields = GENERAL_SERIALIZER_FIELDS + ["allowed_count"]
 | 
			
		||||
        fields = GENERAL_SERIALIZER_FIELDS + ["password_field", "allowed_count"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HaveIBeenPwendPolicyViewSet(ModelViewSet):
 | 
			
		||||
 | 
			
		||||
@ -14,9 +14,9 @@ class HaveIBeenPwnedPolicyForm(forms.ModelForm):
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = HaveIBeenPwendPolicy
 | 
			
		||||
        fields = GENERAL_FIELDS + ["allowed_count"]
 | 
			
		||||
        fields = GENERAL_FIELDS + ["password_field", "allowed_count"]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput(),
 | 
			
		||||
            "order": forms.NumberInput(),
 | 
			
		||||
            "password_field": forms.TextInput(),
 | 
			
		||||
            "policies": FilteredSelectMultiple(_("policies"), False),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
# Generated by Django 3.0.8 on 2020-07-10 18:45
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_policies_hibp", "0001_initial"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="haveibeenpwendpolicy",
 | 
			
		||||
            name="password_field",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                default="password",
 | 
			
		||||
                help_text="Field key to check, field keys defined in Prompt stages are available.",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,13 +1,15 @@
 | 
			
		||||
"""passbook HIBP Models"""
 | 
			
		||||
from hashlib import sha1
 | 
			
		||||
from typing import Type
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from requests import get
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
from passbook.policies.models import Policy, PolicyResult
 | 
			
		||||
from passbook.policies.types import PolicyRequest
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
@ -16,20 +18,34 @@ class HaveIBeenPwendPolicy(Policy):
 | 
			
		||||
    """Check if password is on HaveIBeenPwned's list by uploading the first
 | 
			
		||||
    5 characters of the SHA1 Hash."""
 | 
			
		||||
 | 
			
		||||
    password_field = models.TextField(
 | 
			
		||||
        default="password",
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Field key to check, field keys defined in Prompt stages are available."
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    allowed_count = models.IntegerField(default=0)
 | 
			
		||||
 | 
			
		||||
    form = "passbook.policies.hibp.forms.HaveIBeenPwnedPolicyForm"
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from passbook.policies.hibp.forms import HaveIBeenPwnedPolicyForm
 | 
			
		||||
 | 
			
		||||
    def passes(self, user: User) -> PolicyResult:
 | 
			
		||||
        return HaveIBeenPwnedPolicyForm
 | 
			
		||||
 | 
			
		||||
    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
			
		||||
        """Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5
 | 
			
		||||
        characters of Password in request and checks if full hash is in response. Returns 0
 | 
			
		||||
        if Password is not in result otherwise the count of how many times it was used."""
 | 
			
		||||
        # Only check if password is being set
 | 
			
		||||
        if not hasattr(user, "__password__"):
 | 
			
		||||
            return PolicyResult(True)
 | 
			
		||||
        password = getattr(user, "__password__")
 | 
			
		||||
        if self.password_field not in request.context:
 | 
			
		||||
            LOGGER.warning(
 | 
			
		||||
                "Password field not set in Policy Request",
 | 
			
		||||
                field=self.password_field,
 | 
			
		||||
                fields=request.context.keys(),
 | 
			
		||||
            )
 | 
			
		||||
        password = request.context[self.password_field]
 | 
			
		||||
 | 
			
		||||
        pw_hash = sha1(password.encode("utf-8")).hexdigest()  # nosec
 | 
			
		||||
        url = "https://api.pwnedpasswords.com/range/%s" % pw_hash[:5]
 | 
			
		||||
        url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}"
 | 
			
		||||
        result = get(url).text
 | 
			
		||||
        final_count = 0
 | 
			
		||||
        for line in result.split("\r\n"):
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										29
									
								
								passbook/policies/hibp/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								passbook/policies/hibp/tests.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
"""HIBP Policy tests"""
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from guardian.shortcuts import get_anonymous_user
 | 
			
		||||
from oauth2_provider.generators import generate_client_secret
 | 
			
		||||
 | 
			
		||||
from passbook.policies.hibp.models import HaveIBeenPwendPolicy
 | 
			
		||||
from passbook.policies.types import PolicyRequest, PolicyResult
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestHIBPPolicy(TestCase):
 | 
			
		||||
    """Test HIBP Policy"""
 | 
			
		||||
 | 
			
		||||
    def test_false(self):
 | 
			
		||||
        """Failing password case"""
 | 
			
		||||
        policy = HaveIBeenPwendPolicy.objects.create(name="test_false",)
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        request.context["password"] = "password"
 | 
			
		||||
        result: PolicyResult = policy.passes(request)
 | 
			
		||||
        self.assertFalse(result.passing)
 | 
			
		||||
        self.assertTrue(result.messages[0].startswith("Password exists on "))
 | 
			
		||||
 | 
			
		||||
    def test_true(self):
 | 
			
		||||
        """Positive password case"""
 | 
			
		||||
        policy = HaveIBeenPwendPolicy.objects.create(name="test_true",)
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        request.context["password"] = generate_client_secret()
 | 
			
		||||
        result: PolicyResult = policy.passes(request)
 | 
			
		||||
        self.assertTrue(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages, tuple())
 | 
			
		||||
@ -3,12 +3,14 @@ from typing import Optional
 | 
			
		||||
 | 
			
		||||
from django.contrib import messages
 | 
			
		||||
from django.contrib.auth.mixins import AccessMixin
 | 
			
		||||
from django.contrib.auth.views import redirect_to_login
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.shortcuts import redirect
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import Application, Provider, User
 | 
			
		||||
from passbook.flows.views import SESSION_KEY_APPLICATION_PRE
 | 
			
		||||
from passbook.policies.engine import PolicyEngine
 | 
			
		||||
from passbook.policies.types import PolicyResult
 | 
			
		||||
 | 
			
		||||
@ -25,8 +27,18 @@ class PolicyAccessMixin(BaseMixin, AccessMixin):
 | 
			
		||||
    """Mixin class for usage in Authorization views.
 | 
			
		||||
    Provider functions to check application access, etc"""
 | 
			
		||||
 | 
			
		||||
    def handle_no_permission(self, application: Optional[Application] = None):
 | 
			
		||||
        if application:
 | 
			
		||||
            self.request.session[SESSION_KEY_APPLICATION_PRE] = application
 | 
			
		||||
        return redirect_to_login(
 | 
			
		||||
            self.request.get_full_path(),
 | 
			
		||||
            self.get_login_url(),
 | 
			
		||||
            self.get_redirect_field_name(),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def handle_no_permission_authorized(self) -> HttpResponse:
 | 
			
		||||
        """Function called when user has no permissions but is authorized"""
 | 
			
		||||
        # TODO: Remove this URL and render the view instead
 | 
			
		||||
        return redirect("passbook_flows:denied")
 | 
			
		||||
 | 
			
		||||
    def provider_to_application(self, provider: Provider) -> Application:
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,9 @@
 | 
			
		||||
"""Policy base models"""
 | 
			
		||||
from typing import Type
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from model_utils.managers import InheritanceManager
 | 
			
		||||
 | 
			
		||||
@ -73,6 +75,10 @@ class Policy(CreatedUpdatedModel):
 | 
			
		||||
 | 
			
		||||
    objects = InheritanceAutoManager()
 | 
			
		||||
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        """Return Form class used to edit this object"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"Policy {self.name}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ class PasswordPolicySerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = PasswordPolicy
 | 
			
		||||
        fields = GENERAL_SERIALIZER_FIELDS + [
 | 
			
		||||
            "password_field",
 | 
			
		||||
            "amount_uppercase",
 | 
			
		||||
            "amount_lowercase",
 | 
			
		||||
            "amount_symbols",
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ class PasswordPolicyForm(forms.ModelForm):
 | 
			
		||||
 | 
			
		||||
        model = PasswordPolicy
 | 
			
		||||
        fields = GENERAL_FIELDS + [
 | 
			
		||||
            "password_field",
 | 
			
		||||
            "amount_uppercase",
 | 
			
		||||
            "amount_lowercase",
 | 
			
		||||
            "amount_symbols",
 | 
			
		||||
@ -23,6 +24,7 @@ class PasswordPolicyForm(forms.ModelForm):
 | 
			
		||||
        ]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput(),
 | 
			
		||||
            "password_field": forms.TextInput(),
 | 
			
		||||
            "symbol_charset": forms.TextInput(),
 | 
			
		||||
            "error_message": forms.TextInput(),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
# Generated by Django 3.0.8 on 2020-07-10 18:29
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_policies_password", "0001_initial"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="passwordpolicy",
 | 
			
		||||
            name="password_field",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                default="password",
 | 
			
		||||
                help_text="Field key to check, field keys defined in Prompt stages are available.",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,7 +1,9 @@
 | 
			
		||||
"""user field matcher models"""
 | 
			
		||||
import re
 | 
			
		||||
from typing import Type
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
@ -14,6 +16,13 @@ LOGGER = get_logger()
 | 
			
		||||
class PasswordPolicy(Policy):
 | 
			
		||||
    """Policy to make sure passwords have certain properties"""
 | 
			
		||||
 | 
			
		||||
    password_field = models.TextField(
 | 
			
		||||
        default="password",
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Field key to check, field keys defined in Prompt stages are available."
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    amount_uppercase = models.IntegerField(default=0)
 | 
			
		||||
    amount_lowercase = models.IntegerField(default=0)
 | 
			
		||||
    amount_symbols = models.IntegerField(default=0)
 | 
			
		||||
@ -21,22 +30,35 @@ class PasswordPolicy(Policy):
 | 
			
		||||
    symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
 | 
			
		||||
    error_message = models.TextField()
 | 
			
		||||
 | 
			
		||||
    form = "passbook.policies.password.forms.PasswordPolicyForm"
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from passbook.policies.password.forms import PasswordPolicyForm
 | 
			
		||||
 | 
			
		||||
        return PasswordPolicyForm
 | 
			
		||||
 | 
			
		||||
    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
			
		||||
        # Only check if password is being set
 | 
			
		||||
        if not hasattr(request.user, "__password__"):
 | 
			
		||||
            return PolicyResult(True)
 | 
			
		||||
        password = getattr(request.user, "__password__")
 | 
			
		||||
        if self.password_field not in request.context:
 | 
			
		||||
            LOGGER.warning(
 | 
			
		||||
                "Password field not set in Policy Request",
 | 
			
		||||
                field=self.password_field,
 | 
			
		||||
                fields=request.context.keys(),
 | 
			
		||||
            )
 | 
			
		||||
        password = request.context[self.password_field]
 | 
			
		||||
 | 
			
		||||
        filter_regex = r""
 | 
			
		||||
        filter_regex = []
 | 
			
		||||
        if self.amount_lowercase > 0:
 | 
			
		||||
            filter_regex += r"[a-z]{%d,}" % self.amount_lowercase
 | 
			
		||||
            filter_regex.append(r"[a-z]{%d,}" % self.amount_lowercase)
 | 
			
		||||
        if self.amount_uppercase > 0:
 | 
			
		||||
            filter_regex += r"[A-Z]{%d,}" % self.amount_uppercase
 | 
			
		||||
            filter_regex.append(r"[A-Z]{%d,}" % self.amount_uppercase)
 | 
			
		||||
        if self.amount_symbols > 0:
 | 
			
		||||
            filter_regex += r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols)
 | 
			
		||||
        result = bool(re.compile(filter_regex).match(password))
 | 
			
		||||
            filter_regex.append(
 | 
			
		||||
                r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols)
 | 
			
		||||
            )
 | 
			
		||||
        full_regex = "|".join(filter_regex)
 | 
			
		||||
        LOGGER.debug("Built regex", regexp=full_regex)
 | 
			
		||||
        result = bool(re.compile(full_regex).match(password))
 | 
			
		||||
 | 
			
		||||
        result = result and len(password) >= self.length_min
 | 
			
		||||
 | 
			
		||||
        if not result:
 | 
			
		||||
            return PolicyResult(result, self.error_message)
 | 
			
		||||
        return PolicyResult(result)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								passbook/policies/password/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								passbook/policies/password/tests.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
"""Password Policy tests"""
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from guardian.shortcuts import get_anonymous_user
 | 
			
		||||
 | 
			
		||||
from passbook.policies.password.models import PasswordPolicy
 | 
			
		||||
from passbook.policies.types import PolicyRequest, PolicyResult
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPasswordPolicy(TestCase):
 | 
			
		||||
    """Test Password Policy"""
 | 
			
		||||
 | 
			
		||||
    def test_false(self):
 | 
			
		||||
        """Failing password case"""
 | 
			
		||||
        policy = PasswordPolicy.objects.create(
 | 
			
		||||
            name="test_false",
 | 
			
		||||
            amount_uppercase=1,
 | 
			
		||||
            amount_lowercase=2,
 | 
			
		||||
            amount_symbols=3,
 | 
			
		||||
            length_min=24,
 | 
			
		||||
            error_message="test message",
 | 
			
		||||
        )
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        request.context["password"] = "test"
 | 
			
		||||
        result: PolicyResult = policy.passes(request)
 | 
			
		||||
        self.assertFalse(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages, ("test message",))
 | 
			
		||||
 | 
			
		||||
    def test_true(self):
 | 
			
		||||
        """Positive password case"""
 | 
			
		||||
        policy = PasswordPolicy.objects.create(
 | 
			
		||||
            name="test_true",
 | 
			
		||||
            amount_uppercase=1,
 | 
			
		||||
            amount_lowercase=2,
 | 
			
		||||
            amount_symbols=3,
 | 
			
		||||
            length_min=3,
 | 
			
		||||
            error_message="test message",
 | 
			
		||||
        )
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        request.context["password"] = "Test()!"
 | 
			
		||||
        result: PolicyResult = policy.passes(request)
 | 
			
		||||
        self.assertTrue(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages, tuple())
 | 
			
		||||
@ -4,6 +4,7 @@ from multiprocessing.connection import Connection
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from elasticapm import capture_span
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.policies.exceptions import PolicyException
 | 
			
		||||
@ -44,6 +45,7 @@ class PolicyProcess(Process):
 | 
			
		||||
        if connection:
 | 
			
		||||
            self.connection = connection
 | 
			
		||||
 | 
			
		||||
    @capture_span(name="PolicyEngine", span_type="policy.process.execute")
 | 
			
		||||
    def execute(self) -> PolicyResult:
 | 
			
		||||
        """Run actual policy, returns result"""
 | 
			
		||||
        LOGGER.debug(
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,9 @@
 | 
			
		||||
"""passbook reputation request policy"""
 | 
			
		||||
from typing import Type
 | 
			
		||||
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
@ -7,6 +11,9 @@ from passbook.lib.utils.http import get_client_ip
 | 
			
		||||
from passbook.policies.models import Policy
 | 
			
		||||
from passbook.policies.types import PolicyRequest, PolicyResult
 | 
			
		||||
 | 
			
		||||
CACHE_KEY_IP_PREFIX = "passbook_reputation_ip_"
 | 
			
		||||
CACHE_KEY_USER_PREFIX = "passbook_reputation_user_"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ReputationPolicy(Policy):
 | 
			
		||||
    """Return true if request IP/target username's score is below a certain threshold"""
 | 
			
		||||
@ -15,21 +22,20 @@ class ReputationPolicy(Policy):
 | 
			
		||||
    check_username = models.BooleanField(default=True)
 | 
			
		||||
    threshold = models.IntegerField(default=-5)
 | 
			
		||||
 | 
			
		||||
    form = "passbook.policies.reputation.forms.ReputationPolicyForm"
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from passbook.policies.reputation.forms import ReputationPolicyForm
 | 
			
		||||
 | 
			
		||||
        return ReputationPolicyForm
 | 
			
		||||
 | 
			
		||||
    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
			
		||||
        remote_ip = get_client_ip(request.http_request)
 | 
			
		||||
        remote_ip = get_client_ip(request.http_request) or "255.255.255.255"
 | 
			
		||||
        passing = True
 | 
			
		||||
        if self.check_ip:
 | 
			
		||||
            ip_scores = IPReputation.objects.filter(
 | 
			
		||||
                ip=remote_ip, score__lte=self.threshold
 | 
			
		||||
            )
 | 
			
		||||
            passing = passing and ip_scores.exists()
 | 
			
		||||
            score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
 | 
			
		||||
            passing = passing and score <= self.threshold
 | 
			
		||||
        if self.check_username:
 | 
			
		||||
            user_scores = UserReputation.objects.filter(
 | 
			
		||||
                user=request.user, score__lte=self.threshold
 | 
			
		||||
            )
 | 
			
		||||
            passing = passing and user_scores.exists()
 | 
			
		||||
            score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0)
 | 
			
		||||
            passing = passing and score <= self.threshold
 | 
			
		||||
        return PolicyResult(passing)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								passbook/policies/reputation/settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								passbook/policies/reputation/settings.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
"""Reputation Settings"""
 | 
			
		||||
from celery.schedules import crontab
 | 
			
		||||
 | 
			
		||||
CELERY_BEAT_SCHEDULE = {
 | 
			
		||||
    "policies_reputation_ip_save": {
 | 
			
		||||
        "task": "passbook.policies.reputation.tasks.save_ip_reputation",
 | 
			
		||||
        "schedule": crontab(minute="*/5"),
 | 
			
		||||
    },
 | 
			
		||||
    "policies_reputation_user_save": {
 | 
			
		||||
        "task": "passbook.policies.reputation.tasks.save_user_reputation",
 | 
			
		||||
        "schedule": crontab(minute="*/5"),
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
@ -1,36 +1,39 @@
 | 
			
		||||
"""passbook reputation request signals"""
 | 
			
		||||
from django.contrib.auth.signals import user_logged_in, user_login_failed
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.dispatch import receiver
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
from passbook.lib.utils.http import get_client_ip
 | 
			
		||||
from passbook.policies.reputation.models import IPReputation, UserReputation
 | 
			
		||||
from passbook.policies.reputation.models import (
 | 
			
		||||
    CACHE_KEY_IP_PREFIX,
 | 
			
		||||
    CACHE_KEY_USER_PREFIX,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_score(request, username, amount):
 | 
			
		||||
def update_score(request: HttpRequest, username: str, amount: int):
 | 
			
		||||
    """Update score for IP and User"""
 | 
			
		||||
    remote_ip = get_client_ip(request) or "255.255.255.255."
 | 
			
		||||
    ip_score, _ = IPReputation.objects.update_or_create(ip=remote_ip)
 | 
			
		||||
    ip_score.score += amount
 | 
			
		||||
    ip_score.save()
 | 
			
		||||
    LOGGER.debug("Updated score", amount=amount, for_ip=remote_ip)
 | 
			
		||||
    user = User.objects.filter(username=username)
 | 
			
		||||
    if not user.exists():
 | 
			
		||||
        return
 | 
			
		||||
    user_score, _ = UserReputation.objects.update_or_create(user=user.first())
 | 
			
		||||
    user_score.score += amount
 | 
			
		||||
    user_score.save()
 | 
			
		||||
    LOGGER.debug("Updated score", amount=amount, for_user=username)
 | 
			
		||||
    remote_ip = get_client_ip(request) or "255.255.255.255"
 | 
			
		||||
 | 
			
		||||
    # We only update the cache here, as its faster than writing to the DB
 | 
			
		||||
    cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
 | 
			
		||||
    cache.incr(CACHE_KEY_IP_PREFIX + remote_ip, amount)
 | 
			
		||||
 | 
			
		||||
    cache.get_or_set(CACHE_KEY_USER_PREFIX + username, 0)
 | 
			
		||||
    cache.incr(CACHE_KEY_USER_PREFIX + username, amount)
 | 
			
		||||
 | 
			
		||||
    LOGGER.debug("Updated score", amount=amount, for_user=username, for_ip=remote_ip)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(user_login_failed)
 | 
			
		||||
# pylint: disable=unused-argument
 | 
			
		||||
def handle_failed_login(sender, request, credentials, **_):
 | 
			
		||||
    """Lower Score for failed loging attempts"""
 | 
			
		||||
    update_score(request, credentials.get("username"), -1)
 | 
			
		||||
    if "username" in credentials:
 | 
			
		||||
        update_score(request, credentials.get("username"), -1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(user_logged_in)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								passbook/policies/reputation/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								passbook/policies/reputation/tasks.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,45 @@
 | 
			
		||||
"""Reputation tasks"""
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
from passbook.policies.reputation.models import IPReputation, UserReputation
 | 
			
		||||
from passbook.policies.reputation.signals import (
 | 
			
		||||
    CACHE_KEY_IP_PREFIX,
 | 
			
		||||
    CACHE_KEY_USER_PREFIX,
 | 
			
		||||
)
 | 
			
		||||
from passbook.root.celery import CELERY_APP
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task()
 | 
			
		||||
def save_ip_reputation():
 | 
			
		||||
    """Save currently cached reputation to database"""
 | 
			
		||||
    keys = cache.keys(CACHE_KEY_IP_PREFIX + "*")
 | 
			
		||||
    objects_to_update = []
 | 
			
		||||
    for key in keys:
 | 
			
		||||
        score = cache.get(key)
 | 
			
		||||
        remote_ip = key.replace(CACHE_KEY_IP_PREFIX, "")
 | 
			
		||||
        rep, _ = IPReputation.objects.get_or_create(ip=remote_ip)
 | 
			
		||||
        rep.score = score
 | 
			
		||||
        objects_to_update.append(rep)
 | 
			
		||||
    IPReputation.objects.bulk_update(objects_to_update, ["score"])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task()
 | 
			
		||||
def save_user_reputation():
 | 
			
		||||
    """Save currently cached reputation to database"""
 | 
			
		||||
    keys = cache.keys(CACHE_KEY_USER_PREFIX + "*")
 | 
			
		||||
    objects_to_update = []
 | 
			
		||||
    for key in keys:
 | 
			
		||||
        score = cache.get(key)
 | 
			
		||||
        username = key.replace(CACHE_KEY_USER_PREFIX, "")
 | 
			
		||||
        users = User.objects.filter(username=username)
 | 
			
		||||
        if not users.exists():
 | 
			
		||||
            LOGGER.info("User in cache does not exist, ignoring", username=username)
 | 
			
		||||
            continue
 | 
			
		||||
        rep, _ = UserReputation.objects.get_or_create(user=users.first())
 | 
			
		||||
        rep.score = score
 | 
			
		||||
        objects_to_update.append(rep)
 | 
			
		||||
    UserReputation.objects.bulk_update(objects_to_update, ["score"])
 | 
			
		||||
							
								
								
									
										55
									
								
								passbook/policies/reputation/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								passbook/policies/reputation/tests.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
			
		||||
"""test reputation signals and policy"""
 | 
			
		||||
from django.contrib.auth import authenticate
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
from passbook.policies.reputation.models import (
 | 
			
		||||
    CACHE_KEY_IP_PREFIX,
 | 
			
		||||
    CACHE_KEY_USER_PREFIX,
 | 
			
		||||
    IPReputation,
 | 
			
		||||
    ReputationPolicy,
 | 
			
		||||
    UserReputation,
 | 
			
		||||
)
 | 
			
		||||
from passbook.policies.reputation.tasks import save_ip_reputation, save_user_reputation
 | 
			
		||||
from passbook.policies.types import PolicyRequest
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestReputationPolicy(TestCase):
 | 
			
		||||
    """test reputation signals and policy"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.test_ip = "255.255.255.255"
 | 
			
		||||
        self.test_username = "test"
 | 
			
		||||
        cache.delete(CACHE_KEY_IP_PREFIX + self.test_ip)
 | 
			
		||||
        cache.delete(CACHE_KEY_USER_PREFIX + self.test_username)
 | 
			
		||||
        # We need a user for the one-to-one in userreputation
 | 
			
		||||
        self.user = User.objects.create(username=self.test_username)
 | 
			
		||||
 | 
			
		||||
    def test_ip_reputation(self):
 | 
			
		||||
        """test IP reputation"""
 | 
			
		||||
        # Trigger negative reputation
 | 
			
		||||
        authenticate(None, username=self.test_username, password=self.test_username)
 | 
			
		||||
        # Test value in cache
 | 
			
		||||
        self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1)
 | 
			
		||||
        # Save cache and check db values
 | 
			
		||||
        save_ip_reputation()
 | 
			
		||||
        self.assertEqual(IPReputation.objects.get(ip=self.test_ip).score, -1)
 | 
			
		||||
 | 
			
		||||
    def test_user_reputation(self):
 | 
			
		||||
        """test User reputation"""
 | 
			
		||||
        # Trigger negative reputation
 | 
			
		||||
        authenticate(None, username=self.test_username, password=self.test_username)
 | 
			
		||||
        # Test value in cache
 | 
			
		||||
        self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1)
 | 
			
		||||
        # Save cache and check db values
 | 
			
		||||
        save_user_reputation()
 | 
			
		||||
        self.assertEqual(UserReputation.objects.get(user=self.user).score, -1)
 | 
			
		||||
 | 
			
		||||
    def test_policy(self):
 | 
			
		||||
        """Test Policy"""
 | 
			
		||||
        request = PolicyRequest(user=self.user)
 | 
			
		||||
        policy: ReputationPolicy = ReputationPolicy.objects.create(
 | 
			
		||||
            name="reputation-test", threshold=0
 | 
			
		||||
        )
 | 
			
		||||
        self.assertTrue(policy.passes(request).passing)
 | 
			
		||||
@ -14,7 +14,6 @@ def invalidate_policy_cache(sender, instance, **_):
 | 
			
		||||
    from passbook.policies.models import Policy, PolicyBinding
 | 
			
		||||
 | 
			
		||||
    if isinstance(instance, Policy):
 | 
			
		||||
        LOGGER.debug("Invalidating policy cache", policy=instance)
 | 
			
		||||
        total = 0
 | 
			
		||||
        for binding in PolicyBinding.objects.filter(policy=instance):
 | 
			
		||||
            prefix = (
 | 
			
		||||
@ -23,4 +22,4 @@ def invalidate_policy_cache(sender, instance, **_):
 | 
			
		||||
            keys = cache.keys(prefix)
 | 
			
		||||
            total += len(keys)
 | 
			
		||||
            cache.delete_many(keys)
 | 
			
		||||
        LOGGER.debug("Deleted keys", len=total)
 | 
			
		||||
        LOGGER.debug("Invalidating policy cache", policy=instance, keys=total)
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ from passbook.policies.expression.models import ExpressionPolicy
 | 
			
		||||
from passbook.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyTestEngine(TestCase):
 | 
			
		||||
class TestPolicyEngine(TestCase):
 | 
			
		||||
    """PolicyEngine tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
							
								
								
									
										11
									
								
								passbook/policies/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								passbook/policies/utils.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
"""Policy Utils"""
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def delete_none_keys(dict_: Dict[Any, Any]) -> Dict[Any, Any]:
 | 
			
		||||
    """Remove any keys from `dict_` that are None."""
 | 
			
		||||
    new_dict = {}
 | 
			
		||||
    for key, value in dict_.items():
 | 
			
		||||
        if value is not None:
 | 
			
		||||
            new_dict[key] = value
 | 
			
		||||
    return new_dict
 | 
			
		||||
@ -15,17 +15,17 @@ class ApplicationGatewayProviderForm(forms.ModelForm):
 | 
			
		||||
            self.instance.client = Client.objects.create(
 | 
			
		||||
                client_id=generate_client_id(), client_secret=generate_client_secret()
 | 
			
		||||
            )
 | 
			
		||||
        self.instance.client.reuse_consent = False  # This is managed by passbook
 | 
			
		||||
        self.instance.client.require_consent = False  # This is managed by passbook
 | 
			
		||||
        self.instance.client.name = self.instance.name
 | 
			
		||||
        self.instance.client.response_types.set(
 | 
			
		||||
            [ResponseType.objects.get_by_natural_key("code")]
 | 
			
		||||
        )
 | 
			
		||||
        self.instance.client.redirect_uris = [
 | 
			
		||||
            f"http://{self.instance.external_host}/oauth2/callback",
 | 
			
		||||
            f"https://{self.instance.external_host}/oauth2/callback",
 | 
			
		||||
            f"http://{self.instance.internal_host}/oauth2/callback",
 | 
			
		||||
            f"https://{self.instance.internal_host}/oauth2/callback",
 | 
			
		||||
            f"{self.instance.external_host}/oauth2/callback",
 | 
			
		||||
            f"{self.instance.internal_host}/oauth2/callback",
 | 
			
		||||
        ]
 | 
			
		||||
        self.instance.client.scope = ["openid", "email"]
 | 
			
		||||
        self.instance.client.scope = ["openid", "email", "profile"]
 | 
			
		||||
        self.instance.client.save()
 | 
			
		||||
        return super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,24 @@
 | 
			
		||||
# Generated by Django 3.0.8 on 2020-07-26 17:45
 | 
			
		||||
 | 
			
		||||
import django.core.validators
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_providers_app_gw", "0001_initial"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="applicationgatewayprovider",
 | 
			
		||||
            name="external_host",
 | 
			
		||||
            field=models.TextField(validators=[django.core.validators.URLValidator]),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="applicationgatewayprovider",
 | 
			
		||||
            name="internal_host",
 | 
			
		||||
            field=models.TextField(validators=[django.core.validators.URLValidator]),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user