Compare commits
	
		
			94 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e4b66d991c | |||
| 68adc2d5a5 | |||
| 349a3a67d5 | |||
| e1394207e7 | |||
| f265c1f10b | |||
| 1aecdc7f8f | |||
| a18edaf62b | |||
| c91abe448c | |||
| e531e52403 | |||
| cae536fa65 | |||
| 316b15b8a9 | |||
| e6ccd4fa76 | |||
| 86aabba3ed | |||
| 0b36aad5c8 | |||
| 64d2a216f0 | |||
| a5e5e140d6 | |||
| 29f98abd00 | |||
| 7b5ce4e98a | |||
| d7fa52ebf3 | |||
| 2ffaa94825 | |||
| b80b2626a6 | |||
| 3b7bba5a62 | |||
| 2d9efe035e | |||
| 48438e28fd | |||
| 885a2f0a58 | |||
| cf46ee06b7 | |||
| 9e33b49d29 | |||
| 1179ba4ef2 | |||
| 3c12c8b3ff | |||
| 4d22659b6e | |||
| 2c0709eeee | |||
| c24d1b6b84 | |||
| 040e148a73 | |||
| b85d550ee0 | |||
| ce95139d66 | |||
| 46436a5780 | |||
| 835a9aaaf2 | |||
| 42005e7def | |||
| d9956e1e9c | |||
| 4b1e73251a | |||
| 736dbdca33 | |||
| 789b8e5d3e | |||
| 074b55f66b | |||
| d9bc5ea4d1 | |||
| 716bb9f188 | |||
| dd496619a2 | |||
| 51d07f7913 | |||
| 5c4163579b | |||
| 5a73413d58 | |||
| 51a5d4bf49 | |||
| 8bbb854073 | |||
| 9f2e9e8444 | |||
| a3d361f500 | |||
| e9bb583b32 | |||
| efccf47c83 | |||
| a5b144cf8f | |||
| afc5a17fc2 | |||
| b3e0884b2e | |||
| 078d648551 | |||
| 41f9097592 | |||
| 562175741c | |||
| 24e24cb97e | |||
| 69b0a23a7d | |||
| f0f3245388 | |||
| 99ca0d1f9f | |||
| c9f0d048a8 | |||
| 90a94b5e3e | |||
| ae1a8842db | |||
| a3b17d1ed4 | |||
| 41576e27be | |||
| 07082cb3aa | |||
| 426cb33fab | |||
| 9e4f840d2d | |||
| e120d274e9 | |||
| 977d3f6ef9 | |||
| ecdbc917a5 | |||
| 0083cd55df | |||
| d380194e13 | |||
| 32f5d5ba72 | |||
| e818416863 | |||
| 7eed70cfe9 | |||
| ea6ca23f57 | |||
| f056b026d6 | |||
| 1c0a6efeb1 | |||
| 17732eea08 | |||
| aa5381fd59 | |||
| ffee86fcf3 | |||
| 7ff7398aff | |||
| 67925a39f2 | |||
| 3b5e1c7b34 | |||
| 3e49acf7ae | |||
| 76764c4374 | |||
| 9f6f8e1b55 | |||
| 9590180c6c | 
@ -1,5 +1,5 @@
 | 
			
		||||
[bumpversion]
 | 
			
		||||
current_version = 0.13.0-rc3
 | 
			
		||||
current_version = 0.13.2-stable
 | 
			
		||||
tag = True
 | 
			
		||||
commit = True
 | 
			
		||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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/authentik:0.13.0-rc3
 | 
			
		||||
          -t beryju/authentik:0.13.2-stable
 | 
			
		||||
          -t beryju/authentik:latest
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/authentik:0.13.0-rc3
 | 
			
		||||
        run: docker push beryju/authentik:0.13.2-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/authentik:latest
 | 
			
		||||
  build-proxy:
 | 
			
		||||
@ -48,11 +48,11 @@ jobs:
 | 
			
		||||
          cd proxy/
 | 
			
		||||
          docker build \
 | 
			
		||||
          --no-cache \
 | 
			
		||||
          -t beryju/authentik-proxy:0.13.0-rc3 \
 | 
			
		||||
          -t beryju/authentik-proxy:0.13.2-stable \
 | 
			
		||||
          -t beryju/authentik-proxy:latest \
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/authentik-proxy:0.13.0-rc3
 | 
			
		||||
        run: docker push beryju/authentik-proxy:0.13.2-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/authentik-proxy:latest
 | 
			
		||||
  build-static:
 | 
			
		||||
@ -69,11 +69,11 @@ jobs:
 | 
			
		||||
          cd web/
 | 
			
		||||
          docker build \
 | 
			
		||||
          --no-cache \
 | 
			
		||||
          -t beryju/authentik-static:0.13.0-rc3 \
 | 
			
		||||
          -t beryju/authentik-static:0.13.2-stable \
 | 
			
		||||
          -t beryju/authentik-static:latest \
 | 
			
		||||
          -f Dockerfile .
 | 
			
		||||
      - name: Push Docker Container to Registry (versioned)
 | 
			
		||||
        run: docker push beryju/authentik-static:0.13.0-rc3
 | 
			
		||||
        run: docker push beryju/authentik-static:0.13.2-stable
 | 
			
		||||
      - name: Push Docker Container to Registry (latest)
 | 
			
		||||
        run: docker push beryju/authentik-static:latest
 | 
			
		||||
  test-release:
 | 
			
		||||
@ -107,5 +107,5 @@ jobs:
 | 
			
		||||
          SENTRY_PROJECT: authentik
 | 
			
		||||
          SENTRY_URL: https://sentry.beryju.org
 | 
			
		||||
        with:
 | 
			
		||||
          tagName: 0.13.0-rc3
 | 
			
		||||
          tagName: 0.13.2-stable
 | 
			
		||||
          environment: beryjuorg-prod
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@ RUN apt-get update && \
 | 
			
		||||
 | 
			
		||||
COPY ./authentik/ /authentik
 | 
			
		||||
COPY ./pytest.ini /
 | 
			
		||||
COPY ./xml /xml
 | 
			
		||||
COPY ./manage.py /
 | 
			
		||||
COPY ./lifecycle/ /lifecycle
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Makefile
									
									
									
									
									
								
							@ -1,5 +1,10 @@
 | 
			
		||||
all: lint-fix lint coverage gen
 | 
			
		||||
 | 
			
		||||
test-full:
 | 
			
		||||
	coverage run manage.py test --failfast -v 3 .
 | 
			
		||||
	coverage html
 | 
			
		||||
	coverage report
 | 
			
		||||
 | 
			
		||||
test-integration:
 | 
			
		||||
	k3d cluster create || exit 0
 | 
			
		||||
	k3d kubeconfig write -o ~/.kube/config --overwrite
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										150
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										150
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@ -53,10 +53,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "autobahn": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b",
 | 
			
		||||
                "sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb"
 | 
			
		||||
                "sha256:491238c31f78721eaa9d0593909ab455a4ea68127aadd76ecf67185143f5f298",
 | 
			
		||||
                "sha256:72b68a1ce1e10e3cbcc3b280aae86d5b2e7a1f409febab1ab91a8a3274113f6e"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==20.7.1"
 | 
			
		||||
            "version": "==20.12.2"
 | 
			
		||||
        },
 | 
			
		||||
        "automat": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -74,18 +74,18 @@
 | 
			
		||||
        },
 | 
			
		||||
        "boto3": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:616cde1e326949020da85a5bacaa7ad287e9f117d10ac9c5bfb9150a98dfe1a7",
 | 
			
		||||
                "sha256:ddad9ada00eccae1fc2da28c69531ba202fead562994ddcd9a9a232e993cd8a2"
 | 
			
		||||
                "sha256:18d7ba5d623d4794f439201ab900c9c14a50019bc52d9113b0a2bb2e1ef9af2c",
 | 
			
		||||
                "sha256:1ddfd307d409e7bc792bd12923078f59c2f56fbba4065c320b3f768481bbbbf7"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.16.34"
 | 
			
		||||
            "version": "==1.16.38"
 | 
			
		||||
        },
 | 
			
		||||
        "botocore": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:49f5e56a7382a65ee0873371edcd91bdba8fc3f70abe102ebc1a0da2e6fbed06",
 | 
			
		||||
                "sha256:4d81d92127ef646ae0f0ee84c9c220c92fa82312e765c29f8cb3b000fdbdd038"
 | 
			
		||||
                "sha256:1f1ecb1b0c6ffc8fcdd5eeb40f33e986dfe9724dc66c83017014a0506af6378a",
 | 
			
		||||
                "sha256:38ccc132c5b9d1e7a4dd37af78061fd2dd0e4fd611f527b409a4e9a679a85cdb"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.19.34"
 | 
			
		||||
            "version": "==1.19.38"
 | 
			
		||||
        },
 | 
			
		||||
        "cachetools": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -96,11 +96,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "celery": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:45bb7909061862305cefec94289fabc1b89ac004680f4dc7d9dea642a2507e53",
 | 
			
		||||
                "sha256:533f3635065b7ed362ffc04228635b4c82d53a9ab812118ccdedb5eae281fb97"
 | 
			
		||||
                "sha256:5e8d364e058554e83bbb116e8377d90c79be254785f357cb2cec026e79febe13",
 | 
			
		||||
                "sha256:f4efebe6f8629b0da2b8e529424de376494f5b7a743c321c8a2ddc2b1414921c"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==5.0.4"
 | 
			
		||||
            "version": "==5.0.5"
 | 
			
		||||
        },
 | 
			
		||||
        "certifi": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -168,10 +168,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "chardet": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
 | 
			
		||||
                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
 | 
			
		||||
                "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
 | 
			
		||||
                "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==3.0.4"
 | 
			
		||||
            "version": "==4.0.0"
 | 
			
		||||
        },
 | 
			
		||||
        "click": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -343,11 +343,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django-storages": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
 | 
			
		||||
                "sha256:652275ab7844538c462b62810276c0244866f345878256a9e0e86f5b1283ae18"
 | 
			
		||||
                "sha256:056ec3e9e2b0c6f363913976072ffba2923e79e4859578047da139ba1637497e",
 | 
			
		||||
                "sha256:7af56611c62a1c174aab4e862efb7fdd98296dccf76f42135f5b6851fc313c97"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.10.1"
 | 
			
		||||
            "version": "==1.11"
 | 
			
		||||
        },
 | 
			
		||||
        "djangorestframework": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -396,10 +396,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "google-auth": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:5176db85f1e7e837a646cd9cede72c3c404ccf2e3373d9ee14b2db88febad440",
 | 
			
		||||
                "sha256:b728625ff5dfce8f9e56a499c8a4eb51443a67f20f6d28b67d5774c310ec4b6b"
 | 
			
		||||
                "sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e",
 | 
			
		||||
                "sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.23.0"
 | 
			
		||||
            "version": "==1.24.0"
 | 
			
		||||
        },
 | 
			
		||||
        "gunicorn": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -646,26 +646,46 @@
 | 
			
		||||
        },
 | 
			
		||||
        "msgpack": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408",
 | 
			
		||||
                "sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8",
 | 
			
		||||
                "sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84",
 | 
			
		||||
                "sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d",
 | 
			
		||||
                "sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a",
 | 
			
		||||
                "sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322",
 | 
			
		||||
                "sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2",
 | 
			
		||||
                "sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e",
 | 
			
		||||
                "sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97",
 | 
			
		||||
                "sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0",
 | 
			
		||||
                "sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be",
 | 
			
		||||
                "sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf",
 | 
			
		||||
                "sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab",
 | 
			
		||||
                "sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08",
 | 
			
		||||
                "sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e",
 | 
			
		||||
                "sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272",
 | 
			
		||||
                "sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1",
 | 
			
		||||
                "sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140"
 | 
			
		||||
                "sha256:01835e300967e5ad6fdbfc36eafe74df67ff47e16e0d6dee8766630550315903",
 | 
			
		||||
                "sha256:03c5554315317d76c25a15569dd52ac6047b105df71e861f24faf9675672b72d",
 | 
			
		||||
                "sha256:0968b368a9a9081435bfcb7a57a1e8b75c7bf038ef911b369acd2e038c7f873a",
 | 
			
		||||
                "sha256:1d7ab166401f7789bf11262439336c0a01b878f0d602e48f35c35d2e3a555820",
 | 
			
		||||
                "sha256:1e8d27bac821f8aa909904a704a67e5e8bc2e42b153415fc3621b7afbc06702b",
 | 
			
		||||
                "sha256:1fc9f21da9fd77088ebfd3c9941b044ca3f4a048e85f7ff5727f26bcdbffed61",
 | 
			
		||||
                "sha256:20196229acc193939223118c7420838749d5b0cece49cd397739a3a6ffcfe2d1",
 | 
			
		||||
                "sha256:2933443313289725f16bd7b99a8c3aa6a2cca1549e661d7407f056a0af80bf7b",
 | 
			
		||||
                "sha256:2966b155356fd231fa441131d7301e1596ee38974ad56dc57fd752fdbe2bb63f",
 | 
			
		||||
                "sha256:29a6fb3729215b6fcab786ef4f460a5406a5c056f7021191f70ff7712a3f6ba4",
 | 
			
		||||
                "sha256:35cbefa7d7bddfb4b0770a1b9ff721cd8dfe9a680947a68457974d5e3e6acc2f",
 | 
			
		||||
                "sha256:35ff1ac162a77fb78be360d9f771d36cbf1202e94fc6d70e284ad5db6ab72608",
 | 
			
		||||
                "sha256:40dd1ac7420f071e96b3e4a4a7b8e69546a6f8065ff5995dbacf53f86207eb98",
 | 
			
		||||
                "sha256:4bea1938e484c9caca9585105f447d6807c496c153b7244fa726b3cc4a68ec9e",
 | 
			
		||||
                "sha256:4e58b9f4a99bc3a90859bb006ec4422448a5ce39e5cd6e7498c56de5dcec9c34",
 | 
			
		||||
                "sha256:66d47e952856bfcde46d8351380d0b5b928a73112b66bc06d5367dfcc077c06a",
 | 
			
		||||
                "sha256:69f6aa503378548ea1e760c11aeb6fc91952bf3634fd806a38a0e47edb507fcd",
 | 
			
		||||
                "sha256:7033215267a0e9f60f4a5e4fb2228a932c404f237817caff8dc3115d9e7cd975",
 | 
			
		||||
                "sha256:7b50afd767cc053ad92fad39947c3670db27305fd1c49acded44d9d9ac8b56fd",
 | 
			
		||||
                "sha256:99ea9e65876546743b2b8bb5bc7adefbb03b9da78a899827467da197a48f790b",
 | 
			
		||||
                "sha256:abcc62303ac4d789878d4aac4cdba1bbe2adb478d67be99cd4a6d56ac3a4028f",
 | 
			
		||||
                "sha256:b107f9b36665bf7d7c6176a938a361a7aba16aa179d833919448f77287866484",
 | 
			
		||||
                "sha256:b5b27923b6c98a2616b7e906a29e4e10e1b4424aea87a0e0d5636327dc6ea315",
 | 
			
		||||
                "sha256:bf8eedc7bfbf63cbc9abe58287c32d78780a347835e82c23033c68f11f80bb05",
 | 
			
		||||
                "sha256:c144ff4954a6ea40aa603600c8be175349588fc68696092889fa34ab6e055060",
 | 
			
		||||
                "sha256:c4e5f96a1d0d916ce7a16decb7499e8923ddef007cf7d68412fb68767766648a",
 | 
			
		||||
                "sha256:c60e8b2bf754b8dcc1075c5bee0b177ed9193e7cbd2377faaf507120a948e697",
 | 
			
		||||
                "sha256:c82fc6cdba5737eb6ed0c926a30a5d56e7b050297375a16d6c5ad89b576fd979",
 | 
			
		||||
                "sha256:ce4ebe2c79411cd5671b20862831880e7850a2de699cff6626f48853fde61ae6",
 | 
			
		||||
                "sha256:d113c6b1239c62669ef3063693842605a3edbfebc39a333cf91ba60d314afe6d",
 | 
			
		||||
                "sha256:d3cea07ad16919a44e8d1ea67efa5244855cdce807d672f41694acc24d08834e",
 | 
			
		||||
                "sha256:d76672602db16e3f44bc1a85c7ee5f15d79e02fcf5bc9d133c2954753be6eddc",
 | 
			
		||||
                "sha256:decf2091b75987ca2564e3b742f9614eb7d57e39ff04eaa68af7a3fc5648f7ed",
 | 
			
		||||
                "sha256:e13b9007af66a3f62574bc0a13843df0e4402f5ee4b00a02aa1803f01d26b9fb",
 | 
			
		||||
                "sha256:e157edf4213dacafb0f862e0b7a3a18448250cec91aa1334f432f49028acc650",
 | 
			
		||||
                "sha256:e234ff83628ca3ab345bf97fb36ccbf6d2f1700f5e08868643bf4489edc960f8",
 | 
			
		||||
                "sha256:f08d9dd3ce0c5e972dc4653f0fb66d2703941e65356388c13032b578dd718261",
 | 
			
		||||
                "sha256:f20d7d4f1f0728560408ba6933154abccf0c20f24642a2404b43d5c23e4119ab"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.0.0"
 | 
			
		||||
            "version": "==1.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "oauthlib": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -676,11 +696,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "packaging": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
 | 
			
		||||
                "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
 | 
			
		||||
                "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
 | 
			
		||||
                "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==20.7"
 | 
			
		||||
            "version": "==20.8"
 | 
			
		||||
        },
 | 
			
		||||
        "prometheus-client": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -855,10 +875,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pyopenssl": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:898aefbde331ba718570244c3b01dcddb1b31a3b336613436a45e52e27d9a82d",
 | 
			
		||||
                "sha256:92f08eccbd73701cf744e8ffd6989aa7842d48cbe3fea8a7c031c5647f590ac5"
 | 
			
		||||
                "sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51",
 | 
			
		||||
                "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==20.0.0"
 | 
			
		||||
            "version": "==20.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "pyparsing": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -930,10 +950,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "requests": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
 | 
			
		||||
                "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
 | 
			
		||||
                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
 | 
			
		||||
                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==2.25.0"
 | 
			
		||||
            "version": "==2.25.1"
 | 
			
		||||
        },
 | 
			
		||||
        "requests-oauthlib": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -948,7 +968,7 @@
 | 
			
		||||
                "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa",
 | 
			
		||||
                "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.5'",
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==4.6"
 | 
			
		||||
        },
 | 
			
		||||
        "ruamel.yaml": {
 | 
			
		||||
@ -1073,11 +1093,11 @@
 | 
			
		||||
                "standard"
 | 
			
		||||
            ],
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:28420526640d800aabe648038f8e2ea8ba2a8bdc363002eecd5dfc57a0f75ab7",
 | 
			
		||||
                "sha256:5123606e0f1d15ffbe0f63161c5078f7c28f350c5eb102435671eae58046db0f"
 | 
			
		||||
                "sha256:2a7b17f4d9848d6557ccc2274a5f7c97f1daf037d130a0c6918f67cd9bc8cdf5",
 | 
			
		||||
                "sha256:6fcce74c00b77d4f4b3ed7ba1b2a370d27133bfdb46f835b7a76dfe0a8c110ae"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==0.13.0"
 | 
			
		||||
            "version": "==0.13.1"
 | 
			
		||||
        },
 | 
			
		||||
        "uvloop": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1263,11 +1283,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "bandit": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2ff3fe35fe3212c0be5fc9c4899bd0108e2b5239c5ff62fb174639e4660fe958",
 | 
			
		||||
                "sha256:d02dfe250f4aa2d166c127ad81d192579e2bfcdb8501717c0e2005e35a6bcf60"
 | 
			
		||||
                "sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07",
 | 
			
		||||
                "sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.6.3"
 | 
			
		||||
            "version": "==1.7.0"
 | 
			
		||||
        },
 | 
			
		||||
        "black": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1453,11 +1473,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "packaging": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
 | 
			
		||||
                "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
 | 
			
		||||
                "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
 | 
			
		||||
                "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==20.7"
 | 
			
		||||
            "version": "==20.8"
 | 
			
		||||
        },
 | 
			
		||||
        "pathspec": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1496,10 +1516,10 @@
 | 
			
		||||
        },
 | 
			
		||||
        "py": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
 | 
			
		||||
                "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
 | 
			
		||||
                "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
 | 
			
		||||
                "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
 | 
			
		||||
            ],
 | 
			
		||||
            "version": "==1.9.0"
 | 
			
		||||
            "version": "==1.10.0"
 | 
			
		||||
        },
 | 
			
		||||
        "pycodestyle": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1566,11 +1586,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "pytest": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
 | 
			
		||||
                "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
 | 
			
		||||
                "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8",
 | 
			
		||||
                "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==6.1.2"
 | 
			
		||||
            "version": "==6.2.1"
 | 
			
		||||
        },
 | 
			
		||||
        "pytest-django": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
 | 
			
		||||
@ -6,9 +6,9 @@ As authentik is currently in a pre-stable, only the latest "stable" version is s
 | 
			
		||||
 | 
			
		||||
| Version  | Supported          |
 | 
			
		||||
| -------- | ------------------ |
 | 
			
		||||
| 0.10.x   | :white_check_mark: |
 | 
			
		||||
| 0.11.x   | :white_check_mark: |
 | 
			
		||||
| 0.12.x   | :white_check_mark: |
 | 
			
		||||
| 0.13.x   | :white_check_mark: |
 | 
			
		||||
 | 
			
		||||
## Reporting a Vulnerability
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,2 @@
 | 
			
		||||
"""authentik"""
 | 
			
		||||
__version__ = "0.13.0-rc3"
 | 
			
		||||
__version__ = "0.13.2-stable"
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
"""authentik administration overview"""
 | 
			
		||||
"""authentik administration metrics"""
 | 
			
		||||
import time
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
@ -47,7 +47,7 @@ def get_events_per_1h(**filter_kwargs) -> List[Dict[str, int]]:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdministrationMetricsSerializer(Serializer):
 | 
			
		||||
    """Overview View"""
 | 
			
		||||
    """Login Metrics per 1h"""
 | 
			
		||||
 | 
			
		||||
    logins_per_1h = SerializerMethodField()
 | 
			
		||||
    logins_failed_per_1h = SerializerMethodField()
 | 
			
		||||
@ -68,12 +68,12 @@ class AdministrationMetricsSerializer(Serializer):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdministrationMetricsViewSet(ViewSet):
 | 
			
		||||
    """Return single instance of AdministrationMetricsSerializer"""
 | 
			
		||||
    """Login Metrics per 1h"""
 | 
			
		||||
 | 
			
		||||
    permission_classes = [IsAdminUser]
 | 
			
		||||
 | 
			
		||||
    @swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)})
 | 
			
		||||
    def list(self, request: Request) -> Response:
 | 
			
		||||
        """Return single instance of AdministrationMetricsSerializer"""
 | 
			
		||||
        """Login Metrics per 1h"""
 | 
			
		||||
        serializer = AdministrationMetricsSerializer(True)
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
@ -1,79 +0,0 @@
 | 
			
		||||
"""authentik administration overview"""
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from drf_yasg2.utils import swagger_auto_schema
 | 
			
		||||
from rest_framework.fields import SerializerMethodField
 | 
			
		||||
from rest_framework.permissions import IsAdminUser
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
from rest_framework.viewsets import ViewSet
 | 
			
		||||
 | 
			
		||||
from authentik import __version__
 | 
			
		||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
 | 
			
		||||
from authentik.core.models import Provider
 | 
			
		||||
from authentik.policies.models import Policy
 | 
			
		||||
from authentik.root.celery import CELERY_APP
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdministrationOverviewSerializer(Serializer):
 | 
			
		||||
    """Overview View"""
 | 
			
		||||
 | 
			
		||||
    version = SerializerMethodField()
 | 
			
		||||
    version_latest = SerializerMethodField()
 | 
			
		||||
    worker_count = SerializerMethodField()
 | 
			
		||||
    providers_without_application = SerializerMethodField()
 | 
			
		||||
    policies_without_binding = SerializerMethodField()
 | 
			
		||||
    cached_policies = SerializerMethodField()
 | 
			
		||||
    cached_flows = SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_version(self, _) -> str:
 | 
			
		||||
        """Get current version"""
 | 
			
		||||
        return __version__
 | 
			
		||||
 | 
			
		||||
    def get_version_latest(self, _) -> str:
 | 
			
		||||
        """Get latest version from cache"""
 | 
			
		||||
        version_in_cache = cache.get(VERSION_CACHE_KEY)
 | 
			
		||||
        if not version_in_cache:
 | 
			
		||||
            update_latest_version.delay()
 | 
			
		||||
            return __version__
 | 
			
		||||
        return version_in_cache
 | 
			
		||||
 | 
			
		||||
    def get_worker_count(self, _) -> int:
 | 
			
		||||
        """Ping workers"""
 | 
			
		||||
        return len(CELERY_APP.control.ping(timeout=0.5))
 | 
			
		||||
 | 
			
		||||
    def get_providers_without_application(self, _) -> int:
 | 
			
		||||
        """Count of providers without application"""
 | 
			
		||||
        return len(Provider.objects.filter(application=None))
 | 
			
		||||
 | 
			
		||||
    def get_policies_without_binding(self, _) -> int:
 | 
			
		||||
        """Count of policies not bound or use in prompt stages"""
 | 
			
		||||
        return len(
 | 
			
		||||
            Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def get_cached_policies(self, _) -> int:
 | 
			
		||||
        """Get cached policy count"""
 | 
			
		||||
        return len(cache.keys("policy_*"))
 | 
			
		||||
 | 
			
		||||
    def get_cached_flows(self, _) -> int:
 | 
			
		||||
        """Get cached flow count"""
 | 
			
		||||
        return len(cache.keys("flow_*"))
 | 
			
		||||
 | 
			
		||||
    def create(self, request: Request) -> Response:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def update(self, request: Request) -> Response:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdministrationOverviewViewSet(ViewSet):
 | 
			
		||||
    """Return single instance of AdministrationOverviewSerializer"""
 | 
			
		||||
 | 
			
		||||
    permission_classes = [IsAdminUser]
 | 
			
		||||
 | 
			
		||||
    @swagger_auto_schema(responses={200: AdministrationOverviewSerializer(many=True)})
 | 
			
		||||
    def list(self, request: Request) -> Response:
 | 
			
		||||
        """Return single instance of AdministrationOverviewSerializer"""
 | 
			
		||||
        serializer = AdministrationOverviewSerializer(True)
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
@ -66,7 +66,7 @@ class TaskViewSet(ViewSet):
 | 
			
		||||
                    "successful": True,
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        except ImportError:
 | 
			
		||||
        except ImportError:  # pragma: no cover
 | 
			
		||||
            # if we get an import error, the module path has probably changed
 | 
			
		||||
            task.delete()
 | 
			
		||||
            return Response({"successful": False})
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										60
									
								
								authentik/admin/api/version.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								authentik/admin/api/version.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
			
		||||
"""authentik administration overview"""
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from drf_yasg2.utils import swagger_auto_schema
 | 
			
		||||
from packaging.version import parse
 | 
			
		||||
from rest_framework.fields import SerializerMethodField
 | 
			
		||||
from rest_framework.mixins import ListModelMixin
 | 
			
		||||
from rest_framework.permissions import IsAdminUser
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
from rest_framework.viewsets import GenericViewSet
 | 
			
		||||
 | 
			
		||||
from authentik import __version__
 | 
			
		||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VersionSerializer(Serializer):
 | 
			
		||||
    """Get running and latest version."""
 | 
			
		||||
 | 
			
		||||
    version_current = SerializerMethodField()
 | 
			
		||||
    version_latest = SerializerMethodField()
 | 
			
		||||
    outdated = SerializerMethodField()
 | 
			
		||||
 | 
			
		||||
    def get_version_current(self, _) -> str:
 | 
			
		||||
        """Get current version"""
 | 
			
		||||
        return __version__
 | 
			
		||||
 | 
			
		||||
    def get_version_latest(self, _) -> str:
 | 
			
		||||
        """Get latest version from cache"""
 | 
			
		||||
        version_in_cache = cache.get(VERSION_CACHE_KEY)
 | 
			
		||||
        if not version_in_cache:  # pragma: no cover
 | 
			
		||||
            update_latest_version.delay()
 | 
			
		||||
            return __version__
 | 
			
		||||
        return version_in_cache
 | 
			
		||||
 | 
			
		||||
    def get_outdated(self, instance) -> bool:
 | 
			
		||||
        """Check if we're running the latest version"""
 | 
			
		||||
        return parse(self.get_version_current(instance)) < parse(
 | 
			
		||||
            self.get_version_latest(instance)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def create(self, request: Request) -> Response:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def update(self, request: Request) -> Response:
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VersionViewSet(ListModelMixin, GenericViewSet):
 | 
			
		||||
    """Get running and latest version."""
 | 
			
		||||
 | 
			
		||||
    permission_classes = [IsAdminUser]
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @swagger_auto_schema(responses={200: VersionSerializer(many=True)})
 | 
			
		||||
    def list(self, request: Request) -> Response:
 | 
			
		||||
        """Get running and latest version."""
 | 
			
		||||
        return Response(VersionSerializer(True).data)
 | 
			
		||||
							
								
								
									
										25
									
								
								authentik/admin/api/workers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								authentik/admin/api/workers.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
"""authentik administration overview"""
 | 
			
		||||
from rest_framework.mixins import ListModelMixin
 | 
			
		||||
from rest_framework.permissions import IsAdminUser
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
from rest_framework.viewsets import GenericViewSet
 | 
			
		||||
 | 
			
		||||
from authentik.root.celery import CELERY_APP
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WorkerViewSet(ListModelMixin, GenericViewSet):
 | 
			
		||||
    """Get currently connected worker count."""
 | 
			
		||||
 | 
			
		||||
    serializer_class = Serializer
 | 
			
		||||
    permission_classes = [IsAdminUser]
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def list(self, request: Request) -> Response:
 | 
			
		||||
        """Get currently connected worker count."""
 | 
			
		||||
        return Response(
 | 
			
		||||
            {"pagination": {"count": len(CELERY_APP.control.ping(timeout=0.5))}}
 | 
			
		||||
        )
 | 
			
		||||
@ -1,230 +0,0 @@
 | 
			
		||||
{% extends "administration/base.html" %}
 | 
			
		||||
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
{% load static %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<section class="pf-c-page__main-section pf-m-light">
 | 
			
		||||
    <div class="pf-c-content">
 | 
			
		||||
        <h1>{% trans 'System Overview' %}</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
<section class="pf-c-page__main-section">
 | 
			
		||||
    <div class="pf-l-gallery pf-m-gutter">
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-server"></i> {% trans 'Logins over the last 24 hours' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <ak-admin-logins-chart url="{% url 'authentik_api:admin_metrics-list' %}"></ak-admin-logins-chart>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 2;grid-row-end: span 3;">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-server"></i> {% trans 'Apps with most usage' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <table class="pf-c-table pf-m-compact" role="grid">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                        <tr role="row">
 | 
			
		||||
                            <th role="columnheader" scope="col">{% trans 'Application' %}</th>
 | 
			
		||||
                            <th role="columnheader" scope="col">{% trans 'Logins' %}</th>
 | 
			
		||||
                            <th role="columnheader" scope="col"></th>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody role="rowgroup">
 | 
			
		||||
                        {% for app in most_used_applications %}
 | 
			
		||||
                        <tr role="row">
 | 
			
		||||
                            <td role="cell">
 | 
			
		||||
                                {{ app.application.name }}
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <td role="cell">
 | 
			
		||||
                                {{ app.total_logins }}
 | 
			
		||||
                            </td>
 | 
			
		||||
                            <td role="cell">
 | 
			
		||||
                                <progress value="{{ app.total_logins }}" max="{{ most_used_applications.0.total_logins }}"></progress>
 | 
			
		||||
                            </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                </table>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a href="{% url 'authentik_admin:providers' %}">
 | 
			
		||||
                    <i class="fa fa-external-link-alt"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if providers_without_application.exists %}
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ provider_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a href="{% url 'authentik_admin:policies' %}">
 | 
			
		||||
                    <i class="fa fa-external-link-alt"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if policies_without_binding %}
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'Policies without binding exist.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ policy_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a href="{% url 'authentik_admin:users' %}">
 | 
			
		||||
                    <i class="fa fa-external-link-alt"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ user_count }}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <a href="https://github.com/BeryJu/authentik/releases" target="_blank">
 | 
			
		||||
                    <i class="fa fa-external-link-alt"> </i>
 | 
			
		||||
                </a>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    {% if version >= version_latest %}
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ version }}
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ version }}
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% if version >= version_latest %}
 | 
			
		||||
                    {% blocktrans %}
 | 
			
		||||
                    Up-to-date!
 | 
			
		||||
                    {% endblocktrans %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    {% blocktrans with latest=version_latest %}
 | 
			
		||||
                    {{ latest }} is available!
 | 
			
		||||
                    {% endblocktrans %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <fetch-fill-slot class="pf-c-card__body" url="{% url 'authentik_api:admin_overview-list' %}" key="worker_count">
 | 
			
		||||
                <div slot="value < 1">
 | 
			
		||||
                    <p class="ak-aggregate-card">
 | 
			
		||||
                        <i class="fa fa-exclamation-triangle"></i> <span data-value></span>
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>{% trans 'No workers connected.' %}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div slot="value >= 1">
 | 
			
		||||
                    <p class="ak-aggregate-card">
 | 
			
		||||
                        <i class="fa fa-check-circle"></i> <span data-value></span>
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>
 | 
			
		||||
                    <span class="pf-c-spinner" role="progressbar" aria-valuetext="Loading...">
 | 
			
		||||
                        <span class="pf-c-spinner__clipper"></span>
 | 
			
		||||
                        <span class="pf-c-spinner__lead-ball"></span>
 | 
			
		||||
                        <span class="pf-c-spinner__tail-ball"></span>
 | 
			
		||||
                    </span>
 | 
			
		||||
                </div>
 | 
			
		||||
            </fetch-fill-slot>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <ak-modal-button href="{% url 'authentik_admin:overview-clear-policy-cache' %}">
 | 
			
		||||
                    <a slot="trigger">
 | 
			
		||||
                        <i class="fa fa-trash"> </i>
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <div slot="modal"></div>
 | 
			
		||||
                </ak-modal-button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if cached_policies < 1 %}
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ cached_policies }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-compact">
 | 
			
		||||
            <div class="pf-c-card__header pf-l-flex pf-m-justify-content-space-between">
 | 
			
		||||
                <div class="pf-c-card__header-main">
 | 
			
		||||
                    <i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
 | 
			
		||||
                </div>
 | 
			
		||||
                <ak-modal-button href="{% url 'authentik_admin:overview-clear-flow-cache' %}">
 | 
			
		||||
                    <a slot="trigger">
 | 
			
		||||
                        <i class="fa fa-trash"> </i>
 | 
			
		||||
                    </a>
 | 
			
		||||
                    <div slot="modal"></div>
 | 
			
		||||
                </ak-modal-button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="pf-c-card__body">
 | 
			
		||||
                {% if cached_flows < 1 %}
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    <span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
 | 
			
		||||
                </p>
 | 
			
		||||
                <p>{% trans 'No flows cached.' %}</p>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                <p class="ak-aggregate-card">
 | 
			
		||||
                    <i class="fa fa-check-circle"></i> {{ cached_flows }}
 | 
			
		||||
                </p>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</section>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
							
								
								
									
										73
									
								
								authentik/admin/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								authentik/admin/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,73 @@
 | 
			
		||||
"""test admin api"""
 | 
			
		||||
from json import loads
 | 
			
		||||
 | 
			
		||||
from django.shortcuts import reverse
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from authentik import __version__
 | 
			
		||||
from authentik.core.models import Group, User
 | 
			
		||||
from authentik.core.tasks import clean_expired_models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAdminAPI(TestCase):
 | 
			
		||||
    """test admin api"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.user = User.objects.create(username="test-user")
 | 
			
		||||
        self.group = Group.objects.create(name="superusers", is_superuser=True)
 | 
			
		||||
        self.group.users.add(self.user)
 | 
			
		||||
        self.group.save()
 | 
			
		||||
        self.client.force_login(self.user)
 | 
			
		||||
 | 
			
		||||
    def test_tasks(self):
 | 
			
		||||
        """Test Task API"""
 | 
			
		||||
        clean_expired_models.delay()
 | 
			
		||||
        response = self.client.get(reverse("authentik_api:admin_system_tasks-list"))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        body = loads(response.content)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            any([task["task_name"] == "clean_expired_models" for task in body])
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_tasks_retry(self):
 | 
			
		||||
        """Test Task API (retry)"""
 | 
			
		||||
        clean_expired_models.delay()
 | 
			
		||||
        response = self.client.post(
 | 
			
		||||
            reverse(
 | 
			
		||||
                "authentik_api:admin_system_tasks-retry",
 | 
			
		||||
                kwargs={"pk": "clean_expired_models"},
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        body = loads(response.content)
 | 
			
		||||
        self.assertTrue(body["successful"])
 | 
			
		||||
 | 
			
		||||
    def test_tasks_retry_404(self):
 | 
			
		||||
        """Test Task API (retry, 404)"""
 | 
			
		||||
        response = self.client.post(
 | 
			
		||||
            reverse(
 | 
			
		||||
                "authentik_api:admin_system_tasks-retry",
 | 
			
		||||
                kwargs={"pk": "qwerqewrqrqewrqewr"},
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(response.status_code, 404)
 | 
			
		||||
 | 
			
		||||
    def test_version(self):
 | 
			
		||||
        """Test Version API"""
 | 
			
		||||
        response = self.client.get(reverse("authentik_api:admin_version-list"))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        body = loads(response.content)
 | 
			
		||||
        self.assertEqual(body["version_current"], __version__)
 | 
			
		||||
 | 
			
		||||
    def test_workers(self):
 | 
			
		||||
        """Test Workers API"""
 | 
			
		||||
        response = self.client.get(reverse("authentik_api:admin_workers-list"))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        body = loads(response.content)
 | 
			
		||||
        self.assertEqual(body["pagination"]["count"], 0)
 | 
			
		||||
 | 
			
		||||
    def test_metrics(self):
 | 
			
		||||
        """Test metrics API"""
 | 
			
		||||
        response = self.client.get(reverse("authentik_api:admin_metrics-list"))
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
@ -1,9 +1,13 @@
 | 
			
		||||
"""admin tests"""
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.test.client import RequestFactory
 | 
			
		||||
 | 
			
		||||
from authentik.admin.views.policies_bindings import PolicyBindingCreateView
 | 
			
		||||
from authentik.core.models import Application
 | 
			
		||||
from authentik.policies.forms import PolicyBindingForm
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestPolicyBindingView(TestCase):
 | 
			
		||||
@ -18,9 +22,22 @@ class TestPolicyBindingView(TestCase):
 | 
			
		||||
        view = PolicyBindingCreateView(request=request)
 | 
			
		||||
        self.assertEqual(view.get_initial(), {})
 | 
			
		||||
 | 
			
		||||
    def test_with_param(self):
 | 
			
		||||
    def test_with_params_invalid(self):
 | 
			
		||||
        """Test PolicyBindingCreateView with invalid get params"""
 | 
			
		||||
        request = self.factory.get("/", {"target": uuid4()})
 | 
			
		||||
        view = PolicyBindingCreateView(request=request)
 | 
			
		||||
        self.assertEqual(view.get_initial(), {})
 | 
			
		||||
 | 
			
		||||
    def test_with_params(self):
 | 
			
		||||
        """Test PolicyBindingCreateView with get params"""
 | 
			
		||||
        target = Application.objects.create(name="test")
 | 
			
		||||
        request = self.factory.get("/", {"target": target.pk.hex})
 | 
			
		||||
        view = PolicyBindingCreateView(request=request)
 | 
			
		||||
        self.assertEqual(view.get_initial(), {"target": target, "order": 0})
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            isinstance(
 | 
			
		||||
                PolicyBindingForm(initial={"target": "foo"}).fields["target"].widget,
 | 
			
		||||
                forms.HiddenInput,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,12 @@
 | 
			
		||||
"""admin tests"""
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.test.client import RequestFactory
 | 
			
		||||
 | 
			
		||||
from authentik.admin.views.stages_bindings import StageBindingCreateView
 | 
			
		||||
from authentik.flows.forms import FlowStageBindingForm
 | 
			
		||||
from authentik.flows.models import Flow
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,9 +22,22 @@ class TestStageBindingView(TestCase):
 | 
			
		||||
        view = StageBindingCreateView(request=request)
 | 
			
		||||
        self.assertEqual(view.get_initial(), {})
 | 
			
		||||
 | 
			
		||||
    def test_with_param(self):
 | 
			
		||||
    def test_with_params_invalid(self):
 | 
			
		||||
        """Test StageBindingCreateView with invalid get params"""
 | 
			
		||||
        request = self.factory.get("/", {"target": uuid4()})
 | 
			
		||||
        view = StageBindingCreateView(request=request)
 | 
			
		||||
        self.assertEqual(view.get_initial(), {})
 | 
			
		||||
 | 
			
		||||
    def test_with_params(self):
 | 
			
		||||
        """Test StageBindingCreateView with get params"""
 | 
			
		||||
        target = Flow.objects.create(name="test", slug="test")
 | 
			
		||||
        request = self.factory.get("/", {"target": target.pk.hex})
 | 
			
		||||
        view = StageBindingCreateView(request=request)
 | 
			
		||||
        self.assertEqual(view.get_initial(), {"target": target, "order": 0})
 | 
			
		||||
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            isinstance(
 | 
			
		||||
                FlowStageBindingForm(initial={"target": "foo"}).fields["target"].widget,
 | 
			
		||||
                forms.HiddenInput,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,6 @@ urlpatterns = [
 | 
			
		||||
        overview.PolicyCacheClearView.as_view(),
 | 
			
		||||
        name="overview-clear-policy-cache",
 | 
			
		||||
    ),
 | 
			
		||||
    path("overview/", overview.AdministrationOverviewView.as_view(), name="overview"),
 | 
			
		||||
    # Applications
 | 
			
		||||
    path(
 | 
			
		||||
        "applications/", applications.ApplicationListView.as_view(), name="applications"
 | 
			
		||||
 | 
			
		||||
@ -1,65 +1,25 @@
 | 
			
		||||
"""authentik administration overview"""
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.contrib.messages.views import SuccessMessageMixin
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.http.request import HttpRequest
 | 
			
		||||
from django.http.response import HttpResponse
 | 
			
		||||
from django.urls import reverse_lazy
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from django.views.generic import FormView, TemplateView
 | 
			
		||||
from packaging.version import LegacyVersion, Version, parse
 | 
			
		||||
from django.views.generic import FormView
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik import __version__
 | 
			
		||||
from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm
 | 
			
		||||
from authentik.admin.mixins import AdminRequiredMixin
 | 
			
		||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
 | 
			
		||||
from authentik.core.models import Provider, User
 | 
			
		||||
from authentik.policies.models import Policy
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
 | 
			
		||||
    """Overview View"""
 | 
			
		||||
 | 
			
		||||
    template_name = "administration/overview.html"
 | 
			
		||||
 | 
			
		||||
    def get_latest_version(self) -> Union[LegacyVersion, Version]:
 | 
			
		||||
        """Get latest version from cache"""
 | 
			
		||||
        version_in_cache = cache.get(VERSION_CACHE_KEY)
 | 
			
		||||
        if not version_in_cache:
 | 
			
		||||
            if not settings.DEBUG:
 | 
			
		||||
                update_latest_version.delay()
 | 
			
		||||
            return parse(__version__)
 | 
			
		||||
        return parse(version_in_cache)
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs):
 | 
			
		||||
        kwargs["policy_count"] = len(Policy.objects.all())
 | 
			
		||||
        kwargs["user_count"] = len(User.objects.all()) - 1  # Remove anonymous user
 | 
			
		||||
        kwargs["provider_count"] = len(Provider.objects.all())
 | 
			
		||||
        kwargs["version"] = parse(__version__)
 | 
			
		||||
        kwargs["version_latest"] = self.get_latest_version()
 | 
			
		||||
        kwargs["providers_without_application"] = Provider.objects.filter(
 | 
			
		||||
            application=None
 | 
			
		||||
        )
 | 
			
		||||
        kwargs["policies_without_binding"] = len(
 | 
			
		||||
            Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
 | 
			
		||||
        )
 | 
			
		||||
        kwargs["cached_policies"] = len(cache.keys("policy_*"))
 | 
			
		||||
        kwargs["cached_flows"] = len(cache.keys("flow_*"))
 | 
			
		||||
        return super().get_context_data(**kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
 | 
			
		||||
    """View to clear Policy cache"""
 | 
			
		||||
 | 
			
		||||
    form_class = PolicyCacheClearForm
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/form_non_model.html"
 | 
			
		||||
    success_url = reverse_lazy("authentik_admin:overview")
 | 
			
		||||
    success_url = "/"
 | 
			
		||||
    success_message = _("Successfully cleared Policy cache")
 | 
			
		||||
 | 
			
		||||
    def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
			
		||||
@ -75,7 +35,7 @@ class FlowCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView):
 | 
			
		||||
    form_class = FlowCacheClearForm
 | 
			
		||||
 | 
			
		||||
    template_name = "generic/form_non_model.html"
 | 
			
		||||
    success_url = reverse_lazy("authentik_admin:overview")
 | 
			
		||||
    success_url = "/"
 | 
			
		||||
    success_message = _("Successfully cleared Flow cache")
 | 
			
		||||
 | 
			
		||||
    def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
"""API Authentication"""
 | 
			
		||||
from base64 import b64decode
 | 
			
		||||
from binascii import Error
 | 
			
		||||
from typing import Any, Optional, Tuple, Union
 | 
			
		||||
 | 
			
		||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
 | 
			
		||||
@ -24,7 +25,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
 | 
			
		||||
            return None
 | 
			
		||||
    try:
 | 
			
		||||
        auth_credentials = b64decode(auth_credentials.encode()).decode()
 | 
			
		||||
    except UnicodeDecodeError:
 | 
			
		||||
    except (UnicodeDecodeError, Error):
 | 
			
		||||
        return None
 | 
			
		||||
    # Accept credentials with username and without
 | 
			
		||||
    if ":" in auth_credentials:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								authentik/api/tests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								authentik/api/tests.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
"""Test API Authentication"""
 | 
			
		||||
from base64 import b64encode
 | 
			
		||||
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from guardian.shortcuts import get_anonymous_user
 | 
			
		||||
 | 
			
		||||
from authentik.api.auth import token_from_header
 | 
			
		||||
from authentik.core.models import Token, TokenIntents
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestAPIAuth(TestCase):
 | 
			
		||||
    """Test API Authentication"""
 | 
			
		||||
 | 
			
		||||
    def test_valid(self):
 | 
			
		||||
        """Test valid token"""
 | 
			
		||||
        token = Token.objects.create(
 | 
			
		||||
            intent=TokenIntents.INTENT_API, user=get_anonymous_user()
 | 
			
		||||
        )
 | 
			
		||||
        auth = b64encode(f":{token.key}".encode()).decode()
 | 
			
		||||
        self.assertEqual(token_from_header(f"Basic {auth}".encode()), token)
 | 
			
		||||
 | 
			
		||||
    def test_invalid_type(self):
 | 
			
		||||
        """Test invalid type"""
 | 
			
		||||
        self.assertIsNone(token_from_header("foo bar".encode()))
 | 
			
		||||
 | 
			
		||||
    def test_invalid_decode(self):
 | 
			
		||||
        """Test invalid bas64"""
 | 
			
		||||
        self.assertIsNone(token_from_header("Basic bar".encode()))
 | 
			
		||||
 | 
			
		||||
    def test_invalid_empty_password(self):
 | 
			
		||||
        """Test invalid with empty password"""
 | 
			
		||||
        self.assertIsNone(token_from_header("Basic :".encode()))
 | 
			
		||||
 | 
			
		||||
    def test_invalid_no_token(self):
 | 
			
		||||
        """Test invalid with no token"""
 | 
			
		||||
        auth = b64encode(":abc".encode()).decode()
 | 
			
		||||
        self.assertIsNone(token_from_header(f"Basic :{auth}".encode()))
 | 
			
		||||
@ -5,9 +5,10 @@ from drf_yasg2.views import get_schema_view
 | 
			
		||||
from rest_framework import routers
 | 
			
		||||
from rest_framework.permissions import AllowAny
 | 
			
		||||
 | 
			
		||||
from authentik.admin.api.overview import AdministrationOverviewViewSet
 | 
			
		||||
from authentik.admin.api.overview_metrics import AdministrationMetricsViewSet
 | 
			
		||||
from authentik.admin.api.metrics import AdministrationMetricsViewSet
 | 
			
		||||
from authentik.admin.api.tasks import TaskViewSet
 | 
			
		||||
from authentik.admin.api.version import VersionViewSet
 | 
			
		||||
from authentik.admin.api.workers import WorkerViewSet
 | 
			
		||||
from authentik.api.v2.config import ConfigsViewSet
 | 
			
		||||
from authentik.api.v2.messages import MessagesViewSet
 | 
			
		||||
from authentik.audit.api import EventViewSet
 | 
			
		||||
@ -19,13 +20,22 @@ from authentik.core.api.sources import SourceViewSet
 | 
			
		||||
from authentik.core.api.tokens import TokenViewSet
 | 
			
		||||
from authentik.core.api.users import UserViewSet
 | 
			
		||||
from authentik.crypto.api import CertificateKeyPairViewSet
 | 
			
		||||
from authentik.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
 | 
			
		||||
from authentik.flows.api import (
 | 
			
		||||
    FlowCacheViewSet,
 | 
			
		||||
    FlowStageBindingViewSet,
 | 
			
		||||
    FlowViewSet,
 | 
			
		||||
    StageViewSet,
 | 
			
		||||
)
 | 
			
		||||
from authentik.outposts.api import (
 | 
			
		||||
    DockerServiceConnectionViewSet,
 | 
			
		||||
    KubernetesServiceConnectionViewSet,
 | 
			
		||||
    OutpostViewSet,
 | 
			
		||||
)
 | 
			
		||||
from authentik.policies.api import PolicyBindingViewSet, PolicyViewSet
 | 
			
		||||
from authentik.policies.api import (
 | 
			
		||||
    PolicyBindingViewSet,
 | 
			
		||||
    PolicyCacheViewSet,
 | 
			
		||||
    PolicyViewSet,
 | 
			
		||||
)
 | 
			
		||||
from authentik.policies.dummy.api import DummyPolicyViewSet
 | 
			
		||||
from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet
 | 
			
		||||
from authentik.policies.expression.api import ExpressionPolicyViewSet
 | 
			
		||||
@ -63,9 +73,8 @@ router = routers.DefaultRouter()
 | 
			
		||||
router.register("root/messages", MessagesViewSet, basename="messages")
 | 
			
		||||
router.register("root/config", ConfigsViewSet, basename="configs")
 | 
			
		||||
 | 
			
		||||
router.register(
 | 
			
		||||
    "admin/overview", AdministrationOverviewViewSet, basename="admin_overview"
 | 
			
		||||
)
 | 
			
		||||
router.register("admin/version", VersionViewSet, basename="admin_version")
 | 
			
		||||
router.register("admin/workers", WorkerViewSet, basename="admin_workers")
 | 
			
		||||
router.register("admin/metrics", AdministrationMetricsViewSet, basename="admin_metrics")
 | 
			
		||||
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
 | 
			
		||||
 | 
			
		||||
@ -82,6 +91,7 @@ router.register(
 | 
			
		||||
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
 | 
			
		||||
 | 
			
		||||
router.register("flows/instances", FlowViewSet)
 | 
			
		||||
router.register("flows/cached", FlowCacheViewSet, basename="flows_cache")
 | 
			
		||||
router.register("flows/bindings", FlowStageBindingViewSet)
 | 
			
		||||
 | 
			
		||||
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
 | 
			
		||||
@ -94,6 +104,7 @@ router.register("sources/saml", SAMLSourceViewSet)
 | 
			
		||||
router.register("sources/oauth", OAuthSourceViewSet)
 | 
			
		||||
 | 
			
		||||
router.register("policies/all", PolicyViewSet)
 | 
			
		||||
router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache")
 | 
			
		||||
router.register("policies/bindings", PolicyBindingViewSet)
 | 
			
		||||
router.register("policies/expression", ExpressionPolicyViewSet)
 | 
			
		||||
router.register("policies/group_membership", GroupMembershipPolicyViewSet)
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ from rest_framework.serializers import ModelSerializer
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
			
		||||
 | 
			
		||||
from authentik.admin.api.overview_metrics import get_events_per_1h
 | 
			
		||||
from authentik.admin.api.metrics import get_events_per_1h
 | 
			
		||||
from authentik.audit.models import EventAction
 | 
			
		||||
from authentik.core.models import Application
 | 
			
		||||
from authentik.policies.engine import PolicyEngine
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
"""Provider API Views"""
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
			
		||||
from rest_framework.viewsets import ReadOnlyModelViewSet
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import Provider
 | 
			
		||||
 | 
			
		||||
@ -14,17 +14,33 @@ class ProviderSerializer(ModelSerializer):
 | 
			
		||||
        """Get object type so that we know which API Endpoint to use to get the full object"""
 | 
			
		||||
        return obj._meta.object_name.lower().replace("provider", "")
 | 
			
		||||
 | 
			
		||||
    def to_representation(self, instance: Provider):
 | 
			
		||||
        # pyright: reportGeneralTypeIssues=false
 | 
			
		||||
        if instance.__class__ == Provider:
 | 
			
		||||
            return super().to_representation(instance)
 | 
			
		||||
        return instance.serializer(instance=instance).data
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = Provider
 | 
			
		||||
        fields = ["pk", "name", "authorization_flow", "property_mappings", "__type__"]
 | 
			
		||||
        fields = [
 | 
			
		||||
            "pk",
 | 
			
		||||
            "name",
 | 
			
		||||
            "application",
 | 
			
		||||
            "authorization_flow",
 | 
			
		||||
            "property_mappings",
 | 
			
		||||
            "__type__",
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ProviderViewSet(ReadOnlyModelViewSet):
 | 
			
		||||
class ProviderViewSet(ModelViewSet):
 | 
			
		||||
    """Provider Viewset"""
 | 
			
		||||
 | 
			
		||||
    queryset = Provider.objects.all()
 | 
			
		||||
    serializer_class = ProviderSerializer
 | 
			
		||||
    filterset_fields = {
 | 
			
		||||
        "application": ["isnull"],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return Provider.objects.select_subclasses()
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
"""Channels base classes"""
 | 
			
		||||
from channels.exceptions import DenyConnection
 | 
			
		||||
from channels.generic.websocket import JsonWebsocketConsumer
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
@ -17,16 +18,13 @@ class AuthJsonConsumer(JsonWebsocketConsumer):
 | 
			
		||||
        headers = dict(self.scope["headers"])
 | 
			
		||||
        if b"authorization" not in headers:
 | 
			
		||||
            LOGGER.warning("WS Request without authorization header")
 | 
			
		||||
            self.close()
 | 
			
		||||
            return False
 | 
			
		||||
            raise DenyConnection()
 | 
			
		||||
 | 
			
		||||
        raw_header = headers[b"authorization"]
 | 
			
		||||
 | 
			
		||||
        token = token_from_header(raw_header)
 | 
			
		||||
        if not token:
 | 
			
		||||
            LOGGER.warning("Failed to authenticate")
 | 
			
		||||
            self.close()
 | 
			
		||||
            return False
 | 
			
		||||
            raise DenyConnection()
 | 
			
		||||
 | 
			
		||||
        self.user = token.user
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ from django.utils.timezone import now
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from guardian.mixins import GuardianUserMixin
 | 
			
		||||
from model_utils.managers import InheritanceManager
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.core.exceptions import PropertyMappingExpressionException
 | 
			
		||||
@ -127,7 +128,7 @@ class User(GuardianUserMixin, AbstractUser):
 | 
			
		||||
        verbose_name_plural = _("Users")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Provider(models.Model):
 | 
			
		||||
class Provider(SerializerModel):
 | 
			
		||||
    """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
 | 
			
		||||
 | 
			
		||||
    name = models.TextField()
 | 
			
		||||
@ -156,6 +157,11 @@ class Provider(models.Model):
 | 
			
		||||
        """Return Form class used to edit this object"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def serializer(self) -> Type[Serializer]:
 | 
			
		||||
        """Get serializer for this model"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -22,16 +22,15 @@ class CertificateKeyPairSerializer(ModelSerializer):
 | 
			
		||||
    def validate_key_data(self, value):
 | 
			
		||||
        """Verify that input is a valid PEM RSA Key"""
 | 
			
		||||
        # Since this field is optional, data can be empty.
 | 
			
		||||
        if value == "":
 | 
			
		||||
            return value
 | 
			
		||||
        try:
 | 
			
		||||
            load_pem_private_key(
 | 
			
		||||
                str.encode("\n".join([x.strip() for x in value.split("\n")])),
 | 
			
		||||
                password=None,
 | 
			
		||||
                backend=default_backend(),
 | 
			
		||||
            )
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            raise ValidationError("Unable to load private key.")
 | 
			
		||||
        if value != "":
 | 
			
		||||
            try:
 | 
			
		||||
                load_pem_private_key(
 | 
			
		||||
                    str.encode("\n".join([x.strip() for x in value.split("\n")])),
 | 
			
		||||
                    password=None,
 | 
			
		||||
                    backend=default_backend(),
 | 
			
		||||
                )
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                raise ValidationError("Unable to load private key.")
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
@ -26,16 +26,15 @@ class CertificateKeyPairForm(forms.ModelForm):
 | 
			
		||||
        """Verify that input is a valid PEM RSA Key"""
 | 
			
		||||
        key_data = self.cleaned_data["key_data"]
 | 
			
		||||
        # Since this field is optional, data can be empty.
 | 
			
		||||
        if key_data == "":
 | 
			
		||||
            return key_data
 | 
			
		||||
        try:
 | 
			
		||||
            load_pem_private_key(
 | 
			
		||||
                str.encode("\n".join([x.strip() for x in key_data.split("\n")])),
 | 
			
		||||
                password=None,
 | 
			
		||||
                backend=default_backend(),
 | 
			
		||||
            )
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            raise forms.ValidationError("Unable to load private key.")
 | 
			
		||||
        if key_data != "":
 | 
			
		||||
            try:
 | 
			
		||||
                load_pem_private_key(
 | 
			
		||||
                    str.encode("\n".join([x.strip() for x in key_data.split("\n")])),
 | 
			
		||||
                    password=None,
 | 
			
		||||
                    backend=default_backend(),
 | 
			
		||||
                )
 | 
			
		||||
            except ValueError:
 | 
			
		||||
                raise forms.ValidationError("Unable to load private key.")
 | 
			
		||||
        return key_data
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,14 @@
 | 
			
		||||
"""Flow API Views"""
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
 | 
			
		||||
from rest_framework.mixins import ListModelMixin
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.serializers import (
 | 
			
		||||
    ModelSerializer,
 | 
			
		||||
    Serializer,
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
)
 | 
			
		||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
 | 
			
		||||
 | 
			
		||||
from authentik.flows.models import Flow, FlowStageBinding, Stage
 | 
			
		||||
from authentik.flows.planner import cache_key
 | 
			
		||||
@ -98,3 +105,14 @@ class FlowStageBindingViewSet(ModelViewSet):
 | 
			
		||||
    queryset = FlowStageBinding.objects.all()
 | 
			
		||||
    serializer_class = FlowStageBindingSerializer
 | 
			
		||||
    filterset_fields = "__all__"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FlowCacheViewSet(ListModelMixin, GenericViewSet):
 | 
			
		||||
    """Info about cached flows"""
 | 
			
		||||
 | 
			
		||||
    queryset = Flow.objects.none()
 | 
			
		||||
    serializer_class = Serializer
 | 
			
		||||
 | 
			
		||||
    def list(self, request: Request) -> Response:
 | 
			
		||||
        """Info about cached flows"""
 | 
			
		||||
        return Response(data={"pagination": {"count": len(cache.keys("flow_*"))}})
 | 
			
		||||
 | 
			
		||||
@ -3,14 +3,17 @@ from unittest.mock import MagicMock, PropertyMock, patch
 | 
			
		||||
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.shortcuts import reverse
 | 
			
		||||
from django.test import Client, TestCase
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.test.client import RequestFactory
 | 
			
		||||
from django.utils.encoding import force_str
 | 
			
		||||
 | 
			
		||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
from authentik.flows.exceptions import FlowNonApplicableException
 | 
			
		||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
 | 
			
		||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
 | 
			
		||||
from authentik.flows.planner import FlowPlan
 | 
			
		||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
 | 
			
		||||
from authentik.flows.planner import FlowPlan, FlowPlanner
 | 
			
		||||
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
 | 
			
		||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
from authentik.policies.dummy.models import DummyPolicy
 | 
			
		||||
from authentik.policies.http import AccessDeniedResponse
 | 
			
		||||
@ -35,8 +38,12 @@ class TestFlowExecutor(TestCase):
 | 
			
		||||
    """Test views logic"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.client = Client()
 | 
			
		||||
        self.request_factory = RequestFactory()
 | 
			
		||||
 | 
			
		||||
    @patch(
 | 
			
		||||
        "authentik.flows.views.to_stage_response",
 | 
			
		||||
        TO_STAGE_RESPONSE_MOCK,
 | 
			
		||||
    )
 | 
			
		||||
    def test_existing_plan_diff_flow(self):
 | 
			
		||||
        """Check that a plan for a different flow cancels the current plan"""
 | 
			
		||||
        flow = Flow.objects.create(
 | 
			
		||||
@ -59,7 +66,7 @@ class TestFlowExecutor(TestCase):
 | 
			
		||||
                    "authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
            self.assertEqual(response.status_code, 200)
 | 
			
		||||
            self.assertEqual(response.status_code, 302)
 | 
			
		||||
            self.assertEqual(cancel_mock.call_count, 2)
 | 
			
		||||
 | 
			
		||||
    @patch(
 | 
			
		||||
@ -102,10 +109,13 @@ class TestFlowExecutor(TestCase):
 | 
			
		||||
        response = self.client.get(
 | 
			
		||||
            reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        self.assertIsInstance(response, AccessDeniedResponse)
 | 
			
		||||
        self.assertInHTML(EmptyFlowException.__doc__, response.rendered_content)
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse("authentik_core:shell"))
 | 
			
		||||
 | 
			
		||||
    @patch(
 | 
			
		||||
        "authentik.flows.views.to_stage_response",
 | 
			
		||||
        TO_STAGE_RESPONSE_MOCK,
 | 
			
		||||
    )
 | 
			
		||||
    def test_invalid_flow_redirect(self):
 | 
			
		||||
        """Tests that an invalid flow still redirects"""
 | 
			
		||||
        flow = Flow.objects.create(
 | 
			
		||||
@ -118,11 +128,8 @@ class TestFlowExecutor(TestCase):
 | 
			
		||||
        dest = "/unique-string"
 | 
			
		||||
        url = reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug})
 | 
			
		||||
        response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        self.assertJSONEqual(
 | 
			
		||||
            force_str(response.content),
 | 
			
		||||
            {"type": "redirect", "to": dest},
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(response.status_code, 302)
 | 
			
		||||
        self.assertEqual(response.url, reverse("authentik_core:shell"))
 | 
			
		||||
 | 
			
		||||
    def test_multi_stage_flow(self):
 | 
			
		||||
        """Test a full flow with multiple stages"""
 | 
			
		||||
@ -158,6 +165,10 @@ class TestFlowExecutor(TestCase):
 | 
			
		||||
        plan: FlowPlan = session[SESSION_KEY_PLAN]
 | 
			
		||||
        self.assertEqual(len(plan.stages), 1)
 | 
			
		||||
 | 
			
		||||
    @patch(
 | 
			
		||||
        "authentik.flows.views.to_stage_response",
 | 
			
		||||
        TO_STAGE_RESPONSE_MOCK,
 | 
			
		||||
    )
 | 
			
		||||
    def test_reevaluate_remove_last(self):
 | 
			
		||||
        """Test planner with re-evaluate (last stage is removed)"""
 | 
			
		||||
        flow = Flow.objects.create(
 | 
			
		||||
@ -276,6 +287,83 @@ class TestFlowExecutor(TestCase):
 | 
			
		||||
            {"type": "redirect", "to": reverse("authentik_core:shell")},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_reevaluate_keep(self):
 | 
			
		||||
        """Test planner with re-evaluate (everything is kept)"""
 | 
			
		||||
        flow = Flow.objects.create(
 | 
			
		||||
            name="test-default-context",
 | 
			
		||||
            slug="test-default-context",
 | 
			
		||||
            designation=FlowDesignation.AUTHENTICATION,
 | 
			
		||||
        )
 | 
			
		||||
        true_policy = DummyPolicy.objects.create(result=True, wait_min=1, wait_max=2)
 | 
			
		||||
 | 
			
		||||
        binding = FlowStageBinding.objects.create(
 | 
			
		||||
            target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
 | 
			
		||||
        )
 | 
			
		||||
        binding2 = FlowStageBinding.objects.create(
 | 
			
		||||
            target=flow,
 | 
			
		||||
            stage=DummyStage.objects.create(name="dummy2"),
 | 
			
		||||
            order=1,
 | 
			
		||||
            re_evaluate_policies=True,
 | 
			
		||||
        )
 | 
			
		||||
        binding3 = FlowStageBinding.objects.create(
 | 
			
		||||
            target=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0)
 | 
			
		||||
 | 
			
		||||
        # Here we patch the dummy policy to evaluate to true so the stage is included
 | 
			
		||||
        with patch(
 | 
			
		||||
            "authentik.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
 | 
			
		||||
        ):
 | 
			
		||||
 | 
			
		||||
            exec_url = reverse(
 | 
			
		||||
                "authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}
 | 
			
		||||
            )
 | 
			
		||||
            # First request, run the planner
 | 
			
		||||
            response = self.client.get(exec_url)
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(response.status_code, 200)
 | 
			
		||||
            plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(plan.stages[0], binding.stage)
 | 
			
		||||
            self.assertEqual(plan.stages[1], binding2.stage)
 | 
			
		||||
            self.assertEqual(plan.stages[2], binding3.stage)
 | 
			
		||||
 | 
			
		||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
			
		||||
            self.assertIsInstance(plan.markers[1], ReevaluateMarker)
 | 
			
		||||
            self.assertIsInstance(plan.markers[2], StageMarker)
 | 
			
		||||
 | 
			
		||||
            # Second request, this passes the first dummy stage
 | 
			
		||||
            response = self.client.post(exec_url)
 | 
			
		||||
            self.assertEqual(response.status_code, 302)
 | 
			
		||||
 | 
			
		||||
            plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(plan.stages[0], binding2.stage)
 | 
			
		||||
            self.assertEqual(plan.stages[1], binding3.stage)
 | 
			
		||||
 | 
			
		||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
			
		||||
            self.assertIsInstance(plan.markers[1], StageMarker)
 | 
			
		||||
 | 
			
		||||
            # Third request, this passes the first dummy stage
 | 
			
		||||
            response = self.client.post(exec_url)
 | 
			
		||||
            self.assertEqual(response.status_code, 302)
 | 
			
		||||
 | 
			
		||||
            plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
 | 
			
		||||
 | 
			
		||||
            self.assertEqual(plan.stages[0], binding3.stage)
 | 
			
		||||
 | 
			
		||||
            self.assertIsInstance(plan.markers[0], StageMarker)
 | 
			
		||||
 | 
			
		||||
        # third request, this should trigger the re-evaluate
 | 
			
		||||
        # We do this request without the patch, so the policy results in false
 | 
			
		||||
        response = self.client.post(exec_url)
 | 
			
		||||
        self.assertEqual(response.status_code, 200)
 | 
			
		||||
        self.assertJSONEqual(
 | 
			
		||||
            force_str(response.content),
 | 
			
		||||
            {"type": "redirect", "to": reverse("authentik_core:shell")},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_reevaluate_remove_consecutive(self):
 | 
			
		||||
        """Test planner with re-evaluate (consecutive stages are removed)"""
 | 
			
		||||
        flow = Flow.objects.create(
 | 
			
		||||
@ -351,3 +439,33 @@ class TestFlowExecutor(TestCase):
 | 
			
		||||
            force_str(response.content),
 | 
			
		||||
            {"type": "redirect", "to": reverse("authentik_core:shell")},
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_stageview_user_identifier(self):
 | 
			
		||||
        """Test PLAN_CONTEXT_PENDING_USER_IDENTIFIER"""
 | 
			
		||||
        flow = Flow.objects.create(
 | 
			
		||||
            name="test-default-context",
 | 
			
		||||
            slug="test-default-context",
 | 
			
		||||
            designation=FlowDesignation.AUTHENTICATION,
 | 
			
		||||
        )
 | 
			
		||||
        FlowStageBinding.objects.create(
 | 
			
		||||
            target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        ident = "test-identifier"
 | 
			
		||||
 | 
			
		||||
        user = User.objects.create(username="test-user")
 | 
			
		||||
        request = self.request_factory.get(
 | 
			
		||||
            reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
			
		||||
        )
 | 
			
		||||
        request.user = user
 | 
			
		||||
        planner = FlowPlanner(flow)
 | 
			
		||||
        plan = planner.plan(
 | 
			
		||||
            request, default_context={PLAN_CONTEXT_PENDING_USER_IDENTIFIER: ident}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        executor = FlowExecutorView()
 | 
			
		||||
        executor.plan = plan
 | 
			
		||||
        executor.flow = flow
 | 
			
		||||
 | 
			
		||||
        stage_view = StageView(executor)
 | 
			
		||||
        self.assertEqual(ident, stage_view.get_context_data()["user"].username)
 | 
			
		||||
 | 
			
		||||
@ -61,7 +61,7 @@ class DataclassEncoder(JSONEncoder):
 | 
			
		||||
            return asdict(o)
 | 
			
		||||
        if isinstance(o, UUID):
 | 
			
		||||
            return str(o)
 | 
			
		||||
        return super().default(o)
 | 
			
		||||
        return super().default(o)  # pragma: no cover
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EntryInvalidError(SentryIgnoredException):
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ from authentik.flows.transfer.common import (
 | 
			
		||||
    FlowBundle,
 | 
			
		||||
    FlowBundleEntry,
 | 
			
		||||
)
 | 
			
		||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
			
		||||
from authentik.policies.models import Policy, PolicyBinding
 | 
			
		||||
from authentik.stages.prompt.models import PromptStage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -31,11 +31,6 @@ class FlowExporter:
 | 
			
		||||
 | 
			
		||||
    def _prepare_pbm(self):
 | 
			
		||||
        self.pbm_uuids = [self.flow.pbm_uuid]
 | 
			
		||||
        for stage_subclass in Stage.__subclasses__():
 | 
			
		||||
            if issubclass(stage_subclass, PolicyBindingModel):
 | 
			
		||||
                self.pbm_uuids += stage_subclass.objects.filter(
 | 
			
		||||
                    flow=self.flow
 | 
			
		||||
                ).values_list("pbm_uuid", flat=True)
 | 
			
		||||
        self.pbm_uuids += FlowStageBinding.objects.filter(target=self.flow).values_list(
 | 
			
		||||
            "pbm_uuid", flat=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -83,7 +83,9 @@ class FlowExecutorView(View):
 | 
			
		||||
                return to_stage_response(self.request, self.handle_invalid_flow(exc))
 | 
			
		||||
            except EmptyFlowException as exc:
 | 
			
		||||
                LOGGER.warning("f(exec): Flow is empty", exc=exc)
 | 
			
		||||
                return to_stage_response(self.request, self.handle_invalid_flow(exc))
 | 
			
		||||
                # To match behaviour with loading an empty flow plan from cache,
 | 
			
		||||
                # we don't show an error message here, but rather call _flow_done()
 | 
			
		||||
                return self._flow_done()
 | 
			
		||||
        # We don't save the Plan after getting the next stage
 | 
			
		||||
        # as it hasn't been successfully passed yet
 | 
			
		||||
        next_stage = self.plan.next(self.request)
 | 
			
		||||
@ -147,7 +149,7 @@ class FlowExecutorView(View):
 | 
			
		||||
            NEXT_ARG_NAME, "authentik_core:shell"
 | 
			
		||||
        )
 | 
			
		||||
        self.cancel()
 | 
			
		||||
        return redirect_with_qs(next_param)
 | 
			
		||||
        return to_stage_response(self.request, redirect_with_qs(next_param))
 | 
			
		||||
 | 
			
		||||
    def stage_ok(self) -> HttpResponse:
 | 
			
		||||
        """Callback called by stages upon successful completion.
 | 
			
		||||
 | 
			
		||||
@ -1,55 +0,0 @@
 | 
			
		||||
"""authentik lib navbar Templatetag"""
 | 
			
		||||
from django import template
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
register = template.Library()
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
ACTIVE_STRING = "pf-m-current"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(takes_context=True)
 | 
			
		||||
def is_active(context, *args: str, **_) -> str:
 | 
			
		||||
    """Return whether a navbar link is active or not."""
 | 
			
		||||
    request: HttpRequest = context.get("request")
 | 
			
		||||
    if not request.resolver_match:
 | 
			
		||||
        return ""
 | 
			
		||||
    match = request.resolver_match
 | 
			
		||||
    for url in args:
 | 
			
		||||
        if ":" in url:
 | 
			
		||||
            app_name, url = url.split(":")
 | 
			
		||||
            if match.app_name == app_name and match.url_name == url:
 | 
			
		||||
                return ACTIVE_STRING
 | 
			
		||||
        else:
 | 
			
		||||
            if match.url_name == url:
 | 
			
		||||
                return ACTIVE_STRING
 | 
			
		||||
    return ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(takes_context=True)
 | 
			
		||||
def is_active_url(context, view: str) -> str:
 | 
			
		||||
    """Return whether a navbar link is active or not."""
 | 
			
		||||
    request: HttpRequest = context.get("request")
 | 
			
		||||
    if not request.resolver_match:
 | 
			
		||||
        return ""
 | 
			
		||||
 | 
			
		||||
    match = request.resolver_match
 | 
			
		||||
    current_full_url = f"{match.app_name}:{match.url_name}"
 | 
			
		||||
 | 
			
		||||
    if current_full_url == view:
 | 
			
		||||
        return ACTIVE_STRING
 | 
			
		||||
    return ""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register.simple_tag(takes_context=True)
 | 
			
		||||
def is_active_app(context, *args: str) -> str:
 | 
			
		||||
    """Return True if current link is from app"""
 | 
			
		||||
 | 
			
		||||
    request: HttpRequest = context.get("request")
 | 
			
		||||
    if not request.resolver_match:
 | 
			
		||||
        return ""
 | 
			
		||||
    for app_name in args:
 | 
			
		||||
        if request.resolver_match.app_name == app_name:
 | 
			
		||||
            return ACTIVE_STRING
 | 
			
		||||
    return ""
 | 
			
		||||
							
								
								
									
										0
									
								
								authentik/lib/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/lib/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										18
									
								
								authentik/lib/tests/test_sentry.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								authentik/lib/tests/test_sentry.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
"""test sentry integration"""
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from authentik.lib.sentry import SentryIgnoredException, before_send
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSentry(TestCase):
 | 
			
		||||
    """test sentry integration"""
 | 
			
		||||
 | 
			
		||||
    def test_error_not_sent(self):
 | 
			
		||||
        """Test SentryIgnoredError not sent"""
 | 
			
		||||
        self.assertIsNone(
 | 
			
		||||
            before_send(None, {"exc_info": (0, SentryIgnoredException(), 0)})
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_error_sent(self):
 | 
			
		||||
        """Test error sent"""
 | 
			
		||||
        self.assertIsNone(before_send(None, {"exc_info": (0, ValueError(), 0)}))
 | 
			
		||||
@ -20,6 +20,8 @@ class TestTimeUtils(TestCase):
 | 
			
		||||
        """Test invalid expression"""
 | 
			
		||||
        with self.assertRaises(ValueError):
 | 
			
		||||
            timedelta_from_string("foo")
 | 
			
		||||
        with self.assertRaises(ValueError):
 | 
			
		||||
            timedelta_from_string("bar=baz")
 | 
			
		||||
 | 
			
		||||
    def test_validation(self):
 | 
			
		||||
        """Test Django model field validator"""
 | 
			
		||||
@ -35,4 +35,6 @@ def timedelta_from_string(expr: str) -> datetime.timedelta:
 | 
			
		||||
        if key.lower() not in ALLOWED_KEYS:
 | 
			
		||||
            continue
 | 
			
		||||
        kwargs[key.lower()] = float(value)
 | 
			
		||||
    if len(kwargs) < 1:
 | 
			
		||||
        raise ValueError("No valid keys to pass to timedelta")
 | 
			
		||||
    return datetime.timedelta(**kwargs)
 | 
			
		||||
 | 
			
		||||
@ -22,7 +22,6 @@ class AuthentikOutpostConfig(AppConfig):
 | 
			
		||||
 | 
			
		||||
    name = "authentik.outposts"
 | 
			
		||||
    label = "authentik_outposts"
 | 
			
		||||
    mountpoint = "outposts/"
 | 
			
		||||
    verbose_name = "authentik Outpost"
 | 
			
		||||
 | 
			
		||||
    def ready(self):
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,9 @@
 | 
			
		||||
from dataclasses import asdict, dataclass, field
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from enum import IntEnum
 | 
			
		||||
from typing import Any, Dict
 | 
			
		||||
from typing import Any, Dict, Optional
 | 
			
		||||
 | 
			
		||||
from channels.exceptions import DenyConnection
 | 
			
		||||
from dacite import from_dict
 | 
			
		||||
from dacite.data import Data
 | 
			
		||||
from guardian.shortcuts import get_objects_for_user
 | 
			
		||||
@ -39,18 +40,16 @@ class WebsocketMessage:
 | 
			
		||||
class OutpostConsumer(AuthJsonConsumer):
 | 
			
		||||
    """Handler for Outposts that connect over websockets for health checks and live updates"""
 | 
			
		||||
 | 
			
		||||
    outpost: Outpost
 | 
			
		||||
    outpost: Optional[Outpost] = None
 | 
			
		||||
 | 
			
		||||
    def connect(self):
 | 
			
		||||
        if not super().connect():
 | 
			
		||||
            return
 | 
			
		||||
        super().connect()
 | 
			
		||||
        uuid = self.scope["url_route"]["kwargs"]["pk"]
 | 
			
		||||
        outpost = get_objects_for_user(
 | 
			
		||||
            self.user, "authentik_outposts.view_outpost"
 | 
			
		||||
        ).filter(pk=uuid)
 | 
			
		||||
        if not outpost.exists():
 | 
			
		||||
            self.close()
 | 
			
		||||
            return
 | 
			
		||||
            raise DenyConnection()
 | 
			
		||||
        self.accept()
 | 
			
		||||
        self.outpost = outpost.first()
 | 
			
		||||
        OutpostState(
 | 
			
		||||
@ -60,7 +59,8 @@ class OutpostConsumer(AuthJsonConsumer):
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=unused-argument
 | 
			
		||||
    def disconnect(self, close_code):
 | 
			
		||||
        OutpostState.for_channel(self.outpost, self.channel_name).delete()
 | 
			
		||||
        if self.outpost:
 | 
			
		||||
            OutpostState.for_channel(self.outpost, self.channel_name).delete()
 | 
			
		||||
        LOGGER.debug("removed channel from cache", channel_name=self.channel_name)
 | 
			
		||||
 | 
			
		||||
    def receive_json(self, content: Data):
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
			
		||||
                "authentik_host_insecure": b64string(
 | 
			
		||||
                    str(self.controller.outpost.config.authentik_host_insecure)
 | 
			
		||||
                ),
 | 
			
		||||
                "token": b64string(self.controller.outpost.token.token_uuid.hex),
 | 
			
		||||
                "token": b64string(self.controller.outpost.token.key),
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										38
									
								
								authentik/outposts/migrations/0014_auto_20201213_1407.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								authentik/outposts/migrations/0014_auto_20201213_1407.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
# Generated by Django 3.1.4 on 2020-12-13 14:07
 | 
			
		||||
 | 
			
		||||
from django.apps.registry import Apps
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def update_config_prefix(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
			
		||||
    alias = schema_editor.connection.alias
 | 
			
		||||
    Outpost = apps.get_model("authentik_outposts", "Outpost")
 | 
			
		||||
 | 
			
		||||
    for outpost in Outpost.objects.using(alias).all():
 | 
			
		||||
        config = outpost._config
 | 
			
		||||
        for key in list(config):
 | 
			
		||||
            if "passbook" in key:
 | 
			
		||||
                new_key = key.replace("passbook", "authentik")
 | 
			
		||||
                config[new_key] = config[key]
 | 
			
		||||
                del config[key]
 | 
			
		||||
        outpost._config = config
 | 
			
		||||
        outpost.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("authentik_outposts", "0013_auto_20201203_2009"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.RunPython(update_config_prefix),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="dockerserviceconnection",
 | 
			
		||||
            name="url",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                help_text="Can be in the format of 'unix://<path>' when connecting to a local docker daemon, or 'https://<hostname>:2376' when connecting to a remote system."
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -113,17 +113,24 @@ class OutpostServiceConnection(models.Model):
 | 
			
		||||
 | 
			
		||||
    objects = InheritanceManager()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def state_key(self) -> str:
 | 
			
		||||
        """Key used to save connection state in cache"""
 | 
			
		||||
        return f"outpost_service_connection_{self.pk.hex}"
 | 
			
		||||
 | 
			
		||||
    @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)
 | 
			
		||||
        from authentik.outposts.tasks import outpost_service_connection_state
 | 
			
		||||
 | 
			
		||||
        state = cache.get(self.state_key, None)
 | 
			
		||||
        if not state:
 | 
			
		||||
            state = self._get_state()
 | 
			
		||||
            cache.set(state_key, state, timeout=0)
 | 
			
		||||
            outpost_service_connection_state.delay(self.pk)
 | 
			
		||||
            return OutpostServiceConnectionState("", False)
 | 
			
		||||
        return state
 | 
			
		||||
 | 
			
		||||
    def _get_state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
    def fetch_state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
        """Fetch current Service Connection state"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
@ -140,7 +147,14 @@ class OutpostServiceConnection(models.Model):
 | 
			
		||||
class DockerServiceConnection(OutpostServiceConnection):
 | 
			
		||||
    """Service Connection to a Docker endpoint"""
 | 
			
		||||
 | 
			
		||||
    url = models.TextField()
 | 
			
		||||
    url = models.TextField(
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            (
 | 
			
		||||
                "Can be in the format of 'unix://<path>' when connecting to a local docker daemon, "
 | 
			
		||||
                "or 'https://<hostname>:2376' when connecting to a remote system."
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
    tls_verification = models.ForeignKey(
 | 
			
		||||
        CertificateKeyPair,
 | 
			
		||||
        null=True,
 | 
			
		||||
@ -196,7 +210,7 @@ class DockerServiceConnection(OutpostServiceConnection):
 | 
			
		||||
            raise ServiceConnectionInvalid from exc
 | 
			
		||||
        return client
 | 
			
		||||
 | 
			
		||||
    def _get_state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
    def fetch_state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
        try:
 | 
			
		||||
            client = self.client()
 | 
			
		||||
            return OutpostServiceConnectionState(
 | 
			
		||||
@ -232,7 +246,7 @@ class KubernetesServiceConnection(OutpostServiceConnection):
 | 
			
		||||
    def __str__(self) -> str:
 | 
			
		||||
        return f"Kubernetes Service-Connection {self.name}"
 | 
			
		||||
 | 
			
		||||
    def _get_state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
    def fetch_state(self) -> OutpostServiceConnectionState:
 | 
			
		||||
        try:
 | 
			
		||||
            client = self.client()
 | 
			
		||||
            api_instance = VersionApi(client)
 | 
			
		||||
@ -240,7 +254,7 @@ class KubernetesServiceConnection(OutpostServiceConnection):
 | 
			
		||||
            return OutpostServiceConnectionState(
 | 
			
		||||
                version=version.git_version, healthy=True
 | 
			
		||||
            )
 | 
			
		||||
        except (OpenApiException, HTTPError):
 | 
			
		||||
        except (OpenApiException, HTTPError, ServiceConnectionInvalid):
 | 
			
		||||
            return OutpostServiceConnectionState(version="", healthy=False)
 | 
			
		||||
 | 
			
		||||
    def client(self) -> ApiClient:
 | 
			
		||||
 | 
			
		||||
@ -12,4 +12,9 @@ CELERY_BEAT_SCHEDULE = {
 | 
			
		||||
        "schedule": crontab(minute=0, hour="*"),
 | 
			
		||||
        "options": {"queue": "authentik_scheduled"},
 | 
			
		||||
    },
 | 
			
		||||
    "outpost_token_ensurer": {
 | 
			
		||||
        "task": "authentik.outposts.tasks.outpost_token_ensurer",
 | 
			
		||||
        "schedule": crontab(minute="*/5"),
 | 
			
		||||
        "options": {"queue": "authentik_scheduled"},
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -35,21 +35,22 @@ def outpost_controller_all():
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task()
 | 
			
		||||
def outpost_service_connection_state(state_pk: Any):
 | 
			
		||||
def outpost_service_connection_state(connection_pk: Any):
 | 
			
		||||
    """Update cached state of a service connection"""
 | 
			
		||||
    connection: OutpostServiceConnection = (
 | 
			
		||||
        OutpostServiceConnection.objects.filter(pk=state_pk).select_subclasses().first()
 | 
			
		||||
        OutpostServiceConnection.objects.filter(pk=connection_pk)
 | 
			
		||||
        .select_subclasses()
 | 
			
		||||
        .first()
 | 
			
		||||
    )
 | 
			
		||||
    cache.delete(f"outpost_service_connection_{connection.pk.hex}")
 | 
			
		||||
    _ = connection.state
 | 
			
		||||
    state = connection.fetch_state()
 | 
			
		||||
    cache.set(connection.state_key, state, timeout=None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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
 | 
			
		||||
    for connection in OutpostServiceConnection.objects.all():
 | 
			
		||||
        outpost_service_connection_state.delay(connection.pk)
 | 
			
		||||
    self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -90,6 +91,21 @@ def outpost_pre_delete(outpost_pk: str):
 | 
			
		||||
            ProxyKubernetesController(outpost, service_connection).down()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
			
		||||
def outpost_token_ensurer(self: MonitoredTask):
 | 
			
		||||
    """Periodically ensure that all Outposts have valid Service Accounts
 | 
			
		||||
    and Tokens"""
 | 
			
		||||
    all_outposts = Outpost.objects.all()
 | 
			
		||||
    for outpost in all_outposts:
 | 
			
		||||
        _ = outpost.token
 | 
			
		||||
    self.set_status(
 | 
			
		||||
        TaskResult(
 | 
			
		||||
            TaskResultStatus.SUCCESSFUL,
 | 
			
		||||
            [f"Successfully checked {len(all_outposts)} Outposts."],
 | 
			
		||||
        )
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task()
 | 
			
		||||
def outpost_post_save(model_class: str, model_pk: Any):
 | 
			
		||||
    """If an Outpost is saved, Ensure that token is created/updated
 | 
			
		||||
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
"""authentik outposts urls"""
 | 
			
		||||
from django.urls import path
 | 
			
		||||
 | 
			
		||||
from authentik.outposts.views import KubernetesManifestView, SetupView
 | 
			
		||||
 | 
			
		||||
urlpatterns = [
 | 
			
		||||
    path(
 | 
			
		||||
        "<uuid:outpost_pk>/k8s/", KubernetesManifestView.as_view(), name="k8s-manifest"
 | 
			
		||||
    ),
 | 
			
		||||
    path("<uuid:outpost_pk>/", SetupView.as_view(), name="setup"),
 | 
			
		||||
]
 | 
			
		||||
@ -1,89 +0,0 @@
 | 
			
		||||
"""authentik outpost views"""
 | 
			
		||||
from typing import Any, Dict, List
 | 
			
		||||
 | 
			
		||||
from django.contrib.auth.mixins import LoginRequiredMixin
 | 
			
		||||
from django.db.models import Model
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from django.views import View
 | 
			
		||||
from django.views.generic import TemplateView
 | 
			
		||||
from guardian.shortcuts import get_objects_for_user
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
from authentik.outposts.controllers.docker import DockerController
 | 
			
		||||
from authentik.outposts.models import (
 | 
			
		||||
    DockerServiceConnection,
 | 
			
		||||
    KubernetesServiceConnection,
 | 
			
		||||
    Outpost,
 | 
			
		||||
    OutpostType,
 | 
			
		||||
)
 | 
			
		||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_object_for_user_or_404(user: User, perm: str, **filters) -> Model:
 | 
			
		||||
    """Wrapper that combines get_objects_for_user and get_object_or_404"""
 | 
			
		||||
    return get_object_or_404(get_objects_for_user(user, perm), **filters)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DockerComposeView(LoginRequiredMixin, View):
 | 
			
		||||
    """Generate docker-compose yaml"""
 | 
			
		||||
 | 
			
		||||
    def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
 | 
			
		||||
        """Render docker-compose file"""
 | 
			
		||||
        outpost: Outpost = get_object_for_user_or_404(
 | 
			
		||||
            request.user,
 | 
			
		||||
            "authentik_outposts.view_outpost",
 | 
			
		||||
            pk=outpost_pk,
 | 
			
		||||
        )
 | 
			
		||||
        manifest = ""
 | 
			
		||||
        if outpost.type == OutpostType.PROXY:
 | 
			
		||||
            controller = DockerController(outpost, DockerServiceConnection())
 | 
			
		||||
            manifest = controller.get_static_deployment()
 | 
			
		||||
 | 
			
		||||
        return HttpResponse(manifest, content_type="text/vnd.yaml")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KubernetesManifestView(LoginRequiredMixin, View):
 | 
			
		||||
    """Generate Kubernetes Deployment and SVC for proxy"""
 | 
			
		||||
 | 
			
		||||
    def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
 | 
			
		||||
        """Render deployment template"""
 | 
			
		||||
        outpost: Outpost = get_object_for_user_or_404(
 | 
			
		||||
            request.user,
 | 
			
		||||
            "authentik_outposts.view_outpost",
 | 
			
		||||
            pk=outpost_pk,
 | 
			
		||||
        )
 | 
			
		||||
        manifest = ""
 | 
			
		||||
        if outpost.type == OutpostType.PROXY:
 | 
			
		||||
            controller = ProxyKubernetesController(
 | 
			
		||||
                outpost, KubernetesServiceConnection()
 | 
			
		||||
            )
 | 
			
		||||
            manifest = controller.get_static_deployment()
 | 
			
		||||
 | 
			
		||||
        return HttpResponse(manifest, content_type="text/vnd.yaml")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SetupView(LoginRequiredMixin, TemplateView):
 | 
			
		||||
    """Setup view"""
 | 
			
		||||
 | 
			
		||||
    def get_template_names(self) -> List[str]:
 | 
			
		||||
        allowed = ["dc", "custom", "k8s_manual", "k8s_integration"]
 | 
			
		||||
        setup_type = self.request.GET.get("type", "dc")
 | 
			
		||||
        if setup_type not in allowed:
 | 
			
		||||
            setup_type = allowed[0]
 | 
			
		||||
        return [f"outposts/setup_{setup_type}.html"]
 | 
			
		||||
 | 
			
		||||
    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
 | 
			
		||||
        kwargs = super().get_context_data(**kwargs)
 | 
			
		||||
        outpost: Outpost = get_object_for_user_or_404(
 | 
			
		||||
            self.request.user,
 | 
			
		||||
            "authentik_outposts.view_outpost",
 | 
			
		||||
            pk=self.kwargs["outpost_pk"],
 | 
			
		||||
        )
 | 
			
		||||
        kwargs.update(
 | 
			
		||||
            {"host": self.request.build_absolute_uri("/"), "outpost": outpost}
 | 
			
		||||
        )
 | 
			
		||||
        return kwargs
 | 
			
		||||
@ -1,11 +1,16 @@
 | 
			
		||||
"""policy API Views"""
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.core.exceptions import ObjectDoesNotExist
 | 
			
		||||
from rest_framework.mixins import ListModelMixin
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.serializers import (
 | 
			
		||||
    ModelSerializer,
 | 
			
		||||
    PrimaryKeyRelatedField,
 | 
			
		||||
    Serializer,
 | 
			
		||||
    SerializerMethodField,
 | 
			
		||||
)
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
 | 
			
		||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
 | 
			
		||||
 | 
			
		||||
from authentik.policies.forms import GENERAL_FIELDS
 | 
			
		||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
			
		||||
@ -68,6 +73,10 @@ class PolicyViewSet(ReadOnlyModelViewSet):
 | 
			
		||||
 | 
			
		||||
    queryset = Policy.objects.all()
 | 
			
		||||
    serializer_class = PolicySerializer
 | 
			
		||||
    filterset_fields = {
 | 
			
		||||
        "bindings": ["isnull"],
 | 
			
		||||
        "promptstage": ["isnull"],
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return Policy.objects.select_subclasses()
 | 
			
		||||
@ -98,3 +107,14 @@ class PolicyBindingViewSet(ModelViewSet):
 | 
			
		||||
    serializer_class = PolicyBindingSerializer
 | 
			
		||||
    filterset_fields = ["policy", "target", "enabled", "order", "timeout"]
 | 
			
		||||
    search_fields = ["policy__name"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PolicyCacheViewSet(ListModelMixin, GenericViewSet):
 | 
			
		||||
    """Info about cached policies"""
 | 
			
		||||
 | 
			
		||||
    queryset = Policy.objects.none()
 | 
			
		||||
    serializer_class = Serializer
 | 
			
		||||
 | 
			
		||||
    def list(self, request: Request) -> Response:
 | 
			
		||||
        """Info about cached policies"""
 | 
			
		||||
        return Response(data={"pagination": {"count": len(cache.keys("policy_*"))}})
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
    <label for="" class="pf-c-form__label"></label>
 | 
			
		||||
    <div class="c-form__horizontal-group">
 | 
			
		||||
        <p>
 | 
			
		||||
            Expression using Python. See <a target="_blank" href="https://goauthentik.io/policies/expression/">here</a> for a list of all variables.
 | 
			
		||||
            Expression using Python. See <a target="_blank" href="https://goauthentik.io/docs/policies/expression/">here</a> for a list of all variables.
 | 
			
		||||
        </p>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,7 @@ class HaveIBeenPwendPolicy(Policy):
 | 
			
		||||
                field=self.password_field,
 | 
			
		||||
                fields=request.context.keys(),
 | 
			
		||||
            )
 | 
			
		||||
            return PolicyResult(False, _("Password not set in context"))
 | 
			
		||||
        password = request.context[self.password_field]
 | 
			
		||||
 | 
			
		||||
        pw_hash = sha1(password.encode("utf-8")).hexdigest()  # nosec
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,16 @@ from authentik.providers.oauth2.generators import generate_client_secret
 | 
			
		||||
class TestHIBPPolicy(TestCase):
 | 
			
		||||
    """Test HIBP Policy"""
 | 
			
		||||
 | 
			
		||||
    def test_invalid(self):
 | 
			
		||||
        """Test without password"""
 | 
			
		||||
        policy = HaveIBeenPwendPolicy.objects.create(
 | 
			
		||||
            name="test_invalid",
 | 
			
		||||
        )
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        result: PolicyResult = policy.passes(request)
 | 
			
		||||
        self.assertFalse(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages[0], "Password not set in context")
 | 
			
		||||
 | 
			
		||||
    def test_false(self):
 | 
			
		||||
        """Failing password case"""
 | 
			
		||||
        policy = HaveIBeenPwendPolicy.objects.create(
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,7 @@ class PasswordPolicy(Policy):
 | 
			
		||||
                field=self.password_field,
 | 
			
		||||
                fields=request.context.keys(),
 | 
			
		||||
            )
 | 
			
		||||
            return PolicyResult(False, _("Password not set in context"))
 | 
			
		||||
        password = request.context[self.password_field]
 | 
			
		||||
 | 
			
		||||
        filter_regex = []
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,21 @@ from authentik.policies.types import PolicyRequest, PolicyResult
 | 
			
		||||
class TestPasswordPolicy(TestCase):
 | 
			
		||||
    """Test Password Policy"""
 | 
			
		||||
 | 
			
		||||
    def test_invalid(self):
 | 
			
		||||
        """Test without password"""
 | 
			
		||||
        policy = PasswordPolicy.objects.create(
 | 
			
		||||
            name="test_invalid",
 | 
			
		||||
            amount_uppercase=1,
 | 
			
		||||
            amount_lowercase=2,
 | 
			
		||||
            amount_symbols=3,
 | 
			
		||||
            length_min=24,
 | 
			
		||||
            error_message="test message",
 | 
			
		||||
        )
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        result: PolicyResult = policy.passes(request)
 | 
			
		||||
        self.assertFalse(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages[0], "Password not set in context")
 | 
			
		||||
 | 
			
		||||
    def test_false(self):
 | 
			
		||||
        """Failing password case"""
 | 
			
		||||
        policy = PasswordPolicy.objects.create(
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ from django.utils import dateformat, timezone
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key
 | 
			
		||||
from jwkest.jws import JWS
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
@ -263,6 +264,12 @@ class OAuth2Provider(Provider):
 | 
			
		||||
        launch_url = urlparse(main_url)
 | 
			
		||||
        return main_url.replace(launch_url.path, "")
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def serializer(self) -> Type[Serializer]:
 | 
			
		||||
        from authentik.providers.oauth2.api import OAuth2ProviderSerializer
 | 
			
		||||
 | 
			
		||||
        return OAuth2ProviderSerializer
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from authentik.providers.oauth2.forms import OAuth2ProviderForm
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
    <label for="" class="pf-c-form__label"></label>
 | 
			
		||||
    <div class="c-form__horizontal-group">
 | 
			
		||||
        <p>
 | 
			
		||||
            Expression using Python. See <a href="https://goauthentik.io/property-mappings/expression/">here</a> for a list of all variables.
 | 
			
		||||
            Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
 | 
			
		||||
        </p>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								authentik/providers/oauth2/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/providers/oauth2/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										46
									
								
								authentik/providers/oauth2/tests/test_views_authorize.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								authentik/providers/oauth2/tests/test_views_authorize.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
"""Test authorize view"""
 | 
			
		||||
from django.test import RequestFactory, TestCase
 | 
			
		||||
 | 
			
		||||
from authentik.flows.models import Flow
 | 
			
		||||
from authentik.providers.oauth2.errors import (
 | 
			
		||||
    AuthorizeError,
 | 
			
		||||
    ClientIdError,
 | 
			
		||||
    RedirectUriError,
 | 
			
		||||
)
 | 
			
		||||
from authentik.providers.oauth2.models import OAuth2Provider
 | 
			
		||||
from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestViewsAuthorize(TestCase):
 | 
			
		||||
    """Test authorize view"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.factory = RequestFactory()
 | 
			
		||||
 | 
			
		||||
    def test_invalid_grant_type(self):
 | 
			
		||||
        """Test with invalid grant type"""
 | 
			
		||||
        with self.assertRaises(AuthorizeError):
 | 
			
		||||
            request = self.factory.get("/", data={"response_type": "invalid"})
 | 
			
		||||
            OAuthAuthorizationParams.from_request(request)
 | 
			
		||||
 | 
			
		||||
    def test_invalid_client_id(self):
 | 
			
		||||
        """Test invalid client ID"""
 | 
			
		||||
        with self.assertRaises(ClientIdError):
 | 
			
		||||
            request = self.factory.get(
 | 
			
		||||
                "/", data={"response_type": "code", "client_id": "invalid"}
 | 
			
		||||
            )
 | 
			
		||||
            OAuthAuthorizationParams.from_request(request)
 | 
			
		||||
 | 
			
		||||
    def test_missing_redirect_uri(self):
 | 
			
		||||
        """test missing redirect URI"""
 | 
			
		||||
        OAuth2Provider.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            client_id="test",
 | 
			
		||||
            authorization_flow=Flow.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        with self.assertRaises(RedirectUriError):
 | 
			
		||||
            request = self.factory.get(
 | 
			
		||||
                "/", data={"response_type": "code", "client_id": "test"}
 | 
			
		||||
            )
 | 
			
		||||
            OAuthAuthorizationParams.from_request(request)
 | 
			
		||||
@ -139,7 +139,7 @@ class OAuthAuthorizationParams:
 | 
			
		||||
        is_open_id = SCOPE_OPENID in self.scope
 | 
			
		||||
 | 
			
		||||
        # Redirect URI validation.
 | 
			
		||||
        if is_open_id and not self.redirect_uri:
 | 
			
		||||
        if not self.redirect_uri:
 | 
			
		||||
            LOGGER.warning("Missing redirect uri.")
 | 
			
		||||
            raise RedirectUriError()
 | 
			
		||||
        if self.redirect_uri.lower() not in [
 | 
			
		||||
 | 
			
		||||
@ -1,34 +1,6 @@
 | 
			
		||||
# Generated by Django 3.1.1 on 2020-09-30 08:10
 | 
			
		||||
 | 
			
		||||
from django.apps.registry import Apps
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
			
		||||
 | 
			
		||||
SCOPE_AK_PROXY_EXPRESSION = """return {
 | 
			
		||||
    "ak_proxy": {
 | 
			
		||||
        "user_attributes": user.group_attributes()
 | 
			
		||||
    }
 | 
			
		||||
}"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_proxy_scope(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
			
		||||
    from authentik.providers.proxy.models import SCOPE_AK_PROXY, ProxyProvider
 | 
			
		||||
 | 
			
		||||
    ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping")
 | 
			
		||||
 | 
			
		||||
    ScopeMapping.objects.update_or_create(
 | 
			
		||||
        scope_name=SCOPE_AK_PROXY,
 | 
			
		||||
        defaults={
 | 
			
		||||
            "name": "Autogenerated OAuth2 Mapping: authentik Proxy",
 | 
			
		||||
            "scope_name": SCOPE_AK_PROXY,
 | 
			
		||||
            "description": "",
 | 
			
		||||
            "expression": SCOPE_AK_PROXY_EXPRESSION,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    for provider in ProxyProvider.objects.all():
 | 
			
		||||
        provider.set_oauth_defaults()
 | 
			
		||||
        provider.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
@ -74,5 +46,4 @@ class Migration(migrations.Migration):
 | 
			
		||||
                verbose_name="HTTP-Basic Username",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RunPython(create_proxy_scope),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,41 @@
 | 
			
		||||
# Generated by Django 3.1.4 on 2020-12-14 09:42
 | 
			
		||||
from django.apps.registry import Apps
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
			
		||||
 | 
			
		||||
SCOPE_AK_PROXY_EXPRESSION = """return {
 | 
			
		||||
    "ak_proxy": {
 | 
			
		||||
        "user_attributes": user.group_attributes()
 | 
			
		||||
    }
 | 
			
		||||
}"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_proxy_scope(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
			
		||||
    from authentik.providers.proxy.models import SCOPE_AK_PROXY, ProxyProvider
 | 
			
		||||
 | 
			
		||||
    ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping")
 | 
			
		||||
 | 
			
		||||
    ScopeMapping.objects.filter(scope_name="pb_proxy").delete()
 | 
			
		||||
 | 
			
		||||
    ScopeMapping.objects.update_or_create(
 | 
			
		||||
        scope_name=SCOPE_AK_PROXY,
 | 
			
		||||
        defaults={
 | 
			
		||||
            "name": "Autogenerated OAuth2 Mapping: authentik Proxy",
 | 
			
		||||
            "scope_name": SCOPE_AK_PROXY,
 | 
			
		||||
            "description": "",
 | 
			
		||||
            "expression": SCOPE_AK_PROXY_EXPRESSION,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    for provider in ProxyProvider.objects.all():
 | 
			
		||||
        provider.set_oauth_defaults()
 | 
			
		||||
        provider.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("authentik_providers_proxy", "0009_auto_20201007_1721"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [migrations.RunPython(create_proxy_scope)]
 | 
			
		||||
@ -8,6 +8,7 @@ from django.db import models
 | 
			
		||||
from django.forms import ModelForm
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.lib.models import DomainlessURLValidator
 | 
			
		||||
@ -108,6 +109,12 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
 | 
			
		||||
 | 
			
		||||
        return ProxyProviderForm
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def serializer(self) -> Type[Serializer]:
 | 
			
		||||
        from authentik.providers.proxy.api import ProxyProviderSerializer
 | 
			
		||||
 | 
			
		||||
        return ProxyProviderSerializer
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def launch_url(self) -> Optional[str]:
 | 
			
		||||
        """Use external_host as launch URL"""
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ from django.forms import ModelForm
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from django.shortcuts import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
from structlog import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import PropertyMapping, Provider
 | 
			
		||||
@ -145,6 +146,12 @@ class SAMLProvider(Provider):
 | 
			
		||||
        launch_url = urlparse(self.acs_url)
 | 
			
		||||
        return self.acs_url.replace(launch_url.path, "")
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def serializer(self) -> Type[Serializer]:
 | 
			
		||||
        from authentik.providers.saml.api import SAMLPropertyMappingSerializer
 | 
			
		||||
 | 
			
		||||
        return SAMLPropertyMappingSerializer
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def form(self) -> Type[ModelForm]:
 | 
			
		||||
        from authentik.providers.saml.forms import SAMLProviderForm
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
    <label for="" class="pf-c-form__label"></label>
 | 
			
		||||
    <div class="c-form__horizontal-group">
 | 
			
		||||
        <p>
 | 
			
		||||
            Expression using Python. See <a href="https://goauthentik.io/property-mappings/expression/">here</a> for a list of all variables.
 | 
			
		||||
            Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
 | 
			
		||||
        </p>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										84
									
								
								authentik/providers/saml/tests/test_schema.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								authentik/providers/saml/tests/test_schema.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
"""Test Requests and Responses against schema"""
 | 
			
		||||
from base64 import b64encode
 | 
			
		||||
 | 
			
		||||
from django.contrib.sessions.middleware import SessionMiddleware
 | 
			
		||||
from django.test import RequestFactory, TestCase
 | 
			
		||||
from guardian.utils import get_anonymous_user
 | 
			
		||||
from lxml import etree  # nosec
 | 
			
		||||
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.flows.models import Flow
 | 
			
		||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
 | 
			
		||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
 | 
			
		||||
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
 | 
			
		||||
from authentik.providers.saml.tests.test_auth_n_request import dummy_get_response
 | 
			
		||||
from authentik.sources.saml.models import SAMLSource
 | 
			
		||||
from authentik.sources.saml.processors.request import RequestProcessor
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestSchema(TestCase):
 | 
			
		||||
    """Test Requests and Responses against schema"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        cert = CertificateKeyPair.objects.first()
 | 
			
		||||
        self.provider: SAMLProvider = SAMLProvider.objects.create(
 | 
			
		||||
            authorization_flow=Flow.objects.get(
 | 
			
		||||
                slug="default-provider-authorization-implicit-consent"
 | 
			
		||||
            ),
 | 
			
		||||
            acs_url="http://testserver/source/saml/provider/acs/",
 | 
			
		||||
            signing_kp=cert,
 | 
			
		||||
            verification_kp=cert,
 | 
			
		||||
        )
 | 
			
		||||
        self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
 | 
			
		||||
        self.provider.save()
 | 
			
		||||
        self.source = SAMLSource.objects.create(
 | 
			
		||||
            slug="provider",
 | 
			
		||||
            issuer="authentik",
 | 
			
		||||
            signing_kp=cert,
 | 
			
		||||
        )
 | 
			
		||||
        self.factory = RequestFactory()
 | 
			
		||||
 | 
			
		||||
    def test_request_schema(self):
 | 
			
		||||
        """Test generated AuthNRequest against Schema"""
 | 
			
		||||
        http_request = self.factory.get("/")
 | 
			
		||||
 | 
			
		||||
        middleware = SessionMiddleware(dummy_get_response)
 | 
			
		||||
        middleware.process_request(http_request)
 | 
			
		||||
        http_request.session.save()
 | 
			
		||||
 | 
			
		||||
        # First create an AuthNRequest
 | 
			
		||||
        request_proc = RequestProcessor(self.source, http_request, "test_state")
 | 
			
		||||
        request = request_proc.build_auth_n()
 | 
			
		||||
 | 
			
		||||
        metadata = etree.fromstring(request)  # nosec
 | 
			
		||||
 | 
			
		||||
        schema = etree.XMLSchema(
 | 
			
		||||
            etree.parse("xml/saml-schema-protocol-2.0.xsd")
 | 
			
		||||
        )  # nosec
 | 
			
		||||
        self.assertTrue(schema.validate(metadata))
 | 
			
		||||
 | 
			
		||||
    def test_response_schema(self):
 | 
			
		||||
        """Test generated AuthNRequest against Schema"""
 | 
			
		||||
        http_request = self.factory.get("/")
 | 
			
		||||
        http_request.user = get_anonymous_user()
 | 
			
		||||
 | 
			
		||||
        middleware = SessionMiddleware(dummy_get_response)
 | 
			
		||||
        middleware.process_request(http_request)
 | 
			
		||||
        http_request.session.save()
 | 
			
		||||
 | 
			
		||||
        # First create an AuthNRequest
 | 
			
		||||
        request_proc = RequestProcessor(self.source, http_request, "test_state")
 | 
			
		||||
        request = request_proc.build_auth_n()
 | 
			
		||||
 | 
			
		||||
        # To get an assertion we need a parsed request (parsed by provider)
 | 
			
		||||
        parsed_request = AuthNRequestParser(self.provider).parse(
 | 
			
		||||
            b64encode(request.encode()).decode(), "test_state"
 | 
			
		||||
        )
 | 
			
		||||
        # Now create a response and convert it to string (provider)
 | 
			
		||||
        response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
 | 
			
		||||
        response = response_proc.build_response()
 | 
			
		||||
 | 
			
		||||
        metadata = etree.fromstring(response)  # nosec
 | 
			
		||||
 | 
			
		||||
        schema = etree.XMLSchema(etree.parse("xml/saml-schema-protocol-2.0.xsd"))
 | 
			
		||||
        self.assertTrue(schema.validate(metadata))
 | 
			
		||||
@ -1,15 +1,6 @@
 | 
			
		||||
"""Small helper functions"""
 | 
			
		||||
import uuid
 | 
			
		||||
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from django.shortcuts import render
 | 
			
		||||
from django.template.context import Context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def render_xml(request: HttpRequest, template: str, ctx: Context) -> HttpResponse:
 | 
			
		||||
    """Render template with content_type application/xml"""
 | 
			
		||||
    return render(request, template, context=ctx, content_type="application/xml")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_random_id() -> str:
 | 
			
		||||
    """Random hex id"""
 | 
			
		||||
 | 
			
		||||
@ -32,3 +32,10 @@ class TestRecovery(TestCase):
 | 
			
		||||
            reverse("authentik_recovery:use-token", kwargs={"key": token.key})
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(int(self.client.session["_auth_user_id"]), token.user.pk)
 | 
			
		||||
 | 
			
		||||
    def test_recovery_view_invalid(self):
 | 
			
		||||
        """Test recovery view with invalid token"""
 | 
			
		||||
        response = self.client.get(
 | 
			
		||||
            reverse("authentik_recovery:use-token", kwargs={"key": "abc"})
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(response.status_code, 404)
 | 
			
		||||
 | 
			
		||||
@ -7,13 +7,16 @@ class MessageConsumer(JsonWebsocketConsumer):
 | 
			
		||||
    """Consumer which sends django.contrib.messages Messages over WS.
 | 
			
		||||
    channel_name is saved into cache with user_id, and when a add_message is called"""
 | 
			
		||||
 | 
			
		||||
    session_key: str
 | 
			
		||||
 | 
			
		||||
    def connect(self):
 | 
			
		||||
        self.accept()
 | 
			
		||||
        cache.set(f"user_{self.scope['user'].pk}_messages_{self.channel_name}", True)
 | 
			
		||||
        self.session_key = self.scope["session"].session_key
 | 
			
		||||
        cache.set(f"user_{self.session_key}_messages_{self.channel_name}", True, None)
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=unused-argument
 | 
			
		||||
    def disconnect(self, close_code):
 | 
			
		||||
        cache.delete(f"user_{self.scope['user'].pk}_messages_{self.channel_name}")
 | 
			
		||||
        cache.delete(f"user_{self.session_key}_messages_{self.channel_name}")
 | 
			
		||||
 | 
			
		||||
    def event_update(self, event: dict):
 | 
			
		||||
        """Event handler which is called by Messages Storage backend"""
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ class ChannelsStorage(FallbackStorage):
 | 
			
		||||
        self.channel = get_channel_layer()
 | 
			
		||||
 | 
			
		||||
    def _store(self, messages: list[Message], response, *args, **kwargs):
 | 
			
		||||
        prefix = f"user_{self.request.user.pk}_messages_"
 | 
			
		||||
        prefix = f"user_{self.request.session.session_key}_messages_"
 | 
			
		||||
        keys = cache.keys(f"{prefix}*")
 | 
			
		||||
        if len(keys) < 1:
 | 
			
		||||
            return super()._store(messages, response, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ from django.conf import settings
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PytestTestRunner:
 | 
			
		||||
class PytestTestRunner:  # pragma: no cover
 | 
			
		||||
    """Runs pytest to discover and run tests."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, verbosity=1, failfast=False, keepdb=False, **_):
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@
 | 
			
		||||
    <label for="" class="pf-c-form__label"></label>
 | 
			
		||||
    <div class="c-form__horizontal-group">
 | 
			
		||||
        <p>
 | 
			
		||||
            Expression using Python. See <a href="https://goauthentik.io/property-mappings/expression/">here</a> for a list of all variables.
 | 
			
		||||
            Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
 | 
			
		||||
        </p>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										0
									
								
								authentik/sources/oauth/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/sources/oauth/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										41
									
								
								authentik/sources/oauth/tests/test_type_discord.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								authentik/sources/oauth/tests/test_type_discord.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
"""Discord Type tests"""
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
 | 
			
		||||
from authentik.sources.oauth.types.discord import DiscordOAuth2Callback
 | 
			
		||||
 | 
			
		||||
# https://discord.com/developers/docs/resources/user#user-object
 | 
			
		||||
DISCORD_USER = {
 | 
			
		||||
    "id": "80351110224678912",
 | 
			
		||||
    "username": "Nelly",
 | 
			
		||||
    "discriminator": "1337",
 | 
			
		||||
    "avatar": "8342729096ea3675442027381ff50dfe",
 | 
			
		||||
    "verified": True,
 | 
			
		||||
    "email": "nelly@discord.com",
 | 
			
		||||
    "flags": 64,
 | 
			
		||||
    "premium_type": 1,
 | 
			
		||||
    "public_flags": 64,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTypeGitHub(TestCase):
 | 
			
		||||
    """OAuth Source tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.source = OAuthSource.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            slug="test",
 | 
			
		||||
            provider_type="openid-connect",
 | 
			
		||||
            authorization_url="",
 | 
			
		||||
            profile_url="",
 | 
			
		||||
            consumer_key="",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_enroll_context(self):
 | 
			
		||||
        """Test GitHub Enrollment context"""
 | 
			
		||||
        ak_context = DiscordOAuth2Callback().get_user_enroll_context(
 | 
			
		||||
            self.source, UserOAuthSourceConnection(), DISCORD_USER
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(ak_context["username"], DISCORD_USER["username"])
 | 
			
		||||
        self.assertEqual(ak_context["email"], DISCORD_USER["email"])
 | 
			
		||||
        self.assertEqual(ak_context["name"], DISCORD_USER["username"])
 | 
			
		||||
							
								
								
									
										71
									
								
								authentik/sources/oauth/tests/test_type_github.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								authentik/sources/oauth/tests/test_type_github.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
"""GitHub Type tests"""
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
 | 
			
		||||
from authentik.sources.oauth.types.github import GitHubOAuth2Callback
 | 
			
		||||
 | 
			
		||||
# https://developer.github.com/v3/users/#get-the-authenticated-user
 | 
			
		||||
GITHUB_USER = {
 | 
			
		||||
    "login": "octocat",
 | 
			
		||||
    "id": 1,
 | 
			
		||||
    "node_id": "MDQ6VXNlcjE=",
 | 
			
		||||
    "avatar_url": "https://github.com/images/error/octocat_happy.gif",
 | 
			
		||||
    "gravatar_id": "",
 | 
			
		||||
    "url": "https://api.github.com/users/octocat",
 | 
			
		||||
    "html_url": "https://github.com/octocat",
 | 
			
		||||
    "followers_url": "https://api.github.com/users/octocat/followers",
 | 
			
		||||
    "following_url": "https://api.github.com/users/octocat/following{/other_user}",
 | 
			
		||||
    "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
 | 
			
		||||
    "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
 | 
			
		||||
    "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
 | 
			
		||||
    "organizations_url": "https://api.github.com/users/octocat/orgs",
 | 
			
		||||
    "repos_url": "https://api.github.com/users/octocat/repos",
 | 
			
		||||
    "events_url": "https://api.github.com/users/octocat/events{/privacy}",
 | 
			
		||||
    "received_events_url": "https://api.github.com/users/octocat/received_events",
 | 
			
		||||
    "type": "User",
 | 
			
		||||
    "site_admin": False,
 | 
			
		||||
    "name": "monalisa octocat",
 | 
			
		||||
    "company": "GitHub",
 | 
			
		||||
    "blog": "https://github.com/blog",
 | 
			
		||||
    "location": "San Francisco",
 | 
			
		||||
    "email": "octocat@github.com",
 | 
			
		||||
    "hireable": False,
 | 
			
		||||
    "bio": "There once was...",
 | 
			
		||||
    "twitter_username": "monatheoctocat",
 | 
			
		||||
    "public_repos": 2,
 | 
			
		||||
    "public_gists": 1,
 | 
			
		||||
    "followers": 20,
 | 
			
		||||
    "following": 0,
 | 
			
		||||
    "created_at": "2008-01-14T04:33:35Z",
 | 
			
		||||
    "updated_at": "2008-01-14T04:33:35Z",
 | 
			
		||||
    "private_gists": 81,
 | 
			
		||||
    "total_private_repos": 100,
 | 
			
		||||
    "owned_private_repos": 100,
 | 
			
		||||
    "disk_usage": 10000,
 | 
			
		||||
    "collaborators": 8,
 | 
			
		||||
    "two_factor_authentication": True,
 | 
			
		||||
    "plan": {"name": "Medium", "space": 400, "private_repos": 20, "collaborators": 0},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTypeGitHub(TestCase):
 | 
			
		||||
    """OAuth Source tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.source = OAuthSource.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            slug="test",
 | 
			
		||||
            provider_type="openid-connect",
 | 
			
		||||
            authorization_url="",
 | 
			
		||||
            profile_url="",
 | 
			
		||||
            consumer_key="",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_enroll_context(self):
 | 
			
		||||
        """Test GitHub Enrollment context"""
 | 
			
		||||
        ak_context = GitHubOAuth2Callback().get_user_enroll_context(
 | 
			
		||||
            self.source, UserOAuthSourceConnection(), GITHUB_USER
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(ak_context["username"], GITHUB_USER["login"])
 | 
			
		||||
        self.assertEqual(ak_context["email"], GITHUB_USER["email"])
 | 
			
		||||
        self.assertEqual(ak_context["name"], GITHUB_USER["name"])
 | 
			
		||||
							
								
								
									
										112
									
								
								authentik/sources/oauth/tests/test_type_twitter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								authentik/sources/oauth/tests/test_type_twitter.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
			
		||||
"""Twitter Type tests"""
 | 
			
		||||
from django.test import Client, TestCase
 | 
			
		||||
 | 
			
		||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
 | 
			
		||||
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
 | 
			
		||||
 | 
			
		||||
# https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \
 | 
			
		||||
# api-reference/get-account-verify_credentials
 | 
			
		||||
TWITTER_USER = {
 | 
			
		||||
    "contributors_enabled": True,
 | 
			
		||||
    "created_at": "Sat May 09 17:58:22 +0000 2009",
 | 
			
		||||
    "default_profile": False,
 | 
			
		||||
    "default_profile_image": False,
 | 
			
		||||
    "description": "I taught your phone that thing you like.",
 | 
			
		||||
    "favourites_count": 588,
 | 
			
		||||
    "follow_request_sent": None,
 | 
			
		||||
    "followers_count": 10625,
 | 
			
		||||
    "following": None,
 | 
			
		||||
    "friends_count": 1181,
 | 
			
		||||
    "geo_enabled": True,
 | 
			
		||||
    "id": 38895958,
 | 
			
		||||
    "id_str": "38895958",
 | 
			
		||||
    "is_translator": False,
 | 
			
		||||
    "lang": "en",
 | 
			
		||||
    "listed_count": 190,
 | 
			
		||||
    "location": "San Francisco",
 | 
			
		||||
    "name": "Sean Cook",
 | 
			
		||||
    "notifications": None,
 | 
			
		||||
    "profile_background_color": "1A1B1F",
 | 
			
		||||
    "profile_background_image_url": "",
 | 
			
		||||
    "profile_background_image_url_https": "",
 | 
			
		||||
    "profile_background_tile": True,
 | 
			
		||||
    "profile_image_url": "",
 | 
			
		||||
    "profile_image_url_https": "",
 | 
			
		||||
    "profile_link_color": "2FC2EF",
 | 
			
		||||
    "profile_sidebar_border_color": "181A1E",
 | 
			
		||||
    "profile_sidebar_fill_color": "252429",
 | 
			
		||||
    "profile_text_color": "666666",
 | 
			
		||||
    "profile_use_background_image": True,
 | 
			
		||||
    "protected": False,
 | 
			
		||||
    "screen_name": "theSeanCook",
 | 
			
		||||
    "show_all_inline_media": True,
 | 
			
		||||
    "status": {
 | 
			
		||||
        "contributors": None,
 | 
			
		||||
        "coordinates": {"coordinates": [-122.45037293, 37.76484123], "type": "Point"},
 | 
			
		||||
        "created_at": "Tue Aug 28 05:44:24 +0000 2012",
 | 
			
		||||
        "favorited": False,
 | 
			
		||||
        "geo": {"coordinates": [37.76484123, -122.45037293], "type": "Point"},
 | 
			
		||||
        "id": 240323931419062272,
 | 
			
		||||
        "id_str": "240323931419062272",
 | 
			
		||||
        "in_reply_to_screen_name": "messl",
 | 
			
		||||
        "in_reply_to_status_id": 240316959173009410,
 | 
			
		||||
        "in_reply_to_status_id_str": "240316959173009410",
 | 
			
		||||
        "in_reply_to_user_id": 18707866,
 | 
			
		||||
        "in_reply_to_user_id_str": "18707866",
 | 
			
		||||
        "place": {
 | 
			
		||||
            "attributes": {},
 | 
			
		||||
            "bounding_box": {
 | 
			
		||||
                "coordinates": [
 | 
			
		||||
                    [
 | 
			
		||||
                        [-122.45778216, 37.75932999],
 | 
			
		||||
                        [-122.44248216, 37.75932999],
 | 
			
		||||
                        [-122.44248216, 37.76752899],
 | 
			
		||||
                        [-122.45778216, 37.76752899],
 | 
			
		||||
                    ]
 | 
			
		||||
                ],
 | 
			
		||||
                "type": "Polygon",
 | 
			
		||||
            },
 | 
			
		||||
            "country": "United States",
 | 
			
		||||
            "country_code": "US",
 | 
			
		||||
            "full_name": "Ashbury Heights, San Francisco",
 | 
			
		||||
            "id": "866269c983527d5a",
 | 
			
		||||
            "name": "Ashbury Heights",
 | 
			
		||||
            "place_type": "neighborhood",
 | 
			
		||||
            "url": "http://api.twitter.com/1/geo/id/866269c983527d5a.json",
 | 
			
		||||
        },
 | 
			
		||||
        "retweet_count": 0,
 | 
			
		||||
        "retweeted": False,
 | 
			
		||||
        "source": "Twitter for  iPhone",
 | 
			
		||||
        "text": "@messl congrats! So happy for all 3 of you.",
 | 
			
		||||
        "truncated": False,
 | 
			
		||||
    },
 | 
			
		||||
    "statuses_count": 2609,
 | 
			
		||||
    "time_zone": "Pacific Time (US & Canada)",
 | 
			
		||||
    "url": None,
 | 
			
		||||
    "utc_offset": -28800,
 | 
			
		||||
    "verified": False,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestTypeGitHub(TestCase):
 | 
			
		||||
    """OAuth Source tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.client = Client()
 | 
			
		||||
        self.source = OAuthSource.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            slug="test",
 | 
			
		||||
            provider_type="openid-connect",
 | 
			
		||||
            authorization_url="",
 | 
			
		||||
            profile_url="",
 | 
			
		||||
            consumer_key="",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_enroll_context(self):
 | 
			
		||||
        """Test Twitter Enrollment context"""
 | 
			
		||||
        ak_context = TwitterOAuthCallback().get_user_enroll_context(
 | 
			
		||||
            self.source, UserOAuthSourceConnection(), TWITTER_USER
 | 
			
		||||
        )
 | 
			
		||||
        self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"])
 | 
			
		||||
        self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None))
 | 
			
		||||
        self.assertEqual(ak_context["name"], TWITTER_USER["name"])
 | 
			
		||||
@ -1,15 +1,14 @@
 | 
			
		||||
"""OAuth Source tests"""
 | 
			
		||||
from django.shortcuts import reverse
 | 
			
		||||
from django.test import Client, TestCase
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
 | 
			
		||||
from authentik.sources.oauth.models import OAuthSource
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OAuthSourceTests(TestCase):
 | 
			
		||||
class TestOAuthSource(TestCase):
 | 
			
		||||
    """OAuth Source tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.client = Client()
 | 
			
		||||
        self.source = OAuthSource.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            slug="test",
 | 
			
		||||
@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
			
		||||
class DiscordOAuthRedirect(OAuthRedirect):
 | 
			
		||||
    """Discord OAuth2 Redirect"""
 | 
			
		||||
 | 
			
		||||
    def get_additional_parameters(self, source):
 | 
			
		||||
    def get_additional_parameters(self, source):  # pragma: no cover
 | 
			
		||||
        return {
 | 
			
		||||
            "scope": "email identify",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
			
		||||
class FacebookOAuthRedirect(OAuthRedirect):
 | 
			
		||||
    """Facebook OAuth2 Redirect"""
 | 
			
		||||
 | 
			
		||||
    def get_additional_parameters(self, source):
 | 
			
		||||
    def get_additional_parameters(self, source):  # pragma: no cover
 | 
			
		||||
        return {
 | 
			
		||||
            "scope": "email",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
			
		||||
class GoogleOAuthRedirect(OAuthRedirect):
 | 
			
		||||
    """Google OAuth2 Redirect"""
 | 
			
		||||
 | 
			
		||||
    def get_additional_parameters(self, source):
 | 
			
		||||
    def get_additional_parameters(self, source):  # pragma: no cover
 | 
			
		||||
        return {
 | 
			
		||||
            "scope": "email profile",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
			
		||||
class OpenIDConnectOAuthRedirect(OAuthRedirect):
 | 
			
		||||
    """OpenIDConnect OAuth2 Redirect"""
 | 
			
		||||
 | 
			
		||||
    def get_additional_parameters(self, source: OAuthSource):
 | 
			
		||||
    def get_additional_parameters(self, source: OAuthSource):  # pragma: no cover
 | 
			
		||||
        return {
 | 
			
		||||
            "scope": "openid email profile",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
			
		||||
class RedditOAuthRedirect(OAuthRedirect):
 | 
			
		||||
    """Reddit OAuth2 Redirect"""
 | 
			
		||||
 | 
			
		||||
    def get_additional_parameters(self, source):
 | 
			
		||||
    def get_additional_parameters(self, source):  # pragma: no cover
 | 
			
		||||
        return {
 | 
			
		||||
            "scope": "identity",
 | 
			
		||||
            "duration": "permanent",
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,6 @@ class TwitterOAuthCallback(OAuthCallback):
 | 
			
		||||
    ) -> Dict[str, Any]:
 | 
			
		||||
        return {
 | 
			
		||||
            "username": info.get("screen_name"),
 | 
			
		||||
            "email": info.get("email"),
 | 
			
		||||
            "email": info.get("email", None),
 | 
			
		||||
            "name": info.get("name"),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
"""SAML Source tests"""
 | 
			
		||||
from defusedxml import ElementTree
 | 
			
		||||
from django.test import RequestFactory, TestCase
 | 
			
		||||
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.sources.saml.models import SAMLSource
 | 
			
		||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestMetadataProcessor(TestCase):
 | 
			
		||||
    """Test MetadataProcessor"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.source = SAMLSource.objects.create(
 | 
			
		||||
            slug="provider",
 | 
			
		||||
            issuer="authentik",
 | 
			
		||||
            signing_kp=CertificateKeyPair.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        self.factory = RequestFactory()
 | 
			
		||||
 | 
			
		||||
    def test_metadata(self):
 | 
			
		||||
        """Test Metadata generation being valid"""
 | 
			
		||||
        request = self.factory.get("/")
 | 
			
		||||
        xml = MetadataProcessor(self.source, request).build_entity_descriptor()
 | 
			
		||||
        metadata = ElementTree.fromstring(xml)
 | 
			
		||||
        self.assertEqual(metadata.attrib["entityID"], "authentik")
 | 
			
		||||
							
								
								
									
										0
									
								
								authentik/sources/saml/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/sources/saml/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										55
									
								
								authentik/sources/saml/tests/test_metadata.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								authentik/sources/saml/tests/test_metadata.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
			
		||||
"""SAML Source tests"""
 | 
			
		||||
from defusedxml import ElementTree
 | 
			
		||||
from django.test import RequestFactory, TestCase
 | 
			
		||||
from lxml import etree  # nosec
 | 
			
		||||
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.sources.saml.models import SAMLSource
 | 
			
		||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestMetadataProcessor(TestCase):
 | 
			
		||||
    """Test MetadataProcessor"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        self.factory = RequestFactory()
 | 
			
		||||
 | 
			
		||||
    def test_metadata_schema(self):
 | 
			
		||||
        """Test Metadata generation being valid"""
 | 
			
		||||
        source = SAMLSource.objects.create(
 | 
			
		||||
            slug="provider",
 | 
			
		||||
            issuer="authentik",
 | 
			
		||||
            signing_kp=CertificateKeyPair.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        request = self.factory.get("/")
 | 
			
		||||
        xml = MetadataProcessor(source, request).build_entity_descriptor()
 | 
			
		||||
        metadata = etree.fromstring(xml)  # nosec
 | 
			
		||||
 | 
			
		||||
        schema = etree.XMLSchema(
 | 
			
		||||
            etree.parse("xml/saml-schema-metadata-2.0.xsd")
 | 
			
		||||
        )  # nosec
 | 
			
		||||
        self.assertTrue(schema.validate(metadata))
 | 
			
		||||
 | 
			
		||||
    def test_metadata(self):
 | 
			
		||||
        """Test Metadata generation being valid"""
 | 
			
		||||
        source = SAMLSource.objects.create(
 | 
			
		||||
            slug="provider",
 | 
			
		||||
            issuer="authentik",
 | 
			
		||||
            signing_kp=CertificateKeyPair.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        request = self.factory.get("/")
 | 
			
		||||
        xml = MetadataProcessor(source, request).build_entity_descriptor()
 | 
			
		||||
        metadata = ElementTree.fromstring(xml)
 | 
			
		||||
        self.assertEqual(metadata.attrib["entityID"], "authentik")
 | 
			
		||||
 | 
			
		||||
    def test_metadata_without_signautre(self):
 | 
			
		||||
        """Test Metadata generation being valid"""
 | 
			
		||||
        source = SAMLSource.objects.create(
 | 
			
		||||
            slug="provider",
 | 
			
		||||
            issuer="authentik",
 | 
			
		||||
            # signing_kp=CertificateKeyPair.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        request = self.factory.get("/")
 | 
			
		||||
        xml = MetadataProcessor(source, request).build_entity_descriptor()
 | 
			
		||||
        metadata = ElementTree.fromstring(xml)
 | 
			
		||||
        self.assertEqual(metadata.attrib["entityID"], "authentik")
 | 
			
		||||
@ -87,6 +87,7 @@ class TestUserWriteStage(TestCase):
 | 
			
		||||
            "username": "test-user-new",
 | 
			
		||||
            "password": new_password,
 | 
			
		||||
            "attribute_some-custom-attribute": "test",
 | 
			
		||||
            "some_ignored_attribute": "bar",
 | 
			
		||||
        }
 | 
			
		||||
        session = self.client.session
 | 
			
		||||
        session[SESSION_KEY_PLAN] = plan
 | 
			
		||||
@ -109,6 +110,7 @@ class TestUserWriteStage(TestCase):
 | 
			
		||||
        self.assertTrue(user_qs.exists())
 | 
			
		||||
        self.assertTrue(user_qs.first().check_password(new_password))
 | 
			
		||||
        self.assertEqual(user_qs.first().attributes["some-custom-attribute"], "test")
 | 
			
		||||
        self.assertNotIn("some_ignored_attribute", user_qs.first().attributes)
 | 
			
		||||
 | 
			
		||||
    @patch(
 | 
			
		||||
        "authentik.flows.views.to_stage_response",
 | 
			
		||||
 | 
			
		||||
@ -10,8 +10,8 @@ services:
 | 
			
		||||
      - internal
 | 
			
		||||
    environment:
 | 
			
		||||
      - POSTGRES_PASSWORD=${PG_PASS:-thisisnotagoodpassword}
 | 
			
		||||
      - POSTGRES_USER=authentik
 | 
			
		||||
      - POSTGRES_DB=authentik
 | 
			
		||||
      - POSTGRES_USER=${PG_USER:-authentik}
 | 
			
		||||
      - POSTGRES_DB=${PG_DB:-authentik}
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
  redis:
 | 
			
		||||
@ -19,11 +19,13 @@ services:
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
  server:
 | 
			
		||||
    image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-rc3}
 | 
			
		||||
    image: beryju/authentik:${AUTHENTIK_TAG:-0.13.2-stable}
 | 
			
		||||
    command: server
 | 
			
		||||
    environment:
 | 
			
		||||
      AUTHENTIK_REDIS__HOST: redis
 | 
			
		||||
      AUTHENTIK_POSTGRESQL__HOST: postgresql
 | 
			
		||||
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
 | 
			
		||||
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
 | 
			
		||||
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./media:/media
 | 
			
		||||
@ -42,13 +44,15 @@ services:
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
  worker:
 | 
			
		||||
    image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-rc3}
 | 
			
		||||
    image: beryju/authentik:${AUTHENTIK_TAG:-0.13.2-stable}
 | 
			
		||||
    command: worker
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
    environment:
 | 
			
		||||
      AUTHENTIK_REDIS__HOST: redis
 | 
			
		||||
      AUTHENTIK_POSTGRESQL__HOST: postgresql
 | 
			
		||||
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
 | 
			
		||||
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
 | 
			
		||||
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./backups:/backups
 | 
			
		||||
@ -56,7 +60,7 @@ services:
 | 
			
		||||
    env_file:
 | 
			
		||||
      - .env
 | 
			
		||||
  static:
 | 
			
		||||
    image: beryju/authentik-static:${AUTHENTIK_TAG:-0.13.0-rc3}
 | 
			
		||||
    image: beryju/authentik-static:${AUTHENTIK_TAG:-0.13.2-stable}
 | 
			
		||||
    networks:
 | 
			
		||||
      - internal
 | 
			
		||||
    labels:
 | 
			
		||||
@ -68,7 +72,7 @@ services:
 | 
			
		||||
      traefik.http.services.static-service.loadbalancer.healthcheck.path: /
 | 
			
		||||
      traefik.http.services.static-service.loadbalancer.server.port: '80'
 | 
			
		||||
    volumes:
 | 
			
		||||
      - ./media:/media
 | 
			
		||||
      - ./media:/usr/share/nginx/html/media
 | 
			
		||||
  traefik:
 | 
			
		||||
    image: traefik:2.3
 | 
			
		||||
    command:
 | 
			
		||||
@ -81,7 +85,6 @@ services:
 | 
			
		||||
    volumes:
 | 
			
		||||
      - /var/run/docker.sock:/var/run/docker.sock:ro
 | 
			
		||||
    ports:
 | 
			
		||||
      - "0.0.0.0:80:80"
 | 
			
		||||
      - "0.0.0.0:443:443"
 | 
			
		||||
      - "127.0.0.1:8080:8080"
 | 
			
		||||
    networks:
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ name: authentik
 | 
			
		||||
home: https://goauthentik.io
 | 
			
		||||
sources:
 | 
			
		||||
  - https://github.com/BeryJu/authentik
 | 
			
		||||
version: "0.13.0-rc3"
 | 
			
		||||
version: "0.13.2-stable"
 | 
			
		||||
icon: https://raw.githubusercontent.com/BeryJu/authentik/master/web/icons/icon.svg
 | 
			
		||||
dependencies:
 | 
			
		||||
  - name: postgresql
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@
 | 
			
		||||
|-----------------------------------|-------------------------|-------------|
 | 
			
		||||
| image.name                        | beryju/authentik         | Image used to run the authentik server and worker |
 | 
			
		||||
| image.name_static                 | beryju/authentik-static  | Image used to run the authentik static server (CSS and JS Files) |
 | 
			
		||||
| image.tag                         | 0.13.0-rc3              | Image tag |
 | 
			
		||||
| image.tag                         | 0.13.2-stable              | Image tag |
 | 
			
		||||
| image.pullPolicy                  | IfNotPresent            | Image Pull Policy used for all deployments |
 | 
			
		||||
| serverReplicas                    | 1                       | Replicas for the Server deployment |
 | 
			
		||||
| workerReplicas                    | 1                       | Replicas for the Worker deployment |
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,10 @@ spec:
 | 
			
		||||
            backend:
 | 
			
		||||
              serviceName: {{ $fullName }}-static
 | 
			
		||||
              servicePort: http
 | 
			
		||||
          - path: /media/
 | 
			
		||||
            backend:
 | 
			
		||||
              serviceName: {{ $fullName }}-static
 | 
			
		||||
              servicePort: http
 | 
			
		||||
          - path: /robots.txt
 | 
			
		||||
            backend:
 | 
			
		||||
              serviceName: {{ $fullName }}-static
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ image:
 | 
			
		||||
  name: beryju/authentik
 | 
			
		||||
  name_static: beryju/authentik-static
 | 
			
		||||
  name_outposts: beryju/authentik # Prefix used for Outpost deployments, Outpost type and version is appended
 | 
			
		||||
  tag: 0.13.0-rc3
 | 
			
		||||
  tag: 0.13.2-stable
 | 
			
		||||
  pullPolicy: IfNotPresent
 | 
			
		||||
 | 
			
		||||
serverReplicas: 1
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								proxy/go.mod
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								proxy/go.mod
									
									
									
									
									
								
							@ -5,12 +5,12 @@ go 1.14
 | 
			
		||||
require (
 | 
			
		||||
	cloud.google.com/go v0.64.0 // 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.9
 | 
			
		||||
	github.com/getsentry/sentry-go v0.9.0
 | 
			
		||||
	github.com/go-openapi/errors v0.19.9 // indirect
 | 
			
		||||
	github.com/go-openapi/runtime v0.19.24
 | 
			
		||||
	github.com/go-openapi/strfmt v0.19.11
 | 
			
		||||
	github.com/go-openapi/swag v0.19.12
 | 
			
		||||
	github.com/go-openapi/validate v0.19.15
 | 
			
		||||
	github.com/go-openapi/swag v0.19.12 // indirect
 | 
			
		||||
	github.com/go-openapi/validate v0.19.15 // indirect
 | 
			
		||||
	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
 | 
			
		||||
@ -22,7 +22,7 @@ require (
 | 
			
		||||
	github.com/oauth2-proxy/oauth2-proxy v1.1.2-0.20200817154438-5fa5b3186f39
 | 
			
		||||
	github.com/pelletier/go-toml v1.8.1 // indirect
 | 
			
		||||
	github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect
 | 
			
		||||
	github.com/recws-org/recws v1.2.1
 | 
			
		||||
	github.com/recws-org/recws v1.2.2
 | 
			
		||||
	github.com/sirupsen/logrus v1.7.0
 | 
			
		||||
	github.com/spf13/afero v1.4.1 // indirect
 | 
			
		||||
	github.com/spf13/cast v1.3.1 // indirect
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								proxy/go.sum
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								proxy/go.sum
									
									
									
									
									
								
							@ -35,13 +35,17 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
 | 
			
		||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 | 
			
		||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 | 
			
		||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
 | 
			
		||||
github.com/BeryJu/authentik v0.0.0-20201213234502-f0f32453882b h1:tLc7ERt2fWSu14nXdsER4EP62KUPXwAB0OeLVAA4Rx0=
 | 
			
		||||
github.com/BeryJu/authentik v0.0.0-20201214075318-41f9097592da h1:XB/MCgb0K+ngEETjBYKGiXcN2des8YtANetYboeT4Lg=
 | 
			
		||||
github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw=
 | 
			
		||||
github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss=
 | 
			
		||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
 | 
			
		||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 | 
			
		||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 | 
			
		||||
github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw=
 | 
			
		||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
 | 
			
		||||
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
 | 
			
		||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
 | 
			
		||||
github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc=
 | 
			
		||||
github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
 | 
			
		||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
 | 
			
		||||
@ -139,6 +143,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
 | 
			
		||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
 | 
			
		||||
github.com/getsentry/sentry-go v0.7.0 h1:MR2yfR4vFfv/2+iBuSnkdQwVg7N9cJzihZ6KJu7srwQ=
 | 
			
		||||
github.com/getsentry/sentry-go v0.7.0/go.mod h1:pLFpD2Y5RHIKF9Bw3KH6/68DeN2K/XBJd8awjdPnUwg=
 | 
			
		||||
github.com/getsentry/sentry-go v0.9.0 h1:KIfpY/D9hX3gWAEd3d8z6ImuHNWtqEsjlpdF8zXFsHM=
 | 
			
		||||
github.com/getsentry/sentry-go v0.9.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
 | 
			
		||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 | 
			
		||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
 | 
			
		||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
 | 
			
		||||
@ -409,6 +415,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 | 
			
		||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
 | 
			
		||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
 | 
			
		||||
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
 | 
			
		||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
 | 
			
		||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
 | 
			
		||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
 | 
			
		||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
 | 
			
		||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 | 
			
		||||
@ -438,9 +446,14 @@ github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q
 | 
			
		||||
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
 | 
			
		||||
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
 | 
			
		||||
github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk=
 | 
			
		||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
 | 
			
		||||
github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U=
 | 
			
		||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
 | 
			
		||||
github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw=
 | 
			
		||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
 | 
			
		||||
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0=
 | 
			
		||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
 | 
			
		||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
 | 
			
		||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
 | 
			
		||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
 | 
			
		||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
 | 
			
		||||
@ -448,6 +461,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
 | 
			
		||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 | 
			
		||||
github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 | 
			
		||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 | 
			
		||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 | 
			
		||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 | 
			
		||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 | 
			
		||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
 | 
			
		||||
@ -496,6 +510,7 @@ github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa h1:hI1uC2A3vJFjwvB
 | 
			
		||||
github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s=
 | 
			
		||||
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg=
 | 
			
		||||
github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
 | 
			
		||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
 | 
			
		||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
 | 
			
		||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 | 
			
		||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
 | 
			
		||||
@ -519,8 +534,11 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
 | 
			
		||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
 | 
			
		||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
 | 
			
		||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 | 
			
		||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
 | 
			
		||||
github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM=
 | 
			
		||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
 | 
			
		||||
github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
 | 
			
		||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
 | 
			
		||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 | 
			
		||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
 | 
			
		||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 | 
			
		||||
@ -577,6 +595,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
 | 
			
		||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 | 
			
		||||
github.com/recws-org/recws v1.2.1 h1:bYocRkAsS71hlQ9AMCVS+hYXHEgEyQsAbYKXf394gZ8=
 | 
			
		||||
github.com/recws-org/recws v1.2.1/go.mod h1:SxTgwQU/jqYSzEgUh4ifDxq/7enApS150f8nZ5Sczk8=
 | 
			
		||||
github.com/recws-org/recws v1.2.2 h1:TkyyCEgMjsr1D2fnutY/DPhGnUKCLpJeXDAGy6rLmGE=
 | 
			
		||||
github.com/recws-org/recws v1.2.2/go.mod h1:SxTgwQU/jqYSzEgUh4ifDxq/7enApS150f8nZ5Sczk8=
 | 
			
		||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 | 
			
		||||
@ -584,6 +604,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 | 
			
		||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
 | 
			
		||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 | 
			
		||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 | 
			
		||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
 | 
			
		||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 | 
			
		||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 | 
			
		||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 | 
			
		||||
@ -711,6 +732,7 @@ golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8U
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
 | 
			
		||||
@ -1046,6 +1068,7 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8
 | 
			
		||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
 | 
			
		||||
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.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/ini.v1 v1.57.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=
 | 
			
		||||
@ -1070,6 +1093,7 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 | 
			
		||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 | 
			
		||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 | 
			
		||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
 | 
			
		||||
@ -59,6 +59,7 @@ func getCommonOptions() *options.Options {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func doGlobalSetup(config map[string]interface{}) {
 | 
			
		||||
	log.SetFormatter(&log.JSONFormatter{})
 | 
			
		||||
	switch config[ConfigLogLevel].(string) {
 | 
			
		||||
	case "debug":
 | 
			
		||||
		log.SetLevel(log.DebugLevel)
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,3 @@
 | 
			
		||||
package pkg
 | 
			
		||||
 | 
			
		||||
const VERSION = "0.13.0-rc3"
 | 
			
		||||
const VERSION = "0.13.2-stable"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										344
									
								
								swagger.yaml
									
									
									
									
									
								
							
							
						
						
									
										344
									
								
								swagger.yaml
									
									
									
									
									
								
							@ -22,11 +22,11 @@ paths:
 | 
			
		||||
  /admin/metrics/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: admin_metrics_list
 | 
			
		||||
      description: Return single instance of AdministrationMetricsSerializer
 | 
			
		||||
      description: Login Metrics per 1h
 | 
			
		||||
      parameters: []
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: Overview View
 | 
			
		||||
          description: Login Metrics per 1h
 | 
			
		||||
          schema:
 | 
			
		||||
            description: ''
 | 
			
		||||
            type: array
 | 
			
		||||
@ -35,22 +35,6 @@ paths:
 | 
			
		||||
      tags:
 | 
			
		||||
        - admin
 | 
			
		||||
    parameters: []
 | 
			
		||||
  /admin/overview/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: admin_overview_list
 | 
			
		||||
      description: Return single instance of AdministrationOverviewSerializer
 | 
			
		||||
      parameters: []
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: Overview View
 | 
			
		||||
          schema:
 | 
			
		||||
            description: ''
 | 
			
		||||
            type: array
 | 
			
		||||
            items:
 | 
			
		||||
              $ref: '#/definitions/AdministrationOverview'
 | 
			
		||||
      tags:
 | 
			
		||||
        - admin
 | 
			
		||||
    parameters: []
 | 
			
		||||
  /admin/system_tasks/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: admin_system_tasks_list
 | 
			
		||||
@ -82,6 +66,95 @@ paths:
 | 
			
		||||
        in: path
 | 
			
		||||
        required: true
 | 
			
		||||
        type: string
 | 
			
		||||
  /admin/version/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: admin_version_list
 | 
			
		||||
      description: Get running and latest version.
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: ordering
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Which field to use when ordering the results.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: search
 | 
			
		||||
          in: query
 | 
			
		||||
          description: A search term.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: page
 | 
			
		||||
          in: query
 | 
			
		||||
          description: A page number within the paginated result set.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: integer
 | 
			
		||||
        - name: page_size
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Number of results to return per page.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: integer
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: Get running and latest version.
 | 
			
		||||
          schema:
 | 
			
		||||
            description: ''
 | 
			
		||||
            type: array
 | 
			
		||||
            items:
 | 
			
		||||
              $ref: '#/definitions/Version'
 | 
			
		||||
      tags:
 | 
			
		||||
        - admin
 | 
			
		||||
    parameters: []
 | 
			
		||||
  /admin/workers/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: admin_workers_list
 | 
			
		||||
      description: Get currently connected worker count.
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: ordering
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Which field to use when ordering the results.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: search
 | 
			
		||||
          in: query
 | 
			
		||||
          description: A search term.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: page
 | 
			
		||||
          in: query
 | 
			
		||||
          description: A page number within the paginated result set.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: integer
 | 
			
		||||
        - name: page_size
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Number of results to return per page.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: integer
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: ''
 | 
			
		||||
          schema:
 | 
			
		||||
            required:
 | 
			
		||||
              - count
 | 
			
		||||
              - results
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
              count:
 | 
			
		||||
                type: integer
 | 
			
		||||
              next:
 | 
			
		||||
                type: string
 | 
			
		||||
                format: uri
 | 
			
		||||
                x-nullable: true
 | 
			
		||||
              previous:
 | 
			
		||||
                type: string
 | 
			
		||||
                format: uri
 | 
			
		||||
                x-nullable: true
 | 
			
		||||
              results:
 | 
			
		||||
                type: array
 | 
			
		||||
                items:
 | 
			
		||||
                  description: ''
 | 
			
		||||
                  type: object
 | 
			
		||||
                  properties: {}
 | 
			
		||||
      tags:
 | 
			
		||||
        - admin
 | 
			
		||||
    parameters: []
 | 
			
		||||
  /audit/events/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: audit_events_list
 | 
			
		||||
@ -1062,6 +1135,59 @@ paths:
 | 
			
		||||
        required: true
 | 
			
		||||
        type: string
 | 
			
		||||
        format: uuid
 | 
			
		||||
  /flows/cached/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: flows_cached_list
 | 
			
		||||
      description: Info about cached flows
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: ordering
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Which field to use when ordering the results.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: search
 | 
			
		||||
          in: query
 | 
			
		||||
          description: A search term.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: page
 | 
			
		||||
          in: query
 | 
			
		||||
          description: A page number within the paginated result set.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: integer
 | 
			
		||||
        - name: page_size
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Number of results to return per page.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: integer
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: ''
 | 
			
		||||
          schema:
 | 
			
		||||
            required:
 | 
			
		||||
              - count
 | 
			
		||||
              - results
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
              count:
 | 
			
		||||
                type: integer
 | 
			
		||||
              next:
 | 
			
		||||
                type: string
 | 
			
		||||
                format: uri
 | 
			
		||||
                x-nullable: true
 | 
			
		||||
              previous:
 | 
			
		||||
                type: string
 | 
			
		||||
                format: uri
 | 
			
		||||
                x-nullable: true
 | 
			
		||||
              results:
 | 
			
		||||
                type: array
 | 
			
		||||
                items:
 | 
			
		||||
                  description: ''
 | 
			
		||||
                  type: object
 | 
			
		||||
                  properties: {}
 | 
			
		||||
      tags:
 | 
			
		||||
        - flows
 | 
			
		||||
    parameters: []
 | 
			
		||||
  /flows/instances/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: flows_instances_list
 | 
			
		||||
@ -1702,6 +1828,16 @@ paths:
 | 
			
		||||
      operationId: policies_all_list
 | 
			
		||||
      description: Policy Viewset
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: bindings__isnull
 | 
			
		||||
          in: query
 | 
			
		||||
          description: ''
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: promptstage__isnull
 | 
			
		||||
          in: query
 | 
			
		||||
          description: ''
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: ordering
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Which field to use when ordering the results.
 | 
			
		||||
@ -1919,6 +2055,59 @@ paths:
 | 
			
		||||
        required: true
 | 
			
		||||
        type: string
 | 
			
		||||
        format: uuid
 | 
			
		||||
  /policies/cached/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: policies_cached_list
 | 
			
		||||
      description: Info about cached policies
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: ordering
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Which field to use when ordering the results.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: search
 | 
			
		||||
          in: query
 | 
			
		||||
          description: A search term.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: page
 | 
			
		||||
          in: query
 | 
			
		||||
          description: A page number within the paginated result set.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: integer
 | 
			
		||||
        - name: page_size
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Number of results to return per page.
 | 
			
		||||
          required: false
 | 
			
		||||
          type: integer
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: ''
 | 
			
		||||
          schema:
 | 
			
		||||
            required:
 | 
			
		||||
              - count
 | 
			
		||||
              - results
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
              count:
 | 
			
		||||
                type: integer
 | 
			
		||||
              next:
 | 
			
		||||
                type: string
 | 
			
		||||
                format: uri
 | 
			
		||||
                x-nullable: true
 | 
			
		||||
              previous:
 | 
			
		||||
                type: string
 | 
			
		||||
                format: uri
 | 
			
		||||
                x-nullable: true
 | 
			
		||||
              results:
 | 
			
		||||
                type: array
 | 
			
		||||
                items:
 | 
			
		||||
                  description: ''
 | 
			
		||||
                  type: object
 | 
			
		||||
                  properties: {}
 | 
			
		||||
      tags:
 | 
			
		||||
        - policies
 | 
			
		||||
    parameters: []
 | 
			
		||||
  /policies/dummy/:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: policies_dummy_list
 | 
			
		||||
@ -3264,6 +3453,11 @@ paths:
 | 
			
		||||
      operationId: providers_all_list
 | 
			
		||||
      description: Provider Viewset
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: application__isnull
 | 
			
		||||
          in: query
 | 
			
		||||
          description: ''
 | 
			
		||||
          required: false
 | 
			
		||||
          type: string
 | 
			
		||||
        - name: ordering
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Which field to use when ordering the results.
 | 
			
		||||
@ -3309,6 +3503,22 @@ paths:
 | 
			
		||||
                  $ref: '#/definitions/Provider'
 | 
			
		||||
      tags:
 | 
			
		||||
        - providers
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: providers_all_create
 | 
			
		||||
      description: Provider Viewset
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: data
 | 
			
		||||
          in: body
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/Provider'
 | 
			
		||||
      responses:
 | 
			
		||||
        '201':
 | 
			
		||||
          description: ''
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/Provider'
 | 
			
		||||
      tags:
 | 
			
		||||
        - providers
 | 
			
		||||
    parameters: []
 | 
			
		||||
  /providers/all/{id}/:
 | 
			
		||||
    get:
 | 
			
		||||
@ -3322,6 +3532,47 @@ paths:
 | 
			
		||||
            $ref: '#/definitions/Provider'
 | 
			
		||||
      tags:
 | 
			
		||||
        - providers
 | 
			
		||||
    put:
 | 
			
		||||
      operationId: providers_all_update
 | 
			
		||||
      description: Provider Viewset
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: data
 | 
			
		||||
          in: body
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/Provider'
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: ''
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/Provider'
 | 
			
		||||
      tags:
 | 
			
		||||
        - providers
 | 
			
		||||
    patch:
 | 
			
		||||
      operationId: providers_all_partial_update
 | 
			
		||||
      description: Provider Viewset
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: data
 | 
			
		||||
          in: body
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/Provider'
 | 
			
		||||
      responses:
 | 
			
		||||
        '200':
 | 
			
		||||
          description: ''
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/Provider'
 | 
			
		||||
      tags:
 | 
			
		||||
        - providers
 | 
			
		||||
    delete:
 | 
			
		||||
      operationId: providers_all_delete
 | 
			
		||||
      description: Provider Viewset
 | 
			
		||||
      parameters: []
 | 
			
		||||
      responses:
 | 
			
		||||
        '204':
 | 
			
		||||
          description: ''
 | 
			
		||||
      tags:
 | 
			
		||||
        - providers
 | 
			
		||||
    parameters:
 | 
			
		||||
      - name: id
 | 
			
		||||
        in: path
 | 
			
		||||
@ -6421,7 +6672,7 @@ paths:
 | 
			
		||||
        format: uuid
 | 
			
		||||
definitions:
 | 
			
		||||
  AdministrationMetrics:
 | 
			
		||||
    description: Overview View
 | 
			
		||||
    description: Login Metrics per 1h
 | 
			
		||||
    type: object
 | 
			
		||||
    properties:
 | 
			
		||||
      logins_per_1h:
 | 
			
		||||
@ -6432,38 +6683,6 @@ definitions:
 | 
			
		||||
        title: Logins failed per 1h
 | 
			
		||||
        type: string
 | 
			
		||||
        readOnly: true
 | 
			
		||||
  AdministrationOverview:
 | 
			
		||||
    description: Overview View
 | 
			
		||||
    type: object
 | 
			
		||||
    properties:
 | 
			
		||||
      version:
 | 
			
		||||
        title: Version
 | 
			
		||||
        type: string
 | 
			
		||||
        readOnly: true
 | 
			
		||||
      version_latest:
 | 
			
		||||
        title: Version latest
 | 
			
		||||
        type: string
 | 
			
		||||
        readOnly: true
 | 
			
		||||
      worker_count:
 | 
			
		||||
        title: Worker count
 | 
			
		||||
        type: integer
 | 
			
		||||
        readOnly: true
 | 
			
		||||
      providers_without_application:
 | 
			
		||||
        title: Providers without application
 | 
			
		||||
        type: integer
 | 
			
		||||
        readOnly: true
 | 
			
		||||
      policies_without_binding:
 | 
			
		||||
        title: Policies without binding
 | 
			
		||||
        type: integer
 | 
			
		||||
        readOnly: true
 | 
			
		||||
      cached_policies:
 | 
			
		||||
        title: Cached policies
 | 
			
		||||
        type: integer
 | 
			
		||||
        readOnly: true
 | 
			
		||||
      cached_flows:
 | 
			
		||||
        title: Cached flows
 | 
			
		||||
        type: integer
 | 
			
		||||
        readOnly: true
 | 
			
		||||
  Task:
 | 
			
		||||
    description: Serialize TaskInfo and TaskResult
 | 
			
		||||
    required:
 | 
			
		||||
@ -6494,6 +6713,22 @@ definitions:
 | 
			
		||||
        type: array
 | 
			
		||||
        items:
 | 
			
		||||
          type: string
 | 
			
		||||
  Version:
 | 
			
		||||
    description: Get running and latest version.
 | 
			
		||||
    type: object
 | 
			
		||||
    properties:
 | 
			
		||||
      version_current:
 | 
			
		||||
        title: Version current
 | 
			
		||||
        type: string
 | 
			
		||||
        readOnly: true
 | 
			
		||||
      version_latest:
 | 
			
		||||
        title: Version latest
 | 
			
		||||
        type: string
 | 
			
		||||
        readOnly: true
 | 
			
		||||
      outdated:
 | 
			
		||||
        title: Outdated
 | 
			
		||||
        type: boolean
 | 
			
		||||
        readOnly: true
 | 
			
		||||
  Event:
 | 
			
		||||
    description: Event Serializer
 | 
			
		||||
    required:
 | 
			
		||||
@ -7087,6 +7322,9 @@ definitions:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      url:
 | 
			
		||||
        title: Url
 | 
			
		||||
        description: Can be in the format of 'unix://<path>' when connecting to a
 | 
			
		||||
          local docker daemon, or 'https://<hostname>:2376' when connecting to a remote
 | 
			
		||||
          system.
 | 
			
		||||
        type: string
 | 
			
		||||
        minLength: 1
 | 
			
		||||
      tls_verification:
 | 
			
		||||
@ -7477,6 +7715,7 @@ definitions:
 | 
			
		||||
    description: Provider Serializer
 | 
			
		||||
    required:
 | 
			
		||||
      - name
 | 
			
		||||
      - application
 | 
			
		||||
      - authorization_flow
 | 
			
		||||
    type: object
 | 
			
		||||
    properties:
 | 
			
		||||
@ -7488,6 +7727,9 @@ definitions:
 | 
			
		||||
        title: Name
 | 
			
		||||
        type: string
 | 
			
		||||
        minLength: 1
 | 
			
		||||
      application:
 | 
			
		||||
        title: Application
 | 
			
		||||
        type: string
 | 
			
		||||
      authorization_flow:
 | 
			
		||||
        title: Authorization flow
 | 
			
		||||
        description: Flow used when authorizing this provider.
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										95
									
								
								tests/integration/test_outpost_docker.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								tests/integration/test_outpost_docker.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,95 @@
 | 
			
		||||
"""outpost tests"""
 | 
			
		||||
from shutil import rmtree
 | 
			
		||||
from tempfile import mkdtemp
 | 
			
		||||
from time import sleep
 | 
			
		||||
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from docker import DockerClient, from_env
 | 
			
		||||
from docker.models.containers import Container
 | 
			
		||||
from docker.types.healthcheck import Healthcheck
 | 
			
		||||
 | 
			
		||||
from authentik.crypto.models import CertificateKeyPair
 | 
			
		||||
from authentik.flows.models import Flow
 | 
			
		||||
from authentik.outposts.apps import AuthentikOutpostConfig
 | 
			
		||||
from authentik.outposts.controllers.docker import DockerController
 | 
			
		||||
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType
 | 
			
		||||
from authentik.providers.proxy.models import ProxyProvider
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutpostDockerTests(TestCase):
 | 
			
		||||
    """Test Docker Controllers"""
 | 
			
		||||
 | 
			
		||||
    def _start_container(self, ssl_folder: str) -> Container:
 | 
			
		||||
        client: DockerClient = from_env()
 | 
			
		||||
        container = client.containers.run(
 | 
			
		||||
            image="docker.beryju.org/proxy/library/docker:dind",
 | 
			
		||||
            detach=True,
 | 
			
		||||
            network_mode="host",
 | 
			
		||||
            remove=True,
 | 
			
		||||
            privileged=True,
 | 
			
		||||
            healthcheck=Healthcheck(
 | 
			
		||||
                test=["CMD", "docker", "info"],
 | 
			
		||||
                interval=5 * 100 * 1000000,
 | 
			
		||||
                start_period=5 * 100 * 1000000,
 | 
			
		||||
            ),
 | 
			
		||||
            environment={"DOCKER_TLS_CERTDIR": "/ssl"},
 | 
			
		||||
            volumes={
 | 
			
		||||
                f"{ssl_folder}/": {
 | 
			
		||||
                    "bind": "/ssl",
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
        )
 | 
			
		||||
        while True:
 | 
			
		||||
            container.reload()
 | 
			
		||||
            status = container.attrs.get("State", {}).get("Health", {}).get("Status")
 | 
			
		||||
            if status == "healthy":
 | 
			
		||||
                return container
 | 
			
		||||
            sleep(1)
 | 
			
		||||
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        super().setUp()
 | 
			
		||||
        self.ssl_folder = mkdtemp()
 | 
			
		||||
        self.container = self._start_container(self.ssl_folder)
 | 
			
		||||
        # Ensure that local connection have been created
 | 
			
		||||
        AuthentikOutpostConfig.init_local_connection()
 | 
			
		||||
        self.provider: ProxyProvider = ProxyProvider.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            internal_host="http://localhost",
 | 
			
		||||
            external_host="http://localhost",
 | 
			
		||||
            authorization_flow=Flow.objects.first(),
 | 
			
		||||
        )
 | 
			
		||||
        authentication_kp = CertificateKeyPair.objects.create(
 | 
			
		||||
            name="docker-authentication",
 | 
			
		||||
            certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(),
 | 
			
		||||
            key_data=open(f"{self.ssl_folder}/client/key.pem").read(),
 | 
			
		||||
        )
 | 
			
		||||
        verification_kp = CertificateKeyPair.objects.create(
 | 
			
		||||
            name="docker-verification",
 | 
			
		||||
            certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(),
 | 
			
		||||
        )
 | 
			
		||||
        self.service_connection = DockerServiceConnection.objects.create(
 | 
			
		||||
            url="https://localhost:2376",
 | 
			
		||||
            tls_verification=verification_kp,
 | 
			
		||||
            tls_authentication=authentication_kp,
 | 
			
		||||
        )
 | 
			
		||||
        self.outpost: Outpost = Outpost.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            type=OutpostType.PROXY,
 | 
			
		||||
            service_connection=self.service_connection,
 | 
			
		||||
        )
 | 
			
		||||
        self.outpost.providers.add(self.provider)
 | 
			
		||||
        self.outpost.save()
 | 
			
		||||
 | 
			
		||||
    def tearDown(self) -> None:
 | 
			
		||||
        super().tearDown()
 | 
			
		||||
        self.container.kill()
 | 
			
		||||
        try:
 | 
			
		||||
            rmtree(self.ssl_folder)
 | 
			
		||||
        except PermissionError:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    def test_docker_controller(self):
 | 
			
		||||
        """test that deployment requires update"""
 | 
			
		||||
        controller = DockerController(self.outpost, self.service_connection)
 | 
			
		||||
        controller.up()
 | 
			
		||||
        controller.down()
 | 
			
		||||
@ -8,3 +8,4 @@ FROM nginx
 | 
			
		||||
 | 
			
		||||
COPY --from=npm-builder /static/robots.txt /usr/share/nginx/html/robots.txt
 | 
			
		||||
COPY --from=npm-builder /static/dist/ /usr/share/nginx/html/static/dist/
 | 
			
		||||
COPY --from=npm-builder /static/authentik/ /usr/share/nginx/html/static/authentik/
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										298
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										298
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -102,14 +102,14 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@patternfly/patternfly": {
 | 
			
		||||
            "version": "4.65.6",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.65.6.tgz",
 | 
			
		||||
            "integrity": "sha512-dENO2nZbf5SoEH68coW9U+6FpZmdVnFVjztl7rUeWUPSBUuF1eWld5LT03Q6PVoZuWqqbJxFJodyFKwLb+L9vw=="
 | 
			
		||||
            "version": "4.70.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.70.2.tgz",
 | 
			
		||||
            "integrity": "sha512-XKCHnOjx1JThY3s98AJhsApSsGHPvEdlY7r+b18OecqUnmThVGw3nslzYYrwfCGlJ/xQtV5so29SduH2/uhHzA=="
 | 
			
		||||
        },
 | 
			
		||||
        "@rollup/plugin-typescript": {
 | 
			
		||||
            "version": "8.0.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.0.0.tgz",
 | 
			
		||||
            "integrity": "sha512-2L/kKvM5U4VOm+yVMvPIBF3yMZtQUyopf4YIT+KQbqZBZ8Fsdm7X6yeezy92PMyvvHQG1Pa322MVwxPojQvukA==",
 | 
			
		||||
            "version": "8.1.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.1.0.tgz",
 | 
			
		||||
            "integrity": "sha512-pyQlcGQYRsONUDwXK3ckGPHjPzmjlq4sinzr7emW8ZMb2oZjg9WLcdcP8wyHSvBjvHrLzMayyPy079RROqb4vw==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@rollup/pluginutils": "^3.1.0",
 | 
			
		||||
@ -142,13 +142,13 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@sentry/browser": {
 | 
			
		||||
            "version": "5.29.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.29.0.tgz",
 | 
			
		||||
            "integrity": "sha512-kRlt1mE2wrYjspnIupNnPxqsUrRuy02SuXhbpP7J6uu8QasoEmJ78hk0hHz4jOZRmuWwfs2zIXD4tLGgWOKq8A==",
 | 
			
		||||
            "version": "5.29.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.29.1.tgz",
 | 
			
		||||
            "integrity": "sha512-cVlXoQBJ64eNNkQuOB+bS6sK5KWV+Fw+ZYxT+XqjpeXkOPaxh8aeoi9CHz2DsFfbLV91P4AnXZEUdDl+7ktQNg==",
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@sentry/core": "5.29.0",
 | 
			
		||||
                "@sentry/types": "5.29.0",
 | 
			
		||||
                "@sentry/utils": "5.29.0",
 | 
			
		||||
                "@sentry/core": "5.29.1",
 | 
			
		||||
                "@sentry/types": "5.29.1",
 | 
			
		||||
                "@sentry/utils": "5.29.1",
 | 
			
		||||
                "tslib": "^1.9.3"
 | 
			
		||||
            },
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
@ -160,14 +160,14 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@sentry/core": {
 | 
			
		||||
            "version": "5.29.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.29.0.tgz",
 | 
			
		||||
            "integrity": "sha512-a1sZBJ2u3NG0YDlGvOTwUCWiNjhfmDtAQiKK1o6RIIbcrWy9TlSps7CYDkBP239Y3A4pnvohjEEKEP3v3L3LZQ==",
 | 
			
		||||
            "version": "5.29.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.29.1.tgz",
 | 
			
		||||
            "integrity": "sha512-SMybIx9IlswkJ7a61ez/zjdiMdAo51Adpo4nVrzke2k84U/t726/EbJj0FJ4vVgsGdLCvSSZ6v7BQlINcwWupg==",
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@sentry/hub": "5.29.0",
 | 
			
		||||
                "@sentry/minimal": "5.29.0",
 | 
			
		||||
                "@sentry/types": "5.29.0",
 | 
			
		||||
                "@sentry/utils": "5.29.0",
 | 
			
		||||
                "@sentry/hub": "5.29.1",
 | 
			
		||||
                "@sentry/minimal": "5.29.1",
 | 
			
		||||
                "@sentry/types": "5.29.1",
 | 
			
		||||
                "@sentry/utils": "5.29.1",
 | 
			
		||||
                "tslib": "^1.9.3"
 | 
			
		||||
            },
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
@ -179,12 +179,12 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@sentry/hub": {
 | 
			
		||||
            "version": "5.29.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.0.tgz",
 | 
			
		||||
            "integrity": "sha512-kcDPQsRG4cFdmqDh+TzjeO7lWYxU8s1dZYAbbl1J4uGKmhNB0J7I4ak4SGwTsXLY6fhbierxr6PRaoNojCxjPw==",
 | 
			
		||||
            "version": "5.29.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.1.tgz",
 | 
			
		||||
            "integrity": "sha512-Ig/vqCiJcsnGaWajkWRFH+5IKeo50ZtsjM0zJb8IfTadLjQuF/gTQst0aXO3l6q4HzveeGsELY8jlm6WVcq9Aw==",
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@sentry/types": "5.29.0",
 | 
			
		||||
                "@sentry/utils": "5.29.0",
 | 
			
		||||
                "@sentry/types": "5.29.1",
 | 
			
		||||
                "@sentry/utils": "5.29.1",
 | 
			
		||||
                "tslib": "^1.9.3"
 | 
			
		||||
            },
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
@ -196,12 +196,12 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@sentry/minimal": {
 | 
			
		||||
            "version": "5.29.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.0.tgz",
 | 
			
		||||
            "integrity": "sha512-nhXofdjtO41/caiF1wk1oT3p/QuhOZDYdF/b29DoD2MiAMK9IjhhOXI/gqaRpDKkXlDvd95fDTcx4t/MqqcKXA==",
 | 
			
		||||
            "version": "5.29.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.1.tgz",
 | 
			
		||||
            "integrity": "sha512-lAa3+Duxum1qQvR0tKiBUsH6Ehit3g/vO53SqBib7YK3qdvIUWHacmkJvfz/AeSvVnpJ9bsBMCVRJNSVe8BPVA==",
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@sentry/hub": "5.29.0",
 | 
			
		||||
                "@sentry/types": "5.29.0",
 | 
			
		||||
                "@sentry/hub": "5.29.1",
 | 
			
		||||
                "@sentry/types": "5.29.1",
 | 
			
		||||
                "tslib": "^1.9.3"
 | 
			
		||||
            },
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
@ -213,51 +213,17 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@sentry/tracing": {
 | 
			
		||||
            "version": "5.29.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.29.0.tgz",
 | 
			
		||||
            "integrity": "sha512-2ZITUH7Eur7IkmRAd5gw8Xt2Sfc28btCnT7o2P2J8ZPD65e99ATqjxXPokx0+6zEkTsstIDD3mbyuwkpbuvuTA==",
 | 
			
		||||
            "version": "5.29.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.29.1.tgz",
 | 
			
		||||
            "integrity": "sha512-iWfPtDhf5X7N9R5WB3vX/wlyFVsGG8iMx4hLIP+6bj8EcPYnZfeP6Sxn65a0ACT/FKv7SMBoZ1qPDzmvk0bviw==",
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@sentry/hub": "5.29.0",
 | 
			
		||||
                "@sentry/minimal": "5.29.0",
 | 
			
		||||
                "@sentry/types": "5.29.0",
 | 
			
		||||
                "@sentry/utils": "5.29.0",
 | 
			
		||||
                "@sentry/hub": "5.29.1",
 | 
			
		||||
                "@sentry/minimal": "5.29.1",
 | 
			
		||||
                "@sentry/types": "5.29.1",
 | 
			
		||||
                "@sentry/utils": "5.29.1",
 | 
			
		||||
                "tslib": "^1.9.3"
 | 
			
		||||
            },
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@sentry/hub": {
 | 
			
		||||
                    "version": "5.29.0",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.29.0.tgz",
 | 
			
		||||
                    "integrity": "sha512-kcDPQsRG4cFdmqDh+TzjeO7lWYxU8s1dZYAbbl1J4uGKmhNB0J7I4ak4SGwTsXLY6fhbierxr6PRaoNojCxjPw==",
 | 
			
		||||
                    "requires": {
 | 
			
		||||
                        "@sentry/types": "5.29.0",
 | 
			
		||||
                        "@sentry/utils": "5.29.0",
 | 
			
		||||
                        "tslib": "^1.9.3"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "@sentry/minimal": {
 | 
			
		||||
                    "version": "5.29.0",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.29.0.tgz",
 | 
			
		||||
                    "integrity": "sha512-nhXofdjtO41/caiF1wk1oT3p/QuhOZDYdF/b29DoD2MiAMK9IjhhOXI/gqaRpDKkXlDvd95fDTcx4t/MqqcKXA==",
 | 
			
		||||
                    "requires": {
 | 
			
		||||
                        "@sentry/hub": "5.29.0",
 | 
			
		||||
                        "@sentry/types": "5.29.0",
 | 
			
		||||
                        "tslib": "^1.9.3"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "@sentry/types": {
 | 
			
		||||
                    "version": "5.29.0",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.0.tgz",
 | 
			
		||||
                    "integrity": "sha512-iDkxT/9sT3UF+Xb+JyLjZ5caMXsgLfRyV9VXQEiR2J6mgpMielj184d9jeF3bm/VMuAf/VFFqrHlcVsVgmrrMw=="
 | 
			
		||||
                },
 | 
			
		||||
                "@sentry/utils": {
 | 
			
		||||
                    "version": "5.29.0",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.0.tgz",
 | 
			
		||||
                    "integrity": "sha512-b2B1gshw2u3EHlAi84PuI5sfmLKXW1z9enMMhNuuNT/CoRp+g5kMAcUv/qYTws7UNnYSvTuVGuZG30v1e0hP9A==",
 | 
			
		||||
                    "requires": {
 | 
			
		||||
                        "@sentry/types": "5.29.0",
 | 
			
		||||
                        "tslib": "^1.9.3"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "tslib": {
 | 
			
		||||
                    "version": "1.14.1",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
 | 
			
		||||
@ -266,16 +232,16 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@sentry/types": {
 | 
			
		||||
            "version": "5.29.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.0.tgz",
 | 
			
		||||
            "integrity": "sha512-iDkxT/9sT3UF+Xb+JyLjZ5caMXsgLfRyV9VXQEiR2J6mgpMielj184d9jeF3bm/VMuAf/VFFqrHlcVsVgmrrMw=="
 | 
			
		||||
            "version": "5.29.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.29.1.tgz",
 | 
			
		||||
            "integrity": "sha512-QXZBA1gJheMYTGFV+UUhr3+jKpGZqPx8kEJABs8htlKabCDJlEeoFNmeqPuVxCxukoy5ZaaHACoE+2Z87T0g2A=="
 | 
			
		||||
        },
 | 
			
		||||
        "@sentry/utils": {
 | 
			
		||||
            "version": "5.29.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.0.tgz",
 | 
			
		||||
            "integrity": "sha512-b2B1gshw2u3EHlAi84PuI5sfmLKXW1z9enMMhNuuNT/CoRp+g5kMAcUv/qYTws7UNnYSvTuVGuZG30v1e0hP9A==",
 | 
			
		||||
            "version": "5.29.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.29.1.tgz",
 | 
			
		||||
            "integrity": "sha512-FOhWxASvIQREAlSuWf3Vmb4uIkG0fmRdHkULpuv5dFmrMX2PpudYAppQtS8K9V4BYxFy6KFdUht1Qz5zYTecMw==",
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@sentry/types": "5.29.0",
 | 
			
		||||
                "@sentry/types": "5.29.1",
 | 
			
		||||
                "tslib": "^1.9.3"
 | 
			
		||||
            },
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
@ -287,9 +253,9 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@types/chart.js": {
 | 
			
		||||
            "version": "2.9.28",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.28.tgz",
 | 
			
		||||
            "integrity": "sha512-9YYhsxRngRJb0dkuaU5BezkF+zvvVHnwdRw+rtlahtFb4zqNf9YSgWsOq+dLYeh0fqsWmHUYLR64eNigh02F+w==",
 | 
			
		||||
            "version": "2.9.29",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.29.tgz",
 | 
			
		||||
            "integrity": "sha512-WOZMitUU3gHDM0oQsCsVivX+oDsIki93szcTmmUPBm39cCvAELBjokjSDVOoA3xiIEbb+jp17z/3S2tIqruwOQ==",
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "moment": "^2.10.2"
 | 
			
		||||
            }
 | 
			
		||||
@ -393,156 +359,70 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": {
 | 
			
		||||
            "version": "4.9.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.9.1.tgz",
 | 
			
		||||
            "integrity": "sha512-QRLDSvIPeI1pz5tVuurD+cStNR4sle4avtHhxA+2uyixWGFjKzJ+EaFVRW6dA/jOgjV5DTAjOxboQkRDE8cRlQ==",
 | 
			
		||||
            "version": "4.10.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.10.0.tgz",
 | 
			
		||||
            "integrity": "sha512-h6/V46o6aXpKRlarP1AiJEXuCJ7cMQdlpfMDrcllIgX3dFkLwEBTXAoNP98ZoOmqd1xvymMVRAI4e7yVvlzWEg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@typescript-eslint/experimental-utils": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/scope-manager": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/experimental-utils": "4.10.0",
 | 
			
		||||
                "@typescript-eslint/scope-manager": "4.10.0",
 | 
			
		||||
                "debug": "^4.1.1",
 | 
			
		||||
                "functional-red-black-tree": "^1.0.1",
 | 
			
		||||
                "regexpp": "^3.0.0",
 | 
			
		||||
                "semver": "^7.3.2",
 | 
			
		||||
                "tsutils": "^3.17.1"
 | 
			
		||||
            },
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@typescript-eslint/scope-manager": {
 | 
			
		||||
                    "version": "4.9.1",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.9.1.tgz",
 | 
			
		||||
                    "integrity": "sha512-sa4L9yUfD/1sg9Kl8OxPxvpUcqxKXRjBeZxBuZSSV1v13hjfEJkn84n0An2hN8oLQ1PmEl2uA6FkI07idXeFgQ==",
 | 
			
		||||
                    "dev": true,
 | 
			
		||||
                    "requires": {
 | 
			
		||||
                        "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                        "@typescript-eslint/visitor-keys": "4.9.1"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "@typescript-eslint/types": {
 | 
			
		||||
                    "version": "4.9.1",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.9.1.tgz",
 | 
			
		||||
                    "integrity": "sha512-fjkT+tXR13ks6Le7JiEdagnwEFc49IkOyys7ueWQ4O8k4quKPwPJudrwlVOJCUQhXo45PrfIvIarcrEjFTNwUA==",
 | 
			
		||||
                    "dev": true
 | 
			
		||||
                },
 | 
			
		||||
                "@typescript-eslint/visitor-keys": {
 | 
			
		||||
                    "version": "4.9.1",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.9.1.tgz",
 | 
			
		||||
                    "integrity": "sha512-9gspzc6UqLQHd7lXQS7oWs+hrYggspv/rk6zzEMhCbYwPE/sF7oxo7GAjkS35Tdlt7wguIG+ViWCPtVZHz/ybQ==",
 | 
			
		||||
                    "dev": true,
 | 
			
		||||
                    "requires": {
 | 
			
		||||
                        "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                        "eslint-visitor-keys": "^2.0.0"
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@typescript-eslint/experimental-utils": {
 | 
			
		||||
            "version": "4.9.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.9.1.tgz",
 | 
			
		||||
            "integrity": "sha512-c3k/xJqk0exLFs+cWSJxIjqLYwdHCuLWhnpnikmPQD2+NGAx9KjLYlBDcSI81EArh9FDYSL6dslAUSwILeWOxg==",
 | 
			
		||||
            "version": "4.10.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.10.0.tgz",
 | 
			
		||||
            "integrity": "sha512-opX+7ai1sdWBOIoBgpVJrH5e89ra1KoLrJTz0UtWAa4IekkKmqDosk5r6xqRaNJfCXEfteW4HXQAwMdx+jjEmw==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@types/json-schema": "^7.0.3",
 | 
			
		||||
                "@typescript-eslint/scope-manager": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/typescript-estree": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/scope-manager": "4.10.0",
 | 
			
		||||
                "@typescript-eslint/types": "4.10.0",
 | 
			
		||||
                "@typescript-eslint/typescript-estree": "4.10.0",
 | 
			
		||||
                "eslint-scope": "^5.0.0",
 | 
			
		||||
                "eslint-utils": "^2.0.0"
 | 
			
		||||
            },
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@typescript-eslint/scope-manager": {
 | 
			
		||||
                    "version": "4.9.1",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.9.1.tgz",
 | 
			
		||||
                    "integrity": "sha512-sa4L9yUfD/1sg9Kl8OxPxvpUcqxKXRjBeZxBuZSSV1v13hjfEJkn84n0An2hN8oLQ1PmEl2uA6FkI07idXeFgQ==",
 | 
			
		||||
                    "dev": true,
 | 
			
		||||
                    "requires": {
 | 
			
		||||
                        "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                        "@typescript-eslint/visitor-keys": "4.9.1"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "@typescript-eslint/types": {
 | 
			
		||||
                    "version": "4.9.1",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.9.1.tgz",
 | 
			
		||||
                    "integrity": "sha512-fjkT+tXR13ks6Le7JiEdagnwEFc49IkOyys7ueWQ4O8k4quKPwPJudrwlVOJCUQhXo45PrfIvIarcrEjFTNwUA==",
 | 
			
		||||
                    "dev": true
 | 
			
		||||
                },
 | 
			
		||||
                "@typescript-eslint/typescript-estree": {
 | 
			
		||||
                    "version": "4.9.1",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.9.1.tgz",
 | 
			
		||||
                    "integrity": "sha512-bzP8vqwX6Vgmvs81bPtCkLtM/Skh36NE6unu6tsDeU/ZFoYthlTXbBmpIrvosgiDKlWTfb2ZpPELHH89aQjeQw==",
 | 
			
		||||
                    "dev": true,
 | 
			
		||||
                    "requires": {
 | 
			
		||||
                        "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                        "@typescript-eslint/visitor-keys": "4.9.1",
 | 
			
		||||
                        "debug": "^4.1.1",
 | 
			
		||||
                        "globby": "^11.0.1",
 | 
			
		||||
                        "is-glob": "^4.0.1",
 | 
			
		||||
                        "lodash": "^4.17.15",
 | 
			
		||||
                        "semver": "^7.3.2",
 | 
			
		||||
                        "tsutils": "^3.17.1"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "@typescript-eslint/visitor-keys": {
 | 
			
		||||
                    "version": "4.9.1",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.9.1.tgz",
 | 
			
		||||
                    "integrity": "sha512-9gspzc6UqLQHd7lXQS7oWs+hrYggspv/rk6zzEMhCbYwPE/sF7oxo7GAjkS35Tdlt7wguIG+ViWCPtVZHz/ybQ==",
 | 
			
		||||
                    "dev": true,
 | 
			
		||||
                    "requires": {
 | 
			
		||||
                        "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                        "eslint-visitor-keys": "^2.0.0"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "globby": {
 | 
			
		||||
                    "version": "11.0.1",
 | 
			
		||||
                    "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz",
 | 
			
		||||
                    "integrity": "sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ==",
 | 
			
		||||
                    "dev": true,
 | 
			
		||||
                    "requires": {
 | 
			
		||||
                        "array-union": "^2.1.0",
 | 
			
		||||
                        "dir-glob": "^3.0.1",
 | 
			
		||||
                        "fast-glob": "^3.1.1",
 | 
			
		||||
                        "ignore": "^5.1.4",
 | 
			
		||||
                        "merge2": "^1.3.0",
 | 
			
		||||
                        "slash": "^3.0.0"
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@typescript-eslint/parser": {
 | 
			
		||||
            "version": "4.9.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.9.1.tgz",
 | 
			
		||||
            "integrity": "sha512-Gv2VpqiomvQ2v4UL+dXlQcZ8zCX4eTkoIW+1aGVWT6yTO+6jbxsw7yQl2z2pPl/4B9qa5JXeIbhJpONKjXIy3g==",
 | 
			
		||||
            "version": "4.10.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.10.0.tgz",
 | 
			
		||||
            "integrity": "sha512-amBvUUGBMadzCW6c/qaZmfr3t9PyevcSWw7hY2FuevdZVp5QPw/K76VSQ5Sw3BxlgYCHZcK6DjIhSZK0PQNsQg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@typescript-eslint/scope-manager": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/typescript-estree": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/scope-manager": "4.10.0",
 | 
			
		||||
                "@typescript-eslint/types": "4.10.0",
 | 
			
		||||
                "@typescript-eslint/typescript-estree": "4.10.0",
 | 
			
		||||
                "debug": "^4.1.1"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@typescript-eslint/scope-manager": {
 | 
			
		||||
            "version": "4.9.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.9.1.tgz",
 | 
			
		||||
            "integrity": "sha512-sa4L9yUfD/1sg9Kl8OxPxvpUcqxKXRjBeZxBuZSSV1v13hjfEJkn84n0An2hN8oLQ1PmEl2uA6FkI07idXeFgQ==",
 | 
			
		||||
            "version": "4.10.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.10.0.tgz",
 | 
			
		||||
            "integrity": "sha512-WAPVw35P+fcnOa8DEic0tQUhoJJsgt+g6DEcz257G7vHFMwmag58EfowdVbiNcdfcV27EFR0tUBVXkDoIvfisQ==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/visitor-keys": "4.9.1"
 | 
			
		||||
                "@typescript-eslint/types": "4.10.0",
 | 
			
		||||
                "@typescript-eslint/visitor-keys": "4.10.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@typescript-eslint/types": {
 | 
			
		||||
            "version": "4.9.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.9.1.tgz",
 | 
			
		||||
            "integrity": "sha512-fjkT+tXR13ks6Le7JiEdagnwEFc49IkOyys7ueWQ4O8k4quKPwPJudrwlVOJCUQhXo45PrfIvIarcrEjFTNwUA==",
 | 
			
		||||
            "version": "4.10.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.10.0.tgz",
 | 
			
		||||
            "integrity": "sha512-+dt5w1+Lqyd7wIPMa4XhJxUuE8+YF+vxQ6zxHyhLGHJjHiunPf0wSV8LtQwkpmAsRi1lEOoOIR30FG5S2HS33g==",
 | 
			
		||||
            "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "@typescript-eslint/typescript-estree": {
 | 
			
		||||
            "version": "4.9.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.9.1.tgz",
 | 
			
		||||
            "integrity": "sha512-bzP8vqwX6Vgmvs81bPtCkLtM/Skh36NE6unu6tsDeU/ZFoYthlTXbBmpIrvosgiDKlWTfb2ZpPELHH89aQjeQw==",
 | 
			
		||||
            "version": "4.10.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.10.0.tgz",
 | 
			
		||||
            "integrity": "sha512-mGK0YRp9TOk6ZqZ98F++bW6X5kMTzCRROJkGXH62d2azhghmq+1LNLylkGe6uGUOQzD452NOAEth5VAF6PDo5g==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/visitor-keys": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/types": "4.10.0",
 | 
			
		||||
                "@typescript-eslint/visitor-keys": "4.10.0",
 | 
			
		||||
                "debug": "^4.1.1",
 | 
			
		||||
                "globby": "^11.0.1",
 | 
			
		||||
                "is-glob": "^4.0.1",
 | 
			
		||||
@ -568,12 +448,12 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "@typescript-eslint/visitor-keys": {
 | 
			
		||||
            "version": "4.9.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.9.1.tgz",
 | 
			
		||||
            "integrity": "sha512-9gspzc6UqLQHd7lXQS7oWs+hrYggspv/rk6zzEMhCbYwPE/sF7oxo7GAjkS35Tdlt7wguIG+ViWCPtVZHz/ybQ==",
 | 
			
		||||
            "version": "4.10.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.10.0.tgz",
 | 
			
		||||
            "integrity": "sha512-hPyz5qmDMuZWFtHZkjcCpkAKHX8vdu1G3YsCLEd25ryZgnJfj6FQuJ5/O7R+dB1ueszilJmAFMtlU4CA6se3Jg==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "@typescript-eslint/types": "4.9.1",
 | 
			
		||||
                "@typescript-eslint/types": "4.10.0",
 | 
			
		||||
                "eslint-visitor-keys": "^2.0.0"
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
@ -1215,9 +1095,9 @@
 | 
			
		||||
            "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "eslint-plugin-lit": {
 | 
			
		||||
            "version": "1.2.4",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.2.4.tgz",
 | 
			
		||||
            "integrity": "sha512-LfmtuaW9ZcE8R2ji5cMj+SG5bSjEY+IBexuAOKKE+IUB8b6N0AnhVATbfaAX5V2KibeKvIBit2Xpo3QabpeiuA==",
 | 
			
		||||
            "version": "1.3.0",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.3.0.tgz",
 | 
			
		||||
            "integrity": "sha512-fy6Lr5vYI3kvCYaDXA20lwyKAp1keS9UjR5ntj8U2TeV+1yUta3S7xxXe+rABKRPbcNzi1ZvQLE1LmNKc9yr4Q==",
 | 
			
		||||
            "dev": true,
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "parse5": "^6.0.1",
 | 
			
		||||
@ -2680,9 +2560,9 @@
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "rollup": {
 | 
			
		||||
            "version": "2.34.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.34.2.tgz",
 | 
			
		||||
            "integrity": "sha512-mvtQLqu3cNeoctS+kZ09iOPxrc1P1/Bt1z15enuQ5feyKOdM3MJAVFjjsygurDpSWn530xB4AlA83TWIzRstXA==",
 | 
			
		||||
            "version": "2.35.1",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.35.1.tgz",
 | 
			
		||||
            "integrity": "sha512-q5KxEyWpprAIcainhVy6HfRttD9kutQpHbeqDTWnqAFNJotiojetK6uqmcydNMymBEtC4I8bCYR+J3mTMqeaUA==",
 | 
			
		||||
            "requires": {
 | 
			
		||||
                "fsevents": "~2.1.2"
 | 
			
		||||
            }
 | 
			
		||||
@ -3317,9 +3197,9 @@
 | 
			
		||||
            "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "typescript": {
 | 
			
		||||
            "version": "4.1.2",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.2.tgz",
 | 
			
		||||
            "integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==",
 | 
			
		||||
            "version": "4.1.3",
 | 
			
		||||
            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
 | 
			
		||||
            "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
 | 
			
		||||
            "dev": true
 | 
			
		||||
        },
 | 
			
		||||
        "uglify-js": {
 | 
			
		||||
 | 
			
		||||
@ -8,35 +8,35 @@
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@fortawesome/fontawesome-free": "^5.15.1",
 | 
			
		||||
        "@patternfly/patternfly": "^4.65.6",
 | 
			
		||||
        "@sentry/browser": "^5.29.0",
 | 
			
		||||
        "@sentry/tracing": "^5.29.0",
 | 
			
		||||
        "@types/chart.js": "^2.9.28",
 | 
			
		||||
        "@patternfly/patternfly": "^4.70.2",
 | 
			
		||||
        "@sentry/browser": "^5.29.1",
 | 
			
		||||
        "@sentry/tracing": "^5.29.1",
 | 
			
		||||
        "@types/chart.js": "^2.9.29",
 | 
			
		||||
        "@types/codemirror": "0.0.102",
 | 
			
		||||
        "chart.js": "^2.9.4",
 | 
			
		||||
        "codemirror": "^5.58.3",
 | 
			
		||||
        "construct-style-sheets-polyfill": "^2.4.3",
 | 
			
		||||
        "lit-element": "^2.4.0",
 | 
			
		||||
        "lit-html": "^1.3.0",
 | 
			
		||||
        "rollup": "^2.34.2",
 | 
			
		||||
        "rollup": "^2.35.1",
 | 
			
		||||
        "rollup-plugin-copy": "^3.3.0",
 | 
			
		||||
        "rollup-plugin-cssimport": "^1.0.2",
 | 
			
		||||
        "rollup-plugin-external-globals": "^0.6.1",
 | 
			
		||||
        "tslib": "^2.0.3"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@rollup/plugin-typescript": "^8.0.0",
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^4.9.1",
 | 
			
		||||
        "@typescript-eslint/parser": "^4.9.1",
 | 
			
		||||
        "@rollup/plugin-typescript": "^8.1.0",
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^4.10.0",
 | 
			
		||||
        "@typescript-eslint/parser": "^4.10.0",
 | 
			
		||||
        "eslint": "^7.15.0",
 | 
			
		||||
        "eslint-config-google": "^0.14.0",
 | 
			
		||||
        "eslint-plugin-lit": "^1.2.4",
 | 
			
		||||
        "eslint-plugin-lit": "^1.3.0",
 | 
			
		||||
        "rollup-plugin-commonjs": "^10.1.0",
 | 
			
		||||
        "rollup-plugin-minify-html-literals": "^1.2.5",
 | 
			
		||||
        "rollup-plugin-node-resolve": "^5.2.0",
 | 
			
		||||
        "rollup-plugin-sourcemaps": "^0.6.3",
 | 
			
		||||
        "rollup-plugin-terser": "^7.0.2",
 | 
			
		||||
        "ts-lit-plugin": "^1.2.1",
 | 
			
		||||
        "typescript": "^4.1.2"
 | 
			
		||||
        "typescript": "^4.1.3"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { DefaultClient, PBResponse, QueryArguments } from "./client";
 | 
			
		||||
import { DefaultClient, PBResponse, QueryArguments } from "./Client";
 | 
			
		||||
 | 
			
		||||
export class Application {
 | 
			
		||||
    pk: string;
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user