Compare commits
	
		
			65 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7401278707 | |||
| e99f6e289b | |||
| 07da6ffa69 | |||
| dc18730094 | |||
| a202679bfb | |||
| 1edcda58ba | |||
| 5cb7f0794e | |||
| 7e8e3893eb | |||
| e91e286ebc | |||
| ef4a115b61 | |||
| b79b73f5c6 | |||
| 056e3ed15b | |||
| fb5e210af8 | |||
| e5e2615f15 | |||
| 6c72a9e2e8 | |||
| c04d0a373a | |||
| bd74e518a7 | |||
| 3b76af4eaa | |||
| 706448dc14 | |||
| 34793f7cef | |||
| ba96c9526e | |||
| 617432deaa | |||
| 36bf2be16d | |||
| 912ed343e6 | |||
| 2e15df295a | |||
| eaab3f62cb | |||
| aa615b0fd6 | |||
| b775f2788c | |||
| 9c28db3d89 | |||
| 67360bd6e9 | |||
| 4f6f8c7cae | |||
| 3b82ad798b | |||
| 8827f06ac1 | |||
| 251672a67d | |||
| 4ffc0e2a08 | |||
| 4e1808632d | |||
| 791627d3ce | |||
| f3df3a0157 | |||
| 6aaae53a19 | |||
| 4d84f6d598 | |||
| 4e2349b6d9 | |||
| cd57b8f7f3 | |||
| 40b1fc06b0 | |||
| 02fa217e28 | |||
| 6652514358 | |||
| dcd3dc9744 | |||
| d6afdc575e | |||
| 287b38efee | |||
| e805fb62fb | |||
| c92dda77f1 | |||
| f12fd78822 | |||
| caba183c9b | |||
| 3aeaa121a3 | |||
| a9f3118a7d | |||
| 054b819262 | |||
| 6b3411f63b | |||
| 6a8000ea0d | |||
| 352d4db0d7 | |||
| 4b665cfb8f | |||
| 4e12003944 | |||
| 6bfd465855 | |||
| e8670aa693 | |||
| 5263e750b1 | |||
| a2a9d73296 | |||
| 6befc9d627 | 
@ -1,5 +1,5 @@
 | 
			
		||||
[bumpversion]
 | 
			
		||||
current_version = 0.12.6-stable
 | 
			
		||||
current_version = 0.12.9-stable
 | 
			
		||||
tag = True
 | 
			
		||||
commit = True
 | 
			
		||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										16
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										16
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							@ -24,3 +24,19 @@ updates:
 | 
			
		||||
  open-pull-requests-limit: 10
 | 
			
		||||
  assignees:
 | 
			
		||||
  - BeryJu
 | 
			
		||||
- package-ecosystem: docker
 | 
			
		||||
  directory: "/"
 | 
			
		||||
  schedule:
 | 
			
		||||
    interval: daily
 | 
			
		||||
    time: "04:00"
 | 
			
		||||
  open-pull-requests-limit: 10
 | 
			
		||||
  assignees:
 | 
			
		||||
  - BeryJu
 | 
			
		||||
- package-ecosystem: docker
 | 
			
		||||
  directory: "/proxy"
 | 
			
		||||
  schedule:
 | 
			
		||||
    interval: daily
 | 
			
		||||
    time: "04:00"
 | 
			
		||||
  open-pull-requests-limit: 10
 | 
			
		||||
  assignees:
 | 
			
		||||
  - BeryJu
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@ -18,11 +18,11 @@ jobs:
 | 
			
		||||
      - name: Building Docker Image
 | 
			
		||||
        run: docker build
 | 
			
		||||
          --no-cache
 | 
			
		||||
          -t beryju/passbook:0.12.6-stable
 | 
			
		||||
          -t beryju/passbook:0.12.9-stable
 | 
			
		||||
          -t beryju/passbook:latest
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook:0.12.6-stable
 | 
			
		||||
        run: docker push beryju/passbook:0.12.9-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook:latest
 | 
			
		||||
  build-proxy:
 | 
			
		||||
@ -48,11 +48,11 @@ jobs:
 | 
			
		||||
          cd proxy
 | 
			
		||||
          docker build \
 | 
			
		||||
          --no-cache \
 | 
			
		||||
          -t beryju/passbook-proxy:0.12.6-stable \
 | 
			
		||||
          -t beryju/passbook-proxy:0.12.9-stable \
 | 
			
		||||
          -t beryju/passbook-proxy:latest \
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook-proxy:0.12.6-stable
 | 
			
		||||
        run: docker push beryju/passbook-proxy:0.12.9-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook-proxy:latest
 | 
			
		||||
  build-static:
 | 
			
		||||
@ -77,11 +77,11 @@ jobs:
 | 
			
		||||
        run: docker build
 | 
			
		||||
          --no-cache
 | 
			
		||||
          --network=$(docker network ls | grep github | awk '{print $1}')
 | 
			
		||||
          -t beryju/passbook-static:0.12.6-stable
 | 
			
		||||
          -t beryju/passbook-static:0.12.9-stable
 | 
			
		||||
          -t beryju/passbook-static:latest
 | 
			
		||||
          -f static.Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/passbook-static:0.12.6-stable
 | 
			
		||||
        run: docker push beryju/passbook-static:0.12.9-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/passbook-static:latest
 | 
			
		||||
  test-release:
 | 
			
		||||
@ -114,5 +114,5 @@ jobs:
 | 
			
		||||
          SENTRY_PROJECT: passbook
 | 
			
		||||
          SENTRY_URL: https://sentry.beryju.org
 | 
			
		||||
        with:
 | 
			
		||||
          tagName: 0.12.6-stable
 | 
			
		||||
          tagName: 0.12.9-stable
 | 
			
		||||
          environment: beryjuorg-prod
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,9 @@ RUN apt-get update && \
 | 
			
		||||
    groupadd -g 999 docker_999 && \
 | 
			
		||||
    adduser --system --no-create-home --uid 1000 --group --home /passbook passbook && \
 | 
			
		||||
    usermod -a -G docker_998 passbook && \
 | 
			
		||||
    usermod -a -G docker_999 passbook
 | 
			
		||||
    usermod -a -G docker_999 passbook && \
 | 
			
		||||
    mkdir /backups && \
 | 
			
		||||
    chown passbook:passbook /backups
 | 
			
		||||
 | 
			
		||||
COPY ./passbook/ /passbook
 | 
			
		||||
COPY ./manage.py /
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
									
									
									
									
								
							@ -12,7 +12,7 @@ lint-fix:
 | 
			
		||||
 | 
			
		||||
lint:
 | 
			
		||||
	pyright passbook e2e lifecycle
 | 
			
		||||
	bandit -r passbook e2e lifecycle
 | 
			
		||||
	bandit -r passbook e2e lifecycle -x node_modules
 | 
			
		||||
	pylint passbook e2e lifecycle
 | 
			
		||||
	prospector
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										526
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										526
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@ -25,17 +25,17 @@
 | 
			
		||||
        },
 | 
			
		||||
        "amqp": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:9881f8e6fe23e3db9faa6cfd8c05390213e1d1b95c0162bc50552cad75bffa5f",
 | 
			
		||||
                "sha256:a8fb8151eb9d12204c9f1784c0da920476077609fa0a70f2468001e3a4258484"
 | 
			
		||||
                "sha256:5b9062d5c0812335c75434bf17ce33d7a20ecfedaa0733faec7379868eb4068a",
 | 
			
		||||
                "sha256:fcd5b3baeeb7fc19b3486ff6d10543099d40ae1f5c9196eae695d1cde1b2f784"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==5.0.1"
 | 
			
		||||
            "version": "==5.0.2"
 | 
			
		||||
        },
 | 
			
		||||
        "asgiref": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
 | 
			
		||||
                "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
 | 
			
		||||
                "sha256:a5098bc870b80e7b872bff60bb363c7f2c2c89078759f6c47b53ff8c525a152e",
 | 
			
		||||
                "sha256:cd88907ecaec59d78e4ac00ea665b03e571cb37e3a0e37b3702af1a9e86c365a"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.2.10"
 | 
			
		||||
            "version": "==3.3.0"
 | 
			
		||||
        },
 | 
			
		||||
        "async-timeout": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -46,10 +46,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "attrs": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
 | 
			
		||||
                "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
 | 
			
		||||
                "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
 | 
			
		||||
                "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==20.2.0"
 | 
			
		||||
            "version": "==20.3.0"
 | 
			
		||||
        },
 | 
			
		||||
        "autobahn": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -74,18 +74,18 @@
 | 
			
		||||
        },
 | 
			
		||||
        "boto3": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:270ac22a66ce3313e908946193df6e0fb3e81cdf60f5113d62da1d8991b75030",
 | 
			
		||||
                "sha256:e2857738affb394bbe96473de2ed01331685d6e313bb1a3328fd5f47841429cc"
 | 
			
		||||
                "sha256:23d2575b7bd01c4e7153f283c1d1c12d329dabf78a6279d4192f2e752bb67b1a",
 | 
			
		||||
                "sha256:cb3f4c2f2576153b845e5b4f325d54a04f4ca00c156f2063965432bfa5d714dd"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.16.3"
 | 
			
		||||
            "version": "==1.16.13"
 | 
			
		||||
        },
 | 
			
		||||
        "botocore": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:4ea4c74d244c1b4701387fd1abe6a5e1833dc621c6d39f8888f0bfa95ddd82f5",
 | 
			
		||||
                "sha256:f5084376a8519332a200737f5cd80e87f47868b7da4d57fc192397670e0af022"
 | 
			
		||||
                "sha256:1b1b4cf5efd552ecc7f1ce0fc674d5fba56857db5ffe6394ee76edd1a568d7a6",
 | 
			
		||||
                "sha256:b3b4b8fa33620f015c52e426a92e7db21b5e667ed4785c5fbc484ebfdb2b5153"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.19.3"
 | 
			
		||||
            "version": "==1.19.13"
 | 
			
		||||
        },
 | 
			
		||||
        "cachetools": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -96,18 +96,18 @@
 | 
			
		||||
        },
 | 
			
		||||
        "celery": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7aa4ee46ed318bc177900ae7c01500354aee62d723255b0925db0754bcd4d390",
 | 
			
		||||
                "sha256:e3e8956d74af986b1e9770e0a294338b259618bf70283d6157416328e50c2bd6"
 | 
			
		||||
                "sha256:012c814967fe89e3f5d2cf49df2dba3de5f29253a7f4f2270e8fce6b901b4ebf",
 | 
			
		||||
                "sha256:930c3acd55349d028c4e7104a7d377729cbcca19d9fce470c17172d9e7f9a8b6"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==5.0.1"
 | 
			
		||||
            "version": "==5.0.2"
 | 
			
		||||
        },
 | 
			
		||||
        "certifi": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
 | 
			
		||||
                "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
 | 
			
		||||
                "sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
 | 
			
		||||
                "sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2020.6.20"
 | 
			
		||||
            "version": "==2020.11.8"
 | 
			
		||||
        },
 | 
			
		||||
        "cffi": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -152,19 +152,19 @@
 | 
			
		||||
        },
 | 
			
		||||
        "channels": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:08e756406d7165cb32f6fc3090c0643f41ca9f7e0f7fada0b31194662f20f414",
 | 
			
		||||
                "sha256:80a5ad1962ae039a3dcc0a5cb5212413e66e2f11ad9e9db8004834436daf3400"
 | 
			
		||||
                "sha256:5cdd9c6b9ee663cdf1bbb00de7cdab885a3c418f9d32a29f04b09498828020f6",
 | 
			
		||||
                "sha256:b02e150b48704ec3607d4168402ac5c26138dd183fcdb7f2aeb965e6e19fd558"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.4.0"
 | 
			
		||||
            "version": "==3.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "channels-redis": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:3ce9832b64a2d7f950dd11e4f0dca784de7cbee99e95a3c345a1460c8878b682",
 | 
			
		||||
                "sha256:41ee0af352d3b6b31a6b613985b51dc5695d2da60688c38e6caa0a1772735a9f"
 | 
			
		||||
                "sha256:18d63f6462a58011740dc8eeb57ea4b31ec220eb551cb71b27de9c6779a549de",
 | 
			
		||||
                "sha256:2fb31a63b05373f6402da2e6a91a22b9e66eb8b56626c6bfc93e156c734c5ae6"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.1.0"
 | 
			
		||||
            "version": "==3.2.0"
 | 
			
		||||
        },
 | 
			
		||||
        "chardet": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -216,27 +216,30 @@
 | 
			
		||||
        },
 | 
			
		||||
        "cryptography": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6",
 | 
			
		||||
                "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b",
 | 
			
		||||
                "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5",
 | 
			
		||||
                "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf",
 | 
			
		||||
                "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e",
 | 
			
		||||
                "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b",
 | 
			
		||||
                "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae",
 | 
			
		||||
                "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b",
 | 
			
		||||
                "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0",
 | 
			
		||||
                "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b",
 | 
			
		||||
                "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d",
 | 
			
		||||
                "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229",
 | 
			
		||||
                "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3",
 | 
			
		||||
                "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365",
 | 
			
		||||
                "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55",
 | 
			
		||||
                "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270",
 | 
			
		||||
                "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e",
 | 
			
		||||
                "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785",
 | 
			
		||||
                "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0"
 | 
			
		||||
                "sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538",
 | 
			
		||||
                "sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f",
 | 
			
		||||
                "sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77",
 | 
			
		||||
                "sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b",
 | 
			
		||||
                "sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33",
 | 
			
		||||
                "sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e",
 | 
			
		||||
                "sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb",
 | 
			
		||||
                "sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e",
 | 
			
		||||
                "sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7",
 | 
			
		||||
                "sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297",
 | 
			
		||||
                "sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d",
 | 
			
		||||
                "sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7",
 | 
			
		||||
                "sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b",
 | 
			
		||||
                "sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7",
 | 
			
		||||
                "sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4",
 | 
			
		||||
                "sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8",
 | 
			
		||||
                "sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b",
 | 
			
		||||
                "sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851",
 | 
			
		||||
                "sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13",
 | 
			
		||||
                "sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b",
 | 
			
		||||
                "sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3",
 | 
			
		||||
                "sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.9.2"
 | 
			
		||||
            "version": "==3.2.1"
 | 
			
		||||
        },
 | 
			
		||||
        "dacite": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -248,10 +251,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "daphne": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:1ca46d7419103958bbc9576fb7ba3b25b053006e22058bc97084ee1a7d44f4ba",
 | 
			
		||||
                "sha256:aa64840015709bbc9daa3c4464a4a4d437937d6cda10a9b51e913eb319272553"
 | 
			
		||||
                "sha256:60856f7efa0b1e1b969efa074e8698bd09de4713ecc06e6a4d19d04c66c4a3bd",
 | 
			
		||||
                "sha256:b43e70d74ff832a634ff6c92badd208824e4530e08b340116517e5aad0aca774"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.5.0"
 | 
			
		||||
            "version": "==3.0.0"
 | 
			
		||||
        },
 | 
			
		||||
        "defusedxml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -263,11 +266,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc",
 | 
			
		||||
                "sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4"
 | 
			
		||||
                "sha256:14a4b7cd77297fba516fc0d92444cc2e2e388aa9de32d7a68d4a83d58f5a4927",
 | 
			
		||||
                "sha256:14b87775ffedab2ef6299b73343d1b4b41e5d4e2aa58c6581f114dbec01e3f8f"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.1.2"
 | 
			
		||||
            "version": "==3.1.3"
 | 
			
		||||
        },
 | 
			
		||||
        "django-cors-middleware": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -310,11 +313,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django-otp": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2fb1c8dbd7e7ae76a65b63d89d3d8c3e1105a48bc29830b81c6e417a89380658",
 | 
			
		||||
                "sha256:fef1f2de9a52bc37e16211b98b4323e5b34fa24739116fbe3d1ff018c17ebea8"
 | 
			
		||||
                "sha256:8ba5ab9bd2738c7321376c349d7cce49cf4404e79f6804e0a3cc462a91728e18",
 | 
			
		||||
                "sha256:f523fb9dec420f28a29d3e2ad72ac06f64588956ed4f2b5b430d8e957ebb8287"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.0.1"
 | 
			
		||||
            "version": "==1.0.2"
 | 
			
		||||
        },
 | 
			
		||||
        "django-prometheus": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -349,11 +352,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "djangorestframework": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:5c5071fcbad6dce16f566d492015c829ddb0df42965d488b878594aabc3aed21",
 | 
			
		||||
                "sha256:d54452aedebb4b650254ca092f9f4f5df947cb1de6ab245d817b08b4f4156249"
 | 
			
		||||
                "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.12.1"
 | 
			
		||||
            "version": "==3.12.2"
 | 
			
		||||
        },
 | 
			
		||||
        "djangorestframework-guardian": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -373,11 +375,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "drf-yasg2": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:65826bf19e5222d38b84380468303c8c389d0b9e2335ee6efa4151ba87ca0a3f",
 | 
			
		||||
                "sha256:6c662de6e0ffd4f74c49c06a88b8a9d1eb4bc9d7bfe82dac9f80a51a23cacecb"
 | 
			
		||||
                "sha256:7037a8041eb5d1073fa504a284fc889685f93d0bfd008a963db1b366db786734",
 | 
			
		||||
                "sha256:75e661ca5cf15eb44fcfab408c7b864f87c20794f564aa08b3a31817a857f19d"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.19.3"
 | 
			
		||||
            "version": "==1.19.4"
 | 
			
		||||
        },
 | 
			
		||||
        "eight": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -402,10 +404,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "google-auth": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:712dd7d140a9a1ea218e5688c7fcb04af71b431a29ec9ce433e384c60e387b98",
 | 
			
		||||
                "sha256:9c0f71789438d703f77b94aad4ea545afaec9a65f10e6cc1bc8b89ce242244bb"
 | 
			
		||||
                "sha256:5176db85f1e7e837a646cd9cede72c3c404ccf2e3373d9ee14b2db88febad440",
 | 
			
		||||
                "sha256:b728625ff5dfce8f9e56a499c8a4eb51443a67f20f6d28b67d5774c310ec4b6b"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.22.1"
 | 
			
		||||
            "version": "==1.23.0"
 | 
			
		||||
        },
 | 
			
		||||
        "gunicorn": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -571,6 +573,7 @@
 | 
			
		||||
        },
 | 
			
		||||
        "lxml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:098fb713b31050463751dcc694878e1d39f316b86366fb9fe3fbbe5396ac9fab",
 | 
			
		||||
                "sha256:0e89f5d422988c65e6936e4ec0fe54d6f73f3128c80eb7ecc3b87f595523607b",
 | 
			
		||||
                "sha256:189ad47203e846a7a4951c17694d845b6ade7917c47c64b29b86526eefc3adf5",
 | 
			
		||||
                "sha256:1d87936cb5801c557f3e981c9c193861264c01209cb3ad0964a16310ca1b3301",
 | 
			
		||||
@ -706,9 +709,11 @@
 | 
			
		||||
                "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
 | 
			
		||||
                "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
 | 
			
		||||
                "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
 | 
			
		||||
                "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
 | 
			
		||||
                "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
 | 
			
		||||
                "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
 | 
			
		||||
                "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
 | 
			
		||||
                "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
 | 
			
		||||
                "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
 | 
			
		||||
                "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
 | 
			
		||||
                "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
 | 
			
		||||
@ -763,84 +768,84 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pycryptodome": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba",
 | 
			
		||||
                "sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299",
 | 
			
		||||
                "sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4",
 | 
			
		||||
                "sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1",
 | 
			
		||||
                "sha256:1e655746f539421d923fd48df8f6f40b3443d80b75532501c0085b64afed9df5",
 | 
			
		||||
                "sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b",
 | 
			
		||||
                "sha256:360955eece2cd0fa694a708d10303c6abd7b39614fa2547b6bd245da76198beb",
 | 
			
		||||
                "sha256:39ef9fb52d6ec7728fce1f1693cb99d60ce302aeebd59bcedea70ca3203fda60",
 | 
			
		||||
                "sha256:4350a42028240c344ee855f032c7d4ad6ff4f813bfbe7121547b7dc579ecc876",
 | 
			
		||||
                "sha256:50348edd283afdccddc0938cdc674484533912ba8a99a27c7bfebb75030aa856",
 | 
			
		||||
                "sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2",
 | 
			
		||||
                "sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68",
 | 
			
		||||
                "sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2",
 | 
			
		||||
                "sha256:663f8de2b3df2e744d6e1610506e0ea4e213bde906795953c1e82279c169f0a7",
 | 
			
		||||
                "sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739",
 | 
			
		||||
                "sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0",
 | 
			
		||||
                "sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149",
 | 
			
		||||
                "sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82",
 | 
			
		||||
                "sha256:87006cf0d81505408f1ae4f55cf8a5d95a8e029a4793360720ae17c6500f7ecc",
 | 
			
		||||
                "sha256:9f62d21bc693f3d7d444f17ed2ad7a913b4c37c15cd807895d013c39c0517dfd",
 | 
			
		||||
                "sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23",
 | 
			
		||||
                "sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c",
 | 
			
		||||
                "sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e",
 | 
			
		||||
                "sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc",
 | 
			
		||||
                "sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a",
 | 
			
		||||
                "sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8",
 | 
			
		||||
                "sha256:cecbf67e81d6144a50dc615629772859463b2e4f815d0c082fa421db362f040e",
 | 
			
		||||
                "sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a",
 | 
			
		||||
                "sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6",
 | 
			
		||||
                "sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a",
 | 
			
		||||
                "sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21",
 | 
			
		||||
                "sha256:f2e045224074d5664dc9cbabbf4f4d4d46f1ee90f24780e3a9a668fd096ff17f",
 | 
			
		||||
                "sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345",
 | 
			
		||||
                "sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982",
 | 
			
		||||
                "sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94"
 | 
			
		||||
                "sha256:19cb674df6c74a14b8b408aa30ba8a89bd1c01e23505100fb45f930fbf0ed0d9",
 | 
			
		||||
                "sha256:1cfdb92dca388e27e732caa72a1cc624520fe93752a665c3b6cd8f1a91b34916",
 | 
			
		||||
                "sha256:27397aee992af69d07502126561d851ba3845aa808f0e55c71ad0efa264dd7d4",
 | 
			
		||||
                "sha256:28f75e58d02019a7edc7d4135203d2501dfc47256d175c72c9798f9a129a49a7",
 | 
			
		||||
                "sha256:2a68df525b387201a43b27b879ce8c08948a430e883a756d6c9e3acdaa7d7bd8",
 | 
			
		||||
                "sha256:411745c6dce4eff918906eebcde78771d44795d747e194462abb120d2e537cd9",
 | 
			
		||||
                "sha256:46e96aeb8a9ca8b1edf9b1fd0af4bf6afcf3f1ca7fa35529f5d60b98f3e4e959",
 | 
			
		||||
                "sha256:4ed27951b0a17afd287299e2206a339b5b6d12de9321e1a1575261ef9c4a851b",
 | 
			
		||||
                "sha256:50826b49fbca348a61529693b0031cdb782c39060fb9dca5ac5dff858159dc5a",
 | 
			
		||||
                "sha256:5598dc6c9dbfe882904e54584322893eff185b98960bbe2cdaaa20e8a437b6e5",
 | 
			
		||||
                "sha256:5c3c4865730dfb0263f822b966d6d58429d8b1e560d1ddae37685fd9e7c63161",
 | 
			
		||||
                "sha256:5f19e6ef750f677d924d9c7141f54bade3cd56695bbfd8a9ef15d0378557dfe4",
 | 
			
		||||
                "sha256:60febcf5baf70c566d9d9351c47fbd8321da9a4edf2eff45c4c31c86164ca794",
 | 
			
		||||
                "sha256:62c488a21c253dadc9f731a32f0ac61e4e436d81a1ea6f7d1d9146ed4d20d6bd",
 | 
			
		||||
                "sha256:6d3baaf82681cfb1a842f1c8f77beac791ceedd99af911e4f5fabec32bae2259",
 | 
			
		||||
                "sha256:6e4227849e4231a3f5b35ea5bdedf9a82b3883500e5624f00a19156e9a9ef861",
 | 
			
		||||
                "sha256:6e89bb3826e6f84501e8e3b205c22595d0c5492c2f271cbb9ee1c48eb1866645",
 | 
			
		||||
                "sha256:70d807d11d508433daf96244ec1c64e55039e8a35931fc5ea9eee94dbe3cb6b5",
 | 
			
		||||
                "sha256:76b1a34d74bb2c91bce460cdc74d1347592045627a955e9a252554481c17c52f",
 | 
			
		||||
                "sha256:7798e73225a699651888489fbb1dbc565e03a509942a8ce6194bbe6fb582a41f",
 | 
			
		||||
                "sha256:834b790bbb6bd18956f625af4004d9c15eed12d5186d8e57851454ae76d52215",
 | 
			
		||||
                "sha256:843e5f10ecdf9d307032b8b91afe9da1d6ed5bb89d0bbec5c8dcb4ba44008e11",
 | 
			
		||||
                "sha256:8f9f84059039b672a5a705b3c5aa21747867bacc30a72e28bf0d147cc8ef85ed",
 | 
			
		||||
                "sha256:9000877383e2189dafd1b2fc68c6c726eca9a3cfb6d68148fbb72ccf651959b6",
 | 
			
		||||
                "sha256:910e202a557e1131b1c1b3f17a63914d57aac55cf9fb9b51644962841c3995c4",
 | 
			
		||||
                "sha256:946399d15eccebafc8ce0257fc4caffe383c75e6b0633509bd011e357368306c",
 | 
			
		||||
                "sha256:a199e9ca46fc6e999e5f47fce342af4b56c7de85fae893c69ab6aa17531fb1e1",
 | 
			
		||||
                "sha256:a3d8a9efa213be8232c59cdc6b65600276508e375e0a119d710826248fd18d37",
 | 
			
		||||
                "sha256:a4599c0ca0fc027c780c1c45ed996d5bef03e571470b7b1c7171ec1e1a90914c",
 | 
			
		||||
                "sha256:b4e6b269a8ddaede774e5c3adbef6bf452ee144e6db8a716d23694953348cd86",
 | 
			
		||||
                "sha256:b68794fba45bdb367eeb71249c26d23e61167510a1d0c3d6cf0f2f14636e62ee",
 | 
			
		||||
                "sha256:d7ec2bd8f57c559dd24e71891c51c25266a8deb66fc5f02cc97c7fb593d1780a",
 | 
			
		||||
                "sha256:e15bde67ccb7d4417f627dd16ffe2f5a4c2941ce5278444e884cb26d73ecbc61",
 | 
			
		||||
                "sha256:eb01f9997e4d6a8ec8a1ad1f676ba5a362781ff64e8189fe2985258ba9cb9706",
 | 
			
		||||
                "sha256:faa682c404c218e8788c3126c9a4b8fbcc54dc245b5b6e8ea5b46f3b63bd0c84"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.9.8"
 | 
			
		||||
            "version": "==3.9.9"
 | 
			
		||||
        },
 | 
			
		||||
        "pycryptodomex": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:06f5a458624c9b0e04c0086c7f84bcc578567dab0ddc816e0476b3057b18339f",
 | 
			
		||||
                "sha256:1714675fb4ac29a26ced38ca22eb8ffd923ac851b7a6140563863194d7158422",
 | 
			
		||||
                "sha256:17272d06e4b2f6455ee2cbe93e8eb50d9450a5dc6223d06862ee1ea5d1235861",
 | 
			
		||||
                "sha256:2199708ebeed4b82eb45b10e1754292677f5a0df7d627ee91ea01290b9bab7e6",
 | 
			
		||||
                "sha256:2275a663c9e744ee4eace816ef2d446b3060554c5773a92fbc79b05bf47debda",
 | 
			
		||||
                "sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32",
 | 
			
		||||
                "sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11",
 | 
			
		||||
                "sha256:3b23d63030819b7d9ac7db9360305fd1241e6870ca5b7e8d59fee4db4674a490",
 | 
			
		||||
                "sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f",
 | 
			
		||||
                "sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9",
 | 
			
		||||
                "sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e",
 | 
			
		||||
                "sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be",
 | 
			
		||||
                "sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9",
 | 
			
		||||
                "sha256:8044eae59301dd392fbb4a7c5d64e1aea8ef0be2540549807ecbe703d6233d68",
 | 
			
		||||
                "sha256:85c108b42e47d4073344ff61d4e019f1d95bb7725ca0fe87d0a2deb237c10e49",
 | 
			
		||||
                "sha256:89be1bf55e50116fe7e493a7c0c483099770dd7f81b87ac8d04a43b1a203e259",
 | 
			
		||||
                "sha256:8fcdda24dddf47f716400d54fc7f75cadaaba1dd47cc127e59d752c9c0fc3c48",
 | 
			
		||||
                "sha256:914fbb18e29c54585e6aa39d300385f90d0fa3b3cc02ed829b08f95c1acf60c2",
 | 
			
		||||
                "sha256:93a75d1acd54efed314b82c952b39eac96ce98d241ad7431547442e5c56138aa",
 | 
			
		||||
                "sha256:9fd758e5e2fe02d57860b85da34a1a1e7037155c4eadc2326fc7af02f9cae214",
 | 
			
		||||
                "sha256:a2bc4e1a2e6ca3a18b2e0be6131a23af76fecb37990c159df6edc7da6df913e3",
 | 
			
		||||
                "sha256:a2ee8ba99d33e1a434fcd27d7d0aa7964163efeee0730fe2efc9d60edae1fc71",
 | 
			
		||||
                "sha256:b2d756620078570d3f940c84bc94dd30aa362b795cce8b2723300a8800b87f1c",
 | 
			
		||||
                "sha256:c0d085c8187a1e4d3402f626c9e438b5861151ab132d8761d9c5ce6491a87761",
 | 
			
		||||
                "sha256:c315262e26d54a9684e323e37ac9254f481d57fcc4fd94002992460898ef5c04",
 | 
			
		||||
                "sha256:c990f2c58f7c67688e9e86e6557ed05952669ff6f1343e77b459007d85f7df00",
 | 
			
		||||
                "sha256:ccbbec59bf4b74226170c54476da5780c9176bae084878fc94d9a2c841218e34",
 | 
			
		||||
                "sha256:dc2bed32c7b138f1331794e454a953360c8cedf3ee62ae31f063822da6007489",
 | 
			
		||||
                "sha256:ddb1ae2891c8cb83a25da87a3e00111a9654fc5f0b70f18879c41aece45d6182",
 | 
			
		||||
                "sha256:e070a1f91202ed34c396be5ea842b886f6fa2b90d2db437dc9fb35a26c80c060",
 | 
			
		||||
                "sha256:e42860fbe1292668b682f6dabd225fbe2a7a4fa1632f0c39881c019e93dea594",
 | 
			
		||||
                "sha256:e4e1c486bf226822c8dceac81d0ec59c0a2399dbd1b9e04f03c3efa3605db677",
 | 
			
		||||
                "sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46",
 | 
			
		||||
                "sha256:f5bd6891380e0fb5467251daf22525644fdf6afd9ae8bc2fe065c78ea1882e0d",
 | 
			
		||||
                "sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb"
 | 
			
		||||
                "sha256:15c03ffdac17731b126880622823d30d0a3cc7203cd219e6b9814140a44e7fab",
 | 
			
		||||
                "sha256:20fb7f4efc494016eab1bc2f555bc0a12dd5ca61f35c95df8061818ffb2c20a3",
 | 
			
		||||
                "sha256:28ee3bcb4d609aea3040cad995a8e2c9c6dc57c12183dadd69e53880c35333b9",
 | 
			
		||||
                "sha256:305e3c46f20d019cd57543c255e7ba49e432e275d7c0de8913b6dbe57a851bc8",
 | 
			
		||||
                "sha256:3547b87b16aad6afb28c9b3a9cd870e11b5e7b5ac649b74265258d96d8de1130",
 | 
			
		||||
                "sha256:3642252d7bfc4403a42050e18ba748bedebd5a998a8cba89665a4f42aea4c380",
 | 
			
		||||
                "sha256:404faa3e518f8bea516aae2aac47d4d960397199a15b4bd6f66cad97825469a0",
 | 
			
		||||
                "sha256:42669638e4f7937b7141044a2fbd1019caca62bd2cdd8b535f731426ab07bde1",
 | 
			
		||||
                "sha256:4632d55a140b28e20be3cd7a3057af52fb747298ff0fd3290d4e9f245b5004ba",
 | 
			
		||||
                "sha256:4a88c9383d273bdce3afc216020282c9c5c39ec0bd9462b1a206af6afa377cf0",
 | 
			
		||||
                "sha256:4ce1fc1e6d2fd2d6dc197607153327989a128c093e0e94dca63408f506622c3e",
 | 
			
		||||
                "sha256:55cf4e99b3ba0122dee570dc7661b97bf35c16aab3e2ccb5070709d282a1c7ab",
 | 
			
		||||
                "sha256:5e486cab2dfcfaec934dd4f5d5837f4a9428b690f4d92a3b020fd31d1497ca64",
 | 
			
		||||
                "sha256:65ec88c8271448d2ea109d35c1f297b09b872c57214ab7e832e413090d3469a9",
 | 
			
		||||
                "sha256:6c95a3361ce70068cf69526a58751f73ddac5ba27a3c2379b057efa2f5338c8c",
 | 
			
		||||
                "sha256:73240335f4a1baf12880ebac6df66ab4d3a9212db9f3efe809c36a27280d16f8",
 | 
			
		||||
                "sha256:7651211e15109ac0058a49159265d9f6e6423c8a81c65434d3c56d708417a05b",
 | 
			
		||||
                "sha256:7b5b7c5896f8172ea0beb283f7f9428e0ab88ec248ce0a5b8c98d73e26267d51",
 | 
			
		||||
                "sha256:836fe39282e75311ce4c38468be148f7fac0df3d461c5de58c5ff1ddb8966bac",
 | 
			
		||||
                "sha256:871852044f55295449fbf225538c2c4118525093c32f0a6c43c91bed0452d7e3",
 | 
			
		||||
                "sha256:892e93f3e7e10c751d6c17fa0dc422f7984cfd5eb6690011f9264dc73e2775fc",
 | 
			
		||||
                "sha256:934e460c5058346c6f1d62fdf3db5680fbdfbfd212722d24d8277bf47cd9ebdc",
 | 
			
		||||
                "sha256:9736f3f3e1761024200637a080a4f922f5298ad5d780e10dbb5634fe8c65b34c",
 | 
			
		||||
                "sha256:a1d38a96da57e6103423a446079ead600b450cf0f8ebf56a231895abf77e7ffc",
 | 
			
		||||
                "sha256:a385fceaa0cdb97f0098f1c1e9ec0b46cc09186ddf60ec23538e871b1dddb6dc",
 | 
			
		||||
                "sha256:a7cf1c14e47027d9fb9d26aa62e5d603994227bd635e58a8df4b1d2d1b6a8ed7",
 | 
			
		||||
                "sha256:a9aac1a30b00b5038d3d8e48248f3b58ea15c827b67325c0d18a447552e30fc8",
 | 
			
		||||
                "sha256:b696876ee583d15310be57311e90e153a84b7913ac93e6b99675c0c9867926d0",
 | 
			
		||||
                "sha256:bef9e9d39393dc7baec39ba4bac6c73826a4db02114cdeade2552a9d6afa16e2",
 | 
			
		||||
                "sha256:c885fe4d5f26ce8ca20c97d02e88f5fdd92c01e1cc771ad0951b21e1641faf6d",
 | 
			
		||||
                "sha256:d2d1388595cb5d27d9220d5cbaff4f37c6ec696a25882eb06d224d241e6e93fb",
 | 
			
		||||
                "sha256:d2e853e0f9535e693fade97768cf7293f3febabecc5feb1e9b2ffdfe1044ab96",
 | 
			
		||||
                "sha256:d62fbab185a6b01c5469eda9f0795f3d1a5bba24f5a5813f362e4b73a3c4dc70",
 | 
			
		||||
                "sha256:f20a62397e09704049ce9007bea4f6bad965ba9336a760c6f4ef1b4192e12d6d",
 | 
			
		||||
                "sha256:f81f7311250d9480e36dec819127897ae772e7e8de07abfabe931b8566770b8e"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.9.8"
 | 
			
		||||
            "version": "==3.9.9"
 | 
			
		||||
        },
 | 
			
		||||
        "pyhamcrest": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -885,17 +890,17 @@
 | 
			
		||||
        },
 | 
			
		||||
        "python-dotenv": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d",
 | 
			
		||||
                "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"
 | 
			
		||||
                "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e",
 | 
			
		||||
                "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.14.0"
 | 
			
		||||
            "version": "==0.15.0"
 | 
			
		||||
        },
 | 
			
		||||
        "pytz": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
 | 
			
		||||
                "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
 | 
			
		||||
                "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
 | 
			
		||||
                "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2020.1"
 | 
			
		||||
            "version": "==2020.4"
 | 
			
		||||
        },
 | 
			
		||||
        "pyyaml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -998,11 +1003,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "sentry-sdk": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0eea248408d36e8e7037c7b73827bea20b13a4375bf1719c406cae6fcbc094e3",
 | 
			
		||||
                "sha256:5cf36eb6b1dc62d55f3c64289792cbaebc8ffa5a9da14474f49b46d20caa7fc8"
 | 
			
		||||
                "sha256:17b725df2258354ccb39618ae4ead29651aa92c01a92acf72f98efe06ee2e45a",
 | 
			
		||||
                "sha256:9040539485226708b5cad0401d76628fba4eed9154bf301c50579767afe344fd"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==0.19.1"
 | 
			
		||||
            "version": "==0.19.2"
 | 
			
		||||
        },
 | 
			
		||||
        "service-identity": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1014,11 +1019,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "signxml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:4c996153153c9b1eb7ff40cf624722946f8c2ab059febfa641e54cd59725acd9",
 | 
			
		||||
                "sha256:d116c283f2c940bc2b4edf011330107ba02f197650a4878466987e04142d43b1"
 | 
			
		||||
                "sha256:b70e151d10d99cbc74a50a3344f508ee481fe3c376d61cd1cae850912d303d19",
 | 
			
		||||
                "sha256:bab03a6823c9a5b225d1e6266ce66b5d08c4ebfb42029fdb5d3e588b8128c86d"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.8.0"
 | 
			
		||||
            "version": "==2.8.1"
 | 
			
		||||
        },
 | 
			
		||||
        "six": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1189,48 +1194,60 @@
 | 
			
		||||
        },
 | 
			
		||||
        "zope.interface": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:040f833694496065147e76581c0bf32b229a8b8c5eda120a0293afb008222387",
 | 
			
		||||
                "sha256:11198b44e4a3d8c7a80cc20bbdd65522258a4d82fe467cd310c9fcce8ffe2ed2",
 | 
			
		||||
                "sha256:121a9dccfe0c34be9c33b2c28225f0284f9b8e090580ffdff26c38fa16c7ffe1",
 | 
			
		||||
                "sha256:15f3082575e7e19581a80b866664f843719b647a7f7189c811ba7f9ab3309f83",
 | 
			
		||||
                "sha256:1d73d8986f948525536956ddd902e8a587a6846ebf4492117db16daba2865ddf",
 | 
			
		||||
                "sha256:208e82f73b242275b8566ac07a25158e7b21fa2f14e642a7881048430612d1a6",
 | 
			
		||||
                "sha256:2557833df892558123d791d6ff80ac4a2a0351f69c7421c7d5f0c07db72c8865",
 | 
			
		||||
                "sha256:25ea6906f9987d42546329d06f9750e69f0ee62307a2e7092955ed0758e64f09",
 | 
			
		||||
                "sha256:2c867914f7608674a555ac8daf20265644ac7be709e1da7d818089eebdfe544e",
 | 
			
		||||
                "sha256:2eadac20711a795d3bb7a2bfc87c04091cb5274d9c3281b43088a1227099b662",
 | 
			
		||||
                "sha256:37999d5ebd5d7bcd32438b725ca3470df05a7de8b1e9c0395bef24296b31ca99",
 | 
			
		||||
                "sha256:3ae8946d51789779f76e4fa326fd6676d8c19c1c3b4c4c5e9342807185264875",
 | 
			
		||||
                "sha256:5636cd7e60583b1608044ae4405e91575399430e66a5e1812f4bf30bcc55864e",
 | 
			
		||||
                "sha256:570e637cb6509998555f7e4af13006d89fad6c09cfc5c4795855385391063e4b",
 | 
			
		||||
                "sha256:590a40447ff3803c44050ce3c17c3958f11ca028dae3eacdd7b96775184394fa",
 | 
			
		||||
                "sha256:5aab51b9c1af1b8a84f40aa49ffe1684d41810b18d6c3e94aa50194e0a563f01",
 | 
			
		||||
                "sha256:5ffe4e0753393bcbcfc9a58133ed3d3a584634cc7cc2e667f8e3e6fbcbb2155d",
 | 
			
		||||
                "sha256:663982381bd428a275a841009e52983cc69c471a4979ce01344fadbf72cf353d",
 | 
			
		||||
                "sha256:6d06bf8e24dd6c473c4fbd8e16a83bd2e6d74add6ba25169043deb46d497b211",
 | 
			
		||||
                "sha256:6e5b9a4bf133cf1887b4a04c21c10ca9f548114f19c83957b2820d5c84254940",
 | 
			
		||||
                "sha256:70a2aed9615645bbe9d82c0f52bc7e676d2c0f8a63933d68418e0cb307f30536",
 | 
			
		||||
                "sha256:7750746421c4395e3d2cc3d805919f4f57bb9f2a9a0ccd955566a9341050a1b4",
 | 
			
		||||
                "sha256:7fc8708bc996e50fc7a9a2ad394e1f015348e389da26789fa6916630237143d7",
 | 
			
		||||
                "sha256:91abd2f080065a7c007540f6bbd93ef7bdbbffa6df4a4cfab3892d8623b83c98",
 | 
			
		||||
                "sha256:988f8b2281f3d95c66c01bdb141cefef1cc97db0d473c25c3fe2927ef00293b9",
 | 
			
		||||
                "sha256:9f56121d8a676802044584e6cc41250bbcde069d8adf725b9b817a6b0fd87f09",
 | 
			
		||||
                "sha256:a0f51536ce6e817a7aa25b0dca8b62feb210d4dc22cabfe8d1a92d47979372cd",
 | 
			
		||||
                "sha256:a1cdd7390d7f66ddcebf545203ca3728c4890d605f9f2697bc8e31437906e8e7",
 | 
			
		||||
                "sha256:b10eb4d0a77609679bf5f23708e20b1cd461a1643bd8ea42b1ca4149b1a5406c",
 | 
			
		||||
                "sha256:b274ac8e511b55ffb62e8292316bd2baa80c10e9fe811b1aa5ce81da6b6697d8",
 | 
			
		||||
                "sha256:c75b502af2c83fcfa2ee9c2257c1ba5806634a91a50db6129ff70e67c42c7e7b",
 | 
			
		||||
                "sha256:c9c8e53a5472b77f6a391b515c771105011f4b40740ce53af8428d1c8ca20004",
 | 
			
		||||
                "sha256:d867998a56c5133b9d31992beb699892e33b72150a8bf40f86cb52b8c606c83f",
 | 
			
		||||
                "sha256:eb566cab630ec176b2d6115ed08b2cf4d921b47caa7f02cca1b4a9525223ee94",
 | 
			
		||||
                "sha256:f61e6b95b414431ffe9dc460928fe9f351095fde074e2c2f5c6dda7b67a2192d",
 | 
			
		||||
                "sha256:f718675fd071bcce4f7cbf9250cbaaf64e2e91ef1b0b32a1af596e7412647556",
 | 
			
		||||
                "sha256:f9d4bfbd015e4b80dbad11c97049975f94592a6a0440e903ee647309f6252a1f",
 | 
			
		||||
                "sha256:fae50fc12a5e8541f6f1cc4ed744ca8f76a9543876cf63f618fb0e6aca8f8375",
 | 
			
		||||
                "sha256:fcf9c8edda7f7b2fd78069e97f4197815df5e871ec47b0f22580d330c6dec561",
 | 
			
		||||
                "sha256:fdedce3bc5360bd29d4bb90396e8d4d3c09af49bc0383909fe84c7233c5ee675"
 | 
			
		||||
                "sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1",
 | 
			
		||||
                "sha256:07d61722dd7d85547b7c6b0f5486b4338001fab349f2ac5cabc0b7182eb3425d",
 | 
			
		||||
                "sha256:0a990dcc97806e5980bbb54b2e46b9cde9e48932d8e6984daf71ef1745516123",
 | 
			
		||||
                "sha256:150e8bcb7253a34a4535aeea3de36c0bb3b1a6a47a183a95d65a194b3e07f232",
 | 
			
		||||
                "sha256:1743bcfe45af8846b775086471c28258f4c6e9ee8ef37484de4495f15a98b549",
 | 
			
		||||
                "sha256:1b5f6c8fff4ed32aa2dd43e84061bc8346f32d3ba6ad6e58f088fe109608f102",
 | 
			
		||||
                "sha256:21e49123f375703cf824214939d39df0af62c47d122d955b2a8d9153ea08cfd5",
 | 
			
		||||
                "sha256:21f579134a47083ffb5ddd1307f0405c91aa8b61ad4be6fd5af0171474fe0c45",
 | 
			
		||||
                "sha256:27c267dc38a0f0079e96a2945ee65786d38ef111e413c702fbaaacbab6361d00",
 | 
			
		||||
                "sha256:299bde0ab9e5c4a92f01a152b7fbabb460f31343f1416f9b7b983167ab1e33bc",
 | 
			
		||||
                "sha256:2ab88d8f228f803fcb8cb7d222c579d13dab2d3622c51e8cf321280da01102a7",
 | 
			
		||||
                "sha256:2ced4c35061eea623bc84c7711eedce8ecc3c2c51cd9c6afa6290df3bae9e104",
 | 
			
		||||
                "sha256:2dcab01c660983ba5e5a612e0c935141ccbee67d2e2e14b833e01c2354bd8034",
 | 
			
		||||
                "sha256:32546af61a9a9b141ca38d971aa6eb9800450fa6620ce6323cc30eec447861f3",
 | 
			
		||||
                "sha256:32b40a4c46d199827d79c86bb8cb88b1bbb764f127876f2cb6f3a47f63dbada3",
 | 
			
		||||
                "sha256:3cc94c69f6bd48ed86e8e24f358cb75095c8129827df1298518ab860115269a4",
 | 
			
		||||
                "sha256:42b278ac0989d6f5cf58d7e0828ea6b5951464e3cf2ff229dd09a96cb6ba0c86",
 | 
			
		||||
                "sha256:495b63fd0302f282ee6c1e6ea0f1c12cb3d1a49c8292d27287f01845ff252a96",
 | 
			
		||||
                "sha256:4af87cdc0d4b14e600e6d3d09793dce3b7171348a094ba818e2a68ae7ee67546",
 | 
			
		||||
                "sha256:4b94df9f2fdde7b9314321bab8448e6ad5a23b80542dcab53e329527d4099dcb",
 | 
			
		||||
                "sha256:4c48ddb63e2b20fba4c6a2bf81b4d49e99b6d4587fb67a6cd33a2c1f003af3e3",
 | 
			
		||||
                "sha256:4df9afd17bd5477e9f8c8b6bb8507e18dd0f8b4efe73bb99729ff203279e9e3b",
 | 
			
		||||
                "sha256:518950fe6a5d56f94ba125107895f938a4f34f704c658986eae8255edb41163b",
 | 
			
		||||
                "sha256:538298e4e113ccb8b41658d5a4b605bebe75e46a30ceca22a5a289cf02c80bec",
 | 
			
		||||
                "sha256:55465121e72e208a7b69b53de791402affe6165083b2ea71b892728bd19ba9ae",
 | 
			
		||||
                "sha256:588384d70a0f19b47409cfdb10e0c27c20e4293b74fc891df3d8eb47782b8b3e",
 | 
			
		||||
                "sha256:6278c080d4afffc9016e14325f8734456831124e8c12caa754fd544435c08386",
 | 
			
		||||
                "sha256:64ea6c221aeee4796860405e1aedec63424cda4202a7ad27a5066876db5b0fd2",
 | 
			
		||||
                "sha256:681dbb33e2b40262b33fd383bae63c36d33fd79fa1a8e4092945430744ffd34a",
 | 
			
		||||
                "sha256:6936aa9da390402d646a32a6a38d5409c2d2afb2950f045a7d02ab25a4e7d08d",
 | 
			
		||||
                "sha256:778d0ec38bbd288b150a3ae363c8ffd88d2207a756842495e9bffd8a8afbc89a",
 | 
			
		||||
                "sha256:8251f06a77985a2729a8bdbefbae79ee78567dddc3acbd499b87e705ca59fe24",
 | 
			
		||||
                "sha256:83b4aa5344cce005a9cff5d0321b2e318e871cc1dfc793b66c32dd4f59e9770d",
 | 
			
		||||
                "sha256:844fad925ac5c2ad4faaceb3b2520ad016b5280105c6e16e79838cf951903a7b",
 | 
			
		||||
                "sha256:8ceb3667dd13b8133f2e4d637b5b00f240f066448e2aa89a41f4c2d78a26ce50",
 | 
			
		||||
                "sha256:92dc0fb79675882d0b6138be4bf0cec7ea7c7eede60aaca78303d8e8dbdaa523",
 | 
			
		||||
                "sha256:9789bd945e9f5bd026ed3f5b453d640befb8b1fc33a779c1fe8d3eb21fe3fb4a",
 | 
			
		||||
                "sha256:a2b6d6eb693bc2fc6c484f2e5d93bd0b0da803fa77bf974f160533e555e4d095",
 | 
			
		||||
                "sha256:aab9f1e34d810feb00bf841993552b8fcc6ae71d473c505381627143d0018a6a",
 | 
			
		||||
                "sha256:abb61afd84f23099ac6099d804cdba9bd3b902aaaded3ffff47e490b0a495520",
 | 
			
		||||
                "sha256:adf9ee115ae8ff8b6da4b854b4152f253b390ba64407a22d75456fe07dcbda65",
 | 
			
		||||
                "sha256:aedc6c672b351afe6dfe17ff83ee5e7eb6ed44718f879a9328a68bdb20b57e11",
 | 
			
		||||
                "sha256:b7a00ecb1434f8183395fac5366a21ee73d14900082ca37cf74993cf46baa56c",
 | 
			
		||||
                "sha256:ba32f4a91c1cb7314c429b03afbf87b1fff4fb1c8db32260e7310104bd77f0c7",
 | 
			
		||||
                "sha256:cbd0f2cbd8689861209cd89141371d3a22a11613304d1f0736492590aa0ab332",
 | 
			
		||||
                "sha256:e4bc372b953bf6cec65a8d48482ba574f6e051621d157cf224227dbb55486b1e",
 | 
			
		||||
                "sha256:eccac3d9aadc68e994b6d228cb0c8919fc47a5350d85a1b4d3d81d1e98baf40c",
 | 
			
		||||
                "sha256:efd550b3da28195746bb43bd1d815058181a7ca6d9d6aa89dd37f5eefe2cacb7",
 | 
			
		||||
                "sha256:efef581c8ba4d990770875e1a2218e856849d32ada2680e53aebc5d154a17e20",
 | 
			
		||||
                "sha256:f057897711a630a0b7a6a03f1acf379b6ba25d37dc5dc217a97191984ba7f2fc",
 | 
			
		||||
                "sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
 | 
			
		||||
                "sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==5.1.2"
 | 
			
		||||
            "version": "==5.2.0"
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "develop": {
 | 
			
		||||
@ -1243,10 +1260,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "asgiref": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
 | 
			
		||||
                "sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
 | 
			
		||||
                "sha256:a5098bc870b80e7b872bff60bb363c7f2c2c89078759f6c47b53ff8c525a152e",
 | 
			
		||||
                "sha256:cd88907ecaec59d78e4ac00ea665b03e571cb37e3a0e37b3702af1a9e86c365a"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.2.10"
 | 
			
		||||
            "version": "==3.3.0"
 | 
			
		||||
        },
 | 
			
		||||
        "astroid": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1257,10 +1274,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "attrs": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594",
 | 
			
		||||
                "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"
 | 
			
		||||
                "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
 | 
			
		||||
                "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==20.2.0"
 | 
			
		||||
            "version": "==20.3.0"
 | 
			
		||||
        },
 | 
			
		||||
        "autopep8": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1356,11 +1373,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc",
 | 
			
		||||
                "sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4"
 | 
			
		||||
                "sha256:14a4b7cd77297fba516fc0d92444cc2e2e388aa9de32d7a68d4a83d58f5a4927",
 | 
			
		||||
                "sha256:14b87775ffedab2ef6299b73343d1b4b41e5d4e2aa58c6581f114dbec01e3f8f"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.1.2"
 | 
			
		||||
            "version": "==3.1.3"
 | 
			
		||||
        },
 | 
			
		||||
        "django-debug-toolbar": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1400,10 +1417,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "gitpython": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:58483ad99811321e3c0b52c8b2229ff517499229a4854752b7d128005986e409",
 | 
			
		||||
                "sha256:f488d43600d7299567b59fe41497d313e7c1253a9f2a8ebd2df8af2a1151c71d"
 | 
			
		||||
                "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
 | 
			
		||||
                "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.1.10"
 | 
			
		||||
            "version": "==3.1.11"
 | 
			
		||||
        },
 | 
			
		||||
        "iniconfig": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1469,10 +1486,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pathspec": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
 | 
			
		||||
                "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
 | 
			
		||||
                "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd",
 | 
			
		||||
                "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.8.0"
 | 
			
		||||
            "version": "==0.8.1"
 | 
			
		||||
        },
 | 
			
		||||
        "pbr": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1574,11 +1591,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pytest": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7a8190790c17d79a11f847fba0b004ee9a8122582ebff4729a082c109e81a4c9",
 | 
			
		||||
                "sha256:8f593023c1a0f916110285b6efd7f99db07d59546e3d8c36fc60e2ab05d3be92"
 | 
			
		||||
                "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
 | 
			
		||||
                "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==6.1.1"
 | 
			
		||||
            "version": "==6.1.2"
 | 
			
		||||
        },
 | 
			
		||||
        "pytest-django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1590,10 +1607,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pytz": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
 | 
			
		||||
                "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
 | 
			
		||||
                "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
 | 
			
		||||
                "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2020.1"
 | 
			
		||||
            "version": "==2020.4"
 | 
			
		||||
        },
 | 
			
		||||
        "pyyaml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1614,35 +1631,51 @@
 | 
			
		||||
        },
 | 
			
		||||
        "regex": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0cb23ed0e327c18fb7eac61ebbb3180ebafed5b9b86ca2e15438201e5903b5dd",
 | 
			
		||||
                "sha256:1a065e7a6a1b4aa851a0efa1a2579eabc765246b8b3a5fd74000aaa3134b8b4e",
 | 
			
		||||
                "sha256:1a511470db3aa97432ac8c1bf014fcc6c9fbfd0f4b1313024d342549cf86bcd6",
 | 
			
		||||
                "sha256:1c447b0d108cddc69036b1b3910fac159f2b51fdeec7f13872e059b7bc932be1",
 | 
			
		||||
                "sha256:2278453c6a76280b38855a263198961938108ea2333ee145c5168c36b8e2b376",
 | 
			
		||||
                "sha256:240509721a663836b611fa13ca1843079fc52d0b91ef3f92d9bba8da12e768a0",
 | 
			
		||||
                "sha256:4e21340c07090ddc8c16deebfd82eb9c9e1ec5e62f57bb86194a2595fd7b46e0",
 | 
			
		||||
                "sha256:570e916a44a361d4e85f355aacd90e9113319c78ce3c2d098d2ddf9631b34505",
 | 
			
		||||
                "sha256:59d5c6302d22c16d59611a9fd53556554010db1d47e9df5df37be05007bebe75",
 | 
			
		||||
                "sha256:6a46eba253cedcbe8a6469f881f014f0a98819d99d341461630885139850e281",
 | 
			
		||||
                "sha256:6f567df0601e9c7434958143aebea47a9c4b45434ea0ae0286a4ec19e9877169",
 | 
			
		||||
                "sha256:781906e45ef1d10a0ed9ec8ab83a09b5e0d742de70e627b20d61ccb1b1d3964d",
 | 
			
		||||
                "sha256:8469377a437dbc31e480993399fd1fd15fe26f382dc04c51c9cb73e42965cc06",
 | 
			
		||||
                "sha256:8cd0d587aaac74194ad3e68029124c06245acaeddaae14cb45844e5c9bebeea4",
 | 
			
		||||
                "sha256:97a023f97cddf00831ba04886d1596ef10f59b93df7f855856f037190936e868",
 | 
			
		||||
                "sha256:a973d5a7a324e2a5230ad7c43f5e1383cac51ef4903bf274936a5634b724b531",
 | 
			
		||||
                "sha256:af360e62a9790e0a96bc9ac845d87bfa0e4ee0ee68547ae8b5a9c1030517dbef",
 | 
			
		||||
                "sha256:b706c70070eea03411b1761fff3a2675da28d042a1ab7d0863b3efe1faa125c9",
 | 
			
		||||
                "sha256:bfd7a9fddd11d116a58b62ee6c502fd24cfe22a4792261f258f886aa41c2a899",
 | 
			
		||||
                "sha256:c30d8766a055c22e39dd7e1a4f98f6266169f2de05db737efe509c2fb9c8a3c8",
 | 
			
		||||
                "sha256:c53dc8ee3bb7b7e28ee9feb996a0c999137be6c1d3b02cb6b3c4cba4f9e5ed09",
 | 
			
		||||
                "sha256:c95d514093b80e5309bdca5dd99e51bcf82c44043b57c34594d9d7556bd04d05",
 | 
			
		||||
                "sha256:d43cf21df524283daa80ecad551c306b7f52881c8d0fe4e3e76a96b626b6d8d8",
 | 
			
		||||
                "sha256:d62205f00f461fe8b24ade07499454a3b7adf3def1225e258b994e2215fd15c5",
 | 
			
		||||
                "sha256:e289a857dca3b35d3615c3a6a438622e20d1bf0abcb82c57d866c8d0be3f44c4",
 | 
			
		||||
                "sha256:e5f6aa56dda92472e9d6f7b1e6331f4e2d51a67caafff4d4c5121cadac03941e",
 | 
			
		||||
                "sha256:f4b1c65ee86bfbf7d0c3dfd90592a9e3d6e9ecd36c367c884094c050d4c35d04"
 | 
			
		||||
                "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a",
 | 
			
		||||
                "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f",
 | 
			
		||||
                "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb",
 | 
			
		||||
                "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5",
 | 
			
		||||
                "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de",
 | 
			
		||||
                "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c",
 | 
			
		||||
                "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0",
 | 
			
		||||
                "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c",
 | 
			
		||||
                "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64",
 | 
			
		||||
                "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53",
 | 
			
		||||
                "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12",
 | 
			
		||||
                "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740",
 | 
			
		||||
                "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c",
 | 
			
		||||
                "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd",
 | 
			
		||||
                "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504",
 | 
			
		||||
                "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427",
 | 
			
		||||
                "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b",
 | 
			
		||||
                "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e",
 | 
			
		||||
                "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582",
 | 
			
		||||
                "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0",
 | 
			
		||||
                "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c",
 | 
			
		||||
                "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9",
 | 
			
		||||
                "sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1",
 | 
			
		||||
                "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0",
 | 
			
		||||
                "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf",
 | 
			
		||||
                "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898",
 | 
			
		||||
                "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd",
 | 
			
		||||
                "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d",
 | 
			
		||||
                "sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab",
 | 
			
		||||
                "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f",
 | 
			
		||||
                "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e",
 | 
			
		||||
                "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786",
 | 
			
		||||
                "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b",
 | 
			
		||||
                "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de",
 | 
			
		||||
                "sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e",
 | 
			
		||||
                "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789",
 | 
			
		||||
                "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520",
 | 
			
		||||
                "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa",
 | 
			
		||||
                "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b",
 | 
			
		||||
                "sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4",
 | 
			
		||||
                "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625",
 | 
			
		||||
                "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d",
 | 
			
		||||
                "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2020.10.23"
 | 
			
		||||
            "version": "==2020.10.28"
 | 
			
		||||
        },
 | 
			
		||||
        "requirements-detector": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1701,33 +1734,42 @@
 | 
			
		||||
        },
 | 
			
		||||
        "toml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
 | 
			
		||||
                "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
 | 
			
		||||
                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
 | 
			
		||||
                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==0.10.1"
 | 
			
		||||
            "version": "==0.10.2"
 | 
			
		||||
        },
 | 
			
		||||
        "typed-ast": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
 | 
			
		||||
                "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
 | 
			
		||||
                "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
 | 
			
		||||
                "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
 | 
			
		||||
                "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
 | 
			
		||||
                "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
 | 
			
		||||
                "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
 | 
			
		||||
                "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
 | 
			
		||||
                "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
 | 
			
		||||
                "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
 | 
			
		||||
                "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
 | 
			
		||||
                "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
 | 
			
		||||
                "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
 | 
			
		||||
                "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
 | 
			
		||||
                "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
 | 
			
		||||
                "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
 | 
			
		||||
                "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
 | 
			
		||||
                "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
 | 
			
		||||
                "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
 | 
			
		||||
                "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
 | 
			
		||||
                "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
 | 
			
		||||
                "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
 | 
			
		||||
                "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
 | 
			
		||||
                "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
 | 
			
		||||
                "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
 | 
			
		||||
                "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
 | 
			
		||||
                "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
 | 
			
		||||
                "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
 | 
			
		||||
                "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
 | 
			
		||||
                "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.4.1"
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,7 @@ services:
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
  server:
 | 
			
		||||
    image: beryju/passbook:${PASSBOOK_TAG:-0.12.6-stable}
 | 
			
		||||
    image: beryju/passbook:${PASSBOOK_TAG:-0.12.9-stable}
 | 
			
		||||
    command: server
 | 
			
		||||
    environment:
 | 
			
		||||
      PASSBOOK_REDIS__HOST: redis
 | 
			
		||||
@ -40,7 +40,7 @@ services:
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
  worker:
 | 
			
		||||
    image: beryju/passbook:${PASSBOOK_TAG:-0.12.6-stable}
 | 
			
		||||
    image: beryju/passbook:${PASSBOOK_TAG:-0.12.9-stable}
 | 
			
		||||
    command: worker
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
@ -54,7 +54,7 @@ services:
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
  static:
 | 
			
		||||
    image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.6-stable}
 | 
			
		||||
    image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.9-stable}
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
    labels:
 | 
			
		||||
@ -68,7 +68,7 @@ services:
 | 
			
		||||
  traefik:
 | 
			
		||||
    image: traefik:2.3
 | 
			
		||||
    command:
 | 
			
		||||
      - "--accesslog=true"
 | 
			
		||||
      - "--log.format=json"
 | 
			
		||||
      - "--api.insecure=true"
 | 
			
		||||
      - "--providers.docker=true"
 | 
			
		||||
      - "--providers.docker.exposedbydefault=false"
 | 
			
		||||
 | 
			
		||||
@ -117,7 +117,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "session_duration": 0
 | 
			
		||||
                "session_duration": "seconds=-1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -136,7 +136,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "session_duration": 0
 | 
			
		||||
                "session_duration": "seconds=-1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "session_duration": 0
 | 
			
		||||
                "session_duration": "seconds=-1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "session_duration": 0
 | 
			
		||||
                "session_duration": "seconds=-1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -118,7 +118,7 @@
 | 
			
		||||
            },
 | 
			
		||||
            "model": "passbook_stages_user_login.userloginstage",
 | 
			
		||||
            "attrs": {
 | 
			
		||||
                "session_duration": 0
 | 
			
		||||
                "session_duration": "seconds=-1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ Download the latest `docker-compose.yml` from [here](https://raw.githubuserconte
 | 
			
		||||
 | 
			
		||||
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
 | 
			
		||||
 | 
			
		||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.12.6-stable >> .env`
 | 
			
		||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.12.9-stable >> .env`
 | 
			
		||||
 | 
			
		||||
If this is a fresh passbook install run the following commands to generate a password:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
 | 
			
		||||
image:
 | 
			
		||||
  name: beryju/passbook
 | 
			
		||||
  name_static: beryju/passbook-static
 | 
			
		||||
  tag: 0.12.6-stable
 | 
			
		||||
  tag: 0.12.9-stable
 | 
			
		||||
 | 
			
		||||
serverReplicas: 1
 | 
			
		||||
workerReplicas: 1
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,8 @@ server {
 | 
			
		||||
        proxy_set_header X-Forwarded-Proto https;
 | 
			
		||||
        proxy_set_header X-Forwarded-Port 443;
 | 
			
		||||
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 | 
			
		||||
        proxy_set_header Host $http_host;
 | 
			
		||||
        # This needs to be set inside the location block, very important.
 | 
			
		||||
        proxy_set_header Host $host;
 | 
			
		||||
        proxy_set_header Upgrade $http_upgrade;
 | 
			
		||||
        proxy_set_header Connection $connection_upgrade;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										59
									
								
								docs/integrations/services/home-assistant/index.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								docs/integrations/services/home-assistant/index.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,59 @@
 | 
			
		||||
# Home-Assistant Integration
 | 
			
		||||
 | 
			
		||||
## What is Home-Assistant
 | 
			
		||||
 | 
			
		||||
From https://www.home-assistant.io/
 | 
			
		||||
 | 
			
		||||
!!! note ""
 | 
			
		||||
    Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Preparation
 | 
			
		||||
 | 
			
		||||
The following placeholders will be used:
 | 
			
		||||
 | 
			
		||||
- `hass.company` is the FQDN of the Home-Assistant install.
 | 
			
		||||
- `passbook.company` is the FQDN of the passbook install.
 | 
			
		||||
 | 
			
		||||
!!! note
 | 
			
		||||
 | 
			
		||||
    This setup uses https://github.com/BeryJu/hass-auth-header and the passbook proxy for authentication. When this [PR](https://github.com/home-assistant/core/pull/32926) is merged, this will no longer be necessary.
 | 
			
		||||
 | 
			
		||||
## Home-Assistant
 | 
			
		||||
 | 
			
		||||
This guide requires https://github.com/BeryJu/hass-auth-header, which can be installed as described in the Readme.
 | 
			
		||||
 | 
			
		||||
Afterwards, make sure the `trusted_proxies` setting contains the IP(s) of the Host(s) passbook is running on.
 | 
			
		||||
 | 
			
		||||
With the default Header of `X-Forwarded-Preferred-Username` matching is done on a username basis, so your Name in Home-Assistant and your username in passbook have to match.
 | 
			
		||||
 | 
			
		||||
If this is not the case, you can simply add an additional header for your user, which contains the Home-Assistant Name and authenticate based on that.
 | 
			
		||||
 | 
			
		||||
For example add this to your user's properties and set the Header to `X-pb-hass-user`.
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
additionalHeaders:
 | 
			
		||||
  X-pb-hass-user: some other name
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## passbook
 | 
			
		||||
 | 
			
		||||
Create a Proxy Provider with the following values
 | 
			
		||||
 | 
			
		||||
- Internal host
 | 
			
		||||
 | 
			
		||||
    If Home-Assistant is running in docker, and you're deploying the passbook proxy on the same host, set the value to `http://homeassistant:8123`, where Home-Assistant is the name of your container.
 | 
			
		||||
 | 
			
		||||
    If Home-Assistant is running on a different server than where you are deploying the passbook proxy, set the value to `http://hass.company:8123`.
 | 
			
		||||
 | 
			
		||||
- External host
 | 
			
		||||
 | 
			
		||||
    Set this to the external URL you will be accessing Home-Assistant from.
 | 
			
		||||
 | 
			
		||||
Create an application in passbook and select the provider you've created above.
 | 
			
		||||
 | 
			
		||||
## Deployment
 | 
			
		||||
 | 
			
		||||
Create an outpost deployment for the provider you've created above, as described [here](../../../outposts/outposts.md). Deploy this Outpost either on the same host or a different host that can access Home-Assistant.
 | 
			
		||||
 | 
			
		||||
The outpost will connect to passbook and configure itself.
 | 
			
		||||
@ -18,7 +18,7 @@ The following placeholders will be used:
 | 
			
		||||
- `sonarr.company` is the FQDN of the Sonarr install.
 | 
			
		||||
- `passbook.company` is the FQDN of the passbook install.
 | 
			
		||||
 | 
			
		||||
Create an application in passbook. Create a Proxy Provider with the following values
 | 
			
		||||
Create a Proxy Provider with the following values
 | 
			
		||||
 | 
			
		||||
- Internal host
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,8 @@ Create an application in passbook. Create a Proxy Provider with the following va
 | 
			
		||||
 | 
			
		||||
    Set this to the external URL you will be accessing Sonarr from.
 | 
			
		||||
 | 
			
		||||
Create an application in passbook and select the provider you've created above.
 | 
			
		||||
 | 
			
		||||
## Deployment
 | 
			
		||||
 | 
			
		||||
Create an outpost deployment for the provider you've created above, as described [here](../../../outposts/outposts.md). Deploy this Outpost either on the same host or a different host that can access Sonarr.
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,14 @@ The Proxy these extra headers to the application:
 | 
			
		||||
 | 
			
		||||
Header Name | Value
 | 
			
		||||
-------------|-------
 | 
			
		||||
X-Auth-Request-User | The user's unique identifier
 | 
			
		||||
X-Auth-Request-Email | The user's email address
 | 
			
		||||
X-Auth-Request-Preferred-Username | The user's username
 | 
			
		||||
X-Forwarded-User | The user's unique identifier (**not the username**)
 | 
			
		||||
X-Forwarded-Email | The user's email address
 | 
			
		||||
X-Forwarded-Preferred-Username | The user's username
 | 
			
		||||
X-Auth-Username | The user's username
 | 
			
		||||
 | 
			
		||||
Additionally, you can add more custom headers using `additionalHeaders` in the User or Group Properties, for example
 | 
			
		||||
 | 
			
		||||
```yaml
 | 
			
		||||
additionalHeaders:
 | 
			
		||||
  X-additional-header: bar
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ class TestFlowsEnroll(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "mailhog/mailhog:v1.0.1",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/mailhog/mailhog:v1.0.1",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        """Setup client grafana container which we test OAuth against"""
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "grafana/grafana:7.1.0",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/grafana/grafana:7.1.0",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
 | 
			
		||||
@ -47,7 +47,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "grafana/grafana:7.1.0",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/grafana/grafana:7.1.0",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        client.images.pull("beryju/oidc-test-client")
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="beryju/oidc-test-client",
 | 
			
		||||
            image="docker.beryju.org/proxy/beryju/oidc-test-client",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            auto_remove=True,
 | 
			
		||||
 | 
			
		||||
@ -16,9 +16,9 @@ from passbook import __version__
 | 
			
		||||
from passbook.core.models import Application
 | 
			
		||||
from passbook.flows.models import Flow
 | 
			
		||||
from passbook.outposts.models import (
 | 
			
		||||
    DockerServiceConnection,
 | 
			
		||||
    Outpost,
 | 
			
		||||
    OutpostConfig,
 | 
			
		||||
    OutpostDeploymentType,
 | 
			
		||||
    OutpostType,
 | 
			
		||||
)
 | 
			
		||||
from passbook.providers.proxy.models import ProxyProvider
 | 
			
		||||
@ -36,7 +36,7 @@ class TestProviderProxy(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "traefik/whoami:latest",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/traefik/whoami:latest",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
@ -76,7 +76,6 @@ class TestProviderProxy(SeleniumTestCase):
 | 
			
		||||
        outpost: Outpost = Outpost.objects.create(
 | 
			
		||||
            name="proxy_outpost",
 | 
			
		||||
            type=OutpostType.PROXY,
 | 
			
		||||
            deployment_type=OutpostDeploymentType.CUSTOM,
 | 
			
		||||
        )
 | 
			
		||||
        outpost.providers.add(proxy)
 | 
			
		||||
        outpost.save()
 | 
			
		||||
@ -128,10 +127,11 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
 | 
			
		||||
        proxy.save()
 | 
			
		||||
        # we need to create an application to actually access the proxy
 | 
			
		||||
        Application.objects.create(name="proxy", slug="proxy", provider=proxy)
 | 
			
		||||
        service_connection = DockerServiceConnection.objects.get(local=True)
 | 
			
		||||
        outpost: Outpost = Outpost.objects.create(
 | 
			
		||||
            name="proxy_outpost",
 | 
			
		||||
            type=OutpostType.PROXY,
 | 
			
		||||
            deployment_type=OutpostDeploymentType.DOCKER,
 | 
			
		||||
            service_connection=service_connection,
 | 
			
		||||
            _config=asdict(
 | 
			
		||||
                OutpostConfig(passbook_host=self.live_server_url, log_level="debug")
 | 
			
		||||
            ),
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ class TestProviderSAML(SeleniumTestCase):
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        client.images.pull("beryju/oidc-test-client")
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="beryju/saml-test-sp",
 | 
			
		||||
            image="docker.beryju.org/proxy/beryju/saml-test-sp",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            auto_remove=True,
 | 
			
		||||
 | 
			
		||||
@ -258,7 +258,7 @@ class TestSourceOAuth1(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "beryju/oauth1-test-server",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/beryju/oauth1-test-server",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,7 @@ class TestSourceSAML(SeleniumTestCase):
 | 
			
		||||
 | 
			
		||||
    def get_container_specs(self) -> Optional[Dict[str, Any]]:
 | 
			
		||||
        return {
 | 
			
		||||
            "image": "kristophjunge/test-saml-idp:1.15",
 | 
			
		||||
            "image": "docker.beryju.org/proxy/kristophjunge/test-saml-idp:1.15",
 | 
			
		||||
            "detach": True,
 | 
			
		||||
            "network_mode": "host",
 | 
			
		||||
            "auto_remove": True,
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ from django.test.testcases import TransactionTestCase
 | 
			
		||||
from docker import DockerClient, from_env
 | 
			
		||||
from docker.models.containers import Container
 | 
			
		||||
from selenium import webdriver
 | 
			
		||||
from selenium.common.exceptions import TimeoutException
 | 
			
		||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException
 | 
			
		||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
 | 
			
		||||
from selenium.webdriver.remote.webdriver import WebDriver
 | 
			
		||||
from selenium.webdriver.support.ui import WebDriverWait
 | 
			
		||||
@ -132,7 +132,7 @@ def retry(max_retires=3, exceptions=None):
 | 
			
		||||
    """Retry test multiple times. Default to catching Selenium Timeout Exception"""
 | 
			
		||||
 | 
			
		||||
    if not exceptions:
 | 
			
		||||
        exceptions = [TimeoutException]
 | 
			
		||||
        exceptions = [TimeoutException, NoSuchElementException]
 | 
			
		||||
 | 
			
		||||
    logger = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ name: passbook
 | 
			
		||||
home: https://passbook.beryju.org
 | 
			
		||||
sources:
 | 
			
		||||
  - https://github.com/BeryJu/passbook
 | 
			
		||||
version: "0.12.6-stable"
 | 
			
		||||
version: "0.12.9-stable"
 | 
			
		||||
icon: https://raw.githubusercontent.com/BeryJu/passbook/master/docs/images/logo.svg
 | 
			
		||||
dependencies:
 | 
			
		||||
  - name: postgresql
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
image:
 | 
			
		||||
  name: beryju/passbook
 | 
			
		||||
  name_static: beryju/passbook-static
 | 
			
		||||
  tag: 0.12.6-stable
 | 
			
		||||
  tag: 0.12.9-stable
 | 
			
		||||
 | 
			
		||||
serverReplicas: 1
 | 
			
		||||
workerReplicas: 1
 | 
			
		||||
@ -54,3 +54,10 @@ install:
 | 
			
		||||
# These values influence the bundled postgresql and redis charts, but are also used by passbook to connect
 | 
			
		||||
postgresql:
 | 
			
		||||
  postgresqlDatabase: passbook
 | 
			
		||||
 | 
			
		||||
redis:
 | 
			
		||||
  cluster:
 | 
			
		||||
    enabled: false
 | 
			
		||||
  master:
 | 
			
		||||
    # https://stackoverflow.com/a/59189742
 | 
			
		||||
    disableCommands: []
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
"""Gunicorn config"""
 | 
			
		||||
import warnings
 | 
			
		||||
from multiprocessing import cpu_count
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
@ -49,3 +50,5 @@ if Path("/var/run/secrets/kubernetes.io").exists():
 | 
			
		||||
else:
 | 
			
		||||
    worker = cpu_count() * 2 + 1
 | 
			
		||||
threads = 4
 | 
			
		||||
 | 
			
		||||
warnings.simplefilter("once")
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""passbook"""
 | 
			
		||||
__version__ = "0.12.6-stable"
 | 
			
		||||
__version__ = "0.12.9-stable"
 | 
			
		||||
 | 
			
		||||
@ -46,11 +46,28 @@
 | 
			
		||||
                        {% trans 'Providers' %}
 | 
			
		||||
                    </a>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:outposts' %}"
 | 
			
		||||
                        class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}">
 | 
			
		||||
                        {% trans 'Outposts' %}
 | 
			
		||||
                <li class="pf-c-nav__item pf-m-expanded">
 | 
			
		||||
                    <a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Outposts' %}
 | 
			
		||||
                        <span class="pf-c-nav__toggle">
 | 
			
		||||
                            <i class="fas fa-angle-right" aria-hidden="true"></i>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <section class="pf-c-nav__subnav">
 | 
			
		||||
                        <ul class="pf-c-nav__simple-list">
 | 
			
		||||
                            <li class="pf-c-nav__item">
 | 
			
		||||
                                <a href="{% url 'passbook_admin:outposts' %}"
 | 
			
		||||
                                    class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}">
 | 
			
		||||
                                    {% trans 'Outposts' %}
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            <li class="pf-c-nav__item">
 | 
			
		||||
                                <a href="{% url 'passbook_admin:outpost-service-connections' %}"
 | 
			
		||||
                                    class="pf-c-nav__link {% is_active 'passbook_admin:outpost-service-connections' 'passbook_admin:outpost-service-connections-create' 'passbook_admin:outpost-service-connections-update' 'passbook_admin:outpost-service-connections-delete' %}">
 | 
			
		||||
                                    {% trans 'Service Connections' %}
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </section>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="pf-c-nav__item">
 | 
			
		||||
                    <a href="{% url 'passbook_admin:property-mappings' %}"
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,135 @@
 | 
			
		||||
{% extends "administration/base.html" %}
 | 
			
		||||
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load humanize %}
 | 
			
		||||
{% load passbook_utils %}
 | 
			
		||||
{% load admin_reflection %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<section class="pf-c-page__main-section pf-m-light">
 | 
			
		||||
    <div class="pf-c-content">
 | 
			
		||||
        <h1>
 | 
			
		||||
            <i class="pf-icon-integration"></i>
 | 
			
		||||
            {% trans 'Outpost Service-Connections' %}
 | 
			
		||||
        </h1>
 | 
			
		||||
        <p>{% trans "Outpost Service-Connections define how passbook connects to external platforms to manage and deploy Outposts." %}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
 | 
			
		||||
    <div class="pf-c-card">
 | 
			
		||||
        {% if object_list %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
                <div class="pf-c-toolbar__bulk-select">
 | 
			
		||||
                    <div class="pf-c-dropdown">
 | 
			
		||||
                        <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
                            <span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
 | 
			
		||||
                            <i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
 | 
			
		||||
                        </button>
 | 
			
		||||
                        <ul class="pf-c-dropdown__menu" hidden>
 | 
			
		||||
                            {% for type, name in types.items %}
 | 
			
		||||
                            <li>
 | 
			
		||||
                                <a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:outpost-service-connection-create' %}?type={{ type }}&back={{ request.get_full_path }}">
 | 
			
		||||
                                    {{ name|verbose_name }}<br>
 | 
			
		||||
                                    <small>
 | 
			
		||||
                                        {{ name|doc }}
 | 
			
		||||
                                    </small>
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </li>
 | 
			
		||||
                            {% endfor %}
 | 
			
		||||
                        </ul>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {% include 'partials/pagination.html' %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
 | 
			
		||||
            <thead>
 | 
			
		||||
                <tr role="row">
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Name' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Type' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Local?' %}</th>
 | 
			
		||||
                    <th role="columnheader" scope="col">{% trans 'Status' %}</th>
 | 
			
		||||
                    <th role="cell"></th>
 | 
			
		||||
                </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
            <tbody role="rowgroup">
 | 
			
		||||
                {% for sc in object_list %}
 | 
			
		||||
                <tr role="row">
 | 
			
		||||
                    <th role="columnheader">
 | 
			
		||||
                        <span>{{ sc.name }}</span>
 | 
			
		||||
                    </th>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {{ sc|verbose_name }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {{ sc.local|yesno:"Yes,No" }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td role="cell">
 | 
			
		||||
                        <span>
 | 
			
		||||
                            {% if sc.state.healthy %}
 | 
			
		||||
                            <i class="fas fa-check pf-m-success"></i> {{ sc.state.version }}
 | 
			
		||||
                            {% else %}
 | 
			
		||||
                            <i class="fas fa-times pf-m-danger"></i> {% trans 'Unhealthy' %}
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </td>
 | 
			
		||||
                    <td>
 | 
			
		||||
                        <a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-service-connection-update' pk=sc.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
 | 
			
		||||
                        <a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-service-connection-delete' pk=sc.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
 | 
			
		||||
                    </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                {% endfor %}
 | 
			
		||||
            </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
        <div class="pf-c-pagination pf-m-bottom">
 | 
			
		||||
            {% include 'partials/pagination.html' %}
 | 
			
		||||
        </div>
 | 
			
		||||
        {% else %}
 | 
			
		||||
        <div class="pf-c-toolbar">
 | 
			
		||||
            <div class="pf-c-toolbar__content">
 | 
			
		||||
                {% include 'partials/toolbar_search.html' %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-empty-state">
 | 
			
		||||
            <div class="pf-c-empty-state__content">
 | 
			
		||||
                <i class="fas fa-map-marker pf-c-empty-state__icon" aria-hidden="true"></i>
 | 
			
		||||
                <h1 class="pf-c-title pf-m-lg">
 | 
			
		||||
                    {% trans 'No Outpost Service Connections.' %}
 | 
			
		||||
                </h1>
 | 
			
		||||
                <div class="pf-c-empty-state__body">
 | 
			
		||||
                {% if request.GET.search != "" %}
 | 
			
		||||
                    {% trans "Your search query doesn't match any outposts." %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    {% trans 'Currently no service connections exist. Click the button below to create one.' %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="pf-c-dropdown">
 | 
			
		||||
                    <button class="pf-m-primary pf-c-dropdown__toggle" type="button">
 | 
			
		||||
                        <span class="pf-c-dropdown__toggle-text">{% trans 'Create' %}</span>
 | 
			
		||||
                        <i class="fas fa-caret-down pf-c-dropdown__toggle-icon" aria-hidden="true"></i>
 | 
			
		||||
                    </button>
 | 
			
		||||
                    <ul class="pf-c-dropdown__menu" hidden>
 | 
			
		||||
                        {% for type, name in types.items %}
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <a class="pf-c-dropdown__menu-item" href="{% url 'passbook_admin:outpost-service-connection-create' %}?type={{ type }}&back={{ request.get_full_path }}">
 | 
			
		||||
                                {{ name|verbose_name }}<br>
 | 
			
		||||
                                <small>
 | 
			
		||||
                                    {{ name|doc }}
 | 
			
		||||
                                </small>
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@ -7,10 +7,11 @@ from passbook.admin.views import (
 | 
			
		||||
    flows,
 | 
			
		||||
    groups,
 | 
			
		||||
    outposts,
 | 
			
		||||
    outposts_service_connections,
 | 
			
		||||
    overview,
 | 
			
		||||
    policies,
 | 
			
		||||
    policies_bindings,
 | 
			
		||||
    property_mapping,
 | 
			
		||||
    property_mappings,
 | 
			
		||||
    providers,
 | 
			
		||||
    sources,
 | 
			
		||||
    stages,
 | 
			
		||||
@ -225,22 +226,22 @@ urlpatterns = [
 | 
			
		||||
    # Property Mappings
 | 
			
		||||
    path(
 | 
			
		||||
        "property-mappings/",
 | 
			
		||||
        property_mapping.PropertyMappingListView.as_view(),
 | 
			
		||||
        property_mappings.PropertyMappingListView.as_view(),
 | 
			
		||||
        name="property-mappings",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "property-mappings/create/",
 | 
			
		||||
        property_mapping.PropertyMappingCreateView.as_view(),
 | 
			
		||||
        property_mappings.PropertyMappingCreateView.as_view(),
 | 
			
		||||
        name="property-mapping-create",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "property-mappings/<uuid:pk>/update/",
 | 
			
		||||
        property_mapping.PropertyMappingUpdateView.as_view(),
 | 
			
		||||
        property_mappings.PropertyMappingUpdateView.as_view(),
 | 
			
		||||
        name="property-mapping-update",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "property-mappings/<uuid:pk>/delete/",
 | 
			
		||||
        property_mapping.PropertyMappingDeleteView.as_view(),
 | 
			
		||||
        property_mappings.PropertyMappingDeleteView.as_view(),
 | 
			
		||||
        name="property-mapping-delete",
 | 
			
		||||
    ),
 | 
			
		||||
    # Users
 | 
			
		||||
@ -312,6 +313,27 @@ urlpatterns = [
 | 
			
		||||
        outposts.OutpostDeleteView.as_view(),
 | 
			
		||||
        name="outpost-delete",
 | 
			
		||||
    ),
 | 
			
		||||
    # Outpost Service Connections
 | 
			
		||||
    path(
 | 
			
		||||
        "outposts/service_connections/",
 | 
			
		||||
        outposts_service_connections.OutpostServiceConnectionListView.as_view(),
 | 
			
		||||
        name="outpost-service-connections",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "outposts/service_connections/create/",
 | 
			
		||||
        outposts_service_connections.OutpostServiceConnectionCreateView.as_view(),
 | 
			
		||||
        name="outpost-service-connection-create",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "outposts/service_connections/<uuid:pk>/update/",
 | 
			
		||||
        outposts_service_connections.OutpostServiceConnectionUpdateView.as_view(),
 | 
			
		||||
        name="outpost-service-connection-update",
 | 
			
		||||
    ),
 | 
			
		||||
    path(
 | 
			
		||||
        "outposts/service_connections/<uuid:pk>/delete/",
 | 
			
		||||
        outposts_service_connections.OutpostServiceConnectionDeleteView.as_view(),
 | 
			
		||||
        name="outpost-service-connection-delete",
 | 
			
		||||
    ),
 | 
			
		||||
    # Tasks
 | 
			
		||||
    path(
 | 
			
		||||
        "tasks/",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										83
									
								
								passbook/admin/views/outposts_service_connections.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								passbook/admin/views/outposts_service_connections.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
"""passbook OutpostServiceConnection administration"""
 | 
			
		||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
			
		||||
from django.contrib.auth.mixins import (
 | 
			
		||||
    PermissionRequiredMixin as DjangoPermissionRequiredMixin,
 | 
			
		||||
)
 | 
			
		||||
from django.contrib.messages.views import SuccessMessageMixin
 | 
			
		||||
from django.urls import reverse_lazy
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
			
		||||
 | 
			
		||||
from passbook.admin.views.utils import (
 | 
			
		||||
    BackSuccessUrlMixin,
 | 
			
		||||
    DeleteMessageView,
 | 
			
		||||
    InheritanceCreateView,
 | 
			
		||||
    InheritanceListView,
 | 
			
		||||
    InheritanceUpdateView,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
)
 | 
			
		||||
from passbook.outposts.models import OutpostServiceConnection
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostServiceConnectionListView(
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionListMixin,
 | 
			
		||||
    UserPaginateListMixin,
 | 
			
		||||
    SearchListMixin,
 | 
			
		||||
    InheritanceListView,
 | 
			
		||||
):
 | 
			
		||||
    """Show list of all outpost-service-connections"""
 | 
			
		||||
 | 
			
		||||
    model = OutpostServiceConnection
 | 
			
		||||
    permission_required = "passbook_outposts.add_outpostserviceconnection"
 | 
			
		||||
    template_name = "administration/outpost_service_connection/list.html"
 | 
			
		||||
    ordering = "pk"
 | 
			
		||||
    search_fields = ["pk", "name"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostServiceConnectionCreateView(
 | 
			
		||||
    SuccessMessageMixin,
 | 
			
		||||
    BackSuccessUrlMixin,
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    DjangoPermissionRequiredMixin,
 | 
			
		||||
    InheritanceCreateView,
 | 
			
		||||
):
 | 
			
		||||
    """Create new OutpostServiceConnection"""
 | 
			
		||||
 | 
			
		||||
    model = OutpostServiceConnection
 | 
			
		||||
    permission_required = "passbook_outposts.add_outpostserviceconnection"
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/create.html"
 | 
			
		||||
    success_url = reverse_lazy("passbook_admin:outpost-service-connections")
 | 
			
		||||
    success_message = _("Successfully created OutpostServiceConnection")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostServiceConnectionUpdateView(
 | 
			
		||||
    SuccessMessageMixin,
 | 
			
		||||
    BackSuccessUrlMixin,
 | 
			
		||||
    LoginRequiredMixin,
 | 
			
		||||
    PermissionRequiredMixin,
 | 
			
		||||
    InheritanceUpdateView,
 | 
			
		||||
):
 | 
			
		||||
    """Update outpostserviceconnection"""
 | 
			
		||||
 | 
			
		||||
    model = OutpostServiceConnection
 | 
			
		||||
    permission_required = "passbook_outposts.change_outpostserviceconnection"
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/update.html"
 | 
			
		||||
    success_url = reverse_lazy("passbook_admin:outpost-service-connections")
 | 
			
		||||
    success_message = _("Successfully updated OutpostServiceConnection")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostServiceConnectionDeleteView(
 | 
			
		||||
    LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
 | 
			
		||||
):
 | 
			
		||||
    """Delete outpostserviceconnection"""
 | 
			
		||||
 | 
			
		||||
    model = OutpostServiceConnection
 | 
			
		||||
    permission_required = "passbook_outposts.delete_outpostserviceconnection"
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/delete.html"
 | 
			
		||||
    success_url = reverse_lazy("passbook_admin:outpost-service-connections")
 | 
			
		||||
    success_message = _("Successfully deleted OutpostServiceConnection")
 | 
			
		||||
@ -32,8 +32,8 @@ class ProviderListView(
 | 
			
		||||
    model = Provider
 | 
			
		||||
    permission_required = "passbook_core.add_provider"
 | 
			
		||||
    template_name = "administration/provider/list.html"
 | 
			
		||||
    ordering = "id"
 | 
			
		||||
    search_fields = ["id", "name"]
 | 
			
		||||
    ordering = "pk"
 | 
			
		||||
    search_fields = ["pk", "name"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProviderCreateView(
 | 
			
		||||
 | 
			
		||||
@ -25,10 +25,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
 | 
			
		||||
    try:
 | 
			
		||||
        auth_credentials = b64decode(auth_credentials.encode()).decode()
 | 
			
		||||
    except UnicodeDecodeError:
 | 
			
		||||
        # TODO: Remove this workaround
 | 
			
		||||
        # temporary fallback for 0.11 to 0.12 upgrade
 | 
			
		||||
        # 0.11 and below proxy sends authorization header not base64 encoded
 | 
			
		||||
        pass
 | 
			
		||||
        return None
 | 
			
		||||
    # Accept credentials with username and without
 | 
			
		||||
    if ":" in auth_credentials:
 | 
			
		||||
        _, password = auth_credentials.split(":")
 | 
			
		||||
 | 
			
		||||
@ -19,7 +19,11 @@ from passbook.core.api.tokens import TokenViewSet
 | 
			
		||||
from passbook.core.api.users import UserViewSet
 | 
			
		||||
from passbook.crypto.api import CertificateKeyPairViewSet
 | 
			
		||||
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
 | 
			
		||||
from passbook.outposts.api import OutpostViewSet
 | 
			
		||||
from passbook.outposts.api import (
 | 
			
		||||
    DockerServiceConnectionViewSet,
 | 
			
		||||
    KubernetesServiceConnectionViewSet,
 | 
			
		||||
    OutpostViewSet,
 | 
			
		||||
)
 | 
			
		||||
from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet
 | 
			
		||||
from passbook.policies.dummy.api import DummyPolicyViewSet
 | 
			
		||||
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
 | 
			
		||||
@ -29,7 +33,7 @@ from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
 | 
			
		||||
from passbook.policies.password.api import PasswordPolicyViewSet
 | 
			
		||||
from passbook.policies.reputation.api import ReputationPolicyViewSet
 | 
			
		||||
from passbook.providers.oauth2.api import OAuth2ProviderViewSet, ScopeMappingViewSet
 | 
			
		||||
from passbook.providers.proxy.api import OutpostConfigViewSet, ProxyProviderViewSet
 | 
			
		||||
from passbook.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
 | 
			
		||||
from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
 | 
			
		||||
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
 | 
			
		||||
from passbook.sources.oauth.api import OAuthSourceViewSet
 | 
			
		||||
@ -66,7 +70,11 @@ router.register("core/users", UserViewSet)
 | 
			
		||||
router.register("core/tokens", TokenViewSet)
 | 
			
		||||
 | 
			
		||||
router.register("outposts/outposts", OutpostViewSet)
 | 
			
		||||
router.register("outposts/proxy", OutpostConfigViewSet)
 | 
			
		||||
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
 | 
			
		||||
router.register(
 | 
			
		||||
    "outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
 | 
			
		||||
)
 | 
			
		||||
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
 | 
			
		||||
 | 
			
		||||
router.register("flows/instances", FlowViewSet)
 | 
			
		||||
router.register("flows/bindings", FlowStageBindingViewSet)
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ from io import StringIO
 | 
			
		||||
 | 
			
		||||
from boto3.exceptions import Boto3Error
 | 
			
		||||
from botocore.exceptions import BotoCoreError, ClientError
 | 
			
		||||
from dbbackup.db.exceptions import CommandConnectorError
 | 
			
		||||
from django.contrib.humanize.templatetags.humanize import naturaltime
 | 
			
		||||
from django.core import management
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
@ -36,6 +37,7 @@ def clean_expired_models(self: MonitoredTask):
 | 
			
		||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
			
		||||
def backup_database(self: MonitoredTask):  # pragma: no cover
 | 
			
		||||
    """Database backup"""
 | 
			
		||||
    self.result_timeout_hours = 25
 | 
			
		||||
    try:
 | 
			
		||||
        start = datetime.now()
 | 
			
		||||
        out = StringIO()
 | 
			
		||||
@ -50,5 +52,12 @@ def backup_database(self: MonitoredTask):  # pragma: no cover
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        LOGGER.info("Successfully backed up database.")
 | 
			
		||||
    except (IOError, BotoCoreError, ClientError, Boto3Error) as exc:
 | 
			
		||||
    except (
 | 
			
		||||
        IOError,
 | 
			
		||||
        BotoCoreError,
 | 
			
		||||
        ClientError,
 | 
			
		||||
        Boto3Error,
 | 
			
		||||
        PermissionError,
 | 
			
		||||
        CommandConnectorError,
 | 
			
		||||
    ) as exc:
 | 
			
		||||
        self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,7 @@ class CertificateKeyPair(CreatedUpdatedModel):
 | 
			
		||||
    @property
 | 
			
		||||
    def private_key(self) -> Optional[RSAPrivateKey]:
 | 
			
		||||
        """Get python cryptography PrivateKey instance"""
 | 
			
		||||
        if not self._private_key:
 | 
			
		||||
        if not self._private_key and self._private_key != "":
 | 
			
		||||
            self._private_key = load_pem_private_key(
 | 
			
		||||
                str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])),
 | 
			
		||||
                password=None,
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
"""passbook sentry integration"""
 | 
			
		||||
from aioredis.errors import ReplyError
 | 
			
		||||
from billiard.exceptions import WorkerLostError
 | 
			
		||||
from botocore.client import ClientError
 | 
			
		||||
from celery.exceptions import CeleryError
 | 
			
		||||
@ -8,7 +9,7 @@ from django.db import InternalError, OperationalError, ProgrammingError
 | 
			
		||||
from django_redis.exceptions import ConnectionInterrupted
 | 
			
		||||
from ldap3.core.exceptions import LDAPException
 | 
			
		||||
from redis.exceptions import ConnectionError as RedisConnectionError
 | 
			
		||||
from redis.exceptions import RedisError
 | 
			
		||||
from redis.exceptions import RedisError, ResponseError
 | 
			
		||||
from rest_framework.exceptions import APIException
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
from websockets.exceptions import WebSocketException
 | 
			
		||||
@ -23,26 +24,36 @@ class SentryIgnoredException(Exception):
 | 
			
		||||
def before_send(event, hint):
 | 
			
		||||
    """Check if error is database error, and ignore if so"""
 | 
			
		||||
    ignored_classes = (
 | 
			
		||||
        # Inbuilt types
 | 
			
		||||
        KeyboardInterrupt,
 | 
			
		||||
        ConnectionResetError,
 | 
			
		||||
        OSError,
 | 
			
		||||
        # Django DB Errors
 | 
			
		||||
        OperationalError,
 | 
			
		||||
        InternalError,
 | 
			
		||||
        ProgrammingError,
 | 
			
		||||
        ConnectionInterrupted,
 | 
			
		||||
        APIException,
 | 
			
		||||
        ConnectionResetError,
 | 
			
		||||
        RedisConnectionError,
 | 
			
		||||
        WorkerLostError,
 | 
			
		||||
        DisallowedHost,
 | 
			
		||||
        ConnectionResetError,
 | 
			
		||||
        KeyboardInterrupt,
 | 
			
		||||
        ClientError,
 | 
			
		||||
        ValidationError,
 | 
			
		||||
        OSError,
 | 
			
		||||
        # Redis errors
 | 
			
		||||
        RedisConnectionError,
 | 
			
		||||
        ConnectionInterrupted,
 | 
			
		||||
        RedisError,
 | 
			
		||||
        SentryIgnoredException,
 | 
			
		||||
        CeleryError,
 | 
			
		||||
        LDAPException,
 | 
			
		||||
        ResponseError,
 | 
			
		||||
        ReplyError,
 | 
			
		||||
        # websocket errors
 | 
			
		||||
        ChannelFull,
 | 
			
		||||
        WebSocketException,
 | 
			
		||||
        # rest_framework error
 | 
			
		||||
        APIException,
 | 
			
		||||
        # celery errors
 | 
			
		||||
        WorkerLostError,
 | 
			
		||||
        CeleryError,
 | 
			
		||||
        # S3 errors
 | 
			
		||||
        ClientError,
 | 
			
		||||
        # custom baseclass
 | 
			
		||||
        SentryIgnoredException,
 | 
			
		||||
        # ldap errors
 | 
			
		||||
        LDAPException,
 | 
			
		||||
    )
 | 
			
		||||
    if "exc_info" in hint:
 | 
			
		||||
        _, exc_value, _ = hint["exc_info"]
 | 
			
		||||
 | 
			
		||||
@ -66,13 +66,13 @@ class TaskInfo:
 | 
			
		||||
        """Delete task info from cache"""
 | 
			
		||||
        return cache.delete(f"task_{self.task_name}")
 | 
			
		||||
 | 
			
		||||
    def save(self):
 | 
			
		||||
    def save(self, timeout_hours=6):
 | 
			
		||||
        """Save task into cache"""
 | 
			
		||||
        key = f"task_{self.task_name}"
 | 
			
		||||
        if self.result.uid:
 | 
			
		||||
            key += f"_{self.result.uid}"
 | 
			
		||||
            self.task_name += f"_{self.result.uid}"
 | 
			
		||||
        cache.set(key, self, timeout=6 * 60 * 60)
 | 
			
		||||
        cache.set(key, self, timeout=timeout_hours * 60 * 60)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MonitoredTask(Task):
 | 
			
		||||
@ -90,6 +90,7 @@ class MonitoredTask(Task):
 | 
			
		||||
        self.save_on_success = True
 | 
			
		||||
        self._uid = None
 | 
			
		||||
        self._result = TaskResult(status=TaskResultStatus.ERROR, messages=[])
 | 
			
		||||
        self.result_timeout_hours = 6
 | 
			
		||||
 | 
			
		||||
    def set_uid(self, uid: str):
 | 
			
		||||
        """Set UID, so in the case of an unexpected error its saved correctly"""
 | 
			
		||||
@ -115,7 +116,7 @@ class MonitoredTask(Task):
 | 
			
		||||
                task_call_func=self.__name__,
 | 
			
		||||
                task_call_args=args,
 | 
			
		||||
                task_call_kwargs=kwargs,
 | 
			
		||||
            ).save()
 | 
			
		||||
            ).save(self.result_timeout_hours)
 | 
			
		||||
        return super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=too-many-arguments
 | 
			
		||||
@ -131,7 +132,7 @@ class MonitoredTask(Task):
 | 
			
		||||
            task_call_func=self.__name__,
 | 
			
		||||
            task_call_args=args,
 | 
			
		||||
            task_call_kwargs=kwargs,
 | 
			
		||||
        ).save()
 | 
			
		||||
        ).save(self.result_timeout_hours)
 | 
			
		||||
        return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
 | 
			
		||||
 | 
			
		||||
    def run(self, *args, **kwargs):
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,11 @@
 | 
			
		||||
from rest_framework.serializers import JSONField, ModelSerializer
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
 | 
			
		||||
from passbook.outposts.models import Outpost
 | 
			
		||||
from passbook.outposts.models import (
 | 
			
		||||
    DockerServiceConnection,
 | 
			
		||||
    KubernetesServiceConnection,
 | 
			
		||||
    Outpost,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostSerializer(ModelSerializer):
 | 
			
		||||
@ -13,7 +17,7 @@ class OutpostSerializer(ModelSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = Outpost
 | 
			
		||||
        fields = ["pk", "name", "providers", "_config"]
 | 
			
		||||
        fields = ["pk", "name", "providers", "service_connection", "_config"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostViewSet(ModelViewSet):
 | 
			
		||||
@ -21,3 +25,35 @@ class OutpostViewSet(ModelViewSet):
 | 
			
		||||
 | 
			
		||||
    queryset = Outpost.objects.all()
 | 
			
		||||
    serializer_class = OutpostSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DockerServiceConnectionSerializer(ModelSerializer):
 | 
			
		||||
    """DockerServiceConnection Serializer"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = DockerServiceConnection
 | 
			
		||||
        fields = ["pk", "name", "local", "url", "tls"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DockerServiceConnectionViewSet(ModelViewSet):
 | 
			
		||||
    """DockerServiceConnection Viewset"""
 | 
			
		||||
 | 
			
		||||
    queryset = DockerServiceConnection.objects.all()
 | 
			
		||||
    serializer_class = DockerServiceConnectionSerializer
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KubernetesServiceConnectionSerializer(ModelSerializer):
 | 
			
		||||
    """KubernetesServiceConnection Serializer"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = KubernetesServiceConnection
 | 
			
		||||
        fields = ["pk", "name", "local", "kubeconfig"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KubernetesServiceConnectionViewSet(ModelViewSet):
 | 
			
		||||
    """KubernetesServiceConnection Viewset"""
 | 
			
		||||
 | 
			
		||||
    queryset = KubernetesServiceConnection.objects.all()
 | 
			
		||||
    serializer_class = KubernetesServiceConnectionSerializer
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,20 @@
 | 
			
		||||
"""passbook outposts app config"""
 | 
			
		||||
from importlib import import_module
 | 
			
		||||
from os import R_OK, access
 | 
			
		||||
from os.path import expanduser
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from socket import gethostname
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
import yaml
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
from django.db import ProgrammingError
 | 
			
		||||
from docker.constants import DEFAULT_UNIX_SOCKET
 | 
			
		||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
 | 
			
		||||
from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PassbookOutpostConfig(AppConfig):
 | 
			
		||||
@ -14,3 +27,48 @@ class PassbookOutpostConfig(AppConfig):
 | 
			
		||||
 | 
			
		||||
    def ready(self):
 | 
			
		||||
        import_module("passbook.outposts.signals")
 | 
			
		||||
        try:
 | 
			
		||||
            self.init_local_connection()
 | 
			
		||||
        except ProgrammingError:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    def init_local_connection(self):
 | 
			
		||||
        """Check if local kubernetes or docker connections should be created"""
 | 
			
		||||
        from passbook.outposts.models import (
 | 
			
		||||
            KubernetesServiceConnection,
 | 
			
		||||
            DockerServiceConnection,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if Path(SERVICE_TOKEN_FILENAME).exists():
 | 
			
		||||
            LOGGER.debug("Detected in-cluster Kubernetes Config")
 | 
			
		||||
            if not KubernetesServiceConnection.objects.filter(local=True).exists():
 | 
			
		||||
                LOGGER.debug("Created Service Connection for in-cluster")
 | 
			
		||||
                KubernetesServiceConnection.objects.create(
 | 
			
		||||
                    name="Local Kubernetes Cluster", local=True, kubeconfig={}
 | 
			
		||||
                )
 | 
			
		||||
        # For development, check for the existence of a kubeconfig file
 | 
			
		||||
        kubeconfig_path = expanduser(KUBE_CONFIG_DEFAULT_LOCATION)
 | 
			
		||||
        if Path(kubeconfig_path).exists():
 | 
			
		||||
            LOGGER.debug("Detected kubeconfig")
 | 
			
		||||
            kubeconfig_local_name = f"k8s-{gethostname()}"
 | 
			
		||||
            if not KubernetesServiceConnection.objects.filter(
 | 
			
		||||
                name=kubeconfig_local_name
 | 
			
		||||
            ).exists():
 | 
			
		||||
                LOGGER.debug("Creating kubeconfig Service Connection")
 | 
			
		||||
                with open(kubeconfig_path, "r") as _kubeconfig:
 | 
			
		||||
                    KubernetesServiceConnection.objects.create(
 | 
			
		||||
                        name=kubeconfig_local_name,
 | 
			
		||||
                        kubeconfig=yaml.safe_load(_kubeconfig),
 | 
			
		||||
                    )
 | 
			
		||||
        unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path
 | 
			
		||||
        socket = Path(unix_socket_path)
 | 
			
		||||
        if socket.exists() and access(socket, R_OK):
 | 
			
		||||
            LOGGER.debug("Detected local docker socket")
 | 
			
		||||
            if not DockerServiceConnection.objects.filter(local=True).exists():
 | 
			
		||||
                LOGGER.debug("Created Service Connection for docker")
 | 
			
		||||
                DockerServiceConnection.objects.create(
 | 
			
		||||
                    name="Local Docker connection",
 | 
			
		||||
                    local=True,
 | 
			
		||||
                    url=unix_socket_path,
 | 
			
		||||
                    tls=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
@ -5,11 +5,11 @@ from structlog import get_logger
 | 
			
		||||
from structlog.testing import capture_logs
 | 
			
		||||
 | 
			
		||||
from passbook.lib.sentry import SentryIgnoredException
 | 
			
		||||
from passbook.outposts.models import Outpost
 | 
			
		||||
from passbook.outposts.models import Outpost, OutpostServiceConnection
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ControllerException(SentryIgnoredException):
 | 
			
		||||
    """Exception raise when anything fails during controller run"""
 | 
			
		||||
    """Exception raised when anything fails during controller run"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseController:
 | 
			
		||||
@ -18,9 +18,11 @@ class BaseController:
 | 
			
		||||
    deployment_ports: Dict[str, int]
 | 
			
		||||
 | 
			
		||||
    outpost: Outpost
 | 
			
		||||
    connection: OutpostServiceConnection
 | 
			
		||||
 | 
			
		||||
    def __init__(self, outpost: Outpost):
 | 
			
		||||
    def __init__(self, outpost: Outpost, connection: OutpostServiceConnection):
 | 
			
		||||
        self.outpost = outpost
 | 
			
		||||
        self.connection = connection
 | 
			
		||||
        self.logger = get_logger()
 | 
			
		||||
        self.deployment_ports = {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,14 +3,18 @@ from time import sleep
 | 
			
		||||
from typing import Dict, Tuple
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from docker import DockerClient, from_env
 | 
			
		||||
from docker import DockerClient
 | 
			
		||||
from docker.errors import DockerException, NotFound
 | 
			
		||||
from docker.models.containers import Container
 | 
			
		||||
from yaml import safe_dump
 | 
			
		||||
 | 
			
		||||
from passbook import __version__
 | 
			
		||||
from passbook.outposts.controllers.base import BaseController, ControllerException
 | 
			
		||||
from passbook.outposts.models import Outpost
 | 
			
		||||
from passbook.outposts.models import (
 | 
			
		||||
    DockerServiceConnection,
 | 
			
		||||
    Outpost,
 | 
			
		||||
    ServiceConnectionInvalid,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DockerController(BaseController):
 | 
			
		||||
@ -19,14 +23,15 @@ class DockerController(BaseController):
 | 
			
		||||
    client: DockerClient
 | 
			
		||||
 | 
			
		||||
    container: Container
 | 
			
		||||
    connection: DockerServiceConnection
 | 
			
		||||
 | 
			
		||||
    image_base = "beryju/passbook"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, outpost: Outpost) -> None:
 | 
			
		||||
        super().__init__(outpost)
 | 
			
		||||
    def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None:
 | 
			
		||||
        super().__init__(outpost, connection)
 | 
			
		||||
        try:
 | 
			
		||||
            self.client = from_env()
 | 
			
		||||
        except DockerException as exc:
 | 
			
		||||
            self.client = connection.client()
 | 
			
		||||
        except ServiceConnectionInvalid as exc:
 | 
			
		||||
            raise ControllerException from exc
 | 
			
		||||
 | 
			
		||||
    def _get_labels(self) -> Dict[str, str]:
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, controller: "KubernetesController") -> None:
 | 
			
		||||
        super().__init__(controller)
 | 
			
		||||
        self.api = AppsV1Api()
 | 
			
		||||
        self.api = AppsV1Api(controller.client)
 | 
			
		||||
        self.outpost = self.controller.outpost
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, controller: "KubernetesController") -> None:
 | 
			
		||||
        super().__init__(controller)
 | 
			
		||||
        self.api = CoreV1Api()
 | 
			
		||||
        self.api = CoreV1Api(controller.client)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, controller: "KubernetesController") -> None:
 | 
			
		||||
        super().__init__(controller)
 | 
			
		||||
        self.api = CoreV1Api()
 | 
			
		||||
        self.api = CoreV1Api(controller.client)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,7 @@ from io import StringIO
 | 
			
		||||
from typing import Dict, List, Type
 | 
			
		||||
 | 
			
		||||
from kubernetes.client import OpenApiException
 | 
			
		||||
from kubernetes.config import load_incluster_config, load_kube_config
 | 
			
		||||
from kubernetes.config.config_exception import ConfigException
 | 
			
		||||
from kubernetes.client.api_client import ApiClient
 | 
			
		||||
from structlog.testing import capture_logs
 | 
			
		||||
from yaml import dump_all
 | 
			
		||||
 | 
			
		||||
@ -13,7 +12,7 @@ from passbook.outposts.controllers.k8s.base import KubernetesObjectReconciler
 | 
			
		||||
from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler
 | 
			
		||||
from passbook.outposts.controllers.k8s.secret import SecretReconciler
 | 
			
		||||
from passbook.outposts.controllers.k8s.service import ServiceReconciler
 | 
			
		||||
from passbook.outposts.models import Outpost
 | 
			
		||||
from passbook.outposts.models import KubernetesServiceConnection, Outpost
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KubernetesController(BaseController):
 | 
			
		||||
@ -22,12 +21,14 @@ class KubernetesController(BaseController):
 | 
			
		||||
    reconcilers: Dict[str, Type[KubernetesObjectReconciler]]
 | 
			
		||||
    reconcile_order: List[str]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, outpost: Outpost) -> None:
 | 
			
		||||
        super().__init__(outpost)
 | 
			
		||||
        try:
 | 
			
		||||
            load_incluster_config()
 | 
			
		||||
        except ConfigException:
 | 
			
		||||
            load_kube_config()
 | 
			
		||||
    client: ApiClient
 | 
			
		||||
    connection: KubernetesServiceConnection
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, outpost: Outpost, connection: KubernetesServiceConnection
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        super().__init__(outpost, connection)
 | 
			
		||||
        self.client = connection.client()
 | 
			
		||||
        self.reconcilers = {
 | 
			
		||||
            "secret": SecretReconciler,
 | 
			
		||||
            "deployment": DeploymentReconciler,
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,11 @@ from django import forms
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from passbook.admin.fields import CodeMirrorWidget, YAMLField
 | 
			
		||||
from passbook.outposts.models import Outpost
 | 
			
		||||
from passbook.outposts.models import (
 | 
			
		||||
    DockerServiceConnection,
 | 
			
		||||
    KubernetesServiceConnection,
 | 
			
		||||
    Outpost,
 | 
			
		||||
)
 | 
			
		||||
from passbook.providers.proxy.models import ProxyProvider
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -21,7 +25,7 @@ class OutpostForm(forms.ModelForm):
 | 
			
		||||
        fields = [
 | 
			
		||||
            "name",
 | 
			
		||||
            "type",
 | 
			
		||||
            "deployment_type",
 | 
			
		||||
            "service_connection",
 | 
			
		||||
            "providers",
 | 
			
		||||
            "_config",
 | 
			
		||||
        ]
 | 
			
		||||
@ -33,3 +37,40 @@ class OutpostForm(forms.ModelForm):
 | 
			
		||||
            "_config": YAMLField,
 | 
			
		||||
        }
 | 
			
		||||
        labels = {"_config": _("Configuration")}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DockerServiceConnectionForm(forms.ModelForm):
 | 
			
		||||
    """Docker service-connection form"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = DockerServiceConnection
 | 
			
		||||
        fields = ["name", "local", "url", "tls"]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput,
 | 
			
		||||
            "url": forms.TextInput,
 | 
			
		||||
        }
 | 
			
		||||
        labels = {
 | 
			
		||||
            "url": _("URL"),
 | 
			
		||||
            "tls": _("TLS"),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KubernetesServiceConnectionForm(forms.ModelForm):
 | 
			
		||||
    """Kubernetes service-connection form"""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = KubernetesServiceConnection
 | 
			
		||||
        fields = [
 | 
			
		||||
            "name",
 | 
			
		||||
            "local",
 | 
			
		||||
            "kubeconfig",
 | 
			
		||||
        ]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput,
 | 
			
		||||
            "kubeconfig": CodeMirrorWidget,
 | 
			
		||||
        }
 | 
			
		||||
        field_classes = {
 | 
			
		||||
            "kubeconfig": YAMLField,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -6,13 +6,20 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
			
		||||
    User = apps.get_model("passbook_core", "User")
 | 
			
		||||
    Token = apps.get_model("passbook_core", "Token")
 | 
			
		||||
    from passbook.outposts.models import Outpost
 | 
			
		||||
 | 
			
		||||
    for outpost in Outpost.objects.using(schema_editor.connection.alias).all():
 | 
			
		||||
        token = outpost.token
 | 
			
		||||
        if token.identifier != outpost.token_identifier:
 | 
			
		||||
            token.identifier = outpost.token_identifier
 | 
			
		||||
            token.save()
 | 
			
		||||
    for outpost in (
 | 
			
		||||
        Outpost.objects.using(schema_editor.connection.alias).all().only("pk")
 | 
			
		||||
    ):
 | 
			
		||||
        user_identifier = outpost.user_identifier
 | 
			
		||||
        user = User.objects.get(username=user_identifier)
 | 
			
		||||
        tokens = Token.objects.filter(user=user)
 | 
			
		||||
        for token in tokens:
 | 
			
		||||
            if token.identifier != outpost.token_identifier:
 | 
			
		||||
                token.identifier = outpost.token_identifier
 | 
			
		||||
                token.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										172
									
								
								passbook/outposts/migrations/0010_service_connection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								passbook/outposts/migrations/0010_service_connection.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,172 @@
 | 
			
		||||
# Generated by Django 3.1.3 on 2020-11-04 09:11
 | 
			
		||||
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.apps.registry import Apps
 | 
			
		||||
from django.core.exceptions import FieldError
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
			
		||||
 | 
			
		||||
import passbook.lib.models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
			
		||||
    db_alias = schema_editor.connection.alias
 | 
			
		||||
    Outpost = apps.get_model("passbook_outposts", "Outpost")
 | 
			
		||||
    DockerServiceConnection = apps.get_model(
 | 
			
		||||
        "passbook_outposts", "DockerServiceConnection"
 | 
			
		||||
    )
 | 
			
		||||
    KubernetesServiceConnection = apps.get_model(
 | 
			
		||||
        "passbook_outposts", "KubernetesServiceConnection"
 | 
			
		||||
    )
 | 
			
		||||
    from passbook.outposts.apps import PassbookOutpostConfig
 | 
			
		||||
 | 
			
		||||
    # Ensure that local connection have been created
 | 
			
		||||
    PassbookOutpostConfig.init_local_connection(None)
 | 
			
		||||
 | 
			
		||||
    docker = DockerServiceConnection.objects.filter(local=True)
 | 
			
		||||
    k8s = KubernetesServiceConnection.objects.filter(local=True)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        for outpost in (
 | 
			
		||||
            Outpost.objects.using(db_alias).all().exclude(deployment_type="custom")
 | 
			
		||||
        ):
 | 
			
		||||
            if outpost.deployment_type == "kubernetes":
 | 
			
		||||
                outpost.service_connection = k8s
 | 
			
		||||
            elif outpost.deployment_type == "docker":
 | 
			
		||||
                outpost.service_connection = docker
 | 
			
		||||
            outpost.save()
 | 
			
		||||
    except FieldError:
 | 
			
		||||
        # This is triggered during e2e tests when this function is called on an already-upgraded
 | 
			
		||||
        # schema
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_outposts", "0009_fix_missing_token_identifier"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="OutpostServiceConnection",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "uuid",
 | 
			
		||||
                    models.UUIDField(
 | 
			
		||||
                        default=uuid.uuid4,
 | 
			
		||||
                        editable=False,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("name", models.TextField()),
 | 
			
		||||
                (
 | 
			
		||||
                    "local",
 | 
			
		||||
                    models.BooleanField(
 | 
			
		||||
                        default=False,
 | 
			
		||||
                        help_text="If enabled, use the local connection. Required Docker socket/Kubernetes Integration",
 | 
			
		||||
                        unique=True,
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
            ],
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="DockerServiceConnection",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "outpostserviceconnection_ptr",
 | 
			
		||||
                    models.OneToOneField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        parent_link=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        to="passbook_outposts.outpostserviceconnection",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("url", models.TextField()),
 | 
			
		||||
                ("tls", models.BooleanField()),
 | 
			
		||||
            ],
 | 
			
		||||
            bases=("passbook_outposts.outpostserviceconnection",),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="KubernetesServiceConnection",
 | 
			
		||||
            fields=[
 | 
			
		||||
                (
 | 
			
		||||
                    "outpostserviceconnection_ptr",
 | 
			
		||||
                    models.OneToOneField(
 | 
			
		||||
                        auto_created=True,
 | 
			
		||||
                        on_delete=django.db.models.deletion.CASCADE,
 | 
			
		||||
                        parent_link=True,
 | 
			
		||||
                        primary_key=True,
 | 
			
		||||
                        serialize=False,
 | 
			
		||||
                        to="passbook_outposts.outpostserviceconnection",
 | 
			
		||||
                    ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("kubeconfig", models.JSONField()),
 | 
			
		||||
            ],
 | 
			
		||||
            bases=("passbook_outposts.outpostserviceconnection",),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="outpost",
 | 
			
		||||
            name="service_connection",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                default=None,
 | 
			
		||||
                help_text="Select Service-Connection passbook should use to manage this outpost. Leave empty if passbook should not handle the deployment.",
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_DEFAULT,
 | 
			
		||||
                to="passbook_outposts.outpostserviceconnection",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RunPython(migrate_to_service_connection),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name="outpost",
 | 
			
		||||
            name="deployment_type",
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterModelOptions(
 | 
			
		||||
            name="dockerserviceconnection",
 | 
			
		||||
            options={
 | 
			
		||||
                "verbose_name": "Docker Service-Connection",
 | 
			
		||||
                "verbose_name_plural": "Docker Service-Connections",
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterModelOptions(
 | 
			
		||||
            name="kubernetesserviceconnection",
 | 
			
		||||
            options={
 | 
			
		||||
                "verbose_name": "Kubernetes Service-Connection",
 | 
			
		||||
                "verbose_name_plural": "Kubernetes Service-Connections",
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="outpost",
 | 
			
		||||
            name="service_connection",
 | 
			
		||||
            field=passbook.lib.models.InheritanceForeignKey(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                default=None,
 | 
			
		||||
                help_text="Select Service-Connection passbook should use to manage this outpost. Leave empty if passbook should not handle the deployment.",
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_DEFAULT,
 | 
			
		||||
                to="passbook_outposts.outpostserviceconnection",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterModelOptions(
 | 
			
		||||
            name="outpostserviceconnection",
 | 
			
		||||
            options={
 | 
			
		||||
                "verbose_name": "Outpost Service-Connection",
 | 
			
		||||
                "verbose_name_plural": "Outpost Service-Connections",
 | 
			
		||||
            },
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="kubernetesserviceconnection",
 | 
			
		||||
            name="kubeconfig",
 | 
			
		||||
            field=models.JSONField(
 | 
			
		||||
                default=None,
 | 
			
		||||
                help_text="Paste your kubeconfig here. passbook will automatically use the currently selected context.",
 | 
			
		||||
            ),
 | 
			
		||||
            preserve_default=False,
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -1,28 +1,46 @@
 | 
			
		||||
"""Outpost models"""
 | 
			
		||||
from dataclasses import asdict, dataclass, field
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from typing import Dict, Iterable, List, Optional, Union
 | 
			
		||||
from typing import Dict, Iterable, List, Optional, Type, Union
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from dacite import from_dict
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db import models, transaction
 | 
			
		||||
from django.db.models.base import Model
 | 
			
		||||
from django.forms.models import ModelForm
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from docker.client import DockerClient
 | 
			
		||||
from docker.errors import DockerException
 | 
			
		||||
from guardian.models import UserObjectPermission
 | 
			
		||||
from guardian.shortcuts import assign_perm
 | 
			
		||||
from kubernetes.client import VersionApi, VersionInfo
 | 
			
		||||
from kubernetes.client.api_client import ApiClient
 | 
			
		||||
from kubernetes.client.configuration import Configuration
 | 
			
		||||
from kubernetes.client.exceptions import OpenApiException
 | 
			
		||||
from kubernetes.config.config_exception import ConfigException
 | 
			
		||||
from kubernetes.config.incluster_config import load_incluster_config
 | 
			
		||||
from kubernetes.config.kube_config import load_kube_config_from_dict
 | 
			
		||||
from model_utils.managers import InheritanceManager
 | 
			
		||||
from packaging.version import LegacyVersion, Version, parse
 | 
			
		||||
from urllib3.exceptions import HTTPError
 | 
			
		||||
 | 
			
		||||
from passbook import __version__
 | 
			
		||||
from passbook.core.models import Provider, Token, TokenIntents, User
 | 
			
		||||
from passbook.lib.config import CONFIG
 | 
			
		||||
from passbook.lib.models import InheritanceForeignKey
 | 
			
		||||
from passbook.lib.sentry import SentryIgnoredException
 | 
			
		||||
from passbook.lib.utils.template import render_to_string
 | 
			
		||||
 | 
			
		||||
OUR_VERSION = parse(__version__)
 | 
			
		||||
OUTPOST_HELLO_INTERVAL = 10
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ServiceConnectionInvalid(SentryIgnoredException):
 | 
			
		||||
    """"Exception raised when a Service Connection has invalid parameters"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class OutpostConfig:
 | 
			
		||||
    """Configuration an outpost uses to configure it self"""
 | 
			
		||||
@ -60,19 +78,158 @@ class OutpostType(models.TextChoices):
 | 
			
		||||
    PROXY = "proxy"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostDeploymentType(models.TextChoices):
 | 
			
		||||
    """Deployment types that are managed through passbook"""
 | 
			
		||||
 | 
			
		||||
    KUBERNETES = "kubernetes"
 | 
			
		||||
    DOCKER = "docker"
 | 
			
		||||
    CUSTOM = "custom"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def default_outpost_config():
 | 
			
		||||
    """Get default outpost config"""
 | 
			
		||||
    return asdict(OutpostConfig(passbook_host=""))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class OutpostServiceConnectionState:
 | 
			
		||||
    """State of an Outpost Service Connection"""
 | 
			
		||||
 | 
			
		||||
    version: str
 | 
			
		||||
    healthy: bool
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostServiceConnection(models.Model):
 | 
			
		||||
    """Connection details for an Outpost Controller, like Docker or Kubernetes"""
 | 
			
		||||
 | 
			
		||||
    uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
 | 
			
		||||
    name = models.TextField()
 | 
			
		||||
 | 
			
		||||
    local = models.BooleanField(
 | 
			
		||||
        default=False,
 | 
			
		||||
        unique=True,
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            (
 | 
			
		||||
                "If enabled, use the local connection. Required Docker "
 | 
			
		||||
                "socket/Kubernetes Integration"
 | 
			
		||||
            )
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    objects = InheritanceManager()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
        """Get state of service connection"""
 | 
			
		||||
        state_key = f"outpost_service_connection_{self.pk.hex}"
 | 
			
		||||
        state = cache.get(state_key, None)
 | 
			
		||||
        if not state:
 | 
			
		||||
            state = self._get_state()
 | 
			
		||||
            cache.set(state_key, state, timeout=0)
 | 
			
		||||
        return state
 | 
			
		||||
 | 
			
		||||
    def _get_state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        """Return Form class used to edit this object"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _("Outpost Service-Connection")
 | 
			
		||||
        verbose_name_plural = _("Outpost Service-Connections")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DockerServiceConnection(OutpostServiceConnection):
 | 
			
		||||
    """Service Connection to a Docker endpoint"""
 | 
			
		||||
 | 
			
		||||
    url = models.TextField()
 | 
			
		||||
    tls = models.BooleanField()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from passbook.outposts.forms import DockerServiceConnectionForm
 | 
			
		||||
 | 
			
		||||
        return DockerServiceConnectionForm
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return f"Docker Service-Connection {self.name}"
 | 
			
		||||
 | 
			
		||||
    def client(self) -> DockerClient:
 | 
			
		||||
        """Get DockerClient"""
 | 
			
		||||
        try:
 | 
			
		||||
            client = None
 | 
			
		||||
            if self.local:
 | 
			
		||||
                client = DockerClient.from_env()
 | 
			
		||||
            else:
 | 
			
		||||
                client = DockerClient(
 | 
			
		||||
                    base_url=self.url,
 | 
			
		||||
                    tls=self.tls,
 | 
			
		||||
                )
 | 
			
		||||
            client.containers.list()
 | 
			
		||||
        except DockerException as exc:
 | 
			
		||||
            raise ServiceConnectionInvalid from exc
 | 
			
		||||
        return client
 | 
			
		||||
 | 
			
		||||
    def _get_state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
        try:
 | 
			
		||||
            client = self.client()
 | 
			
		||||
            return OutpostServiceConnectionState(
 | 
			
		||||
                version=client.info()["ServerVersion"], healthy=True
 | 
			
		||||
            )
 | 
			
		||||
        except ServiceConnectionInvalid:
 | 
			
		||||
            return OutpostServiceConnectionState(version="", healthy=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _("Docker Service-Connection")
 | 
			
		||||
        verbose_name_plural = _("Docker Service-Connections")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KubernetesServiceConnection(OutpostServiceConnection):
 | 
			
		||||
    """Service Connection to a Kubernetes cluster"""
 | 
			
		||||
 | 
			
		||||
    kubeconfig = models.JSONField(
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            (
 | 
			
		||||
                "Paste your kubeconfig here. passbook will automatically use "
 | 
			
		||||
                "the currently selected context."
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from passbook.outposts.forms import KubernetesServiceConnectionForm
 | 
			
		||||
 | 
			
		||||
        return KubernetesServiceConnectionForm
 | 
			
		||||
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return f"Kubernetes Service-Connection {self.name}"
 | 
			
		||||
 | 
			
		||||
    def _get_state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
        try:
 | 
			
		||||
            client = self.client()
 | 
			
		||||
            api_instance = VersionApi(client)
 | 
			
		||||
            version: VersionInfo = api_instance.get_code()
 | 
			
		||||
            return OutpostServiceConnectionState(
 | 
			
		||||
                version=version.git_version, healthy=True
 | 
			
		||||
            )
 | 
			
		||||
        except (OpenApiException, HTTPError):
 | 
			
		||||
            return OutpostServiceConnectionState(version="", healthy=False)
 | 
			
		||||
 | 
			
		||||
    def client(self) -> ApiClient:
 | 
			
		||||
        """Get Kubernetes client configured from kubeconfig"""
 | 
			
		||||
        config = Configuration()
 | 
			
		||||
        try:
 | 
			
		||||
            if self.local:
 | 
			
		||||
                load_incluster_config(client_configuration=config)
 | 
			
		||||
            else:
 | 
			
		||||
                load_kube_config_from_dict(self.kubeconfig, client_configuration=config)
 | 
			
		||||
            return ApiClient(config)
 | 
			
		||||
        except ConfigException as exc:
 | 
			
		||||
            raise ServiceConnectionInvalid from exc
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        verbose_name = _("Kubernetes Service-Connection")
 | 
			
		||||
        verbose_name_plural = _("Kubernetes Service-Connections")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Outpost(models.Model):
 | 
			
		||||
    """Outpost instance which manages a service user and token"""
 | 
			
		||||
 | 
			
		||||
@ -80,13 +237,20 @@ class Outpost(models.Model):
 | 
			
		||||
    name = models.TextField()
 | 
			
		||||
 | 
			
		||||
    type = models.TextField(choices=OutpostType.choices, default=OutpostType.PROXY)
 | 
			
		||||
    deployment_type = models.TextField(
 | 
			
		||||
        choices=OutpostDeploymentType.choices,
 | 
			
		||||
        default=OutpostDeploymentType.CUSTOM,
 | 
			
		||||
    service_connection = InheritanceForeignKey(
 | 
			
		||||
        OutpostServiceConnection,
 | 
			
		||||
        default=None,
 | 
			
		||||
        null=True,
 | 
			
		||||
        blank=True,
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Select between passbook-managed deployment types or a custom deployment."
 | 
			
		||||
            (
 | 
			
		||||
                "Select Service-Connection passbook should use to manage this outpost. "
 | 
			
		||||
                "Leave empty if passbook should not handle the deployment."
 | 
			
		||||
            )
 | 
			
		||||
        ),
 | 
			
		||||
        on_delete=models.SET_DEFAULT,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    _config = models.JSONField(default=default_outpost_config)
 | 
			
		||||
 | 
			
		||||
    providers = models.ManyToManyField(Provider)
 | 
			
		||||
@ -111,12 +275,17 @@ class Outpost(models.Model):
 | 
			
		||||
        """Get outpost's health status"""
 | 
			
		||||
        return OutpostState.for_outpost(self)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def user_identifier(self):
 | 
			
		||||
        """Username for service user"""
 | 
			
		||||
        return f"pb-outpost-{self.uuid.hex}"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def user(self) -> User:
 | 
			
		||||
        """Get/create user with access to all required objects"""
 | 
			
		||||
        users = User.objects.filter(username=f"pb-outpost-{self.uuid.hex}")
 | 
			
		||||
        users = User.objects.filter(username=self.user_identifier)
 | 
			
		||||
        if not users.exists():
 | 
			
		||||
            user: User = User.objects.create(username=f"pb-outpost-{self.uuid.hex}")
 | 
			
		||||
            user: User = User.objects.create(username=self.user_identifier)
 | 
			
		||||
            user.set_unusable_password()
 | 
			
		||||
            user.save()
 | 
			
		||||
        else:
 | 
			
		||||
 | 
			
		||||
@ -7,4 +7,9 @@ CELERY_BEAT_SCHEDULE = {
 | 
			
		||||
        "schedule": crontab(minute="*/5"),
 | 
			
		||||
        "options": {"queue": "passbook_scheduled"},
 | 
			
		||||
    },
 | 
			
		||||
    "outposts_service_connection_check": {
 | 
			
		||||
        "task": "passbook.outposts.tasks.outpost_service_connection_monitor",
 | 
			
		||||
        "schedule": crontab(minute=0, hour="*"),
 | 
			
		||||
        "options": {"queue": "passbook_scheduled"},
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ from typing import Any
 | 
			
		||||
 | 
			
		||||
from asgiref.sync import async_to_sync
 | 
			
		||||
from channels.layers import get_channel_layer
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.db.models.base import Model
 | 
			
		||||
from django.utils.text import slugify
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
@ -11,9 +12,11 @@ from passbook.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
			
		||||
from passbook.lib.utils.reflection import path_to_class
 | 
			
		||||
from passbook.outposts.controllers.base import ControllerException
 | 
			
		||||
from passbook.outposts.models import (
 | 
			
		||||
    DockerServiceConnection,
 | 
			
		||||
    KubernetesServiceConnection,
 | 
			
		||||
    Outpost,
 | 
			
		||||
    OutpostDeploymentType,
 | 
			
		||||
    OutpostModel,
 | 
			
		||||
    OutpostServiceConnection,
 | 
			
		||||
    OutpostState,
 | 
			
		||||
    OutpostType,
 | 
			
		||||
)
 | 
			
		||||
@ -27,12 +30,29 @@ LOGGER = get_logger()
 | 
			
		||||
@CELERY_APP.task()
 | 
			
		||||
def outpost_controller_all():
 | 
			
		||||
    """Launch Controller for all Outposts which support it"""
 | 
			
		||||
    for outpost in Outpost.objects.exclude(
 | 
			
		||||
        deployment_type=OutpostDeploymentType.CUSTOM
 | 
			
		||||
    ):
 | 
			
		||||
    for outpost in Outpost.objects.exclude(service_connection=None):
 | 
			
		||||
        outpost_controller.delay(outpost.pk.hex)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task()
 | 
			
		||||
def outpost_service_connection_state(state_pk: Any):
 | 
			
		||||
    """Update cached state of a service connection"""
 | 
			
		||||
    connection: OutpostServiceConnection = (
 | 
			
		||||
        OutpostServiceConnection.objects.filter(pk=state_pk).select_subclasses().first()
 | 
			
		||||
    )
 | 
			
		||||
    cache.delete(f"outpost_service_connection_{connection.pk.hex}")
 | 
			
		||||
    _ = connection.state
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
			
		||||
def outpost_service_connection_monitor(self: MonitoredTask):
 | 
			
		||||
    """Regularly check the state of Outpost Service Connections"""
 | 
			
		||||
    for connection in OutpostServiceConnection.objects.select_subclasses():
 | 
			
		||||
        cache.delete(f"outpost_service_connection_{connection.pk.hex}")
 | 
			
		||||
        _ = connection.state
 | 
			
		||||
    self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
			
		||||
def outpost_controller(self: MonitoredTask, outpost_pk: str):
 | 
			
		||||
    """Create/update/monitor the deployment of an Outpost"""
 | 
			
		||||
@ -41,10 +61,13 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str):
 | 
			
		||||
    self.set_uid(slugify(outpost.name))
 | 
			
		||||
    try:
 | 
			
		||||
        if outpost.type == OutpostType.PROXY:
 | 
			
		||||
            if outpost.deployment_type == OutpostDeploymentType.KUBERNETES:
 | 
			
		||||
                logs = ProxyKubernetesController(outpost).up_with_logs()
 | 
			
		||||
            if outpost.deployment_type == OutpostDeploymentType.DOCKER:
 | 
			
		||||
                logs = ProxyDockerController(outpost).up_with_logs()
 | 
			
		||||
            service_connection = outpost.service_connection
 | 
			
		||||
            if isinstance(service_connection, DockerServiceConnection):
 | 
			
		||||
                logs = ProxyDockerController(outpost, service_connection).up_with_logs()
 | 
			
		||||
            if isinstance(service_connection, KubernetesServiceConnection):
 | 
			
		||||
                logs = ProxyKubernetesController(
 | 
			
		||||
                    outpost, service_connection
 | 
			
		||||
                ).up_with_logs()
 | 
			
		||||
    except ControllerException as exc:
 | 
			
		||||
        self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
 | 
			
		||||
    else:
 | 
			
		||||
@ -56,10 +79,11 @@ def outpost_pre_delete(outpost_pk: str):
 | 
			
		||||
    """Delete outpost objects before deleting the DB Object"""
 | 
			
		||||
    outpost = Outpost.objects.get(pk=outpost_pk)
 | 
			
		||||
    if outpost.type == OutpostType.PROXY:
 | 
			
		||||
        if outpost.deployment_type == OutpostDeploymentType.KUBERNETES:
 | 
			
		||||
            ProxyKubernetesController(outpost).down()
 | 
			
		||||
        if outpost.deployment_type == OutpostDeploymentType.DOCKER:
 | 
			
		||||
            ProxyDockerController(outpost).down()
 | 
			
		||||
        service_connection = outpost.service_connection
 | 
			
		||||
        if isinstance(service_connection, DockerServiceConnection):
 | 
			
		||||
            ProxyDockerController(outpost, service_connection).down()
 | 
			
		||||
        if isinstance(service_connection, KubernetesServiceConnection):
 | 
			
		||||
            ProxyKubernetesController(outpost, service_connection).down()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task()
 | 
			
		||||
@ -89,6 +113,10 @@ def outpost_post_save(model_class: str, model_pk: Any):
 | 
			
		||||
        outpost_send_update(instance)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if isinstance(instance, OutpostServiceConnection):
 | 
			
		||||
        LOGGER.debug("triggering ServiceConnection state update", instance=instance)
 | 
			
		||||
        outpost_service_connection_state.delay(instance.pk)
 | 
			
		||||
 | 
			
		||||
    for field in instance._meta.get_fields():
 | 
			
		||||
        # Each field is checked if it has a `related_model` attribute (when ForeginKeys or M2Ms)
 | 
			
		||||
        # are used, and if it has a value
 | 
			
		||||
@ -123,6 +151,9 @@ def outpost_send_update(model_instace: Model):
 | 
			
		||||
 | 
			
		||||
def _outpost_single_update(outpost: Outpost, layer=None):
 | 
			
		||||
    """Update outpost instances connected to a single outpost"""
 | 
			
		||||
    # Ensure token again, because this function is called when anything related to an
 | 
			
		||||
    # OutpostModel is saved, so we can be sure permissions are right
 | 
			
		||||
    _ = outpost.token
 | 
			
		||||
    if not layer:  # pragma: no cover
 | 
			
		||||
        layer = get_channel_layer()
 | 
			
		||||
    for state in OutpostState.for_outpost(outpost):
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ from passbook.flows.models import Flow
 | 
			
		||||
from passbook.outposts.controllers.k8s.base import NeedsUpdate
 | 
			
		||||
from passbook.outposts.controllers.k8s.deployment import DeploymentReconciler
 | 
			
		||||
from passbook.outposts.controllers.kubernetes import KubernetesController
 | 
			
		||||
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
 | 
			
		||||
from passbook.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
 | 
			
		||||
from passbook.providers.proxy.models import ProxyProvider
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,6 @@ class OutpostTests(TestCase):
 | 
			
		||||
        outpost: Outpost = Outpost.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            type=OutpostType.PROXY,
 | 
			
		||||
            deployment_type=OutpostDeploymentType.CUSTOM,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Before we add a provider, the user should only have access to the outpost
 | 
			
		||||
@ -79,17 +78,18 @@ class OutpostKubernetesTests(TestCase):
 | 
			
		||||
            external_host="http://localhost",
 | 
			
		||||
            authorization_flow=Flow.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        self.service_connection = KubernetesServiceConnection.objects.first()
 | 
			
		||||
        self.outpost: Outpost = Outpost.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            type=OutpostType.PROXY,
 | 
			
		||||
            deployment_type=OutpostDeploymentType.KUBERNETES,
 | 
			
		||||
            service_connection=self.service_connection,
 | 
			
		||||
        )
 | 
			
		||||
        self.outpost.providers.add(self.provider)
 | 
			
		||||
        self.outpost.save()
 | 
			
		||||
 | 
			
		||||
    def test_deployment_reconciler(self):
 | 
			
		||||
        """test that deployment requires update"""
 | 
			
		||||
        controller = KubernetesController(self.outpost)
 | 
			
		||||
        controller = KubernetesController(self.outpost, self.service_connection)
 | 
			
		||||
        deployment_reconciler = DeploymentReconciler(controller)
 | 
			
		||||
 | 
			
		||||
        self.assertIsNotNone(deployment_reconciler.retrieve())
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,12 @@ from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.core.models import User
 | 
			
		||||
from passbook.outposts.controllers.docker import DockerController
 | 
			
		||||
from passbook.outposts.models import Outpost, OutpostType
 | 
			
		||||
from passbook.outposts.models import (
 | 
			
		||||
    DockerServiceConnection,
 | 
			
		||||
    KubernetesServiceConnection,
 | 
			
		||||
    Outpost,
 | 
			
		||||
    OutpostType,
 | 
			
		||||
)
 | 
			
		||||
from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
@ -35,7 +40,7 @@ class DockerComposeView(LoginRequiredMixin, View):
 | 
			
		||||
        )
 | 
			
		||||
        manifest = ""
 | 
			
		||||
        if outpost.type == OutpostType.PROXY:
 | 
			
		||||
            controller = DockerController(outpost)
 | 
			
		||||
            controller = DockerController(outpost, DockerServiceConnection())
 | 
			
		||||
            manifest = controller.get_static_deployment()
 | 
			
		||||
 | 
			
		||||
        return HttpResponse(manifest, content_type="text/vnd.yaml")
 | 
			
		||||
@ -53,7 +58,9 @@ class KubernetesManifestView(LoginRequiredMixin, View):
 | 
			
		||||
        )
 | 
			
		||||
        manifest = ""
 | 
			
		||||
        if outpost.type == OutpostType.PROXY:
 | 
			
		||||
            controller = ProxyKubernetesController(outpost)
 | 
			
		||||
            controller = ProxyKubernetesController(
 | 
			
		||||
                outpost, KubernetesServiceConnection()
 | 
			
		||||
            )
 | 
			
		||||
            manifest = controller.get_static_deployment()
 | 
			
		||||
 | 
			
		||||
        return HttpResponse(manifest, content_type="text/vnd.yaml")
 | 
			
		||||
 | 
			
		||||
@ -12,5 +12,4 @@ class PassbookPoliciesConfig(AppConfig):
 | 
			
		||||
    verbose_name = "passbook Policies"
 | 
			
		||||
 | 
			
		||||
    def ready(self):
 | 
			
		||||
        """Load policy cache clearing signals"""
 | 
			
		||||
        import_module("passbook.policies.signals")
 | 
			
		||||
 | 
			
		||||
@ -112,7 +112,7 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
 | 
			
		||||
        return ProviderInfoView(request=self.context["request"]._request).get_info(obj)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostConfigViewSet(ModelViewSet):
 | 
			
		||||
class ProxyOutpostConfigViewSet(ModelViewSet):
 | 
			
		||||
    """ProxyProvider Viewset"""
 | 
			
		||||
 | 
			
		||||
    queryset = ProxyProvider.objects.filter(application__isnull=False)
 | 
			
		||||
 | 
			
		||||
@ -3,15 +3,15 @@ from typing import Dict
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
from passbook.outposts.controllers.docker import DockerController
 | 
			
		||||
from passbook.outposts.models import Outpost
 | 
			
		||||
from passbook.outposts.models import DockerServiceConnection, Outpost
 | 
			
		||||
from passbook.providers.proxy.models import ProxyProvider
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProxyDockerController(DockerController):
 | 
			
		||||
    """Proxy Provider Docker Contoller"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, outpost: Outpost):
 | 
			
		||||
        super().__init__(outpost)
 | 
			
		||||
    def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
 | 
			
		||||
        super().__init__(outpost, connection)
 | 
			
		||||
        self.deployment_ports = {
 | 
			
		||||
            "http": 4180,
 | 
			
		||||
            "https": 4443,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
"""Kubernetes Ingress Reconciler"""
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
from typing import TYPE_CHECKING, Dict
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
from kubernetes.client import (
 | 
			
		||||
@ -30,7 +30,7 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, controller: "KubernetesController") -> None:
 | 
			
		||||
        super().__init__(controller)
 | 
			
		||||
        self.api = NetworkingV1beta1Api()
 | 
			
		||||
        self.api = NetworkingV1beta1Api(controller.client)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def name(self) -> str:
 | 
			
		||||
@ -67,11 +67,24 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
 | 
			
		||||
        if have_hosts_tls != expected_hosts_tls:
 | 
			
		||||
            raise NeedsUpdate()
 | 
			
		||||
 | 
			
		||||
    def get_ingress_annotations(self) -> Dict[str, str]:
 | 
			
		||||
        """Get ingress annotations"""
 | 
			
		||||
        annotations = {
 | 
			
		||||
            # Ensure that with multiple proxy replicas deployed, the same CSRF request
 | 
			
		||||
            # goes to the same pod
 | 
			
		||||
            "nginx.ingress.kubernetes.io/affinity": "cookie",
 | 
			
		||||
            "traefik.ingress.kubernetes.io/affinity": "true",
 | 
			
		||||
        }
 | 
			
		||||
        annotations.update(
 | 
			
		||||
            self.controller.outpost.config.kubernetes_ingress_annotations
 | 
			
		||||
        )
 | 
			
		||||
        return dict()
 | 
			
		||||
 | 
			
		||||
    def get_reference_object(self) -> NetworkingV1beta1Ingress:
 | 
			
		||||
        """Get deployment object for outpost"""
 | 
			
		||||
        meta = self.get_object_meta(
 | 
			
		||||
            name=self.name,
 | 
			
		||||
            annotations=self.controller.outpost.config.kubernetes_ingress_annotations,
 | 
			
		||||
            annotations=self.get_ingress_annotations(),
 | 
			
		||||
        )
 | 
			
		||||
        rules = []
 | 
			
		||||
        tls_hosts = []
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,14 @@
 | 
			
		||||
"""Proxy Provider Kubernetes Contoller"""
 | 
			
		||||
from passbook.outposts.controllers.kubernetes import KubernetesController
 | 
			
		||||
from passbook.outposts.models import Outpost
 | 
			
		||||
from passbook.outposts.models import KubernetesServiceConnection, Outpost
 | 
			
		||||
from passbook.providers.proxy.controllers.k8s.ingress import IngressReconciler
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProxyKubernetesController(KubernetesController):
 | 
			
		||||
    """Proxy Provider Kubernetes Contoller"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, outpost: Outpost):
 | 
			
		||||
        super().__init__(outpost)
 | 
			
		||||
    def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
 | 
			
		||||
        super().__init__(outpost, connection)
 | 
			
		||||
        self.deployment_ports = {
 | 
			
		||||
            "http": 4180,
 | 
			
		||||
            "https": 4443,
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import yaml
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from passbook.flows.models import Flow
 | 
			
		||||
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
 | 
			
		||||
from passbook.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
 | 
			
		||||
from passbook.providers.proxy.controllers.kubernetes import ProxyKubernetesController
 | 
			
		||||
from passbook.providers.proxy.models import ProxyProvider
 | 
			
		||||
 | 
			
		||||
@ -23,15 +23,16 @@ class TestControllers(TestCase):
 | 
			
		||||
            external_host="http://localhost",
 | 
			
		||||
            authorization_flow=Flow.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        service_connection = KubernetesServiceConnection.objects.first()
 | 
			
		||||
        outpost: Outpost = Outpost.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            type=OutpostType.PROXY,
 | 
			
		||||
            deployment_type=OutpostDeploymentType.KUBERNETES,
 | 
			
		||||
            service_connection=service_connection,
 | 
			
		||||
        )
 | 
			
		||||
        outpost.providers.add(provider)
 | 
			
		||||
        outpost.save()
 | 
			
		||||
 | 
			
		||||
        controller = ProxyKubernetesController(outpost)
 | 
			
		||||
        controller = ProxyKubernetesController(outpost, service_connection)
 | 
			
		||||
        manifest = controller.get_static_deployment()
 | 
			
		||||
        self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 4)
 | 
			
		||||
 | 
			
		||||
@ -43,14 +44,15 @@ class TestControllers(TestCase):
 | 
			
		||||
            external_host="http://localhost",
 | 
			
		||||
            authorization_flow=Flow.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        service_connection = KubernetesServiceConnection.objects.first()
 | 
			
		||||
        outpost: Outpost = Outpost.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            type=OutpostType.PROXY,
 | 
			
		||||
            deployment_type=OutpostDeploymentType.KUBERNETES,
 | 
			
		||||
            service_connection=service_connection,
 | 
			
		||||
        )
 | 
			
		||||
        outpost.providers.add(provider)
 | 
			
		||||
        outpost.save()
 | 
			
		||||
 | 
			
		||||
        controller = ProxyKubernetesController(outpost)
 | 
			
		||||
        controller = ProxyKubernetesController(outpost, service_connection)
 | 
			
		||||
        controller.up()
 | 
			
		||||
        controller.down()
 | 
			
		||||
 | 
			
		||||
@ -25,6 +25,7 @@ class SAMLProviderSerializer(ModelSerializer):
 | 
			
		||||
            "signature_algorithm",
 | 
			
		||||
            "signing_kp",
 | 
			
		||||
            "require_signing",
 | 
			
		||||
            "verification_kp",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ from django.utils.translation import gettext as _
 | 
			
		||||
 | 
			
		||||
from passbook.admin.fields import CodeMirrorWidget
 | 
			
		||||
from passbook.core.expression import PropertyMappingEvaluator
 | 
			
		||||
from passbook.crypto.models import CertificateKeyPair
 | 
			
		||||
from passbook.flows.models import Flow, FlowDesignation
 | 
			
		||||
from passbook.providers.saml.models import SAMLPropertyMapping, SAMLProvider
 | 
			
		||||
 | 
			
		||||
@ -20,6 +21,9 @@ class SAMLProviderForm(forms.ModelForm):
 | 
			
		||||
            designation=FlowDesignation.AUTHORIZATION
 | 
			
		||||
        )
 | 
			
		||||
        self.fields["property_mappings"].queryset = SAMLPropertyMapping.objects.all()
 | 
			
		||||
        self.fields["signing_kp"].queryset = CertificateKeyPair.objects.exclude(
 | 
			
		||||
            key_data__iexact=""
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
@ -34,11 +38,12 @@ class SAMLProviderForm(forms.ModelForm):
 | 
			
		||||
            "assertion_valid_not_before",
 | 
			
		||||
            "assertion_valid_not_on_or_after",
 | 
			
		||||
            "session_valid_not_on_or_after",
 | 
			
		||||
            "property_mappings",
 | 
			
		||||
            "digest_algorithm",
 | 
			
		||||
            "require_signing",
 | 
			
		||||
            "signature_algorithm",
 | 
			
		||||
            "signing_kp",
 | 
			
		||||
            "verification_kp",
 | 
			
		||||
            "property_mappings",
 | 
			
		||||
        ]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput(),
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
# Generated by Django 3.1.3 on 2020-11-08 21:22
 | 
			
		||||
 | 
			
		||||
import django.db.models.deletion
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_crypto", "0002_create_self_signed_kp"),
 | 
			
		||||
        ("passbook_providers_saml", "0006_remove_samlprovider_name"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="samlprovider",
 | 
			
		||||
            name="verification_kp",
 | 
			
		||||
            field=models.ForeignKey(
 | 
			
		||||
                default=None,
 | 
			
		||||
                help_text="If selected, incoming assertion's Signatures will be validated.",
 | 
			
		||||
                null=True,
 | 
			
		||||
                on_delete=django.db.models.deletion.SET_NULL,
 | 
			
		||||
                related_name="+",
 | 
			
		||||
                to="passbook_crypto.certificatekeypair",
 | 
			
		||||
                verbose_name="Verification Keypair",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -87,6 +87,15 @@ class SAMLProvider(Provider):
 | 
			
		||||
        default="rsa-sha256",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    verification_kp = models.ForeignKey(
 | 
			
		||||
        CertificateKeyPair,
 | 
			
		||||
        default=None,
 | 
			
		||||
        null=True,
 | 
			
		||||
        help_text=_("If selected, incoming assertion's Signatures will be validated."),
 | 
			
		||||
        on_delete=models.SET_NULL,
 | 
			
		||||
        verbose_name=_("Verification Keypair"),
 | 
			
		||||
        related_name="+",
 | 
			
		||||
    )
 | 
			
		||||
    signing_kp = models.ForeignKey(
 | 
			
		||||
        CertificateKeyPair,
 | 
			
		||||
        default=None,
 | 
			
		||||
 | 
			
		||||
@ -69,10 +69,11 @@ class AuthNRequestParser:
 | 
			
		||||
        """Validate and parse raw request with enveloped signautre."""
 | 
			
		||||
        decoded_xml = decode_base64_and_inflate(saml_request)
 | 
			
		||||
 | 
			
		||||
        if self.provider.signing_kp:
 | 
			
		||||
        if self.provider.verification_kp:
 | 
			
		||||
            try:
 | 
			
		||||
                XMLVerifier().verify(
 | 
			
		||||
                    decoded_xml, x509_cert=self.provider.signing_kp.certificate_data
 | 
			
		||||
                    decoded_xml,
 | 
			
		||||
                    x509_cert=self.provider.verification_kp.certificate_data,
 | 
			
		||||
                )
 | 
			
		||||
            except InvalidSignature as exc:
 | 
			
		||||
                raise CannotHandleAssertion("Failed to verify signature") from exc
 | 
			
		||||
@ -98,7 +99,11 @@ class AuthNRequestParser:
 | 
			
		||||
                querystring += f"RelayState={quote_plus(relay_state)}&"
 | 
			
		||||
            querystring += f"SigAlg={sig_alg}"
 | 
			
		||||
 | 
			
		||||
            public_key = self.provider.signing_kp.private_key.public_key()
 | 
			
		||||
            if not self.provider.verification_kp:
 | 
			
		||||
                raise CannotHandleAssertion(
 | 
			
		||||
                    "Provider does not have a Validation Certificate configured."
 | 
			
		||||
                )
 | 
			
		||||
            public_key = self.provider.verification_kp.private_key.public_key()
 | 
			
		||||
            try:
 | 
			
		||||
                public_key.verify(
 | 
			
		||||
                    b64decode(signature),
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ class TestAuthNRequest(TestCase):
 | 
			
		||||
            ),
 | 
			
		||||
            acs_url="http://testserver/source/saml/provider/acs/",
 | 
			
		||||
            signing_kp=CertificateKeyPair.objects.first(),
 | 
			
		||||
            verification_kp=CertificateKeyPair.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        self.source = SAMLSource.objects.create(
 | 
			
		||||
            slug="provider",
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,8 @@ from typing import Any, ByteString, Dict
 | 
			
		||||
 | 
			
		||||
import django
 | 
			
		||||
from asgiref.compatibility import guarantee_single_callable
 | 
			
		||||
from channels.routing import get_default_application
 | 
			
		||||
from defusedxml import defuse_stdlib
 | 
			
		||||
from django.core.asgi import get_asgi_application
 | 
			
		||||
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
@ -129,5 +129,5 @@ class ASGILogger:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
application = ASGILogger(
 | 
			
		||||
    guarantee_single_callable(SentryAsgiMiddleware(get_default_application()))
 | 
			
		||||
    guarantee_single_callable(SentryAsgiMiddleware(get_asgi_application()))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,8 @@ from passbook.outposts.channels import OutpostConsumer
 | 
			
		||||
application = ProtocolTypeRouter(
 | 
			
		||||
    {
 | 
			
		||||
        # (http->django views is added by default)
 | 
			
		||||
        "websocket": URLRouter([path("ws/outpost/<uuid:pk>/", OutpostConsumer)]),
 | 
			
		||||
        "websocket": URLRouter(
 | 
			
		||||
            [path("ws/outpost/<uuid:pk>/", OutpostConsumer.as_asgi())]
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -322,7 +322,7 @@ if not DEBUG and _ERROR_REPORTING:
 | 
			
		||||
        ],
 | 
			
		||||
        before_send=before_send,
 | 
			
		||||
        release="passbook@%s" % __version__,
 | 
			
		||||
        traces_sample_rate=1.0,
 | 
			
		||||
        traces_sample_rate=0.6,
 | 
			
		||||
        environment=CONFIG.y("error_reporting.environment", "customer"),
 | 
			
		||||
        send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
@ -13,4 +13,5 @@ class UserLoginStageForm(forms.ModelForm):
 | 
			
		||||
        fields = ["name", "session_duration"]
 | 
			
		||||
        widgets = {
 | 
			
		||||
            "name": forms.TextInput(),
 | 
			
		||||
            "session_duration": forms.TextInput(),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
# Generated by Django 3.1.2 on 2020-10-26 20:21
 | 
			
		||||
 | 
			
		||||
from django.apps.registry import Apps
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
			
		||||
 | 
			
		||||
import passbook.lib.utils.time
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_duration(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
			
		||||
    UserLoginStage = apps.get_model("passbook_stages_user_login", "userloginstage")
 | 
			
		||||
 | 
			
		||||
    db_alias = schema_editor.connection.alias
 | 
			
		||||
 | 
			
		||||
    for stage in UserLoginStage.objects.using(db_alias).all():
 | 
			
		||||
        if stage.session_duration.isdigit():
 | 
			
		||||
            stage.session_duration = f"seconds={stage.session_duration}"
 | 
			
		||||
            stage.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("passbook_stages_user_login", "0002_userloginstage_session_duration"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="userloginstage",
 | 
			
		||||
            name="session_duration",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                default="seconds=0",
 | 
			
		||||
                help_text="Determines how long a session lasts. Default of 0 means that the sessions lasts until the browser is closed. (Format: hours=-1;minutes=-2;seconds=-3)",
 | 
			
		||||
                validators=[passbook.lib.utils.time.timedelta_string_validator],
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RunPython(update_duration),
 | 
			
		||||
    ]
 | 
			
		||||
@ -8,16 +8,19 @@ from django.views import View
 | 
			
		||||
from rest_framework.serializers import BaseSerializer
 | 
			
		||||
 | 
			
		||||
from passbook.flows.models import Stage
 | 
			
		||||
from passbook.lib.utils.time import timedelta_string_validator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserLoginStage(Stage):
 | 
			
		||||
    """Attaches the currently pending user to the current session."""
 | 
			
		||||
 | 
			
		||||
    session_duration = models.PositiveIntegerField(
 | 
			
		||||
        default=0,
 | 
			
		||||
    session_duration = models.TextField(
 | 
			
		||||
        default="seconds=0",
 | 
			
		||||
        validators=[timedelta_string_validator],
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            "Determines how long a session lasts, in seconds. Default of 0 means"
 | 
			
		||||
            " that the sessions lasts until the browser is closed."
 | 
			
		||||
            "Determines how long a session lasts. Default of 0 means "
 | 
			
		||||
            "that the sessions lasts until the browser is closed. "
 | 
			
		||||
            "(Format: hours=-1;minutes=-2;seconds=-3)"
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
 | 
			
		||||
from passbook.flows.stage import StageView
 | 
			
		||||
from passbook.lib.utils.time import timedelta_from_string
 | 
			
		||||
from passbook.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
@ -32,7 +33,11 @@ class UserLoginStageView(StageView):
 | 
			
		||||
            self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
 | 
			
		||||
            backend=backend,
 | 
			
		||||
        )
 | 
			
		||||
        self.request.session.set_expiry(self.executor.current_stage.session_duration)
 | 
			
		||||
        delta = timedelta_from_string(self.executor.current_stage.session_duration)
 | 
			
		||||
        if delta.seconds == 0:
 | 
			
		||||
            self.request.session.set_expiry(0)
 | 
			
		||||
        else:
 | 
			
		||||
            self.request.session.set_expiry(delta)
 | 
			
		||||
        LOGGER.debug(
 | 
			
		||||
            "Logged in",
 | 
			
		||||
            user=self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
 | 
			
		||||
 | 
			
		||||
@ -105,5 +105,7 @@ class TestUserLoginStage(TestCase):
 | 
			
		||||
 | 
			
		||||
    def test_form(self):
 | 
			
		||||
        """Test Form"""
 | 
			
		||||
        data = {"name": "test", "session_duration": 0}
 | 
			
		||||
        data = {"name": "test", "session_duration": "seconds=0"}
 | 
			
		||||
        self.assertEqual(UserLoginStageForm(data).is_valid(), True)
 | 
			
		||||
        data = {"name": "test", "session_duration": "123"}
 | 
			
		||||
        self.assertEqual(UserLoginStageForm(data).is_valid(), False)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										18
									
								
								passbook/static/static/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								passbook/static/static/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -34,9 +34,9 @@
 | 
			
		||||
      "integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "@patternfly/patternfly": {
 | 
			
		||||
      "version": "4.50.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.50.4.tgz",
 | 
			
		||||
      "integrity": "sha512-eoJ/U11m+1uJMt8HTFCJeUNazoHC58Ot6gzfNnJvbX5kibpDdvrMvLk2iuGhEfwzQmiH7BSrxjZqMyevbSZ2Cw=="
 | 
			
		||||
      "version": "4.59.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.59.1.tgz",
 | 
			
		||||
      "integrity": "sha512-zk3aqg62JXMTzzJMJsyVgt5fXlcxUUkRKkaxUv/hwpjhGiyLexZ1l3Gupb9ziYl74p38KzbbfcfdnlFCwJZfgg=="
 | 
			
		||||
    },
 | 
			
		||||
    "@rollup/pluginutils": {
 | 
			
		||||
      "version": "3.1.0",
 | 
			
		||||
@ -203,9 +203,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "codemirror": {
 | 
			
		||||
      "version": "5.58.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.1.tgz",
 | 
			
		||||
      "integrity": "sha512-UGb/ueu20U4xqWk8hZB3xIfV2/SFqnSLYONiM3wTMDqko0bsYrsAkGGhqUzbRkYm89aBKPyHtuNEbVWF9FTFzw=="
 | 
			
		||||
      "version": "5.58.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.58.2.tgz",
 | 
			
		||||
      "integrity": "sha512-K/hOh24cCwRutd1Mk3uLtjWzNISOkm4fvXiMO7LucCrqbh6aJDdtqUziim3MZUI6wOY0rvY1SlL1Ork01uMy6w=="
 | 
			
		||||
    },
 | 
			
		||||
    "color-convert": {
 | 
			
		||||
      "version": "1.9.3",
 | 
			
		||||
@ -442,9 +442,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "rollup": {
 | 
			
		||||
      "version": "2.32.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.32.1.tgz",
 | 
			
		||||
      "integrity": "sha512-Op2vWTpvK7t6/Qnm1TTh7VjEZZkN8RWgf0DHbkKzQBwNf748YhXbozHVefqpPp/Fuyk/PQPAnYsBxAEtlMvpUw==",
 | 
			
		||||
      "version": "2.33.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.33.1.tgz",
 | 
			
		||||
      "integrity": "sha512-uY4O/IoL9oNW8MMcbA5hcOaz6tZTMIh7qJHx/tzIJm+n1wLoY38BLn6fuy7DhR57oNFLMbDQtDeJoFURt5933w==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "fsevents": "~2.1.2"
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -6,12 +6,12 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@fortawesome/fontawesome-free": "^5.15.1",
 | 
			
		||||
    "@patternfly/patternfly": "^4.50.4",
 | 
			
		||||
    "@patternfly/patternfly": "^4.59.1",
 | 
			
		||||
    "chart.js": "^2.9.4",
 | 
			
		||||
    "codemirror": "^5.58.1",
 | 
			
		||||
    "codemirror": "^5.58.2",
 | 
			
		||||
    "lit-element": "^2.4.0",
 | 
			
		||||
    "lit-html": "^1.3.0",
 | 
			
		||||
    "rollup": "^2.32.1"
 | 
			
		||||
    "rollup": "^2.33.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "rollup-plugin-commonjs": "^10.1.0",
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1,7 +1,7 @@
 | 
			
		||||
import { LitElement, html } from 'lit-element';
 | 
			
		||||
import { updateMessages } from "./Messages.js";
 | 
			
		||||
 | 
			
		||||
class FetchFillSlot extends LitElement {
 | 
			
		||||
class FlowShellCard extends LitElement {
 | 
			
		||||
 | 
			
		||||
    static get properties() {
 | 
			
		||||
        return {
 | 
			
		||||
@ -15,7 +15,19 @@ class FetchFillSlot extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    firstUpdated() {
 | 
			
		||||
        fetch(this.flowBodyUrl).then(r => r.json()).then(r => this.updateCard(r));
 | 
			
		||||
        fetch(this.flowBodyUrl).then(r => {
 | 
			
		||||
            if (!r.ok) {
 | 
			
		||||
                throw Error(r.statusText);
 | 
			
		||||
            }
 | 
			
		||||
            return r;
 | 
			
		||||
        }).then((r) => {
 | 
			
		||||
            return r.json()
 | 
			
		||||
        }).then((r) => {
 | 
			
		||||
            this.updateCard(r)
 | 
			
		||||
        }).catch((e) => {
 | 
			
		||||
            // Catch JSON or Update errors
 | 
			
		||||
            this.errorMessage(e);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async updateCard(data) {
 | 
			
		||||
@ -83,14 +95,39 @@ class FetchFillSlot extends LitElement {
 | 
			
		||||
                fetch(this.flowBodyUrl, {
 | 
			
		||||
                    method: 'post',
 | 
			
		||||
                    body: formData,
 | 
			
		||||
                }).then(response => response.json()).then(data => {
 | 
			
		||||
                }).then((response) => {
 | 
			
		||||
                    return response.json()
 | 
			
		||||
                }).then(data => {
 | 
			
		||||
                    this.updateCard(data);
 | 
			
		||||
                }).catch((e) => {
 | 
			
		||||
                    this.errorMessage(e);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
            form.classList.add("pb-flow-wrapped");
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    errorMessage(error) {
 | 
			
		||||
        this.flowBody = `
 | 
			
		||||
            <style>
 | 
			
		||||
                .pb-exception {
 | 
			
		||||
                    font-family: monospace;
 | 
			
		||||
                    overflow-x: scroll;
 | 
			
		||||
                }
 | 
			
		||||
            </style>
 | 
			
		||||
            <header class="pf-c-login__main-header">
 | 
			
		||||
                <h1 class="pf-c-title pf-m-3xl">
 | 
			
		||||
                    Whoops!
 | 
			
		||||
                </h1>
 | 
			
		||||
            </header>
 | 
			
		||||
            <div class="pf-c-login__main-body">
 | 
			
		||||
                <h3>
 | 
			
		||||
                    Something went wrong! Please try again later.
 | 
			
		||||
                </h3>
 | 
			
		||||
                <pre class="pb-exception">${error}</pre>
 | 
			
		||||
            </div>`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    loading() {
 | 
			
		||||
        return html`
 | 
			
		||||
            <div class="pf-c-login__main-body pb-loading">
 | 
			
		||||
@ -110,4 +147,4 @@ class FetchFillSlot extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define('flow-shell-card', FetchFillSlot);
 | 
			
		||||
customElements.define('flow-shell-card', FlowShellCard);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										55
									
								
								passbook/static/static/src/Tabs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								passbook/static/static/src/Tabs.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
			
		||||
import { LitElement, html } from 'lit-element';
 | 
			
		||||
 | 
			
		||||
class Tabs extends LitElement {
 | 
			
		||||
 | 
			
		||||
    _currentPage = "";
 | 
			
		||||
    _firstPage = "";
 | 
			
		||||
 | 
			
		||||
    get currentPage() {
 | 
			
		||||
        return this._currentPage
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    set currentPage(value) {
 | 
			
		||||
        try {
 | 
			
		||||
            // Show active tab page
 | 
			
		||||
            this.querySelector(`.pf-c-tab-content[tab-name='${value}']`).removeAttribute("hidden");
 | 
			
		||||
            // Update active status on buttons
 | 
			
		||||
            this.querySelector(`.pf-c-tabs__item[tab-name='${value}']`).classList.add("pf-m-current");
 | 
			
		||||
            // Hide other tab pages
 | 
			
		||||
            this.querySelectorAll(`.pf-c-tab-content:not([tab-name='${value}'])`).forEach((el) => {
 | 
			
		||||
                el.setAttribute("hidden", "");
 | 
			
		||||
            });
 | 
			
		||||
            // Update active status on other buttons
 | 
			
		||||
            this.querySelectorAll(`.pf-c-tabs__item:not([tab-name='${value}'])`).forEach((el) => {
 | 
			
		||||
                el.classList.remove("pf-m-current");
 | 
			
		||||
            });
 | 
			
		||||
            // Update window hash
 | 
			
		||||
            window.location.hash = value;
 | 
			
		||||
            this._currentPage = value;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            this.currentPage = this._firstPage;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createRenderRoot() {
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    firstUpdated() {
 | 
			
		||||
        this._firstPage = this.querySelector(".pf-c-tab-content").getAttribute("tab-name");
 | 
			
		||||
        if (window.location.hash) {
 | 
			
		||||
            this.currentPage = window.location.hash;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.currentPage = this._firstPage;
 | 
			
		||||
        }
 | 
			
		||||
        this.querySelectorAll(".pf-c-tabs__item > button").forEach((button) => {
 | 
			
		||||
            button.addEventListener("click", (e) => {
 | 
			
		||||
                let tabPage = button.parentElement.getAttribute("tab-name");
 | 
			
		||||
                this.currentPage = tabPage;
 | 
			
		||||
            })
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define('pb-tabs', Tabs);
 | 
			
		||||
@ -2,6 +2,7 @@ import './FetchFillSlot.js';
 | 
			
		||||
import './ActionButton.js';
 | 
			
		||||
import './Messages.js';
 | 
			
		||||
import './FlowShellCard.js';
 | 
			
		||||
import './Tabs.js';
 | 
			
		||||
 | 
			
		||||
// Button Dropdowns
 | 
			
		||||
document.querySelectorAll("button.pf-c-dropdown__toggle").forEach((b) => {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								proxy/go.mod
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								proxy/go.mod
									
									
									
									
									
								
							@ -7,12 +7,13 @@ require (
 | 
			
		||||
	github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect
 | 
			
		||||
	github.com/coreos/go-oidc v2.2.1+incompatible
 | 
			
		||||
	github.com/getsentry/sentry-go v0.7.0
 | 
			
		||||
	github.com/go-openapi/errors v0.19.7
 | 
			
		||||
	github.com/go-openapi/runtime v0.19.22
 | 
			
		||||
	github.com/go-openapi/spec v0.19.9 // indirect
 | 
			
		||||
	github.com/go-openapi/strfmt v0.19.5
 | 
			
		||||
	github.com/go-openapi/swag v0.19.9
 | 
			
		||||
	github.com/go-openapi/validate v0.19.11
 | 
			
		||||
	github.com/go-openapi/analysis v0.19.11 // indirect
 | 
			
		||||
	github.com/go-openapi/errors v0.19.8
 | 
			
		||||
	github.com/go-openapi/runtime v0.19.23
 | 
			
		||||
	github.com/go-openapi/spec v0.19.12 // indirect
 | 
			
		||||
	github.com/go-openapi/strfmt v0.19.8
 | 
			
		||||
	github.com/go-openapi/swag v0.19.11
 | 
			
		||||
	github.com/go-openapi/validate v0.19.12
 | 
			
		||||
	github.com/go-redis/redis/v7 v7.4.0 // indirect
 | 
			
		||||
	github.com/go-swagger/go-swagger v0.25.0 // indirect
 | 
			
		||||
	github.com/gorilla/handlers v1.5.1 // indirect
 | 
			
		||||
@ -28,17 +29,16 @@ require (
 | 
			
		||||
	github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect
 | 
			
		||||
	github.com/recws-org/recws v1.2.1
 | 
			
		||||
	github.com/sirupsen/logrus v1.7.0
 | 
			
		||||
	github.com/spf13/afero v1.4.0 // indirect
 | 
			
		||||
	github.com/spf13/afero v1.4.1 // indirect
 | 
			
		||||
	github.com/spf13/cast v1.3.1 // indirect
 | 
			
		||||
	github.com/spf13/jwalterweatherman v1.1.0 // indirect
 | 
			
		||||
	github.com/spf13/pflag v1.0.5 // indirect
 | 
			
		||||
	github.com/spf13/viper v1.7.1 // indirect
 | 
			
		||||
	github.com/stretchr/testify v1.6.1
 | 
			
		||||
	go.mongodb.org/mongo-driver v1.4.1 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de // indirect
 | 
			
		||||
	golang.org/x/net v0.0.0-20200927032502-5d4f70055728 // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c // indirect
 | 
			
		||||
	golang.org/x/tools v0.0.0-20200929223013-bf155c11ec6f // indirect
 | 
			
		||||
	gopkg.in/ini.v1 v1.61.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect
 | 
			
		||||
	golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 // indirect
 | 
			
		||||
	golang.org/x/text v0.3.4 // indirect
 | 
			
		||||
	golang.org/x/tools v0.0.0-20201102043006-b53d4cbd60a6 // indirect
 | 
			
		||||
	gopkg.in/ini.v1 v1.62.0 // indirect
 | 
			
		||||
	gopkg.in/square/go-jose.v2 v2.5.1 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										62
									
								
								proxy/go.sum
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								proxy/go.sum
									
									
									
									
									
								
							@ -76,7 +76,7 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0
 | 
			
		||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
 | 
			
		||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg=
 | 
			
		||||
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
 | 
			
		||||
github.com/aws/aws-sdk-go v1.29.15/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg=
 | 
			
		||||
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
 | 
			
		||||
github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
 | 
			
		||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 | 
			
		||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
 | 
			
		||||
@ -162,6 +162,8 @@ github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9sn
 | 
			
		||||
github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU=
 | 
			
		||||
github.com/go-openapi/analysis v0.19.10 h1:5BHISBAXOc/aJK25irLZnx2D3s6WyYaY9D4gmuz9fdE=
 | 
			
		||||
github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ=
 | 
			
		||||
github.com/go-openapi/analysis v0.19.11 h1:IWit8yDJzyjkN9vSH3KHjAIrRY25zJsLA4uqHjC9PgI=
 | 
			
		||||
github.com/go-openapi/analysis v0.19.11/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ=
 | 
			
		||||
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
 | 
			
		||||
github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
 | 
			
		||||
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
 | 
			
		||||
@ -170,6 +172,8 @@ github.com/go-openapi/errors v0.19.6 h1:xZMThgv5SQ7SMbWtKFkCf9bBdvR2iEyw9k3zGZON
 | 
			
		||||
github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
 | 
			
		||||
github.com/go-openapi/errors v0.19.7 h1:Lcq+o0mSwCLKACMxZhreVHigB9ebghJ/lrmeaqASbjo=
 | 
			
		||||
github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
 | 
			
		||||
github.com/go-openapi/errors v0.19.8 h1:doM+tQdZbUm9gydV9yR+iQNmztbjj7I3sW4sIcAwIzc=
 | 
			
		||||
github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
 | 
			
		||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
 | 
			
		||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
 | 
			
		||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
 | 
			
		||||
@ -197,8 +201,8 @@ github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29g
 | 
			
		||||
github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo=
 | 
			
		||||
github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98=
 | 
			
		||||
github.com/go-openapi/runtime v0.19.20/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
 | 
			
		||||
github.com/go-openapi/runtime v0.19.22 h1:vtT7gJwxIK96BVTd9Ce5OPNQfIsk+q1j/+0e98NoVXk=
 | 
			
		||||
github.com/go-openapi/runtime v0.19.22/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
 | 
			
		||||
github.com/go-openapi/runtime v0.19.23 h1:/SchJDysK/m6nDeP6pO0Et9OZ7QRsNjNqVKBInbr95M=
 | 
			
		||||
github.com/go-openapi/runtime v0.19.23/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
 | 
			
		||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
 | 
			
		||||
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
 | 
			
		||||
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
 | 
			
		||||
@ -206,8 +210,8 @@ github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8
 | 
			
		||||
github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk=
 | 
			
		||||
github.com/go-openapi/spec v0.19.8 h1:qAdZLh1r6QF/hI/gTq+TJTvsQUodZsM7KLqkAJdiJNg=
 | 
			
		||||
github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk=
 | 
			
		||||
github.com/go-openapi/spec v0.19.9 h1:9z9cbFuZJ7AcvOHKIY+f6Aevb4vObNDkTEyoMfO7rAc=
 | 
			
		||||
github.com/go-openapi/spec v0.19.9/go.mod h1:vqK/dIdLGCosfvYsQV3WfC7N3TiZSnGY2RZKoFK7X28=
 | 
			
		||||
github.com/go-openapi/spec v0.19.12 h1:OO9WrvhDwtiMY/Opr1j1iFZzirI3JW4/bxNFRcntAr4=
 | 
			
		||||
github.com/go-openapi/spec v0.19.12/go.mod h1:gwrgJS15eCUgjLpMjBJmbZezCsw88LmgeEip0M63doA=
 | 
			
		||||
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
 | 
			
		||||
github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
 | 
			
		||||
github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
 | 
			
		||||
@ -216,6 +220,8 @@ github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6
 | 
			
		||||
github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
 | 
			
		||||
github.com/go-openapi/strfmt v0.19.5 h1:0utjKrw+BAh8s57XE9Xz8DUBsVvPmRUB6styvl9wWIM=
 | 
			
		||||
github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
 | 
			
		||||
github.com/go-openapi/strfmt v0.19.8 h1:9wAdSoImc5UCnUj79GhcjkJXxuU/nEwlbe6SKe9ZdRs=
 | 
			
		||||
github.com/go-openapi/strfmt v0.19.8/go.mod h1:qBBipho+3EoIqn6YDI+4RnQEtj6jT/IdKm+PAlXxSUc=
 | 
			
		||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
 | 
			
		||||
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
 | 
			
		||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
 | 
			
		||||
@ -223,13 +229,15 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
 | 
			
		||||
github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY=
 | 
			
		||||
github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE=
 | 
			
		||||
github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY=
 | 
			
		||||
github.com/go-openapi/swag v0.19.11 h1:RFTu/dlFySpyVvJDfp/7674JY4SDglYWKztbiIGFpmc=
 | 
			
		||||
github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY=
 | 
			
		||||
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
 | 
			
		||||
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
 | 
			
		||||
github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo=
 | 
			
		||||
github.com/go-openapi/validate v0.19.10 h1:tG3SZ5DC5KF4cyt7nqLVcQXGj5A7mpaYkAcNPlDK+Yk=
 | 
			
		||||
github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8=
 | 
			
		||||
github.com/go-openapi/validate v0.19.11 h1:8lCr0b9lNWKjVjW/hSZZvltUy+bULl7vbnCTsOzlhPo=
 | 
			
		||||
github.com/go-openapi/validate v0.19.11/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4=
 | 
			
		||||
github.com/go-openapi/validate v0.19.12 h1:mPLM/bfbd00PGOCJlU0yJL7IulkZ+q9VjPv7U11RMQQ=
 | 
			
		||||
github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4=
 | 
			
		||||
github.com/go-redis/redis/v7 v7.2.0 h1:CrCexy/jYWZjW0AyVoHlcJUeZN19VWlbepTh1Vq6dJs=
 | 
			
		||||
github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
 | 
			
		||||
github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
 | 
			
		||||
@ -317,6 +325,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 | 
			
		||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
 | 
			
		||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
 | 
			
		||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 | 
			
		||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 | 
			
		||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 | 
			
		||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 | 
			
		||||
@ -384,7 +394,8 @@ github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGAR
 | 
			
		||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 | 
			
		||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a h1:zPPuIq2jAWWPTrGt70eK/BSch+gFAGrNzecsoENgu2o=
 | 
			
		||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 | 
			
		||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 | 
			
		||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 | 
			
		||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 | 
			
		||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 | 
			
		||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 | 
			
		||||
@ -511,6 +522,7 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP
 | 
			
		||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 | 
			
		||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 | 
			
		||||
github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
 | 
			
		||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
 | 
			
		||||
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
 | 
			
		||||
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
 | 
			
		||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 | 
			
		||||
@ -569,8 +581,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO
 | 
			
		||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
 | 
			
		||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 | 
			
		||||
github.com/spf13/afero v1.3.2/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
 | 
			
		||||
github.com/spf13/afero v1.4.0 h1:jsLTaI1zwYO3vjrzHalkVcIHXTNmdQFepW4OI8H3+x8=
 | 
			
		||||
github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 | 
			
		||||
github.com/spf13/afero v1.4.1 h1:asw9sl74539yqavKaglDM5hFpdJVK0Y5Dr/JOgQ89nQ=
 | 
			
		||||
github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 | 
			
		||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
 | 
			
		||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 | 
			
		||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
 | 
			
		||||
@ -650,8 +662,8 @@ go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS
 | 
			
		||||
go.mongodb.org/mongo-driver v1.3.4 h1:zs/dKNwX0gYUtzwrN9lLiR15hCO0nDwQj5xXx+vjCdE=
 | 
			
		||||
go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE=
 | 
			
		||||
go.mongodb.org/mongo-driver v1.3.5/go.mod h1:Ual6Gkco7ZGQw8wE1t4tLnvBsf6yVSM60qW6TgOeJ5c=
 | 
			
		||||
go.mongodb.org/mongo-driver v1.4.1 h1:38NSAyDPagwnFpUA/D5SFgbugUYR3NzYRNa4Qk9UxKs=
 | 
			
		||||
go.mongodb.org/mongo-driver v1.4.1/go.mod h1:llVBH2pkj9HywK0Dtdt6lDikOjFLbceHVu/Rc0iMKLs=
 | 
			
		||||
go.mongodb.org/mongo-driver v1.4.2 h1:WlnEglfTg/PfPq4WXs2Vkl/5ICC6hoG8+r+LraPmGk4=
 | 
			
		||||
go.mongodb.org/mongo-driver v1.4.2/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
 | 
			
		||||
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
 | 
			
		||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 | 
			
		||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 | 
			
		||||
@ -758,11 +770,9 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
 | 
			
		||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 | 
			
		||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc h1:zK/HqS5bZxDptfPJNq8v7vJfXtkU7r9TLIoSr1bXaP4=
 | 
			
		||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 | 
			
		||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 | 
			
		||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
 | 
			
		||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 | 
			
		||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728 h1:5wtQIAulKU5AbLQOkjxl32UufnIOqgBX72pS0AV14H0=
 | 
			
		||||
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 | 
			
		||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
			
		||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw=
 | 
			
		||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 | 
			
		||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
 | 
			
		||||
@ -779,6 +789,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
 | 
			
		||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
@ -831,10 +842,9 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
 | 
			
		||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860 h1:YEu4SMq7D0cmT7CBbXfcH0NZeuChAXwsHe/9XueUO6o=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c h1:/h0vtH0PyU0xAoZJVcRw1k0Ng+U0JAy3QDiFmppIlIE=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
@ -842,6 +852,8 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 | 
			
		||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
			
		||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
 | 
			
		||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 | 
			
		||||
@ -901,8 +913,8 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
 | 
			
		||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 | 
			
		||||
golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f h1:33yHANSyO/TeglgY9rBhUpX43wtonTXoFOsMRtNB6qE=
 | 
			
		||||
golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
 | 
			
		||||
golang.org/x/tools v0.0.0-20200929223013-bf155c11ec6f h1:7+Nz9MyPqt2qMCTvNiRy1G0zYfkB7UCa+ayT6uVvbyI=
 | 
			
		||||
golang.org/x/tools v0.0.0-20200929223013-bf155c11ec6f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
 | 
			
		||||
golang.org/x/tools v0.0.0-20201102043006-b53d4cbd60a6 h1:vTr2e3iWbC27MMR83IzSZeEKQSzJAhwDM+Ld5YSaHA0=
 | 
			
		||||
golang.org/x/tools v0.0.0-20201102043006-b53d4cbd60a6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
			
		||||
@ -1011,8 +1023,8 @@ gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/R
 | 
			
		||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
 | 
			
		||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10=
 | 
			
		||||
gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
 | 
			
		||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
 | 
			
		||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
 | 
			
		||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ import (
 | 
			
		||||
 | 
			
		||||
type Claims struct {
 | 
			
		||||
	Proxy struct {
 | 
			
		||||
		UserAttributes map[string]string `json:"user_attributes"`
 | 
			
		||||
		UserAttributes map[string]interface{} `json:"user_attributes"`
 | 
			
		||||
	} `json:"pb_proxy"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										68
									
								
								proxy/pkg/proxy/cookies.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								proxy/pkg/proxy/cookies.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
package proxy
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// MakeCSRFCookie creates a cookie for CSRF
 | 
			
		||||
func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
 | 
			
		||||
	return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
 | 
			
		||||
	cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
 | 
			
		||||
 | 
			
		||||
	if cookieDomain != "" {
 | 
			
		||||
		domain := cookies.GetRequestHost(req)
 | 
			
		||||
		if h, _, err := net.SplitHostPort(domain); err == nil {
 | 
			
		||||
			domain = h
 | 
			
		||||
		}
 | 
			
		||||
		if !strings.HasSuffix(domain, cookieDomain) {
 | 
			
		||||
			p.logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &http.Cookie{
 | 
			
		||||
		Name:     name,
 | 
			
		||||
		Value:    value,
 | 
			
		||||
		Path:     p.CookiePath,
 | 
			
		||||
		Domain:   cookieDomain,
 | 
			
		||||
		HttpOnly: p.CookieHTTPOnly,
 | 
			
		||||
		Secure:   p.CookieSecure,
 | 
			
		||||
		Expires:  now.Add(expiration),
 | 
			
		||||
		SameSite: cookies.ParseSameSite(p.CookieSameSite),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
 | 
			
		||||
// session
 | 
			
		||||
func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetCSRFCookie adds a CSRF cookie to the response
 | 
			
		||||
func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
 | 
			
		||||
	http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClearSessionCookie creates a cookie to unset the user's authentication cookie
 | 
			
		||||
// stored in the user's session
 | 
			
		||||
func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
 | 
			
		||||
	return p.sessionStore.Clear(rw, req)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadCookiedSession reads the user's authentication details from the request
 | 
			
		||||
func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
 | 
			
		||||
	return p.sessionStore.Load(req)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveSession creates a new session cookie value and sets this on the response
 | 
			
		||||
func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
 | 
			
		||||
	return p.sessionStore.Save(rw, req, s)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										233
									
								
								proxy/pkg/proxy/oauth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								proxy/pkg/proxy/oauth.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,233 @@
 | 
			
		||||
package proxy
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
 | 
			
		||||
// redirect clients to once authenticated
 | 
			
		||||
func (p *OAuthProxy) GetRedirectURI(host string) string {
 | 
			
		||||
	// default to the request Host if not set
 | 
			
		||||
	if p.redirectURL.Host != "" {
 | 
			
		||||
		return p.redirectURL.String()
 | 
			
		||||
	}
 | 
			
		||||
	u := *p.redirectURL
 | 
			
		||||
	if u.Scheme == "" {
 | 
			
		||||
		if p.CookieSecure {
 | 
			
		||||
			u.Scheme = httpsScheme
 | 
			
		||||
		} else {
 | 
			
		||||
			u.Scheme = httpScheme
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	u.Host = host
 | 
			
		||||
	return u.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) {
 | 
			
		||||
	if code == "" {
 | 
			
		||||
		return nil, errors.New("missing code")
 | 
			
		||||
	}
 | 
			
		||||
	redirectURI := p.GetRedirectURI(host)
 | 
			
		||||
	s, err = p.provider.Redeem(ctx, redirectURI, code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.Email == "" {
 | 
			
		||||
		s.Email, err = p.provider.GetEmailAddress(ctx, s)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.PreferredUsername == "" {
 | 
			
		||||
		s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s)
 | 
			
		||||
		if err != nil && err.Error() == "not implemented" {
 | 
			
		||||
			err = nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.User == "" {
 | 
			
		||||
		s.User, err = p.provider.GetUserName(ctx, s)
 | 
			
		||||
		if err != nil && err.Error() == "not implemented" {
 | 
			
		||||
			err = nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRedirect reads the query parameter to get the URL to redirect clients to
 | 
			
		||||
// once authenticated with the OAuthProxy
 | 
			
		||||
func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
 | 
			
		||||
	err = req.ParseForm()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	redirect = req.Header.Get("X-Auth-Request-Redirect")
 | 
			
		||||
	if req.Form.Get("rd") != "" {
 | 
			
		||||
		redirect = req.Form.Get("rd")
 | 
			
		||||
	}
 | 
			
		||||
	if !p.IsValidRedirect(redirect) {
 | 
			
		||||
		// Use RequestURI to preserve ?query
 | 
			
		||||
		redirect = req.URL.RequestURI()
 | 
			
		||||
		if strings.HasPrefix(redirect, p.ProxyPrefix) {
 | 
			
		||||
			redirect = "/"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsValidRedirect checks whether the redirect URL is whitelisted
 | 
			
		||||
func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
 | 
			
		||||
	switch {
 | 
			
		||||
	case redirect == "":
 | 
			
		||||
		// The user didn't specify a redirect, should fallback to `/`
 | 
			
		||||
		return false
 | 
			
		||||
	case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
 | 
			
		||||
		return true
 | 
			
		||||
	case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
 | 
			
		||||
		redirectURL, err := url.Parse(redirect)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			p.logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
		redirectHostname := redirectURL.Hostname()
 | 
			
		||||
 | 
			
		||||
		for _, domain := range p.whitelistDomains {
 | 
			
		||||
			domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
 | 
			
		||||
			if domainHostname == "" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
 | 
			
		||||
				// the domain names match, now validate the ports
 | 
			
		||||
				// if the whitelisted domain's port is '*', allow all ports
 | 
			
		||||
				// if the whitelisted domain contains a specific port, only allow that port
 | 
			
		||||
				// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
 | 
			
		||||
				redirectPort := redirectURL.Port()
 | 
			
		||||
				if (domainPort == "*") ||
 | 
			
		||||
					(domainPort == redirectPort) ||
 | 
			
		||||
					(domainPort == "" && redirectPort == "") {
 | 
			
		||||
					return true
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		p.logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
 | 
			
		||||
		return false
 | 
			
		||||
	default:
 | 
			
		||||
		p.logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsWhitelistedRequest is used to check if auth should be skipped for this request
 | 
			
		||||
func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
 | 
			
		||||
	isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
 | 
			
		||||
	return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsWhitelistedPath is used to check if the request path is allowed without auth
 | 
			
		||||
func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
 | 
			
		||||
	for _, u := range p.compiledRegex {
 | 
			
		||||
		if u.MatchString(path) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OAuthStart starts the OAuth2 authentication flow
 | 
			
		||||
func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	prepareNoCache(rw)
 | 
			
		||||
	nonce, err := encryption.Nonce()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error obtaining nonce: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	p.SetCSRFCookie(rw, req, nonce)
 | 
			
		||||
	redirect, err := p.GetRedirect(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error obtaining redirect: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	redirectURI := p.GetRedirectURI(req.Host)
 | 
			
		||||
	http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OAuthCallback is the OAuth2 authentication flow callback that finishes the
 | 
			
		||||
// OAuth2 authentication flow
 | 
			
		||||
func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
 | 
			
		||||
 | 
			
		||||
	// finish the oauth cycle
 | 
			
		||||
	err := req.ParseForm()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error while parsing OAuth2 callback: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	errorString := req.Form.Get("error")
 | 
			
		||||
	if errorString != "" {
 | 
			
		||||
		p.logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s := strings.SplitN(req.Form.Get("state"), ":", 2)
 | 
			
		||||
	if len(s) != 2 {
 | 
			
		||||
		p.logger.Error("Error while parsing OAuth2 state: invalid length")
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	nonce := s[0]
 | 
			
		||||
	redirect := s[1]
 | 
			
		||||
	c, err := req.Cookie(p.CSRFCookieName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Info("Invalid authentication via OAuth2: unable to obtain CSRF cookie")
 | 
			
		||||
		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	p.ClearCSRFCookie(rw, req)
 | 
			
		||||
	if c.Value != nonce {
 | 
			
		||||
		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Info("Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
 | 
			
		||||
		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !p.IsValidRedirect(redirect) {
 | 
			
		||||
		redirect = "/"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// set cookie, or deny
 | 
			
		||||
	if p.provider.ValidateGroup(session.Email) {
 | 
			
		||||
		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Infof("Authenticated via OAuth2: %s", session)
 | 
			
		||||
		err := p.SaveSession(rw, req, session)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			p.logger.Printf("Error saving session state for %s: %v", remoteAddr, err)
 | 
			
		||||
			p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		http.Redirect(rw, req, redirect, http.StatusFound)
 | 
			
		||||
	} else {
 | 
			
		||||
		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Info("Invalid authentication via OAuth2: unauthorized")
 | 
			
		||||
		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,986 +0,0 @@
 | 
			
		||||
package proxy
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	b64 "encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/coreos/go-oidc"
 | 
			
		||||
	"github.com/justinas/alice"
 | 
			
		||||
	ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
 | 
			
		||||
	middlewareapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/middleware"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
 | 
			
		||||
	sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/authentication/basic"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/cookies"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/ip"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/middleware"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/upstream"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/providers"
 | 
			
		||||
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	httpScheme  = "http"
 | 
			
		||||
	httpsScheme = "https"
 | 
			
		||||
 | 
			
		||||
	applicationJSON = "application/json"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// ErrNeedsLogin means the user should be redirected to the login page
 | 
			
		||||
	ErrNeedsLogin = errors.New("redirect to login page")
 | 
			
		||||
 | 
			
		||||
	// Used to check final redirects are not susceptible to open redirects.
 | 
			
		||||
	// Matches //, /\ and both of these with whitespace in between (eg / / or / \).
 | 
			
		||||
	invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// OAuthProxy is the main authentication proxy
 | 
			
		||||
type OAuthProxy struct {
 | 
			
		||||
	CookieSeed     string
 | 
			
		||||
	CookieName     string
 | 
			
		||||
	CSRFCookieName string
 | 
			
		||||
	CookieDomains  []string
 | 
			
		||||
	CookiePath     string
 | 
			
		||||
	CookieSecure   bool
 | 
			
		||||
	CookieHTTPOnly bool
 | 
			
		||||
	CookieExpire   time.Duration
 | 
			
		||||
	CookieRefresh  time.Duration
 | 
			
		||||
	CookieSameSite string
 | 
			
		||||
 | 
			
		||||
	RobotsPath        string
 | 
			
		||||
	SignInPath        string
 | 
			
		||||
	SignOutPath       string
 | 
			
		||||
	OAuthStartPath    string
 | 
			
		||||
	OAuthCallbackPath string
 | 
			
		||||
	AuthOnlyPath      string
 | 
			
		||||
	UserInfoPath      string
 | 
			
		||||
 | 
			
		||||
	redirectURL                *url.URL // the url to receive requests at
 | 
			
		||||
	whitelistDomains           []string
 | 
			
		||||
	provider                   providers.Provider
 | 
			
		||||
	providerNameOverride       string
 | 
			
		||||
	sessionStore               sessionsapi.SessionStore
 | 
			
		||||
	ProxyPrefix                string
 | 
			
		||||
	SignInMessage              string
 | 
			
		||||
	basicAuthValidator         basic.Validator
 | 
			
		||||
	displayHtpasswdForm        bool
 | 
			
		||||
	serveMux                   http.Handler
 | 
			
		||||
	SetXAuthRequest            bool
 | 
			
		||||
	PassBasicAuth              bool
 | 
			
		||||
	SetBasicAuth               bool
 | 
			
		||||
	SkipProviderButton         bool
 | 
			
		||||
	PassUserHeaders            bool
 | 
			
		||||
	BasicAuthUserAttribute     string
 | 
			
		||||
	BasicAuthPasswordAttribute string
 | 
			
		||||
	PassAccessToken            bool
 | 
			
		||||
	SetAuthorization           bool
 | 
			
		||||
	PassAuthorization          bool
 | 
			
		||||
	PreferEmailToUser          bool
 | 
			
		||||
	skipAuthRegex              []string
 | 
			
		||||
	skipAuthPreflight          bool
 | 
			
		||||
	skipAuthStripHeaders       bool
 | 
			
		||||
	skipJwtBearerTokens        bool
 | 
			
		||||
	mainJwtBearerVerifier      *oidc.IDTokenVerifier
 | 
			
		||||
	extraJwtBearerVerifiers    []*oidc.IDTokenVerifier
 | 
			
		||||
	compiledRegex              []*regexp.Regexp
 | 
			
		||||
	templates                  *template.Template
 | 
			
		||||
	realClientIPParser         ipapi.RealClientIPParser
 | 
			
		||||
	trustedIPs                 *ip.NetSet
 | 
			
		||||
	Banner                     string
 | 
			
		||||
	Footer                     string
 | 
			
		||||
 | 
			
		||||
	sessionChain alice.Chain
 | 
			
		||||
 | 
			
		||||
	logger *log.Entry
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
 | 
			
		||||
func NewOAuthProxy(opts *options.Options) (*OAuthProxy, error) {
 | 
			
		||||
	logger := log.WithField("component", "proxy").WithField("client-id", opts.ClientID)
 | 
			
		||||
	sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error initialising session store: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	templates := getTemplates()
 | 
			
		||||
	proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
 | 
			
		||||
	upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, u := range opts.GetCompiledRegex() {
 | 
			
		||||
		logger.Printf("compiled skip-auth-regex => %q", u)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.SkipJwtBearerTokens {
 | 
			
		||||
		logger.Printf("Skipping JWT tokens from configured OIDC issuer: %q", opts.OIDCIssuerURL)
 | 
			
		||||
		for _, issuer := range opts.ExtraJwtIssuers {
 | 
			
		||||
			logger.Printf("Skipping JWT tokens from extra JWT issuer: %q", issuer)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	redirectURL := opts.GetRedirectURL()
 | 
			
		||||
	if redirectURL.Path == "" {
 | 
			
		||||
		redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Printf("proxy instance configured for Client ID: %s", opts.ClientID)
 | 
			
		||||
 | 
			
		||||
	trustedIPs := ip.NewNetSet()
 | 
			
		||||
	for _, ipStr := range opts.TrustedIPs {
 | 
			
		||||
		if ipNet := ip.ParseIPNet(ipStr); ipNet != nil {
 | 
			
		||||
			trustedIPs.AddIPNet(*ipNet)
 | 
			
		||||
		} else {
 | 
			
		||||
			return nil, fmt.Errorf("could not parse IP network (%s)", ipStr)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var basicAuthValidator basic.Validator
 | 
			
		||||
	if opts.HtpasswdFile != "" {
 | 
			
		||||
		logger.Printf("using htpasswd file: %s", opts.HtpasswdFile)
 | 
			
		||||
		var err error
 | 
			
		||||
		basicAuthValidator, err = basic.NewHTPasswdValidator(opts.HtpasswdFile)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("could not load htpasswdfile: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sessionChain := buildSessionChain(opts, sessionStore, basicAuthValidator)
 | 
			
		||||
 | 
			
		||||
	return &OAuthProxy{
 | 
			
		||||
		CookieName:     opts.Cookie.Name,
 | 
			
		||||
		CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
 | 
			
		||||
		CookieSeed:     opts.Cookie.Secret,
 | 
			
		||||
		CookieDomains:  opts.Cookie.Domains,
 | 
			
		||||
		CookiePath:     opts.Cookie.Path,
 | 
			
		||||
		CookieSecure:   opts.Cookie.Secure,
 | 
			
		||||
		CookieHTTPOnly: opts.Cookie.HTTPOnly,
 | 
			
		||||
		CookieExpire:   opts.Cookie.Expire,
 | 
			
		||||
		CookieRefresh:  opts.Cookie.Refresh,
 | 
			
		||||
		CookieSameSite: opts.Cookie.SameSite,
 | 
			
		||||
 | 
			
		||||
		RobotsPath:        "/robots.txt",
 | 
			
		||||
		SignInPath:        fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
 | 
			
		||||
		SignOutPath:       fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
 | 
			
		||||
		OAuthStartPath:    fmt.Sprintf("%s/start", opts.ProxyPrefix),
 | 
			
		||||
		OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
 | 
			
		||||
		AuthOnlyPath:      fmt.Sprintf("%s/auth", opts.ProxyPrefix),
 | 
			
		||||
		UserInfoPath:      fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
 | 
			
		||||
 | 
			
		||||
		ProxyPrefix:             opts.ProxyPrefix,
 | 
			
		||||
		provider:                opts.GetProvider(),
 | 
			
		||||
		providerNameOverride:    opts.ProviderName,
 | 
			
		||||
		sessionStore:            sessionStore,
 | 
			
		||||
		serveMux:                upstreamProxy,
 | 
			
		||||
		redirectURL:             redirectURL,
 | 
			
		||||
		whitelistDomains:        opts.WhitelistDomains,
 | 
			
		||||
		skipAuthRegex:           opts.SkipAuthRegex,
 | 
			
		||||
		skipAuthPreflight:       opts.SkipAuthPreflight,
 | 
			
		||||
		skipAuthStripHeaders:    opts.SkipAuthStripHeaders,
 | 
			
		||||
		skipJwtBearerTokens:     opts.SkipJwtBearerTokens,
 | 
			
		||||
		mainJwtBearerVerifier:   opts.GetOIDCVerifier(),
 | 
			
		||||
		extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
 | 
			
		||||
		compiledRegex:           opts.GetCompiledRegex(),
 | 
			
		||||
		realClientIPParser:      opts.GetRealClientIPParser(),
 | 
			
		||||
		SetXAuthRequest:         opts.SetXAuthRequest,
 | 
			
		||||
		PassBasicAuth:           opts.PassBasicAuth,
 | 
			
		||||
		SetBasicAuth:            opts.SetBasicAuth,
 | 
			
		||||
		PassUserHeaders:         opts.PassUserHeaders,
 | 
			
		||||
		PassAccessToken:         opts.PassAccessToken,
 | 
			
		||||
		SetAuthorization:        opts.SetAuthorization,
 | 
			
		||||
		PassAuthorization:       opts.PassAuthorization,
 | 
			
		||||
		PreferEmailToUser:       opts.PreferEmailToUser,
 | 
			
		||||
		SkipProviderButton:      opts.SkipProviderButton,
 | 
			
		||||
		templates:               templates,
 | 
			
		||||
		trustedIPs:              trustedIPs,
 | 
			
		||||
		Banner:                  opts.Banner,
 | 
			
		||||
		Footer:                  opts.Footer,
 | 
			
		||||
		SignInMessage:           buildSignInMessage(opts),
 | 
			
		||||
 | 
			
		||||
		basicAuthValidator:  basicAuthValidator,
 | 
			
		||||
		displayHtpasswdForm: basicAuthValidator != nil,
 | 
			
		||||
		sessionChain:        sessionChain,
 | 
			
		||||
 | 
			
		||||
		logger: logger,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore, validator basic.Validator) alice.Chain {
 | 
			
		||||
	chain := alice.New(middleware.NewScope())
 | 
			
		||||
 | 
			
		||||
	if opts.SkipJwtBearerTokens {
 | 
			
		||||
		sessionLoaders := []middlewareapi.TokenToSessionLoader{}
 | 
			
		||||
		if opts.GetOIDCVerifier() != nil {
 | 
			
		||||
			sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
 | 
			
		||||
				Verifier:       opts.GetOIDCVerifier(),
 | 
			
		||||
				TokenToSession: opts.GetProvider().CreateSessionStateFromBearerToken,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, verifier := range opts.GetJWTBearerVerifiers() {
 | 
			
		||||
			sessionLoaders = append(sessionLoaders, middlewareapi.TokenToSessionLoader{
 | 
			
		||||
				Verifier: verifier,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		chain = chain.Append(middleware.NewJwtSessionLoader(sessionLoaders))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if validator != nil {
 | 
			
		||||
		chain = chain.Append(middleware.NewBasicAuthSessionLoader(validator))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
 | 
			
		||||
		SessionStore:           sessionStore,
 | 
			
		||||
		RefreshPeriod:          opts.Cookie.Refresh,
 | 
			
		||||
		RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
 | 
			
		||||
		ValidateSessionState:   opts.GetProvider().ValidateSessionState,
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	return chain
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func buildSignInMessage(opts *options.Options) string {
 | 
			
		||||
	var msg string
 | 
			
		||||
	if len(opts.Banner) >= 1 {
 | 
			
		||||
		if opts.Banner == "-" {
 | 
			
		||||
			msg = ""
 | 
			
		||||
		} else {
 | 
			
		||||
			msg = opts.Banner
 | 
			
		||||
		}
 | 
			
		||||
	} else if len(opts.EmailDomains) != 0 && opts.AuthenticatedEmailsFile == "" {
 | 
			
		||||
		if len(opts.EmailDomains) > 1 {
 | 
			
		||||
			msg = fmt.Sprintf("Authenticate using one of the following domains: %v", strings.Join(opts.EmailDomains, ", "))
 | 
			
		||||
		} else if opts.EmailDomains[0] != "*" {
 | 
			
		||||
			msg = fmt.Sprintf("Authenticate using %v", opts.EmailDomains[0])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return msg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRedirectURI returns the redirectURL that the upstream OAuth Provider will
 | 
			
		||||
// redirect clients to once authenticated
 | 
			
		||||
func (p *OAuthProxy) GetRedirectURI(host string) string {
 | 
			
		||||
	// default to the request Host if not set
 | 
			
		||||
	if p.redirectURL.Host != "" {
 | 
			
		||||
		return p.redirectURL.String()
 | 
			
		||||
	}
 | 
			
		||||
	u := *p.redirectURL
 | 
			
		||||
	if u.Scheme == "" {
 | 
			
		||||
		if p.CookieSecure {
 | 
			
		||||
			u.Scheme = httpsScheme
 | 
			
		||||
		} else {
 | 
			
		||||
			u.Scheme = httpScheme
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	u.Host = host
 | 
			
		||||
	return u.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *OAuthProxy) redeemCode(ctx context.Context, host, code string) (s *sessionsapi.SessionState, err error) {
 | 
			
		||||
	if code == "" {
 | 
			
		||||
		return nil, errors.New("missing code")
 | 
			
		||||
	}
 | 
			
		||||
	redirectURI := p.GetRedirectURI(host)
 | 
			
		||||
	s, err = p.provider.Redeem(ctx, redirectURI, code)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.Email == "" {
 | 
			
		||||
		s.Email, err = p.provider.GetEmailAddress(ctx, s)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.PreferredUsername == "" {
 | 
			
		||||
		s.PreferredUsername, err = p.provider.GetPreferredUsername(ctx, s)
 | 
			
		||||
		if err != nil && err.Error() == "not implemented" {
 | 
			
		||||
			err = nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.User == "" {
 | 
			
		||||
		s.User, err = p.provider.GetUserName(ctx, s)
 | 
			
		||||
		if err != nil && err.Error() == "not implemented" {
 | 
			
		||||
			err = nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MakeCSRFCookie creates a cookie for CSRF
 | 
			
		||||
func (p *OAuthProxy) MakeCSRFCookie(req *http.Request, value string, expiration time.Duration, now time.Time) *http.Cookie {
 | 
			
		||||
	return p.makeCookie(req, p.CSRFCookieName, value, expiration, now)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *OAuthProxy) makeCookie(req *http.Request, name string, value string, expiration time.Duration, now time.Time) *http.Cookie {
 | 
			
		||||
	cookieDomain := cookies.GetCookieDomain(req, p.CookieDomains)
 | 
			
		||||
 | 
			
		||||
	if cookieDomain != "" {
 | 
			
		||||
		domain := cookies.GetRequestHost(req)
 | 
			
		||||
		if h, _, err := net.SplitHostPort(domain); err == nil {
 | 
			
		||||
			domain = h
 | 
			
		||||
		}
 | 
			
		||||
		if !strings.HasSuffix(domain, cookieDomain) {
 | 
			
		||||
			p.logger.Errorf("Warning: request host is %q but using configured cookie domain of %q", domain, cookieDomain)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &http.Cookie{
 | 
			
		||||
		Name:     name,
 | 
			
		||||
		Value:    value,
 | 
			
		||||
		Path:     p.CookiePath,
 | 
			
		||||
		Domain:   cookieDomain,
 | 
			
		||||
		HttpOnly: p.CookieHTTPOnly,
 | 
			
		||||
		Secure:   p.CookieSecure,
 | 
			
		||||
		Expires:  now.Add(expiration),
 | 
			
		||||
		SameSite: cookies.ParseSameSite(p.CookieSameSite),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClearCSRFCookie creates a cookie to unset the CSRF cookie stored in the user's
 | 
			
		||||
// session
 | 
			
		||||
func (p *OAuthProxy) ClearCSRFCookie(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	http.SetCookie(rw, p.MakeCSRFCookie(req, "", time.Hour*-1, time.Now()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetCSRFCookie adds a CSRF cookie to the response
 | 
			
		||||
func (p *OAuthProxy) SetCSRFCookie(rw http.ResponseWriter, req *http.Request, val string) {
 | 
			
		||||
	http.SetCookie(rw, p.MakeCSRFCookie(req, val, p.CookieExpire, time.Now()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClearSessionCookie creates a cookie to unset the user's authentication cookie
 | 
			
		||||
// stored in the user's session
 | 
			
		||||
func (p *OAuthProxy) ClearSessionCookie(rw http.ResponseWriter, req *http.Request) error {
 | 
			
		||||
	return p.sessionStore.Clear(rw, req)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoadCookiedSession reads the user's authentication details from the request
 | 
			
		||||
func (p *OAuthProxy) LoadCookiedSession(req *http.Request) (*sessionsapi.SessionState, error) {
 | 
			
		||||
	return p.sessionStore.Load(req)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SaveSession creates a new session cookie value and sets this on the response
 | 
			
		||||
func (p *OAuthProxy) SaveSession(rw http.ResponseWriter, req *http.Request, s *sessionsapi.SessionState) error {
 | 
			
		||||
	return p.sessionStore.Save(rw, req, s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RobotsTxt disallows scraping pages from the OAuthProxy
 | 
			
		||||
func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
 | 
			
		||||
	_, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Printf("Error writing robots.txt: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	rw.WriteHeader(http.StatusOK)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ErrorPage writes an error response
 | 
			
		||||
func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
 | 
			
		||||
	rw.WriteHeader(code)
 | 
			
		||||
	t := struct {
 | 
			
		||||
		Title       string
 | 
			
		||||
		Message     string
 | 
			
		||||
		ProxyPrefix string
 | 
			
		||||
	}{
 | 
			
		||||
		Title:       fmt.Sprintf("%d %s", code, title),
 | 
			
		||||
		Message:     message,
 | 
			
		||||
		ProxyPrefix: p.ProxyPrefix,
 | 
			
		||||
	}
 | 
			
		||||
	err := p.templates.ExecuteTemplate(rw, "error.html", t)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Printf("Error rendering error.html template: %v", err)
 | 
			
		||||
		http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SignInPage writes the sing in template to the response
 | 
			
		||||
func (p *OAuthProxy) SignInPage(rw http.ResponseWriter, req *http.Request, code int) {
 | 
			
		||||
	prepareNoCache(rw)
 | 
			
		||||
	err := p.ClearSessionCookie(rw, req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Printf("Error clearing session cookie: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	rw.WriteHeader(code)
 | 
			
		||||
 | 
			
		||||
	redirectURL, err := p.GetRedirect(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error obtaining redirect: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if redirectURL == p.SignInPath {
 | 
			
		||||
		redirectURL = "/"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// We allow unescaped template.HTML since it is user configured options
 | 
			
		||||
	/* #nosec G203 */
 | 
			
		||||
	t := struct {
 | 
			
		||||
		ProviderName  string
 | 
			
		||||
		SignInMessage template.HTML
 | 
			
		||||
		CustomLogin   bool
 | 
			
		||||
		Redirect      string
 | 
			
		||||
		Version       string
 | 
			
		||||
		ProxyPrefix   string
 | 
			
		||||
		Footer        template.HTML
 | 
			
		||||
	}{
 | 
			
		||||
		ProviderName:  p.provider.Data().ProviderName,
 | 
			
		||||
		SignInMessage: template.HTML(p.SignInMessage),
 | 
			
		||||
		CustomLogin:   p.displayHtpasswdForm,
 | 
			
		||||
		Redirect:      redirectURL,
 | 
			
		||||
		Version:       "",
 | 
			
		||||
		ProxyPrefix:   p.ProxyPrefix,
 | 
			
		||||
		Footer:        template.HTML(p.Footer),
 | 
			
		||||
	}
 | 
			
		||||
	if p.providerNameOverride != "" {
 | 
			
		||||
		t.ProviderName = p.providerNameOverride
 | 
			
		||||
	}
 | 
			
		||||
	err = p.templates.ExecuteTemplate(rw, "sign_in.html", t)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Printf("Error rendering sign_in.html template: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ManualSignIn handles basic auth logins to the proxy
 | 
			
		||||
func (p *OAuthProxy) ManualSignIn(req *http.Request) (string, bool) {
 | 
			
		||||
	if req.Method != "POST" || p.basicAuthValidator == nil {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
	user := req.FormValue("username")
 | 
			
		||||
	passwd := req.FormValue("password")
 | 
			
		||||
	if user == "" {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
	// check auth
 | 
			
		||||
	if p.basicAuthValidator.Validate(user, passwd) {
 | 
			
		||||
		p.logger.WithField("user", user).WithField("status", "AuthSuccess").Info("Authenticated via HtpasswdFile")
 | 
			
		||||
		return user, true
 | 
			
		||||
	}
 | 
			
		||||
	p.logger.WithField("user", user).WithField("status", "AuthFailure").Info("Invalid authentication via HtpasswdFile")
 | 
			
		||||
	return "", false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRedirect reads the query parameter to get the URL to redirect clients to
 | 
			
		||||
// once authenticated with the OAuthProxy
 | 
			
		||||
func (p *OAuthProxy) GetRedirect(req *http.Request) (redirect string, err error) {
 | 
			
		||||
	err = req.ParseForm()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	redirect = req.Header.Get("X-Auth-Request-Redirect")
 | 
			
		||||
	if req.Form.Get("rd") != "" {
 | 
			
		||||
		redirect = req.Form.Get("rd")
 | 
			
		||||
	}
 | 
			
		||||
	if !p.IsValidRedirect(redirect) {
 | 
			
		||||
		// Use RequestURI to preserve ?query
 | 
			
		||||
		redirect = req.URL.RequestURI()
 | 
			
		||||
		if strings.HasPrefix(redirect, p.ProxyPrefix) {
 | 
			
		||||
			redirect = "/"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// splitHostPort separates host and port. If the port is not valid, it returns
 | 
			
		||||
// the entire input as host, and it doesn't check the validity of the host.
 | 
			
		||||
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
 | 
			
		||||
// *** taken from net/url, modified validOptionalPort() to accept ":*"
 | 
			
		||||
func splitHostPort(hostport string) (host, port string) {
 | 
			
		||||
	host = hostport
 | 
			
		||||
 | 
			
		||||
	colon := strings.LastIndexByte(host, ':')
 | 
			
		||||
	if colon != -1 && validOptionalPort(host[colon:]) {
 | 
			
		||||
		host, port = host[:colon], host[colon+1:]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
 | 
			
		||||
		host = host[1 : len(host)-1]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// validOptionalPort reports whether port is either an empty string
 | 
			
		||||
// or matches /^:\d*$/
 | 
			
		||||
// *** taken from net/url, modified to accept ":*"
 | 
			
		||||
func validOptionalPort(port string) bool {
 | 
			
		||||
	if port == "" || port == ":*" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if port[0] != ':' {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	for _, b := range port[1:] {
 | 
			
		||||
		if b < '0' || b > '9' {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsValidRedirect checks whether the redirect URL is whitelisted
 | 
			
		||||
func (p *OAuthProxy) IsValidRedirect(redirect string) bool {
 | 
			
		||||
	switch {
 | 
			
		||||
	case redirect == "":
 | 
			
		||||
		// The user didn't specify a redirect, should fallback to `/`
 | 
			
		||||
		return false
 | 
			
		||||
	case strings.HasPrefix(redirect, "/") && !strings.HasPrefix(redirect, "//") && !invalidRedirectRegex.MatchString(redirect):
 | 
			
		||||
		return true
 | 
			
		||||
	case strings.HasPrefix(redirect, "http://") || strings.HasPrefix(redirect, "https://"):
 | 
			
		||||
		redirectURL, err := url.Parse(redirect)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			p.logger.Printf("Rejecting invalid redirect %q: scheme unsupported or missing", redirect)
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
		redirectHostname := redirectURL.Hostname()
 | 
			
		||||
 | 
			
		||||
		for _, domain := range p.whitelistDomains {
 | 
			
		||||
			domainHostname, domainPort := splitHostPort(strings.TrimLeft(domain, "."))
 | 
			
		||||
			if domainHostname == "" {
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (redirectHostname == domainHostname) || (strings.HasPrefix(domain, ".") && strings.HasSuffix(redirectHostname, domainHostname)) {
 | 
			
		||||
				// the domain names match, now validate the ports
 | 
			
		||||
				// if the whitelisted domain's port is '*', allow all ports
 | 
			
		||||
				// if the whitelisted domain contains a specific port, only allow that port
 | 
			
		||||
				// if the whitelisted domain doesn't contain a port at all, only allow empty redirect ports ie http and https
 | 
			
		||||
				redirectPort := redirectURL.Port()
 | 
			
		||||
				if (domainPort == "*") ||
 | 
			
		||||
					(domainPort == redirectPort) ||
 | 
			
		||||
					(domainPort == "" && redirectPort == "") {
 | 
			
		||||
					return true
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		p.logger.Printf("Rejecting invalid redirect %q: domain / port not in whitelist", redirect)
 | 
			
		||||
		return false
 | 
			
		||||
	default:
 | 
			
		||||
		p.logger.Printf("Rejecting invalid redirect %q: not an absolute or relative URL", redirect)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsWhitelistedRequest is used to check if auth should be skipped for this request
 | 
			
		||||
func (p *OAuthProxy) IsWhitelistedRequest(req *http.Request) bool {
 | 
			
		||||
	isPreflightRequestAllowed := p.skipAuthPreflight && req.Method == "OPTIONS"
 | 
			
		||||
	return isPreflightRequestAllowed || p.IsWhitelistedPath(req.URL.Path) || p.IsTrustedIP(req)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsWhitelistedPath is used to check if the request path is allowed without auth
 | 
			
		||||
func (p *OAuthProxy) IsWhitelistedPath(path string) bool {
 | 
			
		||||
	for _, u := range p.compiledRegex {
 | 
			
		||||
		if u.MatchString(path) {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
 | 
			
		||||
var noCacheHeaders = map[string]string{
 | 
			
		||||
	"Expires":         time.Unix(0, 0).Format(time.RFC1123),
 | 
			
		||||
	"Cache-Control":   "no-cache, no-store, must-revalidate, max-age=0",
 | 
			
		||||
	"X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// prepareNoCache prepares headers for preventing browser caching.
 | 
			
		||||
func prepareNoCache(w http.ResponseWriter) {
 | 
			
		||||
	// Set NoCache headers
 | 
			
		||||
	for k, v := range noCacheHeaders {
 | 
			
		||||
		w.Header().Set(k, v)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsTrustedIP is used to check if a request comes from a trusted client IP address.
 | 
			
		||||
func (p *OAuthProxy) IsTrustedIP(req *http.Request) bool {
 | 
			
		||||
	if p.trustedIPs == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	remoteAddr, err := ip.GetClientIP(p.realClientIPParser, req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error obtaining real IP for trusted IP list: %v", err)
 | 
			
		||||
		// Possibly spoofed X-Real-IP header
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if remoteAddr == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return p.trustedIPs.Has(remoteAddr)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
 | 
			
		||||
		prepareNoCache(rw)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch path := req.URL.Path; {
 | 
			
		||||
	case path == p.RobotsPath:
 | 
			
		||||
		p.RobotsTxt(rw)
 | 
			
		||||
	case p.IsWhitelistedRequest(req):
 | 
			
		||||
		p.SkipAuthProxy(rw, req)
 | 
			
		||||
	case path == p.SignInPath:
 | 
			
		||||
		p.SignIn(rw, req)
 | 
			
		||||
	case path == p.SignOutPath:
 | 
			
		||||
		p.SignOut(rw, req)
 | 
			
		||||
	case path == p.OAuthStartPath:
 | 
			
		||||
		p.OAuthStart(rw, req)
 | 
			
		||||
	case path == p.OAuthCallbackPath:
 | 
			
		||||
		p.OAuthCallback(rw, req)
 | 
			
		||||
	case path == p.AuthOnlyPath:
 | 
			
		||||
		p.AuthenticateOnly(rw, req)
 | 
			
		||||
	case path == p.UserInfoPath:
 | 
			
		||||
		p.UserInfo(rw, req)
 | 
			
		||||
	default:
 | 
			
		||||
		p.Proxy(rw, req)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SignIn serves a page prompting users to sign in
 | 
			
		||||
func (p *OAuthProxy) SignIn(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	redirect, err := p.GetRedirect(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error obtaining redirect: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user, ok := p.ManualSignIn(req)
 | 
			
		||||
	if ok {
 | 
			
		||||
		session := &sessionsapi.SessionState{User: user}
 | 
			
		||||
		err = p.SaveSession(rw, req, session)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			p.logger.Printf("Error saving session: %v", err)
 | 
			
		||||
			p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		http.Redirect(rw, req, redirect, http.StatusFound)
 | 
			
		||||
	} else {
 | 
			
		||||
		if p.SkipProviderButton {
 | 
			
		||||
			p.OAuthStart(rw, req)
 | 
			
		||||
		} else {
 | 
			
		||||
			p.SignInPage(rw, req, http.StatusOK)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//UserInfo endpoint outputs session email and preferred username in JSON format
 | 
			
		||||
func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
 | 
			
		||||
	session, err := p.getAuthenticatedSession(rw, req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	userInfo := struct {
 | 
			
		||||
		Email             string `json:"email"`
 | 
			
		||||
		PreferredUsername string `json:"preferredUsername,omitempty"`
 | 
			
		||||
	}{
 | 
			
		||||
		Email:             session.Email,
 | 
			
		||||
		PreferredUsername: session.PreferredUsername,
 | 
			
		||||
	}
 | 
			
		||||
	rw.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	rw.WriteHeader(http.StatusOK)
 | 
			
		||||
	err = json.NewEncoder(rw).Encode(userInfo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Printf("Error encoding user info: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SignOut sends a response to clear the authentication cookie
 | 
			
		||||
func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	redirect, err := p.GetRedirect(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error obtaining redirect: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err = p.ClearSessionCookie(rw, req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error clearing session cookie: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	http.Redirect(rw, req, redirect, http.StatusFound)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OAuthStart starts the OAuth2 authentication flow
 | 
			
		||||
func (p *OAuthProxy) OAuthStart(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	prepareNoCache(rw)
 | 
			
		||||
	nonce, err := encryption.Nonce()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error obtaining nonce: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	p.SetCSRFCookie(rw, req, nonce)
 | 
			
		||||
	redirect, err := p.GetRedirect(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error obtaining redirect: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	redirectURI := p.GetRedirectURI(req.Host)
 | 
			
		||||
	http.Redirect(rw, req, p.provider.GetLoginURL(redirectURI, fmt.Sprintf("%v:%v", nonce, redirect)), http.StatusFound)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OAuthCallback is the OAuth2 authentication flow callback that finishes the
 | 
			
		||||
// OAuth2 authentication flow
 | 
			
		||||
func (p *OAuthProxy) OAuthCallback(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	remoteAddr := ip.GetClientString(p.realClientIPParser, req, true)
 | 
			
		||||
 | 
			
		||||
	// finish the oauth cycle
 | 
			
		||||
	err := req.ParseForm()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error while parsing OAuth2 callback: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	errorString := req.Form.Get("error")
 | 
			
		||||
	if errorString != "" {
 | 
			
		||||
		p.logger.Errorf("Error while parsing OAuth2 callback: %s", errorString)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", errorString)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	session, err := p.redeemCode(req.Context(), req.Host, req.Form.Get("code"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error redeeming code during OAuth2 callback: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Internal Error")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s := strings.SplitN(req.Form.Get("state"), ":", 2)
 | 
			
		||||
	if len(s) != 2 {
 | 
			
		||||
		p.logger.Error("Error while parsing OAuth2 state: invalid length")
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", "Invalid State")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	nonce := s[0]
 | 
			
		||||
	redirect := s[1]
 | 
			
		||||
	c, err := req.Cookie(p.CSRFCookieName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Info("Invalid authentication via OAuth2: unable to obtain CSRF cookie")
 | 
			
		||||
		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	p.ClearCSRFCookie(rw, req)
 | 
			
		||||
	if c.Value != nonce {
 | 
			
		||||
		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Info("Invalid authentication via OAuth2: CSRF token mismatch, potential attack")
 | 
			
		||||
		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "CSRF Failed")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !p.IsValidRedirect(redirect) {
 | 
			
		||||
		redirect = "/"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// set cookie, or deny
 | 
			
		||||
	if p.provider.ValidateGroup(session.Email) {
 | 
			
		||||
		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Infof("Authenticated via OAuth2: %s", session)
 | 
			
		||||
		err := p.SaveSession(rw, req, session)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			p.logger.Printf("Error saving session state for %s: %v", remoteAddr, err)
 | 
			
		||||
			p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		http.Redirect(rw, req, redirect, http.StatusFound)
 | 
			
		||||
	} else {
 | 
			
		||||
		p.logger.WithField("user", session.Email).WithField("status", "AuthFailure").Info("Invalid authentication via OAuth2: unauthorized")
 | 
			
		||||
		p.ErrorPage(rw, http.StatusForbidden, "Permission Denied", "Invalid Account")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AuthenticateOnly checks whether the user is currently logged in
 | 
			
		||||
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	session, err := p.getAuthenticatedSession(rw, req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(rw, "unauthorized request", http.StatusUnauthorized)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// we are authenticated
 | 
			
		||||
	p.addHeadersForProxying(rw, req, session)
 | 
			
		||||
	rw.WriteHeader(http.StatusAccepted)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SkipAuthProxy proxies whitelisted requests and skips authentication
 | 
			
		||||
func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	if p.skipAuthStripHeaders {
 | 
			
		||||
		p.stripAuthHeaders(req)
 | 
			
		||||
	}
 | 
			
		||||
	p.serveMux.ServeHTTP(rw, req)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Proxy proxies the user request if the user is authenticated else it prompts
 | 
			
		||||
// them to authenticate
 | 
			
		||||
func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	session, err := p.getAuthenticatedSession(rw, req)
 | 
			
		||||
	switch err {
 | 
			
		||||
	case nil:
 | 
			
		||||
		// we are authenticated
 | 
			
		||||
		p.addHeadersForProxying(rw, req, session)
 | 
			
		||||
		p.serveMux.ServeHTTP(rw, req)
 | 
			
		||||
 | 
			
		||||
	case ErrNeedsLogin:
 | 
			
		||||
		// we need to send the user to a login screen
 | 
			
		||||
		if isAjax(req) {
 | 
			
		||||
			// no point redirecting an AJAX request
 | 
			
		||||
			p.ErrorJSON(rw, http.StatusUnauthorized)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if p.SkipProviderButton {
 | 
			
		||||
			p.OAuthStart(rw, req)
 | 
			
		||||
		} else {
 | 
			
		||||
			p.SignInPage(rw, req, http.StatusForbidden)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		// unknown error
 | 
			
		||||
		p.logger.Errorf("Unexpected internal error: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError,
 | 
			
		||||
			"Internal Error", "Internal Error")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
 | 
			
		||||
// Returns nil, ErrNeedsLogin if user needs to login.
 | 
			
		||||
// Set-Cookie headers may be set on the response as a side-effect of calling this method.
 | 
			
		||||
func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
 | 
			
		||||
	var session *sessionsapi.SessionState
 | 
			
		||||
 | 
			
		||||
	getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
		session = middleware.GetRequestScope(req).Session
 | 
			
		||||
	}))
 | 
			
		||||
	getSession.ServeHTTP(rw, req)
 | 
			
		||||
 | 
			
		||||
	if session == nil {
 | 
			
		||||
		return nil, ErrNeedsLogin
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return session, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// addHeadersForProxying adds the appropriate headers the request / response for proxying
 | 
			
		||||
func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
 | 
			
		||||
	req.Header["X-Forwarded-User"] = []string{session.User}
 | 
			
		||||
	if session.Email != "" {
 | 
			
		||||
		req.Header["X-Forwarded-Email"] = []string{session.Email}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if session.PreferredUsername != "" {
 | 
			
		||||
		req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
 | 
			
		||||
		req.Header["X-Auth-Username"] = []string{session.PreferredUsername}
 | 
			
		||||
	} else {
 | 
			
		||||
		req.Header.Del("X-Forwarded-Preferred-Username")
 | 
			
		||||
		req.Header.Del("X-Auth-Username")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if session.Email != "" {
 | 
			
		||||
		rw.Header().Set("X-Auth-Request-Email", session.Email)
 | 
			
		||||
	} else {
 | 
			
		||||
		rw.Header().Del("X-Auth-Request-Email")
 | 
			
		||||
	}
 | 
			
		||||
	if session.PreferredUsername != "" {
 | 
			
		||||
		rw.Header().Set("X-Auth-Request-Preferred-Username", session.PreferredUsername)
 | 
			
		||||
	} else {
 | 
			
		||||
		rw.Header().Del("X-Auth-Request-Preferred-Username")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.SetBasicAuth {
 | 
			
		||||
		claims := Claims{}
 | 
			
		||||
		err := claims.FromIDToken(session.IDToken)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.WithError(err).Warning("Failed to parse IDToken")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		userAttributes := claims.Proxy.UserAttributes
 | 
			
		||||
		var ok bool
 | 
			
		||||
		var password string
 | 
			
		||||
		if password, ok = userAttributes[p.BasicAuthPasswordAttribute]; !ok {
 | 
			
		||||
			password = ""
 | 
			
		||||
		}
 | 
			
		||||
		// Check if we should use email or a custom attribute as username
 | 
			
		||||
		var username string
 | 
			
		||||
		if username, ok = userAttributes[p.BasicAuthUserAttribute]; !ok {
 | 
			
		||||
			username = session.Email
 | 
			
		||||
		}
 | 
			
		||||
		authVal := b64.StdEncoding.EncodeToString([]byte(username + ":" + password))
 | 
			
		||||
		req.Header["Authorization"] = []string{fmt.Sprintf("Basic %s", authVal)}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if session.Email == "" {
 | 
			
		||||
		rw.Header().Set("GAP-Auth", session.User)
 | 
			
		||||
	} else {
 | 
			
		||||
		rw.Header().Set("GAP-Auth", session.Email)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// stripAuthHeaders removes Auth headers for whitelisted routes from skipAuthRegex
 | 
			
		||||
func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
 | 
			
		||||
	if p.PassBasicAuth {
 | 
			
		||||
		req.Header.Del("X-Forwarded-User")
 | 
			
		||||
		req.Header.Del("X-Forwarded-Email")
 | 
			
		||||
		req.Header.Del("X-Forwarded-Preferred-Username")
 | 
			
		||||
		req.Header.Del("Authorization")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.PassUserHeaders {
 | 
			
		||||
		req.Header.Del("X-Forwarded-User")
 | 
			
		||||
		req.Header.Del("X-Forwarded-Email")
 | 
			
		||||
		req.Header.Del("X-Forwarded-Preferred-Username")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.PassAccessToken {
 | 
			
		||||
		req.Header.Del("X-Forwarded-Access-Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.PassAuthorization {
 | 
			
		||||
		req.Header.Del("Authorization")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// isAjax checks if a request is an ajax request
 | 
			
		||||
func isAjax(req *http.Request) bool {
 | 
			
		||||
	acceptValues := req.Header.Values("Accept")
 | 
			
		||||
	const ajaxReq = applicationJSON
 | 
			
		||||
	for _, v := range acceptValues {
 | 
			
		||||
		if v == ajaxReq {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ErrorJSON returns the error code with an application/json mime type
 | 
			
		||||
func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
 | 
			
		||||
	rw.Header().Set("Content-Type", applicationJSON)
 | 
			
		||||
	rw.WriteHeader(code)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										481
									
								
								proxy/pkg/proxy/proxy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										481
									
								
								proxy/pkg/proxy/proxy.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,481 @@
 | 
			
		||||
package proxy
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	b64 "encoding/base64"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/coreos/go-oidc"
 | 
			
		||||
	"github.com/justinas/alice"
 | 
			
		||||
	ipapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/ip"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/apis/options"
 | 
			
		||||
	sessionsapi "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/middleware"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/sessions"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/pkg/upstream"
 | 
			
		||||
	"github.com/oauth2-proxy/oauth2-proxy/providers"
 | 
			
		||||
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	httpScheme  = "http"
 | 
			
		||||
	httpsScheme = "https"
 | 
			
		||||
 | 
			
		||||
	applicationJSON = "application/json"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	// ErrNeedsLogin means the user should be redirected to the login page
 | 
			
		||||
	ErrNeedsLogin = errors.New("redirect to login page")
 | 
			
		||||
 | 
			
		||||
	// Used to check final redirects are not susceptible to open redirects.
 | 
			
		||||
	// Matches //, /\ and both of these with whitespace in between (eg / / or / \).
 | 
			
		||||
	invalidRedirectRegex = regexp.MustCompile(`[/\\](?:[\s\v]*|\.{1,2})[/\\]`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// OAuthProxy is the main authentication proxy
 | 
			
		||||
type OAuthProxy struct {
 | 
			
		||||
	CookieSeed     string
 | 
			
		||||
	CookieName     string
 | 
			
		||||
	CSRFCookieName string
 | 
			
		||||
	CookieDomains  []string
 | 
			
		||||
	CookiePath     string
 | 
			
		||||
	CookieSecure   bool
 | 
			
		||||
	CookieHTTPOnly bool
 | 
			
		||||
	CookieExpire   time.Duration
 | 
			
		||||
	CookieRefresh  time.Duration
 | 
			
		||||
	CookieSameSite string
 | 
			
		||||
 | 
			
		||||
	RobotsPath        string
 | 
			
		||||
	SignInPath        string
 | 
			
		||||
	SignOutPath       string
 | 
			
		||||
	OAuthStartPath    string
 | 
			
		||||
	OAuthCallbackPath string
 | 
			
		||||
	AuthOnlyPath      string
 | 
			
		||||
	UserInfoPath      string
 | 
			
		||||
 | 
			
		||||
	redirectURL                *url.URL // the url to receive requests at
 | 
			
		||||
	whitelistDomains           []string
 | 
			
		||||
	provider                   providers.Provider
 | 
			
		||||
	sessionStore               sessionsapi.SessionStore
 | 
			
		||||
	ProxyPrefix                string
 | 
			
		||||
	serveMux                   http.Handler
 | 
			
		||||
	SetXAuthRequest            bool
 | 
			
		||||
	SetBasicAuth               bool
 | 
			
		||||
	PassUserHeaders            bool
 | 
			
		||||
	BasicAuthUserAttribute     string
 | 
			
		||||
	BasicAuthPasswordAttribute string
 | 
			
		||||
	PassAccessToken            bool
 | 
			
		||||
	SetAuthorization           bool
 | 
			
		||||
	PassAuthorization          bool
 | 
			
		||||
	PreferEmailToUser          bool
 | 
			
		||||
	skipAuthRegex              []string
 | 
			
		||||
	skipAuthPreflight          bool
 | 
			
		||||
	skipAuthStripHeaders       bool
 | 
			
		||||
	mainJwtBearerVerifier      *oidc.IDTokenVerifier
 | 
			
		||||
	extraJwtBearerVerifiers    []*oidc.IDTokenVerifier
 | 
			
		||||
	compiledRegex              []*regexp.Regexp
 | 
			
		||||
	templates                  *template.Template
 | 
			
		||||
	realClientIPParser         ipapi.RealClientIPParser
 | 
			
		||||
 | 
			
		||||
	sessionChain alice.Chain
 | 
			
		||||
 | 
			
		||||
	logger *log.Entry
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewOAuthProxy creates a new instance of OAuthProxy from the options provided
 | 
			
		||||
func NewOAuthProxy(opts *options.Options) (*OAuthProxy, error) {
 | 
			
		||||
	logger := log.WithField("component", "proxy").WithField("client-id", opts.ClientID)
 | 
			
		||||
	sessionStore, err := sessions.NewSessionStore(&opts.Session, &opts.Cookie)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error initialising session store: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	templates := getTemplates()
 | 
			
		||||
	proxyErrorHandler := upstream.NewProxyErrorHandler(templates.Lookup("error.html"), opts.ProxyPrefix)
 | 
			
		||||
	upstreamProxy, err := upstream.NewProxy(opts.UpstreamServers, opts.GetSignatureData(), proxyErrorHandler)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("error initialising upstream proxy: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, u := range opts.GetCompiledRegex() {
 | 
			
		||||
		logger.Printf("compiled skip-auth-regex => %q", u)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	redirectURL := opts.GetRedirectURL()
 | 
			
		||||
	if redirectURL.Path == "" {
 | 
			
		||||
		redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.Printf("proxy instance configured for Client ID: %s", opts.ClientID)
 | 
			
		||||
 | 
			
		||||
	sessionChain := buildSessionChain(opts, sessionStore)
 | 
			
		||||
 | 
			
		||||
	return &OAuthProxy{
 | 
			
		||||
		CookieName:     opts.Cookie.Name,
 | 
			
		||||
		CSRFCookieName: fmt.Sprintf("%v_%v", opts.Cookie.Name, "csrf"),
 | 
			
		||||
		CookieSeed:     opts.Cookie.Secret,
 | 
			
		||||
		CookieDomains:  opts.Cookie.Domains,
 | 
			
		||||
		CookiePath:     opts.Cookie.Path,
 | 
			
		||||
		CookieSecure:   opts.Cookie.Secure,
 | 
			
		||||
		CookieHTTPOnly: opts.Cookie.HTTPOnly,
 | 
			
		||||
		CookieExpire:   opts.Cookie.Expire,
 | 
			
		||||
		CookieRefresh:  opts.Cookie.Refresh,
 | 
			
		||||
		CookieSameSite: opts.Cookie.SameSite,
 | 
			
		||||
 | 
			
		||||
		RobotsPath:        "/robots.txt",
 | 
			
		||||
		SignInPath:        fmt.Sprintf("%s/sign_in", opts.ProxyPrefix),
 | 
			
		||||
		SignOutPath:       fmt.Sprintf("%s/sign_out", opts.ProxyPrefix),
 | 
			
		||||
		OAuthStartPath:    fmt.Sprintf("%s/start", opts.ProxyPrefix),
 | 
			
		||||
		OAuthCallbackPath: fmt.Sprintf("%s/callback", opts.ProxyPrefix),
 | 
			
		||||
		AuthOnlyPath:      fmt.Sprintf("%s/auth", opts.ProxyPrefix),
 | 
			
		||||
		UserInfoPath:      fmt.Sprintf("%s/userinfo", opts.ProxyPrefix),
 | 
			
		||||
 | 
			
		||||
		ProxyPrefix:             opts.ProxyPrefix,
 | 
			
		||||
		provider:                opts.GetProvider(),
 | 
			
		||||
		sessionStore:            sessionStore,
 | 
			
		||||
		serveMux:                upstreamProxy,
 | 
			
		||||
		redirectURL:             redirectURL,
 | 
			
		||||
		whitelistDomains:        opts.WhitelistDomains,
 | 
			
		||||
		skipAuthRegex:           opts.SkipAuthRegex,
 | 
			
		||||
		skipAuthPreflight:       opts.SkipAuthPreflight,
 | 
			
		||||
		skipAuthStripHeaders:    opts.SkipAuthStripHeaders,
 | 
			
		||||
		mainJwtBearerVerifier:   opts.GetOIDCVerifier(),
 | 
			
		||||
		extraJwtBearerVerifiers: opts.GetJWTBearerVerifiers(),
 | 
			
		||||
		compiledRegex:           opts.GetCompiledRegex(),
 | 
			
		||||
		realClientIPParser:      opts.GetRealClientIPParser(),
 | 
			
		||||
		SetXAuthRequest:         opts.SetXAuthRequest,
 | 
			
		||||
		SetBasicAuth:            opts.SetBasicAuth,
 | 
			
		||||
		PassUserHeaders:         opts.PassUserHeaders,
 | 
			
		||||
		PassAccessToken:         opts.PassAccessToken,
 | 
			
		||||
		SetAuthorization:        opts.SetAuthorization,
 | 
			
		||||
		PassAuthorization:       opts.PassAuthorization,
 | 
			
		||||
		PreferEmailToUser:       opts.PreferEmailToUser,
 | 
			
		||||
		templates:               templates,
 | 
			
		||||
 | 
			
		||||
		sessionChain: sessionChain,
 | 
			
		||||
 | 
			
		||||
		logger: logger,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func buildSessionChain(opts *options.Options, sessionStore sessionsapi.SessionStore) alice.Chain {
 | 
			
		||||
	chain := alice.New(middleware.NewScope())
 | 
			
		||||
 | 
			
		||||
	chain = chain.Append(middleware.NewStoredSessionLoader(&middleware.StoredSessionLoaderOptions{
 | 
			
		||||
		SessionStore:           sessionStore,
 | 
			
		||||
		RefreshPeriod:          opts.Cookie.Refresh,
 | 
			
		||||
		RefreshSessionIfNeeded: opts.GetProvider().RefreshSessionIfNeeded,
 | 
			
		||||
		ValidateSessionState:   opts.GetProvider().ValidateSessionState,
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	return chain
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RobotsTxt disallows scraping pages from the OAuthProxy
 | 
			
		||||
func (p *OAuthProxy) RobotsTxt(rw http.ResponseWriter) {
 | 
			
		||||
	_, err := fmt.Fprintf(rw, "User-agent: *\nDisallow: /")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Printf("Error writing robots.txt: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	rw.WriteHeader(http.StatusOK)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ErrorPage writes an error response
 | 
			
		||||
func (p *OAuthProxy) ErrorPage(rw http.ResponseWriter, code int, title string, message string) {
 | 
			
		||||
	rw.WriteHeader(code)
 | 
			
		||||
	t := struct {
 | 
			
		||||
		Title       string
 | 
			
		||||
		Message     string
 | 
			
		||||
		ProxyPrefix string
 | 
			
		||||
	}{
 | 
			
		||||
		Title:       fmt.Sprintf("%d %s", code, title),
 | 
			
		||||
		Message:     message,
 | 
			
		||||
		ProxyPrefix: p.ProxyPrefix,
 | 
			
		||||
	}
 | 
			
		||||
	err := p.templates.ExecuteTemplate(rw, "error.html", t)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Printf("Error rendering error.html template: %v", err)
 | 
			
		||||
		http.Error(rw, "Internal Server Error", http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// splitHostPort separates host and port. If the port is not valid, it returns
 | 
			
		||||
// the entire input as host, and it doesn't check the validity of the host.
 | 
			
		||||
// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric.
 | 
			
		||||
// *** taken from net/url, modified validOptionalPort() to accept ":*"
 | 
			
		||||
func splitHostPort(hostport string) (host, port string) {
 | 
			
		||||
	host = hostport
 | 
			
		||||
 | 
			
		||||
	colon := strings.LastIndexByte(host, ':')
 | 
			
		||||
	if colon != -1 && validOptionalPort(host[colon:]) {
 | 
			
		||||
		host, port = host[:colon], host[colon+1:]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
 | 
			
		||||
		host = host[1 : len(host)-1]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// validOptionalPort reports whether port is either an empty string
 | 
			
		||||
// or matches /^:\d*$/
 | 
			
		||||
// *** taken from net/url, modified to accept ":*"
 | 
			
		||||
func validOptionalPort(port string) bool {
 | 
			
		||||
	if port == "" || port == ":*" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	if port[0] != ':' {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	for _, b := range port[1:] {
 | 
			
		||||
		if b < '0' || b > '9' {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=en
 | 
			
		||||
var noCacheHeaders = map[string]string{
 | 
			
		||||
	"Expires":         time.Unix(0, 0).Format(time.RFC1123),
 | 
			
		||||
	"Cache-Control":   "no-cache, no-store, must-revalidate, max-age=0",
 | 
			
		||||
	"X-Accel-Expires": "0", // https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// prepareNoCache prepares headers for preventing browser caching.
 | 
			
		||||
func prepareNoCache(w http.ResponseWriter) {
 | 
			
		||||
	// Set NoCache headers
 | 
			
		||||
	for k, v := range noCacheHeaders {
 | 
			
		||||
		w.Header().Set(k, v)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	if req.URL.Path != p.AuthOnlyPath && strings.HasPrefix(req.URL.Path, p.ProxyPrefix) {
 | 
			
		||||
		prepareNoCache(rw)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch path := req.URL.Path; {
 | 
			
		||||
	case path == p.RobotsPath:
 | 
			
		||||
		p.RobotsTxt(rw)
 | 
			
		||||
	case p.IsWhitelistedRequest(req):
 | 
			
		||||
		p.SkipAuthProxy(rw, req)
 | 
			
		||||
	case path == p.SignInPath:
 | 
			
		||||
		p.OAuthStart(rw, req)
 | 
			
		||||
	case path == p.SignOutPath:
 | 
			
		||||
		p.SignOut(rw, req)
 | 
			
		||||
	case path == p.OAuthStartPath:
 | 
			
		||||
		p.OAuthStart(rw, req)
 | 
			
		||||
	case path == p.OAuthCallbackPath:
 | 
			
		||||
		p.OAuthCallback(rw, req)
 | 
			
		||||
	case path == p.AuthOnlyPath:
 | 
			
		||||
		p.AuthenticateOnly(rw, req)
 | 
			
		||||
	case path == p.UserInfoPath:
 | 
			
		||||
		p.UserInfo(rw, req)
 | 
			
		||||
	default:
 | 
			
		||||
		p.Proxy(rw, req)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//UserInfo endpoint outputs session email and preferred username in JSON format
 | 
			
		||||
func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
 | 
			
		||||
	session, err := p.getAuthenticatedSession(rw, req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	userInfo := struct {
 | 
			
		||||
		Email             string `json:"email"`
 | 
			
		||||
		PreferredUsername string `json:"preferredUsername,omitempty"`
 | 
			
		||||
	}{
 | 
			
		||||
		Email:             session.Email,
 | 
			
		||||
		PreferredUsername: session.PreferredUsername,
 | 
			
		||||
	}
 | 
			
		||||
	rw.Header().Set("Content-Type", "application/json")
 | 
			
		||||
	rw.WriteHeader(http.StatusOK)
 | 
			
		||||
	err = json.NewEncoder(rw).Encode(userInfo)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Printf("Error encoding user info: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SignOut sends a response to clear the authentication cookie
 | 
			
		||||
func (p *OAuthProxy) SignOut(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	redirect, err := p.GetRedirect(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error obtaining redirect: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	err = p.ClearSessionCookie(rw, req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		p.logger.Errorf("Error clearing session cookie: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError, "Internal Server Error", err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	http.Redirect(rw, req, redirect, http.StatusFound)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AuthenticateOnly checks whether the user is currently logged in
 | 
			
		||||
func (p *OAuthProxy) AuthenticateOnly(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	session, err := p.getAuthenticatedSession(rw, req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		http.Error(rw, "unauthorized request", http.StatusUnauthorized)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// we are authenticated
 | 
			
		||||
	p.addHeadersForProxying(rw, req, session)
 | 
			
		||||
	rw.WriteHeader(http.StatusAccepted)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SkipAuthProxy proxies whitelisted requests and skips authentication
 | 
			
		||||
func (p *OAuthProxy) SkipAuthProxy(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	if p.skipAuthStripHeaders {
 | 
			
		||||
		p.stripAuthHeaders(req)
 | 
			
		||||
	}
 | 
			
		||||
	p.serveMux.ServeHTTP(rw, req)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Proxy proxies the user request if the user is authenticated else it prompts
 | 
			
		||||
// them to authenticate
 | 
			
		||||
func (p *OAuthProxy) Proxy(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
	session, err := p.getAuthenticatedSession(rw, req)
 | 
			
		||||
	switch err {
 | 
			
		||||
	case nil:
 | 
			
		||||
		// we are authenticated
 | 
			
		||||
		p.addHeadersForProxying(rw, req, session)
 | 
			
		||||
		p.serveMux.ServeHTTP(rw, req)
 | 
			
		||||
 | 
			
		||||
	case ErrNeedsLogin:
 | 
			
		||||
		// we need to send the user to a login screen
 | 
			
		||||
		if isAjax(req) {
 | 
			
		||||
			// no point redirecting an AJAX request
 | 
			
		||||
			p.ErrorJSON(rw, http.StatusUnauthorized)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		p.OAuthStart(rw, req)
 | 
			
		||||
 | 
			
		||||
	default:
 | 
			
		||||
		// unknown error
 | 
			
		||||
		p.logger.Errorf("Unexpected internal error: %v", err)
 | 
			
		||||
		p.ErrorPage(rw, http.StatusInternalServerError,
 | 
			
		||||
			"Internal Error", "Internal Error")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getAuthenticatedSession checks whether a user is authenticated and returns a session object and nil error if so
 | 
			
		||||
// Returns nil, ErrNeedsLogin if user needs to login.
 | 
			
		||||
// Set-Cookie headers may be set on the response as a side-effect of calling this method.
 | 
			
		||||
func (p *OAuthProxy) getAuthenticatedSession(rw http.ResponseWriter, req *http.Request) (*sessionsapi.SessionState, error) {
 | 
			
		||||
	var session *sessionsapi.SessionState
 | 
			
		||||
 | 
			
		||||
	getSession := p.sessionChain.Then(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
 | 
			
		||||
		session = middleware.GetRequestScope(req).Session
 | 
			
		||||
	}))
 | 
			
		||||
	getSession.ServeHTTP(rw, req)
 | 
			
		||||
 | 
			
		||||
	if session == nil {
 | 
			
		||||
		return nil, ErrNeedsLogin
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return session, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// addHeadersForProxying adds the appropriate headers the request / response for proxying
 | 
			
		||||
func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Request, session *sessionsapi.SessionState) {
 | 
			
		||||
	req.Header["X-Forwarded-User"] = []string{session.User}
 | 
			
		||||
	if session.Email != "" {
 | 
			
		||||
		req.Header["X-Forwarded-Email"] = []string{session.Email}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if session.PreferredUsername != "" {
 | 
			
		||||
		req.Header["X-Forwarded-Preferred-Username"] = []string{session.PreferredUsername}
 | 
			
		||||
		req.Header["X-Auth-Username"] = []string{session.PreferredUsername}
 | 
			
		||||
	} else {
 | 
			
		||||
		req.Header.Del("X-Forwarded-Preferred-Username")
 | 
			
		||||
		req.Header.Del("X-Auth-Username")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	claims := Claims{}
 | 
			
		||||
	err := claims.FromIDToken(session.IDToken)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.WithError(err).Warning("Failed to parse IDToken")
 | 
			
		||||
	}
 | 
			
		||||
	userAttributes := claims.Proxy.UserAttributes
 | 
			
		||||
	// Attempt to set basic auth based on user's attributes
 | 
			
		||||
	if p.SetBasicAuth {
 | 
			
		||||
		var ok bool
 | 
			
		||||
		var password string
 | 
			
		||||
		if password, ok = userAttributes[p.BasicAuthPasswordAttribute].(string); !ok {
 | 
			
		||||
			password = ""
 | 
			
		||||
		}
 | 
			
		||||
		// Check if we should use email or a custom attribute as username
 | 
			
		||||
		var username string
 | 
			
		||||
		if username, ok = userAttributes[p.BasicAuthUserAttribute].(string); !ok {
 | 
			
		||||
			username = session.Email
 | 
			
		||||
		}
 | 
			
		||||
		authVal := b64.StdEncoding.EncodeToString([]byte(username + ":" + password))
 | 
			
		||||
		req.Header["Authorization"] = []string{fmt.Sprintf("Basic %s", authVal)}
 | 
			
		||||
	}
 | 
			
		||||
	// Check if user has additional headers set that we should sent
 | 
			
		||||
	if additionalHeaders, ok := userAttributes["additionalHeaders"].(map[string]string); ok {
 | 
			
		||||
		if additionalHeaders == nil {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		for key, value := range additionalHeaders {
 | 
			
		||||
			req.Header.Set(key, value)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// stripAuthHeaders removes Auth headers for whitelisted routes from skipAuthRegex
 | 
			
		||||
func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
 | 
			
		||||
	if p.PassUserHeaders {
 | 
			
		||||
		req.Header.Del("X-Forwarded-User")
 | 
			
		||||
		req.Header.Del("X-Forwarded-Email")
 | 
			
		||||
		req.Header.Del("X-Forwarded-Preferred-Username")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.PassAccessToken {
 | 
			
		||||
		req.Header.Del("X-Forwarded-Access-Token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.PassAuthorization {
 | 
			
		||||
		req.Header.Del("Authorization")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// isAjax checks if a request is an ajax request
 | 
			
		||||
func isAjax(req *http.Request) bool {
 | 
			
		||||
	acceptValues := req.Header.Values("Accept")
 | 
			
		||||
	const ajaxReq = applicationJSON
 | 
			
		||||
	for _, v := range acceptValues {
 | 
			
		||||
		if v == ajaxReq {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ErrorJSON returns the error code with an application/json mime type
 | 
			
		||||
func (p *OAuthProxy) ErrorJSON(rw http.ResponseWriter, code int) {
 | 
			
		||||
	rw.Header().Set("Content-Type", applicationJSON)
 | 
			
		||||
	rw.WriteHeader(code)
 | 
			
		||||
}
 | 
			
		||||
@ -7,148 +7,7 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func getTemplates() *template.Template {
 | 
			
		||||
	t, err := template.New("foo").Parse(`{{define "sign_in.html"}}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en" charset="utf-8">
 | 
			
		||||
<head>
 | 
			
		||||
	<title>Sign In</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}}">
 | 
			
		||||
	{{ if .SignInMessage }}
 | 
			
		||||
	<p>{{.SignInMessage}}</p>
 | 
			
		||||
	{{ end}}
 | 
			
		||||
	<button type="submit" class="btn">Sign in with {{.ProviderName}}</button><br/>
 | 
			
		||||
	</form>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	{{ if .CustomLogin }}
 | 
			
		||||
	<div class="signin">
 | 
			
		||||
	<form method="POST" action="{{.ProxyPrefix}}/sign_in">
 | 
			
		||||
		<input type="hidden" name="rd" value="{{.Redirect}}">
 | 
			
		||||
		<label for="username">Username:</label><input type="text" name="username" id="username" size="10"><br/>
 | 
			
		||||
		<label for="password">Password:</label><input type="password" name="password" id="password" size="10"><br/>
 | 
			
		||||
		<button type="submit" class="btn">Sign In</button>
 | 
			
		||||
	</form>
 | 
			
		||||
	</div>
 | 
			
		||||
	{{ end }}
 | 
			
		||||
	<script>
 | 
			
		||||
		if (window.location.hash) {
 | 
			
		||||
			(function() {
 | 
			
		||||
				var inputs = document.getElementsByName('rd');
 | 
			
		||||
				for (var i = 0; i < inputs.length; i++) {
 | 
			
		||||
					// Add hash, but make sure it is only added once
 | 
			
		||||
					var idx = inputs[i].value.indexOf('#');
 | 
			
		||||
					if (idx >= 0) {
 | 
			
		||||
						// Remove existing hash from URL
 | 
			
		||||
						inputs[i].value = inputs[i].value.substr(0, idx);
 | 
			
		||||
					}
 | 
			
		||||
					inputs[i].value += window.location.hash;
 | 
			
		||||
				}
 | 
			
		||||
			})();
 | 
			
		||||
		}
 | 
			
		||||
	</script>
 | 
			
		||||
	<footer>
 | 
			
		||||
	{{ if eq .Footer "-" }}
 | 
			
		||||
	{{ else if eq .Footer ""}}
 | 
			
		||||
	Secured with <a href="https://github.com/oauth2-proxy/oauth2-proxy#oauth2_proxy">OAuth2 Proxy</a> version {{.Version}}
 | 
			
		||||
	{{ else }}
 | 
			
		||||
	{{.Footer}}
 | 
			
		||||
	{{ end }}
 | 
			
		||||
	</footer>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
{{end}}`)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("failed parsing template %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t, err = t.Parse(`{{define "error.html"}}
 | 
			
		||||
	t, err := template.New("foo").Parse(`{{define "error.html"}}
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en" charset="utf-8">
 | 
			
		||||
<head>
 | 
			
		||||
 | 
			
		||||
@ -1,32 +0,0 @@
 | 
			
		||||
package proxy
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestLoadTemplates(t *testing.T) {
 | 
			
		||||
	data := struct {
 | 
			
		||||
		TestString string
 | 
			
		||||
	}{
 | 
			
		||||
		TestString: "Testing",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	templates := getTemplates()
 | 
			
		||||
	assert.NotEqual(t, templates, nil)
 | 
			
		||||
 | 
			
		||||
	var defaultSignin bytes.Buffer
 | 
			
		||||
	templates.ExecuteTemplate(&defaultSignin, "sign_in.html", data)
 | 
			
		||||
	assert.Equal(t, "\n<!DOCTYPE html>", defaultSignin.String()[0:16])
 | 
			
		||||
 | 
			
		||||
	var defaultError bytes.Buffer
 | 
			
		||||
	templates.ExecuteTemplate(&defaultError, "error.html", data)
 | 
			
		||||
	assert.Equal(t, "\n<!DOCTYPE html>", defaultError.String()[0:16])
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestTemplatesCompile(t *testing.T) {
 | 
			
		||||
	templates := getTemplates()
 | 
			
		||||
	assert.NotEqual(t, templates, nil)
 | 
			
		||||
}
 | 
			
		||||
@ -51,7 +51,6 @@ func getCommonOptions() *options.Options {
 | 
			
		||||
	commonOpts.EmailDomains = []string{"*"}
 | 
			
		||||
	commonOpts.ProviderType = "oidc"
 | 
			
		||||
	commonOpts.ProxyPrefix = "/pbprox"
 | 
			
		||||
	commonOpts.SkipProviderButton = true
 | 
			
		||||
	commonOpts.Logging.SilencePing = true
 | 
			
		||||
	commonOpts.SetAuthorization = false
 | 
			
		||||
	commonOpts.Scope = "openid email profile pb_proxy"
 | 
			
		||||
@ -168,7 +167,7 @@ func (a *APIController) bundleProviders() ([]*providerBundle, error) {
 | 
			
		||||
		}
 | 
			
		||||
		bundles[idx] = &providerBundle{
 | 
			
		||||
			a:    a,
 | 
			
		||||
			Host: externalHost.Hostname(),
 | 
			
		||||
			Host: externalHost.Host,
 | 
			
		||||
		}
 | 
			
		||||
		bundles[idx].Build(provider)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,6 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	log "github.com/sirupsen/logrus"
 | 
			
		||||
 | 
			
		||||
	sentryhttp "github.com/getsentry/sentry-go/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Server represents an HTTP server
 | 
			
		||||
@ -105,9 +103,7 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) serve(listener net.Listener) {
 | 
			
		||||
	sentryHandler := sentryhttp.New(sentryhttp.Options{})
 | 
			
		||||
 | 
			
		||||
	srv := &http.Server{Handler: sentryHandler.HandleFunc(s.handler)}
 | 
			
		||||
	srv := &http.Server{Handler: http.HandlerFunc(s.handler)}
 | 
			
		||||
 | 
			
		||||
	// See https://golang.org/pkg/net/http/#Server.Shutdown
 | 
			
		||||
	idleConnsClosed := make(chan struct{})
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,3 @@
 | 
			
		||||
package pkg
 | 
			
		||||
 | 
			
		||||
const VERSION = "0.12.6-stable"
 | 
			
		||||
const VERSION = "0.12.9-stable"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
[tool.black]
 | 
			
		||||
target-version = ['py38']
 | 
			
		||||
exclude = 'node_modules'
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
#!/bin/bash -xe
 | 
			
		||||
wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash
 | 
			
		||||
 | 
			
		||||
VERSION=3.8.5
 | 
			
		||||
 | 
			
		||||
wget https://www.python.org/ftp/python/$VERSION/Python-$VERSION.tgz
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user