Compare commits
	
		
			467 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 381010600f | |||
| 2a265f706a | |||
| 1b21b50b77 | |||
| fa6324ab1d | |||
| 9e0daf2bcf | |||
| 0273ae16df | |||
| f2f12ef0ba | |||
| 61d3df5f02 | |||
| 971de4fcb9 | |||
| 9c0bc78ca0 | |||
| 92085f1a3c | |||
| 6067406e96 | |||
| 9ccd4d69fe | |||
| 17ec48332d | |||
| d3f5253a6b | |||
| 7a70726d57 | |||
| be303937fb | |||
| 2326fc9ae2 | |||
| 9374b0bcf2 | |||
| 47e6028099 | |||
| 24114e8304 | |||
| 921d9c79a1 | |||
| 1119989ab7 | |||
| e17594f0f7 | |||
| 5ae3b868d4 | |||
| 37ee4af5ff | |||
| 829aaca317 | |||
| 8eb4d53810 | |||
| e60dfc5b3c | |||
| cc403d8777 | |||
| b81e2e69d1 | |||
| 731f5d0199 | |||
| a40cb03b44 | |||
| f6a85c98c9 | |||
| 5727f28784 | |||
| 6fc54ed7c6 | |||
| 4298900ecc | |||
| f04aa09b72 | |||
| 3647633232 | |||
| 2e06786869 | |||
| eba91c6b2b | |||
| ba9f8a5795 | |||
| 02b4173d30 | |||
| 61fab497cf | |||
| 6a95de4e8a | |||
| 621e7f564a | |||
| 535f2eb27e | |||
| 0db4716e92 | |||
| c10ce5c679 | |||
| 070438aabe | |||
| 71798b931c | |||
| 8663134c87 | |||
| 6bcbaeec2e | |||
| 17ce113c6b | |||
| ff600cd5b1 | |||
| 2df4322ecf | |||
| bb8e0c6f59 | |||
| ca682c3ee4 | |||
| f011e8a61a | |||
| bfe27d5979 | |||
| b8aff17d98 | |||
| 3b7e8e3931 | |||
| 03369e2338 | |||
| 5da7d9a573 | |||
| 12110e264d | |||
| f5049d3d0f | |||
| b616253444 | |||
| 41efe49d27 | |||
| 86d0e6ce45 | |||
| 89bb27b95c | |||
| 9333ffd04f | |||
| 2b155964c2 | |||
| c3bd509eb8 | |||
| 72c0da2bdf | |||
| 151c62733f | |||
| dbdea24290 | |||
| 909c4217bc | |||
| 922fc9b8d5 | |||
| 2c06eed8e7 | |||
| a1b3af401d | |||
| 92d38f62b5 | |||
| 98a56c77e3 | |||
| e5906a4115 | |||
| 20c6874bb4 | |||
| 222d3bd358 | |||
| 02c15f7c43 | |||
| ab200eb855 | |||
| 9e8ce012e3 | |||
| 00dc8f8b1f | |||
| ce812e14c7 | |||
| 8d32a53126 | |||
| f9b6b1dd3f | |||
| 9679be39fa | |||
| 0225bf9c99 | |||
| 8040e2b6e4 | |||
| 56a56ffdbf | |||
| afedcc0074 | |||
| 4d93e30147 | |||
| f62786e58b | |||
| f76c1a6f93 | |||
| 56871523e7 | |||
| 5f9dda2e58 | |||
| 0c55eea678 | |||
| 19a343dadb | |||
| 3ab9798f38 | |||
| dd9dc7e596 | |||
| 797e31696a | |||
| 9a42c5815d | |||
| f341479732 | |||
| 8eddb4b95b | |||
| 5c58532121 | |||
| 4b7399f454 | |||
| 27982a771c | |||
| 8296d0c94c | |||
| 9bc9568008 | |||
| 07d619d257 | |||
| 6ee7d5bf9c | |||
| 634375c43f | |||
| 10fc33f7d3 | |||
| ee140014e9 | |||
| 2d363948b6 | |||
| dcb3ef14d1 | |||
| a71ef7f36c | |||
| 4d51ec906d | |||
| cd42281383 | |||
| faf706cbec | |||
| 16c05a7bbc | |||
| 2ad5995332 | |||
| f73a404fd6 | |||
| 178e8e7e43 | |||
| 98907ec889 | |||
| 9dd9ab6da3 | |||
| 80c6b8f0c7 | |||
| 8436814874 | |||
| 3c16bdce45 | |||
| a2bce79796 | |||
| 3e5b05203b | |||
| 57e86582d1 | |||
| dd7cb45733 | |||
| 2b09d97522 | |||
| d39dbc7287 | |||
| 48f96ea55f | |||
| 22a7c25526 | |||
| cc69311ec0 | |||
| 15d7004e25 | |||
| ddb70a283e | |||
| ecfc3a6d93 | |||
| 5753182e03 | |||
| db79244ba4 | |||
| 3231bcea66 | |||
| 5e0299ca82 | |||
| 42e35aace0 | |||
| d96cfc8e30 | |||
| 36c97afc44 | |||
| 9c322be8d7 | |||
| cf09205933 | |||
| e851a7f294 | |||
| e4f141c6c0 | |||
| 35fa93d9aa | |||
| 2bdc0102f9 | |||
| aef9d27706 | |||
| 7bf587af24 | |||
| ef1cf7867c | |||
| da443b443c | |||
| f4322e665a | |||
| e22b8f5fdc | |||
| a18176af56 | |||
| 4132fd139c | |||
| b077bb8783 | |||
| 69665d9547 | |||
| d0f056357d | |||
| 9ed236f7ab | |||
| 83f4830946 | |||
| e23df99a9e | |||
| b80ecd4668 | |||
| 66ca488ea0 | |||
| d959b7a930 | |||
| 62ae3f1e31 | |||
| 619203c177 | |||
| a1adf382af | |||
| 834bddd0da | |||
| 7d9251ce2f | |||
| fb13a46252 | |||
| dfefdbfd7c | |||
| 846c971674 | |||
| 5b7e1f97e0 | |||
| dff0613b3d | |||
| 0a4343d61b | |||
| 09696207a6 | |||
| 8965451073 | |||
| 994c1c4b6a | |||
| 3ee5a672f1 | |||
| b33ea9cc61 | |||
| 50a623d8ab | |||
| cdbf7ae567 | |||
| 1307a39042 | |||
| dca34cfbd3 | |||
| 735f7cbd69 | |||
| 728356d420 | |||
| a9f6f1563d | |||
| 155c28d7cd | |||
| f9a180eb1f | |||
| 4ae476e58d | |||
| f32d35b07c | |||
| 9e936e4436 | |||
| 649abddea7 | |||
| 956382b682 | |||
| 67b88595ad | |||
| b4ee693a5c | |||
| 57e5acaf2f | |||
| 050ec99c89 | |||
| 10fd1c8120 | |||
| 070745e764 | |||
| cbeee27fc1 | |||
| 2bc4d0cedb | |||
| 5105a1c207 | |||
| 64e357ab0e | |||
| 6ca93525aa | |||
| a2c978768c | |||
| f0c7be7144 | |||
| 0f96e3e4b3 | |||
| d42fc37a88 | |||
| 4ecd8f5dcf | |||
| d7a194b512 | |||
| 753f8d38bf | |||
| 118a54517a | |||
| 8c27616d0c | |||
| e444d0d640 | |||
| 3869965b4c | |||
| 097a42bb7b | |||
| 26f1f47cc1 | |||
| 471f9c6d05 | |||
| d4e1b95991 | |||
| 67d13f19a1 | |||
| 1b7c19cf50 | |||
| b012ae600d | |||
| 1838101d60 | |||
| 929add4e9c | |||
| 18edaea658 | |||
| 8030e45d75 | |||
| d75c63d38b | |||
| 52889ffea1 | |||
| 2b730dec54 | |||
| 2aacb311bc | |||
| 40055ef01b | |||
| 6c603cdf80 | |||
| 5f4a1417b2 | |||
| 75608dce5c | |||
| b0f7083879 | |||
| e8420957b1 | |||
| 62bf79ce32 | |||
| 7a16c9cb14 | |||
| d29d161ac6 | |||
| aee58c8d53 | |||
| c47ab4f1fc | |||
| fa6df84de2 | |||
| 1faa403fe2 | |||
| 653631ac77 | |||
| cde303e780 | |||
| 7f5feb9451 | |||
| b85aeae5ef | |||
| aa359a032c | |||
| 6491065aab | |||
| 79eec5a3a0 | |||
| cd5e091937 | |||
| 7ed8952803 | |||
| c1f302fb7c | |||
| cb31e52d0e | |||
| 782764ac73 | |||
| d0c56325ef | |||
| 73d57d6f82 | |||
| 2716a26887 | |||
| 0452537e8b | |||
| d1a1bfbbc5 | |||
| a69fcbca9a | |||
| 1ac4dacc3b | |||
| b72b731320 | |||
| 65de4b8cad | |||
| 9e7e22367b | |||
| 9301b27e43 | |||
| 7b415a24ee | |||
| f5761dc70d | |||
| 4f57dfda93 | |||
| 16380b3f7a | |||
| b0e416e9f0 | |||
| 16f2603130 | |||
| e742494f3d | |||
| 5fdca722f4 | |||
| 847cfed73f | |||
| 19247accd9 | |||
| 05b587ae44 | |||
| a515afae0b | |||
| 8da00585e3 | |||
| b70a72f247 | |||
| 11160b6e04 | |||
| 55259adf38 | |||
| 3f308ad48c | |||
| ee6fd6f609 | |||
| d53d0c353f | |||
| 1360b76d1b | |||
| e22a286a6f | |||
| 62c0f69541 | |||
| 1c340ddbbd | |||
| bcf7e162a4 | |||
| 62af5b2dd3 | |||
| f44956bd61 | |||
| e0859686c4 | |||
| cb37e5c10e | |||
| 73bb778d62 | |||
| b612a82e16 | |||
| 83991c743e | |||
| 09f43ca43b | |||
| 1c91835a26 | |||
| c032914092 | |||
| 3634bf4629 | |||
| 0692663537 | |||
| b5649bdcc4 | |||
| 418e491799 | |||
| fab9a10487 | |||
| 9778050dda | |||
| 9ac808ee98 | |||
| 0f00b27384 | |||
| ab5981836d | |||
| a4418a83f8 | |||
| 36b23c4624 | |||
| 0c6237d8c4 | |||
| e546453250 | |||
| 5b35d71bb3 | |||
| cddff85e1c | |||
| c65c6a62cc | |||
| 1bc51adcac | |||
| c523b799be | |||
| 9d0d779f40 | |||
| 8a791c4eac | |||
| 036a4e86e2 | |||
| 4715e7bf04 | |||
| 45f99fbaf0 | |||
| 83150d9920 | |||
| e31a3307b5 | |||
| d28fcca344 | |||
| c296e1214c | |||
| d30dcda814 | |||
| c720c9f41b | |||
| 62cfb76b39 | |||
| d676cf6e3f | |||
| 39d87841d0 | |||
| fcd879034c | |||
| b285814e24 | |||
| 1c52836060 | |||
| f3cc1be0f2 | |||
| 8dd77793a0 | |||
| f6e8dbfb5e | |||
| 3c1ac4c7ec | |||
| 52bbf454e3 | |||
| 1252c6b07d | |||
| 3493d35af9 | |||
| f8e4ffbc85 | |||
| faca127217 | |||
| f88575cec4 | |||
| 1a6ea72c09 | |||
| b4eac771c2 | |||
| 84e4ec4406 | |||
| c251b87f8c | |||
| 21a9aa229a | |||
| 5f6565ee27 | |||
| afad55a357 | |||
| f25d76fa43 | |||
| 53e15bfbca | |||
| 8bce16e6b4 | |||
| e9bb8c896b | |||
| de5455716d | |||
| 1d879400f2 | |||
| 5136ae17f5 | |||
| 10b45d954e | |||
| 339eaf37f2 | |||
| f723fdd551 | |||
| 4cb8ae760a | |||
| e4898f4b92 | |||
| a2f3c54c2a | |||
| c0a0b52fbb | |||
| 8359f0bfb3 | |||
| ee610a906a | |||
| 828eeb5ebb | |||
| c9c177d8f9 | |||
| c19afa4f16 | |||
| cfd4817bb5 | |||
| 94ae52b576 | |||
| be479f2453 | |||
| c5d066577d | |||
| 9ec6eaf4b8 | |||
| b057120351 | |||
| b8082598a1 | |||
| 1b5a163f46 | |||
| 1f2f48a7bc | |||
| f9ad102915 | |||
| ea4b920264 | |||
| 7d8390ca77 | |||
| 7ae551da65 | |||
| 51b26c2ac7 | |||
| e4a5f22f9b | |||
| 2462d58135 | |||
| 44534153a0 | |||
| facfea035b | |||
| 941bc61b31 | |||
| 282b364606 | |||
| ad4bc4083d | |||
| ebe282eb1a | |||
| 830c26ca25 | |||
| ed3b4a3d4a | |||
| 975c4ddc04 | |||
| 7e2896298a | |||
| cba9cf8361 | |||
| bf12580f64 | |||
| 75ef4ce596 | |||
| c2f3ce11b0 | |||
| 3c256fecc6 | |||
| 0285b84133 | |||
| 99a371a02c | |||
| c7e6eb8896 | |||
| 674bd9e05c | |||
| b79901df87 | |||
| b248f450dd | |||
| 05db9e5c40 | |||
| 234a5e2b66 | |||
| aea1736f70 | |||
| 9f4a4449f5 | |||
| b6b55e2336 | |||
| 8f2805e05b | |||
| 4f3583cd7e | |||
| 617e90dca3 | |||
| f7408626a8 | |||
| 4dcb15af46 | |||
| 89beb7a9f7 | |||
| 28eeb4798e | |||
| 79b92e764e | |||
| 919336a519 | |||
| 27e04589c1 | |||
| ba44fbdac8 | |||
| 0e093a8917 | |||
| d0bfb99859 | |||
| 93bdea3769 | |||
| e681654af7 | |||
| cab7593dca | |||
| cf92f9aefc | |||
| 8d72b3498d | |||
| 42ab858c50 | |||
| a1abae9ab1 | |||
| 8f36b49061 | |||
| 64b4e851ce | |||
| 40a62ac1e5 | |||
| 5df60e4d87 | |||
| 50ebc8522d | |||
| eddca478dc | |||
| 99a7fca08e | |||
| a7e3602908 | |||
| 74169860cf | |||
| 52bb774f73 | |||
| f26fcaf825 | |||
| b8e92e2f11 | |||
| 08adfc94d6 | |||
| 236fafb735 | |||
| 5ad9ddee3c | |||
| 24d220ff49 | |||
| 3364c195b7 | |||
| 50aa87d141 | |||
| 72b375023d | |||
| 77ba186818 | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
[bumpversion]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 2021.9.1
 | 
					current_version = 2021.10.1-rc3
 | 
				
			||||||
tag = True
 | 
					tag = True
 | 
				
			||||||
commit = True
 | 
					commit = True
 | 
				
			||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
 | 
					parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								.github/codespell-words.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.github/codespell-words.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					keypair
 | 
				
			||||||
 | 
					keypairs
 | 
				
			||||||
 | 
					hass
 | 
				
			||||||
							
								
								
									
										157
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										157
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							@ -25,14 +25,14 @@ jobs:
 | 
				
			|||||||
      - uses: actions/setup-python@v2
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: '3.9'
 | 
					          python-version: '3.9'
 | 
				
			||||||
      # - id: cache-pipenv
 | 
					      - id: cache-pipenv
 | 
				
			||||||
      #   uses: actions/cache@v2.1.6
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
      #   with:
 | 
					        with:
 | 
				
			||||||
      #     path: ~/.local/share/virtualenvs
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
      #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
      - name: prepare
 | 
					      - name: prepare
 | 
				
			||||||
        # env:
 | 
					        env:
 | 
				
			||||||
        #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
        run: scripts/ci_prepare.sh
 | 
					        run: scripts/ci_prepare.sh
 | 
				
			||||||
      - name: run pylint
 | 
					      - name: run pylint
 | 
				
			||||||
        run: pipenv run pylint authentik tests lifecycle
 | 
					        run: pipenv run pylint authentik tests lifecycle
 | 
				
			||||||
@ -43,14 +43,14 @@ jobs:
 | 
				
			|||||||
      - uses: actions/setup-python@v2
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: '3.9'
 | 
					          python-version: '3.9'
 | 
				
			||||||
      # - id: cache-pipenv
 | 
					      - id: cache-pipenv
 | 
				
			||||||
      #   uses: actions/cache@v2.1.6
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
      #   with:
 | 
					        with:
 | 
				
			||||||
      #     path: ~/.local/share/virtualenvs
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
      #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
      - name: prepare
 | 
					      - name: prepare
 | 
				
			||||||
        # env:
 | 
					        env:
 | 
				
			||||||
        #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
        run: scripts/ci_prepare.sh
 | 
					        run: scripts/ci_prepare.sh
 | 
				
			||||||
      - name: run black
 | 
					      - name: run black
 | 
				
			||||||
        run: pipenv run black --check authentik tests lifecycle
 | 
					        run: pipenv run black --check authentik tests lifecycle
 | 
				
			||||||
@ -61,14 +61,14 @@ jobs:
 | 
				
			|||||||
      - uses: actions/setup-python@v2
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: '3.9'
 | 
					          python-version: '3.9'
 | 
				
			||||||
      # - id: cache-pipenv
 | 
					      - id: cache-pipenv
 | 
				
			||||||
      #   uses: actions/cache@v2.1.6
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
      #   with:
 | 
					        with:
 | 
				
			||||||
      #     path: ~/.local/share/virtualenvs
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
      #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
      - name: prepare
 | 
					      - name: prepare
 | 
				
			||||||
        # env:
 | 
					        env:
 | 
				
			||||||
        #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
        run: scripts/ci_prepare.sh
 | 
					        run: scripts/ci_prepare.sh
 | 
				
			||||||
      - name: run isort
 | 
					      - name: run isort
 | 
				
			||||||
        run: pipenv run isort --check authentik tests lifecycle
 | 
					        run: pipenv run isort --check authentik tests lifecycle
 | 
				
			||||||
@ -79,14 +79,14 @@ jobs:
 | 
				
			|||||||
      - uses: actions/setup-python@v2
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: '3.9'
 | 
					          python-version: '3.9'
 | 
				
			||||||
      # - id: cache-pipenv
 | 
					      - id: cache-pipenv
 | 
				
			||||||
      #   uses: actions/cache@v2.1.6
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
      #   with:
 | 
					        with:
 | 
				
			||||||
      #     path: ~/.local/share/virtualenvs
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
      #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
      - name: prepare
 | 
					      - name: prepare
 | 
				
			||||||
        # env:
 | 
					        env:
 | 
				
			||||||
        #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
        run: scripts/ci_prepare.sh
 | 
					        run: scripts/ci_prepare.sh
 | 
				
			||||||
      - name: run bandit
 | 
					      - name: run bandit
 | 
				
			||||||
        run: pipenv run bandit -r authentik tests lifecycle
 | 
					        run: pipenv run bandit -r authentik tests lifecycle
 | 
				
			||||||
@ -113,14 +113,14 @@ jobs:
 | 
				
			|||||||
      - uses: actions/setup-python@v2
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: '3.9'
 | 
					          python-version: '3.9'
 | 
				
			||||||
      # - id: cache-pipenv
 | 
					      - id: cache-pipenv
 | 
				
			||||||
      #   uses: actions/cache@v2.1.6
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
      #   with:
 | 
					        with:
 | 
				
			||||||
      #     path: ~/.local/share/virtualenvs
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
      #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
      - name: prepare
 | 
					      - name: prepare
 | 
				
			||||||
        # env:
 | 
					        env:
 | 
				
			||||||
        #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
        run: scripts/ci_prepare.sh
 | 
					        run: scripts/ci_prepare.sh
 | 
				
			||||||
      - name: run migrations
 | 
					      - name: run migrations
 | 
				
			||||||
        run: pipenv run python -m lifecycle.migrate
 | 
					        run: pipenv run python -m lifecycle.migrate
 | 
				
			||||||
@ -133,32 +133,39 @@ jobs:
 | 
				
			|||||||
      - uses: actions/setup-python@v2
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: '3.9'
 | 
					          python-version: '3.9'
 | 
				
			||||||
 | 
					      - name: prepare variables
 | 
				
			||||||
 | 
					        id: ev
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          python ./scripts/gh_env.py
 | 
				
			||||||
 | 
					      - id: cache-pipenv
 | 
				
			||||||
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
      - name: checkout stable
 | 
					      - name: checkout stable
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          # Copy current, latest config to local
 | 
					          # Copy current, latest config to local
 | 
				
			||||||
          cp authentik/lib/default.yml local.env.yml
 | 
					          cp authentik/lib/default.yml local.env.yml
 | 
				
			||||||
          git checkout $(git describe --abbrev=0 --match 'version/*')
 | 
					          git checkout $(git describe --abbrev=0 --match 'version/*')
 | 
				
			||||||
      # - id: cache-pipenv
 | 
					 | 
				
			||||||
      #   uses: actions/cache@v2.1.6
 | 
					 | 
				
			||||||
      #   with:
 | 
					 | 
				
			||||||
      #     path: ~/.local/share/virtualenvs
 | 
					 | 
				
			||||||
      #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
					 | 
				
			||||||
      - name: prepare
 | 
					      - name: prepare
 | 
				
			||||||
        # env:
 | 
					        env:
 | 
				
			||||||
        #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
        run: scripts/ci_prepare.sh
 | 
					        run: |
 | 
				
			||||||
 | 
					          scripts/ci_prepare.sh
 | 
				
			||||||
 | 
					          # Sync anyways since stable will have different dependencies
 | 
				
			||||||
 | 
					          pipenv sync --dev
 | 
				
			||||||
      - name: run migrations to stable
 | 
					      - name: run migrations to stable
 | 
				
			||||||
        run: pipenv run python -m lifecycle.migrate
 | 
					        run: pipenv run python -m lifecycle.migrate
 | 
				
			||||||
      - name: prepare variables
 | 
					 | 
				
			||||||
        id: ev
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          python ./scripts/gh_do_set_branch.py
 | 
					 | 
				
			||||||
      - name: checkout current code
 | 
					      - name: checkout current code
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          set -x
 | 
					          set -x
 | 
				
			||||||
          git fetch
 | 
					          git fetch
 | 
				
			||||||
          git checkout ${{ steps.ev.outputs.branchName }}
 | 
					          git checkout ${{ steps.ev.outputs.branchName }}
 | 
				
			||||||
          pipenv sync --dev
 | 
					          pipenv sync --dev
 | 
				
			||||||
 | 
					      - name: prepare
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
 | 
					        run: scripts/ci_prepare.sh
 | 
				
			||||||
      - name: migrate to latest
 | 
					      - name: migrate to latest
 | 
				
			||||||
        run: pipenv run python -m lifecycle.migrate
 | 
					        run: pipenv run python -m lifecycle.migrate
 | 
				
			||||||
  test-unittest:
 | 
					  test-unittest:
 | 
				
			||||||
@ -168,14 +175,14 @@ jobs:
 | 
				
			|||||||
      - uses: actions/setup-python@v2
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: '3.9'
 | 
					          python-version: '3.9'
 | 
				
			||||||
      # - id: cache-pipenv
 | 
					      - id: cache-pipenv
 | 
				
			||||||
      #   uses: actions/cache@v2.1.6
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
      #   with:
 | 
					        with:
 | 
				
			||||||
      #     path: ~/.local/share/virtualenvs
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
      #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
      - name: prepare
 | 
					      - name: prepare
 | 
				
			||||||
        # env:
 | 
					        env:
 | 
				
			||||||
        #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
        run: scripts/ci_prepare.sh
 | 
					        run: scripts/ci_prepare.sh
 | 
				
			||||||
      - uses: testspace-com/setup-testspace@v1
 | 
					      - uses: testspace-com/setup-testspace@v1
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
@ -197,14 +204,14 @@ jobs:
 | 
				
			|||||||
      - uses: actions/setup-python@v2
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: '3.9'
 | 
					          python-version: '3.9'
 | 
				
			||||||
      # - id: cache-pipenv
 | 
					      - id: cache-pipenv
 | 
				
			||||||
      #   uses: actions/cache@v2.1.6
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
      #   with:
 | 
					        with:
 | 
				
			||||||
      #     path: ~/.local/share/virtualenvs
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
      #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
      - name: prepare
 | 
					      - name: prepare
 | 
				
			||||||
        # env:
 | 
					        env:
 | 
				
			||||||
        #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
        run: scripts/ci_prepare.sh
 | 
					        run: scripts/ci_prepare.sh
 | 
				
			||||||
      - uses: testspace-com/setup-testspace@v1
 | 
					      - uses: testspace-com/setup-testspace@v1
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
@ -236,17 +243,17 @@ jobs:
 | 
				
			|||||||
      - uses: testspace-com/setup-testspace@v1
 | 
					      - uses: testspace-com/setup-testspace@v1
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          domain: ${{github.repository_owner}}
 | 
					          domain: ${{github.repository_owner}}
 | 
				
			||||||
      # - id: cache-pipenv
 | 
					      - id: cache-pipenv
 | 
				
			||||||
      #   uses: actions/cache@v2.1.6
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
      #   with:
 | 
					        with:
 | 
				
			||||||
      #     path: ~/.local/share/virtualenvs
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
      #     key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
      - name: prepare
 | 
					      - name: prepare
 | 
				
			||||||
        # env:
 | 
					        env:
 | 
				
			||||||
        #   INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          scripts/ci_prepare.sh
 | 
					          scripts/ci_prepare.sh
 | 
				
			||||||
          docker-compose -f tests/e2e/ci.docker-compose.yml up -d
 | 
					          docker-compose -f tests/e2e/docker-compose.yml up -d
 | 
				
			||||||
      - id: cache-web
 | 
					      - id: cache-web
 | 
				
			||||||
        uses: actions/cache@v2.1.6
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
@ -290,20 +297,20 @@ jobs:
 | 
				
			|||||||
        env:
 | 
					        env:
 | 
				
			||||||
          DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
 | 
					          DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          python ./scripts/gh_do_set_branch.py
 | 
					          python ./scripts/gh_env.py
 | 
				
			||||||
      - name: Login to Container Registry
 | 
					      - name: Login to Container Registry
 | 
				
			||||||
        uses: docker/login-action@v1
 | 
					        uses: docker/login-action@v1
 | 
				
			||||||
        if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
					        if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          registry: beryju.org
 | 
					          registry: ghcr.io
 | 
				
			||||||
          username: ${{ secrets.HARBOR_USERNAME }}
 | 
					          username: ${{ github.repository_owner }}
 | 
				
			||||||
          password: ${{ secrets.HARBOR_PASSWORD }}
 | 
					          password: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
      - name: Building Docker Image
 | 
					      - name: Building Docker Image
 | 
				
			||||||
        uses: docker/build-push-action@v2
 | 
					        uses: docker/build-push-action@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
					          push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
				
			||||||
          tags: |
 | 
					          tags: |
 | 
				
			||||||
            beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}
 | 
					            ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}
 | 
				
			||||||
            beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }}
 | 
					            ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }}
 | 
				
			||||||
          build-args: |
 | 
					          build-args: |
 | 
				
			||||||
            GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
 | 
					            GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							@ -48,22 +48,22 @@ jobs:
 | 
				
			|||||||
        env:
 | 
					        env:
 | 
				
			||||||
          DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
 | 
					          DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          python ./scripts/gh_do_set_branch.py
 | 
					          python ./scripts/gh_env.py
 | 
				
			||||||
      - name: Login to Container Registry
 | 
					      - name: Login to Container Registry
 | 
				
			||||||
        uses: docker/login-action@v1
 | 
					        uses: docker/login-action@v1
 | 
				
			||||||
        if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
					        if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          registry: beryju.org
 | 
					          registry: ghcr.io
 | 
				
			||||||
          username: ${{ secrets.HARBOR_USERNAME }}
 | 
					          username: ${{ github.repository_owner }}
 | 
				
			||||||
          password: ${{ secrets.HARBOR_PASSWORD }}
 | 
					          password: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
      - name: Building Docker Image
 | 
					      - name: Building Docker Image
 | 
				
			||||||
        uses: docker/build-push-action@v2
 | 
					        uses: docker/build-push-action@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
					          push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
 | 
				
			||||||
          tags: |
 | 
					          tags: |
 | 
				
			||||||
            beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}
 | 
					            ghcr.io/goauthentik/dev-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchNameContainer }}
 | 
				
			||||||
            beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}
 | 
					            ghcr.io/goauthentik/dev-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}
 | 
				
			||||||
            beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }}
 | 
					            ghcr.io/goauthentik/dev-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }}
 | 
				
			||||||
          file: ${{ matrix.type }}.Dockerfile
 | 
					          file: ${{ matrix.type }}.Dockerfile
 | 
				
			||||||
          build-args: |
 | 
					          build-args: |
 | 
				
			||||||
            GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
 | 
					            GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							@ -61,7 +61,7 @@ jobs:
 | 
				
			|||||||
          npm install
 | 
					          npm install
 | 
				
			||||||
      - name: Generate API
 | 
					      - name: Generate API
 | 
				
			||||||
        run: make gen-web
 | 
					        run: make gen-web
 | 
				
			||||||
      - name: prettier
 | 
					      - name: lit-analyse
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          cd web
 | 
					          cd web
 | 
				
			||||||
          npm run lit-analyse
 | 
					          npm run lit-analyse
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										22
									
								
								.github/workflows/ghcr-retention.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/ghcr-retention.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					name: ghcr-retention
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  schedule:
 | 
				
			||||||
 | 
					    - cron: '0 0 * * *'  # every day at midnight
 | 
				
			||||||
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  clean-ghcr:
 | 
				
			||||||
 | 
					    name: Delete old unused container images
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Delete 'dev' containers older than a week
 | 
				
			||||||
 | 
					        uses: sondrelg/container-retention-policy@v1
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          image-names: dev-server,dev-ldap,dev-proxy
 | 
				
			||||||
 | 
					          cut-off: One week ago UTC
 | 
				
			||||||
 | 
					          account-type: org
 | 
				
			||||||
 | 
					          org-name: goauthentik
 | 
				
			||||||
 | 
					          untagged-only: false
 | 
				
			||||||
 | 
					          token: ${{ secrets.GHCR_CLEANUP_TOKEN }}
 | 
				
			||||||
 | 
					          skip-tags: gh-next,gh-master
 | 
				
			||||||
							
								
								
									
										29
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -3,9 +3,6 @@ name: authentik-on-release
 | 
				
			|||||||
on:
 | 
					on:
 | 
				
			||||||
  release:
 | 
					  release:
 | 
				
			||||||
    types: [published, created]
 | 
					    types: [published, created]
 | 
				
			||||||
  push:
 | 
					 | 
				
			||||||
    branches:
 | 
					 | 
				
			||||||
      - version-*
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  # Build
 | 
					  # Build
 | 
				
			||||||
@ -33,14 +30,14 @@ jobs:
 | 
				
			|||||||
        with:
 | 
					        with:
 | 
				
			||||||
          push: ${{ github.event_name == 'release' }}
 | 
					          push: ${{ github.event_name == 'release' }}
 | 
				
			||||||
          tags: |
 | 
					          tags: |
 | 
				
			||||||
            beryju/authentik:2021.9.1,
 | 
					            beryju/authentik:2021.10.1-rc3,
 | 
				
			||||||
            beryju/authentik:latest,
 | 
					            beryju/authentik:latest,
 | 
				
			||||||
            ghcr.io/goauthentik/server:2021.9.1,
 | 
					            ghcr.io/goauthentik/server:2021.10.1-rc3,
 | 
				
			||||||
            ghcr.io/goauthentik/server:latest
 | 
					            ghcr.io/goauthentik/server:latest
 | 
				
			||||||
          platforms: linux/amd64,linux/arm64
 | 
					          platforms: linux/amd64,linux/arm64
 | 
				
			||||||
          context: .
 | 
					          context: .
 | 
				
			||||||
      - name: Building Docker Image (stable)
 | 
					      - name: Building Docker Image (stable)
 | 
				
			||||||
        if: ${{ github.event_name == 'release' && !contains('2021.9.1', 'rc') }}
 | 
					        if: ${{ github.event_name == 'release' && !contains('2021.10.1-rc3', 'rc') }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          docker pull beryju/authentik:latest
 | 
					          docker pull beryju/authentik:latest
 | 
				
			||||||
          docker tag beryju/authentik:latest beryju/authentik:stable
 | 
					          docker tag beryju/authentik:latest beryju/authentik:stable
 | 
				
			||||||
@ -75,14 +72,14 @@ jobs:
 | 
				
			|||||||
        with:
 | 
					        with:
 | 
				
			||||||
          push: ${{ github.event_name == 'release' }}
 | 
					          push: ${{ github.event_name == 'release' }}
 | 
				
			||||||
          tags: |
 | 
					          tags: |
 | 
				
			||||||
            beryju/authentik-proxy:2021.9.1,
 | 
					            beryju/authentik-proxy:2021.10.1-rc3,
 | 
				
			||||||
            beryju/authentik-proxy:latest,
 | 
					            beryju/authentik-proxy:latest,
 | 
				
			||||||
            ghcr.io/goauthentik/proxy:2021.9.1,
 | 
					            ghcr.io/goauthentik/proxy:2021.10.1-rc3,
 | 
				
			||||||
            ghcr.io/goauthentik/proxy:latest
 | 
					            ghcr.io/goauthentik/proxy:latest
 | 
				
			||||||
          file: proxy.Dockerfile
 | 
					          file: proxy.Dockerfile
 | 
				
			||||||
          platforms: linux/amd64,linux/arm64
 | 
					          platforms: linux/amd64,linux/arm64
 | 
				
			||||||
      - name: Building Docker Image (stable)
 | 
					      - name: Building Docker Image (stable)
 | 
				
			||||||
        if: ${{ github.event_name == 'release' && !contains('2021.9.1', 'rc') }}
 | 
					        if: ${{ github.event_name == 'release' && !contains('2021.10.1-rc3', 'rc') }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          docker pull beryju/authentik-proxy:latest
 | 
					          docker pull beryju/authentik-proxy:latest
 | 
				
			||||||
          docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
 | 
					          docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
 | 
				
			||||||
@ -117,14 +114,14 @@ jobs:
 | 
				
			|||||||
        with:
 | 
					        with:
 | 
				
			||||||
          push: ${{ github.event_name == 'release' }}
 | 
					          push: ${{ github.event_name == 'release' }}
 | 
				
			||||||
          tags: |
 | 
					          tags: |
 | 
				
			||||||
            beryju/authentik-ldap:2021.9.1,
 | 
					            beryju/authentik-ldap:2021.10.1-rc3,
 | 
				
			||||||
            beryju/authentik-ldap:latest,
 | 
					            beryju/authentik-ldap:latest,
 | 
				
			||||||
            ghcr.io/goauthentik/ldap:2021.9.1,
 | 
					            ghcr.io/goauthentik/ldap:2021.10.1-rc3,
 | 
				
			||||||
            ghcr.io/goauthentik/ldap:latest
 | 
					            ghcr.io/goauthentik/ldap:latest
 | 
				
			||||||
          file: ldap.Dockerfile
 | 
					          file: ldap.Dockerfile
 | 
				
			||||||
          platforms: linux/amd64,linux/arm64
 | 
					          platforms: linux/amd64,linux/arm64
 | 
				
			||||||
      - name: Building Docker Image (stable)
 | 
					      - name: Building Docker Image (stable)
 | 
				
			||||||
        if: ${{ github.event_name == 'release' && !contains('2021.9.1', 'rc') }}
 | 
					        if: ${{ github.event_name == 'release' && !contains('2021.10.1-rc3', 'rc') }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          docker pull beryju/authentik-ldap:latest
 | 
					          docker pull beryju/authentik-ldap:latest
 | 
				
			||||||
          docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
 | 
					          docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
 | 
				
			||||||
@ -142,15 +139,13 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v2
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
      - name: Run test suite in final docker images
 | 
					      - name: Run test suite in final docker images
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          sudo apt-get install -y pwgen
 | 
					          echo "PG_PASS=$(openssl rand -base64 32)" >> .env
 | 
				
			||||||
          echo "PG_PASS=$(pwgen 40 1)" >> .env
 | 
					          echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
 | 
				
			||||||
          echo "AUTHENTIK_SECRET_KEY=$(pwgen 50 1)" >> .env
 | 
					 | 
				
			||||||
          docker-compose pull -q
 | 
					          docker-compose pull -q
 | 
				
			||||||
          docker-compose up --no-start
 | 
					          docker-compose up --no-start
 | 
				
			||||||
          docker-compose start postgresql redis
 | 
					          docker-compose start postgresql redis
 | 
				
			||||||
          docker-compose run -u root server test
 | 
					          docker-compose run -u root server test
 | 
				
			||||||
  sentry-release:
 | 
					  sentry-release:
 | 
				
			||||||
    if: ${{ github.event_name == 'release' }}
 | 
					 | 
				
			||||||
    needs:
 | 
					    needs:
 | 
				
			||||||
      - test-release
 | 
					      - test-release
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
@ -175,7 +170,7 @@ jobs:
 | 
				
			|||||||
          SENTRY_PROJECT: authentik
 | 
					          SENTRY_PROJECT: authentik
 | 
				
			||||||
          SENTRY_URL: https://sentry.beryju.org
 | 
					          SENTRY_URL: https://sentry.beryju.org
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          version: authentik@2021.9.1
 | 
					          version: authentik@2021.10.1-rc3
 | 
				
			||||||
          environment: beryjuorg-prod
 | 
					          environment: beryjuorg-prod
 | 
				
			||||||
          sourcemaps: './web/dist'
 | 
					          sourcemaps: './web/dist'
 | 
				
			||||||
          url_prefix: '~/static/dist'
 | 
					          url_prefix: '~/static/dist'
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										13
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							@ -13,21 +13,20 @@ jobs:
 | 
				
			|||||||
      - uses: actions/checkout@v2
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
      - name: Pre-release test
 | 
					      - name: Pre-release test
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          sudo apt-get install -y pwgen
 | 
					          echo "PG_PASS=$(openssl rand -base64 32)" >> .env
 | 
				
			||||||
          echo "AUTHENTIK_TAG=latest" >> .env
 | 
					          echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
 | 
				
			||||||
          echo "PG_PASS=$(pwgen 40 1)" >> .env
 | 
					 | 
				
			||||||
          echo "AUTHENTIK_SECRET_KEY=$(pwgen 50 1)" >> .env
 | 
					 | 
				
			||||||
          docker-compose pull -q
 | 
					 | 
				
			||||||
          docker build \
 | 
					          docker build \
 | 
				
			||||||
            --no-cache \
 | 
					            --no-cache \
 | 
				
			||||||
            -t ghcr.io/goauthentik/server:latest \
 | 
					            -t testing:latest \
 | 
				
			||||||
            -f Dockerfile .
 | 
					            -f Dockerfile .
 | 
				
			||||||
 | 
					          echo "AUTHENTIK_IMAGE=testing" >> .env
 | 
				
			||||||
 | 
					          echo "AUTHENTIK_TAG=latest" >> .env
 | 
				
			||||||
          docker-compose up --no-start
 | 
					          docker-compose up --no-start
 | 
				
			||||||
          docker-compose start postgresql redis
 | 
					          docker-compose start postgresql redis
 | 
				
			||||||
          docker-compose run -u root server test
 | 
					          docker-compose run -u root server test
 | 
				
			||||||
      - name: Extract version number
 | 
					      - name: Extract version number
 | 
				
			||||||
        id: get_version
 | 
					        id: get_version
 | 
				
			||||||
        uses: actions/github-script@v4.1
 | 
					        uses: actions/github-script@v5
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
					          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
          script: |
 | 
					          script: |
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										46
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					name: authentik-backend-translate-compile
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches: [ master ]
 | 
				
			||||||
 | 
					    paths:
 | 
				
			||||||
 | 
					      - '/locale/'
 | 
				
			||||||
 | 
					  schedule:
 | 
				
			||||||
 | 
					  - cron: "0 */2 * * *"
 | 
				
			||||||
 | 
					  workflow_dispatch:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					env:
 | 
				
			||||||
 | 
					  POSTGRES_DB: authentik
 | 
				
			||||||
 | 
					  POSTGRES_USER: authentik
 | 
				
			||||||
 | 
					  POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  compile:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@v2
 | 
				
			||||||
 | 
					      - uses: actions/setup-python@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          python-version: '3.9'
 | 
				
			||||||
 | 
					      - id: cache-pipenv
 | 
				
			||||||
 | 
					        uses: actions/cache@v2.1.6
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          path: ~/.local/share/virtualenvs
 | 
				
			||||||
 | 
					          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
				
			||||||
 | 
					      - name: prepare
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          sudo apt-get update
 | 
				
			||||||
 | 
					          sudo apt-get install -y gettext
 | 
				
			||||||
 | 
					          scripts/ci_prepare.sh
 | 
				
			||||||
 | 
					      - name: run compile
 | 
				
			||||||
 | 
					        run: pipenv run ./manage.py compilemessages
 | 
				
			||||||
 | 
					      - name: Create Pull Request
 | 
				
			||||||
 | 
					        uses: peter-evans/create-pull-request@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          token: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					          branch: compile-backend-translation
 | 
				
			||||||
 | 
					          commit-message: "core: compile backend translations"
 | 
				
			||||||
 | 
					          title: "core: compile backend translations"
 | 
				
			||||||
 | 
					          delete-branch: true
 | 
				
			||||||
 | 
					          signoff: true
 | 
				
			||||||
@ -31,7 +31,7 @@ Basically, don't be a dickhead. This is an open-source non-profit project, that
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## I don't want to read this whole thing I just have a question!!!
 | 
					## I don't want to read this whole thing I just have a question!!!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Either [create a question on GitHub](https://github.com/goauthentik/authentik/issues/new?assignees=&labels=question&template=question.md&title=) or join [the Discord server](https://discord.gg/jg33eMhnj6)
 | 
					Either [create a question on GitHub](https://github.com/goauthentik/authentik/issues/new?assignees=&labels=question&template=question.md&title=) or join [the Discord server](https://goauthentik.io/discord)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## What should I know before I get started?
 | 
					## What should I know before I get started?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -117,7 +117,7 @@ This section guides you through submitting a bug report for authentik. Following
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
 | 
					Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This form will have the full stack trace of the error that ocurred and shouldn't contain any sensitive data.
 | 
					This form will have the full stack trace of the error that occurred and shouldn't contain any sensitive data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Suggesting Enhancements
 | 
					### Suggesting Enhancements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,7 +131,7 @@ When you are creating an enhancement suggestion, please fill in [the template](h
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
authentik can be run locally, all though depending on which part you want to work on, different pre-requisites are required.
 | 
					authentik can be run locally, all though depending on which part you want to work on, different pre-requisites are required.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This is documented in the [developer docs](https://goauthentik.io/developer-docs/)
 | 
					This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Pull Requests
 | 
					### Pull Requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										15
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								Dockerfile
									
									
									
									
									
								
							@ -1,5 +1,5 @@
 | 
				
			|||||||
# Stage 1: Lock python dependencies
 | 
					# Stage 1: Lock python dependencies
 | 
				
			||||||
FROM python:3.9-slim-buster as locker
 | 
					FROM docker.io/python:3.9-slim-buster as locker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY ./Pipfile /app/
 | 
					COPY ./Pipfile /app/
 | 
				
			||||||
COPY ./Pipfile.lock /app/
 | 
					COPY ./Pipfile.lock /app/
 | 
				
			||||||
@ -11,7 +11,7 @@ RUN pip install pipenv && \
 | 
				
			|||||||
    pipenv lock -r --dev-only > requirements-dev.txt
 | 
					    pipenv lock -r --dev-only > requirements-dev.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 2: Build website
 | 
					# Stage 2: Build website
 | 
				
			||||||
FROM node as website-builder
 | 
					FROM docker.io/node as website-builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY ./website /static/
 | 
					COPY ./website /static/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -19,7 +19,7 @@ ENV NODE_ENV=production
 | 
				
			|||||||
RUN cd /static && npm i && npm run build-docs-only
 | 
					RUN cd /static && npm i && npm run build-docs-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 3: Build webui
 | 
					# Stage 3: Build webui
 | 
				
			||||||
FROM node as web-builder
 | 
					FROM docker.io/node as web-builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY ./web /static/
 | 
					COPY ./web /static/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,7 +27,7 @@ ENV NODE_ENV=production
 | 
				
			|||||||
RUN cd /static && npm i && npm run build
 | 
					RUN cd /static && npm i && npm run build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 4: Build go proxy
 | 
					# Stage 4: Build go proxy
 | 
				
			||||||
FROM golang:1.17.1 AS builder
 | 
					FROM docker.io/golang:1.17.2 AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /work
 | 
					WORKDIR /work
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -47,7 +47,7 @@ COPY ./go.sum /work/go.sum
 | 
				
			|||||||
RUN go build -o /work/authentik ./cmd/server/main.go
 | 
					RUN go build -o /work/authentik ./cmd/server/main.go
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 5: Run
 | 
					# Stage 5: Run
 | 
				
			||||||
FROM python:3.9-slim-buster
 | 
					FROM docker.io/python:3.9-slim-buster
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /
 | 
					WORKDIR /
 | 
				
			||||||
COPY --from=locker /app/requirements.txt /
 | 
					COPY --from=locker /app/requirements.txt /
 | 
				
			||||||
@ -80,8 +80,11 @@ COPY ./lifecycle/ /lifecycle
 | 
				
			|||||||
COPY --from=builder /work/authentik /authentik-proxy
 | 
					COPY --from=builder /work/authentik /authentik-proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
USER authentik
 | 
					USER authentik
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENV TMPDIR /dev/shm/
 | 
					ENV TMPDIR /dev/shm/
 | 
				
			||||||
ENV PYTHONUNBUFFERED 1
 | 
					ENV PYTHONUNBUFFERED 1
 | 
				
			||||||
ENV prometheus_multiproc_dir /dev/shm/
 | 
					 | 
				
			||||||
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
 | 
					ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "/lifecycle/ak", "healthcheck" ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENTRYPOINT [ "/lifecycle/ak" ]
 | 
					ENTRYPOINT [ "/lifecycle/ak" ]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Makefile
									
									
									
									
									
								
							@ -20,12 +20,24 @@ test:
 | 
				
			|||||||
lint-fix:
 | 
					lint-fix:
 | 
				
			||||||
	isort authentik tests lifecycle
 | 
						isort authentik tests lifecycle
 | 
				
			||||||
	black authentik tests lifecycle
 | 
						black authentik tests lifecycle
 | 
				
			||||||
 | 
						codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \
 | 
				
			||||||
 | 
							authentik \
 | 
				
			||||||
 | 
							internal \
 | 
				
			||||||
 | 
							cmd \
 | 
				
			||||||
 | 
							web/src \
 | 
				
			||||||
 | 
							website/src \
 | 
				
			||||||
 | 
							website/docs \
 | 
				
			||||||
 | 
							website/developer-docs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
lint:
 | 
					lint:
 | 
				
			||||||
	pyright authentik tests lifecycle
 | 
						pyright authentik tests lifecycle
 | 
				
			||||||
	bandit -r authentik tests lifecycle -x node_modules
 | 
						bandit -r authentik tests lifecycle -x node_modules
 | 
				
			||||||
	pylint authentik tests lifecycle
 | 
						pylint authentik tests lifecycle
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					i18n-extract:
 | 
				
			||||||
 | 
						./manage.py makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en
 | 
				
			||||||
 | 
						cd web && npm run extract
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gen-build:
 | 
					gen-build:
 | 
				
			||||||
	./manage.py spectacular --file schema.yml
 | 
						./manage.py spectacular --file schema.yml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -68,4 +80,4 @@ migrate:
 | 
				
			|||||||
	python -m lifecycle.migrate
 | 
						python -m lifecycle.migrate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
run:
 | 
					run:
 | 
				
			||||||
	WORKERS=1 go run -v cmd/server/main.go
 | 
						go run -v cmd/server/main.go
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										9
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								Pipfile
									
									
									
									
									
								
							@ -26,9 +26,9 @@ drf-spectacular = "*"
 | 
				
			|||||||
facebook-sdk = "*"
 | 
					facebook-sdk = "*"
 | 
				
			||||||
geoip2 = "*"
 | 
					geoip2 = "*"
 | 
				
			||||||
gunicorn = "*"
 | 
					gunicorn = "*"
 | 
				
			||||||
kubernetes = "*"
 | 
					kubernetes = "==v19.15.0"
 | 
				
			||||||
ldap3 = "*"
 | 
					ldap3 = "*"
 | 
				
			||||||
lxml = ">=4.6.3"
 | 
					lxml = "*"
 | 
				
			||||||
packaging = "*"
 | 
					packaging = "*"
 | 
				
			||||||
psycopg2-binary = "*"
 | 
					psycopg2-binary = "*"
 | 
				
			||||||
pycryptodome = "*"
 | 
					pycryptodome = "*"
 | 
				
			||||||
@ -48,13 +48,14 @@ duo-client = "*"
 | 
				
			|||||||
ua-parser = "*"
 | 
					ua-parser = "*"
 | 
				
			||||||
deepmerge = "*"
 | 
					deepmerge = "*"
 | 
				
			||||||
colorama = "*"
 | 
					colorama = "*"
 | 
				
			||||||
 | 
					codespell = "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dev-packages]
 | 
					[dev-packages]
 | 
				
			||||||
bandit = "*"
 | 
					bandit = "*"
 | 
				
			||||||
black = "==21.5b1"
 | 
					black = "==21.9b0"
 | 
				
			||||||
bump2version = "*"
 | 
					bump2version = "*"
 | 
				
			||||||
colorama = "*"
 | 
					colorama = "*"
 | 
				
			||||||
coverage = "*"
 | 
					coverage = {extras = ["toml"],version = "*"}
 | 
				
			||||||
pylint = "*"
 | 
					pylint = "*"
 | 
				
			||||||
pylint-django = "*"
 | 
					pylint-django = "*"
 | 
				
			||||||
pytest = "*"
 | 
					pytest = "*"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1289
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1289
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -4,7 +4,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://discord.gg/jg33eMhnj6)
 | 
					[](https://goauthentik.io/discord)
 | 
				
			||||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml)
 | 
					[](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml)
 | 
				
			||||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
 | 
					[](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
 | 
				
			||||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
 | 
					[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
 | 
				
			||||||
@ -20,9 +20,9 @@ authentik is an open-source Identity Provider focused on flexibility and versati
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Installation
 | 
					## Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/)
 | 
					For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/)
 | 
					For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Screenshots
 | 
					## Screenshots
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -33,7 +33,7 @@ Light | Dark
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
See [Development Documentation](https://goauthentik.io/developer-docs/)
 | 
					See [Development Documentation](https://goauthentik.io/developer-docs/?utm_source=github)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Security
 | 
					## Security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,8 +6,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
| Version    | Supported          |
 | 
					| Version    | Supported          |
 | 
				
			||||||
| ---------- | ------------------ |
 | 
					| ---------- | ------------------ |
 | 
				
			||||||
| 2021.7.x   | :white_check_mark: |
 | 
					 | 
				
			||||||
| 2021.8.x   | :white_check_mark: |
 | 
					| 2021.8.x   | :white_check_mark: |
 | 
				
			||||||
 | 
					| 2021.9.x   | :white_check_mark: |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Reporting a Vulnerability
 | 
					## Reporting a Vulnerability
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,3 @@
 | 
				
			|||||||
"""authentik"""
 | 
					"""authentik"""
 | 
				
			||||||
__version__ = "2021.9.1"
 | 
					__version__ = "2021.10.1-rc3"
 | 
				
			||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
					ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
				
			||||||
 | 
				
			|||||||
@ -84,7 +84,7 @@ class SystemSerializer(PassiveSerializer):
 | 
				
			|||||||
        return now()
 | 
					        return now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_embedded_outpost_host(self, request: Request) -> str:
 | 
					    def get_embedded_outpost_host(self, request: Request) -> str:
 | 
				
			||||||
        """Get the FQDN configured on the embeddded outpost"""
 | 
					        """Get the FQDN configured on the embedded outpost"""
 | 
				
			||||||
        outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
					        outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
				
			||||||
        if not outposts.exists():
 | 
					        if not outposts.exists():
 | 
				
			||||||
            return ""
 | 
					            return ""
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
"""authentik administration overview"""
 | 
					"""authentik administration overview"""
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
from drf_spectacular.utils import extend_schema, inline_serializer
 | 
					from drf_spectacular.utils import extend_schema, inline_serializer
 | 
				
			||||||
from prometheus_client import Gauge
 | 
					from prometheus_client import Gauge
 | 
				
			||||||
from rest_framework.fields import IntegerField
 | 
					from rest_framework.fields import IntegerField
 | 
				
			||||||
@ -21,4 +22,7 @@ class WorkerView(APIView):
 | 
				
			|||||||
    def get(self, request: Request) -> Response:
 | 
					    def get(self, request: Request) -> Response:
 | 
				
			||||||
        """Get currently connected worker count."""
 | 
					        """Get currently connected worker count."""
 | 
				
			||||||
        count = len(CELERY_APP.control.ping(timeout=0.5))
 | 
					        count = len(CELERY_APP.control.ping(timeout=0.5))
 | 
				
			||||||
 | 
					        # In debug we run with `CELERY_TASK_ALWAYS_EAGER`, so tasks are ran on the main process
 | 
				
			||||||
 | 
					        if settings.DEBUG:
 | 
				
			||||||
 | 
					            count += 1
 | 
				
			||||||
        return Response({"count": count})
 | 
					        return Response({"count": count})
 | 
				
			||||||
 | 
				
			|||||||
@ -8,3 +8,8 @@ class AuthentikAdminConfig(AppConfig):
 | 
				
			|||||||
    name = "authentik.admin"
 | 
					    name = "authentik.admin"
 | 
				
			||||||
    label = "authentik_admin"
 | 
					    label = "authentik_admin"
 | 
				
			||||||
    verbose_name = "authentik Admin"
 | 
					    verbose_name = "authentik Admin"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def ready(self):
 | 
				
			||||||
 | 
					        from authentik.admin.tasks import clear_update_notifications
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        clear_update_notifications.delay()
 | 
				
			||||||
 | 
				
			|||||||
@ -10,8 +10,13 @@ from requests import RequestException
 | 
				
			|||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik import ENV_GIT_HASH_KEY, __version__
 | 
					from authentik import ENV_GIT_HASH_KEY, __version__
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction, Notification
 | 
				
			||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import (
 | 
				
			||||||
 | 
					    MonitoredTask,
 | 
				
			||||||
 | 
					    TaskResult,
 | 
				
			||||||
 | 
					    TaskResultStatus,
 | 
				
			||||||
 | 
					    prefill_task,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
from authentik.lib.utils.http import get_http_session
 | 
					from authentik.lib.utils.http import get_http_session
 | 
				
			||||||
from authentik.root.celery import CELERY_APP
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
@ -35,7 +40,20 @@ def _set_prom_info():
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@CELERY_APP.task()
 | 
				
			||||||
 | 
					def clear_update_notifications():
 | 
				
			||||||
 | 
					    """Clear update notifications on startup if the notification was for the version
 | 
				
			||||||
 | 
					    we're running now."""
 | 
				
			||||||
 | 
					    for notification in Notification.objects.filter(event__action=EventAction.UPDATE_AVAILABLE):
 | 
				
			||||||
 | 
					        if "new_version" not in notification.event.context:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        notification_version = notification.event.context["new_version"]
 | 
				
			||||||
 | 
					        if notification_version == __version__:
 | 
				
			||||||
 | 
					            notification.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
 | 
					@prefill_task()
 | 
				
			||||||
def update_latest_version(self: MonitoredTask):
 | 
					def update_latest_version(self: MonitoredTask):
 | 
				
			||||||
    """Update latest version info"""
 | 
					    """Update latest version info"""
 | 
				
			||||||
    if CONFIG.y_bool("disable_update_check"):
 | 
					    if CONFIG.y_bool("disable_update_check"):
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,7 @@ from rest_framework.exceptions import AuthenticationFailed
 | 
				
			|||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.middleware import KEY_AUTH_VIA, LOCAL
 | 
				
			||||||
from authentik.core.models import Token, TokenIntents, User
 | 
					from authentik.core.models import Token, TokenIntents, User
 | 
				
			||||||
from authentik.outposts.models import Outpost
 | 
					from authentik.outposts.models import Outpost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -44,6 +45,8 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
 | 
				
			|||||||
        if not user:
 | 
					        if not user:
 | 
				
			||||||
            raise AuthenticationFailed("Token invalid/expired")
 | 
					            raise AuthenticationFailed("Token invalid/expired")
 | 
				
			||||||
        return user
 | 
					        return user
 | 
				
			||||||
 | 
					    if hasattr(LOCAL, "authentik"):
 | 
				
			||||||
 | 
					        LOCAL.authentik[KEY_AUTH_VIA] = "api_token"
 | 
				
			||||||
    return tokens.first().user
 | 
					    return tokens.first().user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -57,7 +60,8 @@ def token_secret_key(value: str) -> Optional[User]:
 | 
				
			|||||||
    outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
					    outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
				
			||||||
    if not outposts:
 | 
					    if not outposts:
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
    LOGGER.info("Authenticating via secret_key")
 | 
					    if hasattr(LOCAL, "authentik"):
 | 
				
			||||||
 | 
					        LOCAL.authentik[KEY_AUTH_VIA] = "secret_key"
 | 
				
			||||||
    outpost = outposts.first()
 | 
					    outpost = outposts.first()
 | 
				
			||||||
    return outpost.user
 | 
					    return outpost.user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@ from drf_spectacular.types import OpenApiTypes
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def build_standard_type(obj, **kwargs):
 | 
					def build_standard_type(obj, **kwargs):
 | 
				
			||||||
    """Build a basic type with optional add ons."""
 | 
					    """Build a basic type with optional add owns."""
 | 
				
			||||||
    schema = build_basic_type(obj)
 | 
					    schema = build_basic_type(obj)
 | 
				
			||||||
    schema.update(kwargs)
 | 
					    schema.update(kwargs)
 | 
				
			||||||
    return schema
 | 
					    return schema
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										18
									
								
								authentik/api/throttle.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								authentik/api/throttle.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					"""Throttling classes"""
 | 
				
			||||||
 | 
					from typing import Type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.views import View
 | 
				
			||||||
 | 
					from rest_framework.request import Request
 | 
				
			||||||
 | 
					from rest_framework.throttling import ScopedRateThrottle
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SessionThrottle(ScopedRateThrottle):
 | 
				
			||||||
 | 
					    """Throttle based on session key"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def allow_request(self, request: Request, view):
 | 
				
			||||||
 | 
					        if request._request.user.is_superuser:
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					        return super().allow_request(request, view)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_cache_key(self, request: Request, view: Type[View]) -> str:
 | 
				
			||||||
 | 
					        return f"authentik-throttle-session-{request._request.session.session_key}"
 | 
				
			||||||
@ -63,7 +63,7 @@ class ConfigView(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @extend_schema(responses={200: ConfigSerializer(many=False)})
 | 
					    @extend_schema(responses={200: ConfigSerializer(many=False)})
 | 
				
			||||||
    def get(self, request: Request) -> Response:
 | 
					    def get(self, request: Request) -> Response:
 | 
				
			||||||
        """Retrive public configuration options"""
 | 
					        """Retrieve public configuration options"""
 | 
				
			||||||
        config = ConfigSerializer(
 | 
					        config = ConfigSerializer(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                "error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
 | 
					                "error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
 | 
				
			||||||
 | 
				
			|||||||
@ -10,10 +10,13 @@ from rest_framework.permissions import AllowAny
 | 
				
			|||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.throttling import AnonRateThrottle
 | 
					from rest_framework.throttling import AnonRateThrottle
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.tasks import sentry_proxy
 | 
					from authentik.api.tasks import sentry_proxy
 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PlainTextParser(BaseParser):
 | 
					class PlainTextParser(BaseParser):
 | 
				
			||||||
    """Plain text parser."""
 | 
					    """Plain text parser."""
 | 
				
			||||||
@ -45,6 +48,7 @@ class SentryTunnelView(APIView):
 | 
				
			|||||||
        """Sentry tunnel, to prevent ad blockers from blocking sentry"""
 | 
					        """Sentry tunnel, to prevent ad blockers from blocking sentry"""
 | 
				
			||||||
        # Only allow usage of this endpoint when error reporting is enabled
 | 
					        # Only allow usage of this endpoint when error reporting is enabled
 | 
				
			||||||
        if not CONFIG.y_bool("error_reporting.enabled", False):
 | 
					        if not CONFIG.y_bool("error_reporting.enabled", False):
 | 
				
			||||||
 | 
					            LOGGER.debug("error reporting disabled")
 | 
				
			||||||
            return HttpResponse(status=400)
 | 
					            return HttpResponse(status=400)
 | 
				
			||||||
        # Body is 2 json objects separated by \n
 | 
					        # Body is 2 json objects separated by \n
 | 
				
			||||||
        full_body = request.body
 | 
					        full_body = request.body
 | 
				
			||||||
@ -55,6 +59,7 @@ class SentryTunnelView(APIView):
 | 
				
			|||||||
        # Check that the DSN is what we expect
 | 
					        # Check that the DSN is what we expect
 | 
				
			||||||
        dsn = header.get("dsn", "")
 | 
					        dsn = header.get("dsn", "")
 | 
				
			||||||
        if dsn != settings.SENTRY_DSN:
 | 
					        if dsn != settings.SENTRY_DSN:
 | 
				
			||||||
 | 
					            LOGGER.debug("Invalid dsn", have=dsn, expected=settings.SENTRY_DSN)
 | 
				
			||||||
            return HttpResponse(status=400)
 | 
					            return HttpResponse(status=400)
 | 
				
			||||||
        sentry_proxy.delay(full_body.decode())
 | 
					        sentry_proxy.delay(full_body.decode())
 | 
				
			||||||
        return HttpResponse(status=204)
 | 
					        return HttpResponse(status=204)
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,8 @@ from authentik.events.api.notification_transport import NotificationTransportVie
 | 
				
			|||||||
from authentik.flows.api.bindings import FlowStageBindingViewSet
 | 
					from authentik.flows.api.bindings import FlowStageBindingViewSet
 | 
				
			||||||
from authentik.flows.api.flows import FlowViewSet
 | 
					from authentik.flows.api.flows import FlowViewSet
 | 
				
			||||||
from authentik.flows.api.stages import StageViewSet
 | 
					from authentik.flows.api.stages import StageViewSet
 | 
				
			||||||
from authentik.flows.views import FlowExecutorView
 | 
					from authentik.flows.views.executor import FlowExecutorView
 | 
				
			||||||
 | 
					from authentik.flows.views.inspector import FlowInspectorView
 | 
				
			||||||
from authentik.outposts.api.outposts import OutpostViewSet
 | 
					from authentik.outposts.api.outposts import OutpostViewSet
 | 
				
			||||||
from authentik.outposts.api.service_connections import (
 | 
					from authentik.outposts.api.service_connections import (
 | 
				
			||||||
    DockerServiceConnectionViewSet,
 | 
					    DockerServiceConnectionViewSet,
 | 
				
			||||||
@ -67,6 +68,11 @@ from authentik.stages.authenticator_duo.api import (
 | 
				
			|||||||
    DuoAdminDeviceViewSet,
 | 
					    DuoAdminDeviceViewSet,
 | 
				
			||||||
    DuoDeviceViewSet,
 | 
					    DuoDeviceViewSet,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					from authentik.stages.authenticator_sms.api import (
 | 
				
			||||||
 | 
					    AuthenticatorSMSStageViewSet,
 | 
				
			||||||
 | 
					    SMSAdminDeviceViewSet,
 | 
				
			||||||
 | 
					    SMSDeviceViewSet,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from authentik.stages.authenticator_static.api import (
 | 
					from authentik.stages.authenticator_static.api import (
 | 
				
			||||||
    AuthenticatorStaticStageViewSet,
 | 
					    AuthenticatorStaticStageViewSet,
 | 
				
			||||||
    StaticAdminDeviceViewSet,
 | 
					    StaticAdminDeviceViewSet,
 | 
				
			||||||
@ -164,6 +170,7 @@ router.register("propertymappings/scope", ScopeMappingViewSet)
 | 
				
			|||||||
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
 | 
					router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.register("authenticators/duo", DuoDeviceViewSet)
 | 
					router.register("authenticators/duo", DuoDeviceViewSet)
 | 
				
			||||||
 | 
					router.register("authenticators/sms", SMSDeviceViewSet)
 | 
				
			||||||
router.register("authenticators/static", StaticDeviceViewSet)
 | 
					router.register("authenticators/static", StaticDeviceViewSet)
 | 
				
			||||||
router.register("authenticators/totp", TOTPDeviceViewSet)
 | 
					router.register("authenticators/totp", TOTPDeviceViewSet)
 | 
				
			||||||
router.register("authenticators/webauthn", WebAuthnDeviceViewSet)
 | 
					router.register("authenticators/webauthn", WebAuthnDeviceViewSet)
 | 
				
			||||||
@ -172,6 +179,11 @@ router.register(
 | 
				
			|||||||
    DuoAdminDeviceViewSet,
 | 
					    DuoAdminDeviceViewSet,
 | 
				
			||||||
    basename="admin-duodevice",
 | 
					    basename="admin-duodevice",
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					router.register(
 | 
				
			||||||
 | 
					    "authenticators/admin/sms",
 | 
				
			||||||
 | 
					    SMSAdminDeviceViewSet,
 | 
				
			||||||
 | 
					    basename="admin-smsdevice",
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
router.register(
 | 
					router.register(
 | 
				
			||||||
    "authenticators/admin/static",
 | 
					    "authenticators/admin/static",
 | 
				
			||||||
    StaticAdminDeviceViewSet,
 | 
					    StaticAdminDeviceViewSet,
 | 
				
			||||||
@ -186,6 +198,7 @@ router.register(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
router.register("stages/all", StageViewSet)
 | 
					router.register("stages/all", StageViewSet)
 | 
				
			||||||
router.register("stages/authenticator/duo", AuthenticatorDuoStageViewSet)
 | 
					router.register("stages/authenticator/duo", AuthenticatorDuoStageViewSet)
 | 
				
			||||||
 | 
					router.register("stages/authenticator/sms", AuthenticatorSMSStageViewSet)
 | 
				
			||||||
router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
 | 
					router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet)
 | 
				
			||||||
router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
 | 
					router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet)
 | 
				
			||||||
router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)
 | 
					router.register("stages/authenticator/validate", AuthenticatorValidateStageViewSet)
 | 
				
			||||||
@ -228,6 +241,11 @@ urlpatterns = (
 | 
				
			|||||||
            FlowExecutorView.as_view(),
 | 
					            FlowExecutorView.as_view(),
 | 
				
			||||||
            name="flow-executor",
 | 
					            name="flow-executor",
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					        path(
 | 
				
			||||||
 | 
					            "flows/inspector/<slug:flow_slug>/",
 | 
				
			||||||
 | 
					            FlowInspectorView.as_view(),
 | 
				
			||||||
 | 
					            name="flow-inspector",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
        path("sentry/", SentryTunnelView.as_view(), name="sentry"),
 | 
					        path("sentry/", SentryTunnelView.as_view(), name="sentry"),
 | 
				
			||||||
        path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
 | 
					        path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
				
			|||||||
@ -82,6 +82,7 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        "description",
 | 
					        "description",
 | 
				
			||||||
        "expires",
 | 
					        "expires",
 | 
				
			||||||
        "expiring",
 | 
					        "expiring",
 | 
				
			||||||
 | 
					        "managed",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    ordering = ["identifier", "expires"]
 | 
					    ordering = ["identifier", "expires"]
 | 
				
			||||||
    permission_classes = [OwnerSuperuserPermissions]
 | 
					    permission_classes = [OwnerSuperuserPermissions]
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,7 @@ from drf_spectacular.utils import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
 | 
					from guardian.shortcuts import get_anonymous_user, get_objects_for_user
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
 | 
					from rest_framework.fields import CharField, DictField, JSONField, SerializerMethodField
 | 
				
			||||||
from rest_framework.permissions import IsAuthenticated
 | 
					from rest_framework.permissions import IsAuthenticated
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
@ -45,6 +45,8 @@ from authentik.core.api.used_by import UsedByMixin
 | 
				
			|||||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
 | 
					from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
 | 
				
			||||||
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
 | 
					from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
 | 
				
			||||||
from authentik.core.models import (
 | 
					from authentik.core.models import (
 | 
				
			||||||
 | 
					    USER_ATTRIBUTE_CHANGE_EMAIL,
 | 
				
			||||||
 | 
					    USER_ATTRIBUTE_CHANGE_USERNAME,
 | 
				
			||||||
    USER_ATTRIBUTE_SA,
 | 
					    USER_ATTRIBUTE_SA,
 | 
				
			||||||
    USER_ATTRIBUTE_TOKEN_EXPIRING,
 | 
					    USER_ATTRIBUTE_TOKEN_EXPIRING,
 | 
				
			||||||
    Group,
 | 
					    Group,
 | 
				
			||||||
@ -90,6 +92,9 @@ class UserSerializer(ModelSerializer):
 | 
				
			|||||||
            "attributes",
 | 
					            "attributes",
 | 
				
			||||||
            "uid",
 | 
					            "uid",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					        extra_kwargs = {
 | 
				
			||||||
 | 
					            "name": {"allow_blank": True},
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserSelfSerializer(ModelSerializer):
 | 
					class UserSelfSerializer(ModelSerializer):
 | 
				
			||||||
@ -98,8 +103,41 @@ class UserSelfSerializer(ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    is_superuser = BooleanField(read_only=True)
 | 
					    is_superuser = BooleanField(read_only=True)
 | 
				
			||||||
    avatar = CharField(read_only=True)
 | 
					    avatar = CharField(read_only=True)
 | 
				
			||||||
    groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
 | 
					    groups = SerializerMethodField()
 | 
				
			||||||
    uid = CharField(read_only=True)
 | 
					    uid = CharField(read_only=True)
 | 
				
			||||||
 | 
					    settings = DictField(source="attributes.settings", default=dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema_field(
 | 
				
			||||||
 | 
					        ListSerializer(
 | 
				
			||||||
 | 
					            child=inline_serializer(
 | 
				
			||||||
 | 
					                "UserSelfGroups",
 | 
				
			||||||
 | 
					                {"name": CharField(read_only=True), "pk": CharField(read_only=True)},
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def get_groups(self, _: User):
 | 
				
			||||||
 | 
					        """Return only the group names a user is member of"""
 | 
				
			||||||
 | 
					        for group in self.instance.ak_groups.all():
 | 
				
			||||||
 | 
					            yield {
 | 
				
			||||||
 | 
					                "name": group.name,
 | 
				
			||||||
 | 
					                "pk": group.pk,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_email(self, email: str):
 | 
				
			||||||
 | 
					        """Check if the user is allowed to change their email"""
 | 
				
			||||||
 | 
					        if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_EMAIL, True):
 | 
				
			||||||
 | 
					            return email
 | 
				
			||||||
 | 
					        if email != self.instance.email:
 | 
				
			||||||
 | 
					            raise ValidationError("Not allowed to change email.")
 | 
				
			||||||
 | 
					        return email
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate_username(self, username: str):
 | 
				
			||||||
 | 
					        """Check if the user is allowed to change their username"""
 | 
				
			||||||
 | 
					        if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_USERNAME, True):
 | 
				
			||||||
 | 
					            return username
 | 
				
			||||||
 | 
					        if username != self.instance.username:
 | 
				
			||||||
 | 
					            raise ValidationError("Not allowed to change username.")
 | 
				
			||||||
 | 
					        return username
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -114,9 +152,11 @@ class UserSelfSerializer(ModelSerializer):
 | 
				
			|||||||
            "email",
 | 
					            "email",
 | 
				
			||||||
            "avatar",
 | 
					            "avatar",
 | 
				
			||||||
            "uid",
 | 
					            "uid",
 | 
				
			||||||
 | 
					            "settings",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        extra_kwargs = {
 | 
					        extra_kwargs = {
 | 
				
			||||||
            "is_active": {"read_only": True},
 | 
					            "is_active": {"read_only": True},
 | 
				
			||||||
 | 
					            "name": {"allow_blank": True},
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -208,6 +248,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
    """User Viewset"""
 | 
					    """User Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = User.objects.none()
 | 
					    queryset = User.objects.none()
 | 
				
			||||||
 | 
					    ordering = ["username"]
 | 
				
			||||||
    serializer_class = UserSerializer
 | 
					    serializer_class = UserSerializer
 | 
				
			||||||
    search_fields = ["username", "name", "is_active", "email"]
 | 
					    search_fields = ["username", "name", "is_active", "email"]
 | 
				
			||||||
    filterset_class = UsersFilter
 | 
					    filterset_class = UsersFilter
 | 
				
			||||||
@ -288,13 +329,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
    # pylint: disable=invalid-name
 | 
					    # pylint: disable=invalid-name
 | 
				
			||||||
    def me(self, request: Request) -> Response:
 | 
					    def me(self, request: Request) -> Response:
 | 
				
			||||||
        """Get information about current user"""
 | 
					        """Get information about current user"""
 | 
				
			||||||
        serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data})
 | 
					        serializer = SessionUserSerializer(
 | 
				
			||||||
 | 
					            data={"user": UserSelfSerializer(instance=request.user).data}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        if SESSION_IMPERSONATE_USER in request._request.session:
 | 
					        if SESSION_IMPERSONATE_USER in request._request.session:
 | 
				
			||||||
            serializer.initial_data["original"] = UserSelfSerializer(
 | 
					            serializer.initial_data["original"] = UserSelfSerializer(
 | 
				
			||||||
                request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
 | 
					                instance=request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
 | 
				
			||||||
            ).data
 | 
					            ).data
 | 
				
			||||||
        serializer.is_valid()
 | 
					        return Response(serializer.initial_data)
 | 
				
			||||||
        return Response(serializer.data)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema(request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)})
 | 
					    @extend_schema(request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)})
 | 
				
			||||||
    @action(
 | 
					    @action(
 | 
				
			||||||
@ -308,15 +350,13 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        """Allow users to change information on their own profile"""
 | 
					        """Allow users to change information on their own profile"""
 | 
				
			||||||
        data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data)
 | 
					        data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data)
 | 
				
			||||||
        if not data.is_valid():
 | 
					        if not data.is_valid():
 | 
				
			||||||
            return Response(data.errors)
 | 
					            return Response(data.errors, status=400)
 | 
				
			||||||
        new_user = data.save()
 | 
					        new_user = data.save()
 | 
				
			||||||
        # If we're impersonating, we need to update that user object
 | 
					        # If we're impersonating, we need to update that user object
 | 
				
			||||||
        # since it caches the full object
 | 
					        # since it caches the full object
 | 
				
			||||||
        if SESSION_IMPERSONATE_USER in request.session:
 | 
					        if SESSION_IMPERSONATE_USER in request.session:
 | 
				
			||||||
            request.session[SESSION_IMPERSONATE_USER] = new_user
 | 
					            request.session[SESSION_IMPERSONATE_USER] = new_user
 | 
				
			||||||
        serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data})
 | 
					        return Response({"user": data.data})
 | 
				
			||||||
        serializer.is_valid()
 | 
					 | 
				
			||||||
        return Response(serializer.data)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @permission_required("authentik_core.view_user", ["authentik_events.view_event"])
 | 
					    @permission_required("authentik_core.view_user", ["authentik_events.view_event"])
 | 
				
			||||||
    @extend_schema(responses={200: UserMetricsSerializer(many=False)})
 | 
					    @extend_schema(responses={200: UserMetricsSerializer(many=False)})
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,7 @@ from django.http.request import HttpRequest
 | 
				
			|||||||
from authentik.core.models import Token, TokenIntents, User
 | 
					from authentik.core.models import Token, TokenIntents, User
 | 
				
			||||||
from authentik.events.utils import cleanse_dict, sanitize_dict
 | 
					from authentik.events.utils import cleanse_dict, sanitize_dict
 | 
				
			||||||
from authentik.flows.planner import FlowPlan
 | 
					from authentik.flows.planner import FlowPlan
 | 
				
			||||||
from authentik.flows.views import SESSION_KEY_PLAN
 | 
					from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
				
			||||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 | 
					from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,9 @@ SESSION_IMPERSONATE_USER = "authentik_impersonate_user"
 | 
				
			|||||||
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
 | 
					SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
 | 
				
			||||||
LOCAL = local()
 | 
					LOCAL = local()
 | 
				
			||||||
RESPONSE_HEADER_ID = "X-authentik-id"
 | 
					RESPONSE_HEADER_ID = "X-authentik-id"
 | 
				
			||||||
 | 
					KEY_AUTH_VIA = "auth_via"
 | 
				
			||||||
 | 
					KEY_USER = "user"
 | 
				
			||||||
 | 
					INTERNAL_HEADER_PREFIX = "X-authentik-internal-"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ImpersonateMiddleware:
 | 
					class ImpersonateMiddleware:
 | 
				
			||||||
@ -50,15 +53,17 @@ class RequestIDMiddleware:
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        response = self.get_response(request)
 | 
					        response = self.get_response(request)
 | 
				
			||||||
        response[RESPONSE_HEADER_ID] = request.request_id
 | 
					        response[RESPONSE_HEADER_ID] = request.request_id
 | 
				
			||||||
        del LOCAL.authentik["request_id"]
 | 
					        if auth_via := LOCAL.authentik.get(KEY_AUTH_VIA, None):
 | 
				
			||||||
        del LOCAL.authentik["host"]
 | 
					            response[INTERNAL_HEADER_PREFIX + KEY_AUTH_VIA] = auth_via
 | 
				
			||||||
 | 
					        response[INTERNAL_HEADER_PREFIX + KEY_USER] = request.user.username
 | 
				
			||||||
 | 
					        for key in list(LOCAL.authentik.keys()):
 | 
				
			||||||
 | 
					            del LOCAL.authentik[key]
 | 
				
			||||||
        return response
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# pylint: disable=unused-argument
 | 
					# pylint: disable=unused-argument
 | 
				
			||||||
def structlog_add_request_id(logger: Logger, method_name: str, event_dict):
 | 
					def structlog_add_request_id(logger: Logger, method_name: str, event_dict: dict):
 | 
				
			||||||
    """If threadlocal has authentik defined, add request_id to log"""
 | 
					    """If threadlocal has authentik defined, add request_id to log"""
 | 
				
			||||||
    if hasattr(LOCAL, "authentik"):
 | 
					    if hasattr(LOCAL, "authentik"):
 | 
				
			||||||
        event_dict["request_id"] = LOCAL.authentik.get("request_id", "")
 | 
					        event_dict.update(LOCAL.authentik)
 | 
				
			||||||
        event_dict["host"] = LOCAL.authentik.get("host", "")
 | 
					 | 
				
			||||||
    return event_dict
 | 
					    return event_dict
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,221 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-10 16:16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.apps.registry import Apps
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import authentik.core.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    # We have to use a direct import here, otherwise we get an object manager error
 | 
				
			||||||
 | 
					    from authentik.core.models import User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    akadmin, _ = User.objects.using(db_alias).get_or_create(
 | 
				
			||||||
 | 
					        username="akadmin", email="root@localhost", name="authentik Default Admin"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    if "TF_BUILD" in environ or "AK_ADMIN_PASS" in environ or settings.TEST:
 | 
				
			||||||
 | 
					        akadmin.set_password(environ.get("AK_ADMIN_PASS", "akadmin"), signal=False)  # noqa # nosec
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        akadmin.set_unusable_password()
 | 
				
			||||||
 | 
					    akadmin.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_default_admin_group(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Group = apps.get_model("authentik_core", "Group")
 | 
				
			||||||
 | 
					    User = apps.get_model("authentik_core", "User")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Creates a default admin group
 | 
				
			||||||
 | 
					    group, _ = Group.objects.using(db_alias).get_or_create(
 | 
				
			||||||
 | 
					        is_superuser=True,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            "name": "authentik Admins",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    group.users.set(User.objects.filter(username="akadmin"))
 | 
				
			||||||
 | 
					    group.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    replaces = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0002_auto_20200523_1133"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0003_default_user"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0004_auto_20200703_2213"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0005_token_intent"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0006_auto_20200709_1608"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0007_auto_20200815_1841"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0008_auto_20200824_1532"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0009_group_is_superuser"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0010_auto_20200917_1021"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0011_provider_name_temp"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0001_initial"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0003_auto_20200523_1133"),
 | 
				
			||||||
 | 
					        ("auth", "0012_alter_user_first_name_max_length"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="application",
 | 
				
			||||||
 | 
					            name="skip_authorization",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="source",
 | 
				
			||||||
 | 
					            name="authentication_flow",
 | 
				
			||||||
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Flow to use when authenticating existing users.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.SET_NULL,
 | 
				
			||||||
 | 
					                related_name="source_authentication",
 | 
				
			||||||
 | 
					                to="authentik_flows.flow",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="source",
 | 
				
			||||||
 | 
					            name="enrollment_flow",
 | 
				
			||||||
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Flow to use when enrolling new users.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.SET_NULL,
 | 
				
			||||||
 | 
					                related_name="source_enrollment",
 | 
				
			||||||
 | 
					                to="authentik_flows.flow",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="provider",
 | 
				
			||||||
 | 
					            name="authorization_flow",
 | 
				
			||||||
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                help_text="Flow used when authorizing this provider.",
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                related_name="provider_authorization",
 | 
				
			||||||
 | 
					                to="authentik_flows.flow",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="is_superuser",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="is_staff",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=create_default_user,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="is_superuser",
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="is_staff",
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="application",
 | 
				
			||||||
 | 
					            options={"verbose_name": "Application", "verbose_name_plural": "Applications"},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="user",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "permissions": (("reset_user_password", "Reset Password"),),
 | 
				
			||||||
 | 
					                "verbose_name": "User",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Users",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            name="intent",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[("verification", "Intent Verification"), ("api", "Intent Api")],
 | 
				
			||||||
 | 
					                default="verification",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="source",
 | 
				
			||||||
 | 
					            name="slug",
 | 
				
			||||||
 | 
					            field=models.SlugField(help_text="Internal source name, used in URLs.", unique=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="first_name",
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=150, verbose_name="first name"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="groups",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="groups",
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
 | 
				
			||||||
 | 
					                related_name="user_set",
 | 
				
			||||||
 | 
					                related_query_name="user",
 | 
				
			||||||
 | 
					                to="auth.Group",
 | 
				
			||||||
 | 
					                verbose_name="groups",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="is_superuser",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="is_staff",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            name="pb_groups",
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(related_name="users", to="authentik_core.Group"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="group",
 | 
				
			||||||
 | 
					            name="is_superuser",
 | 
				
			||||||
 | 
					            field=models.BooleanField(
 | 
				
			||||||
 | 
					                default=False, help_text="Users added to this group will be superusers."
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=create_default_admin_group,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelManagers(
 | 
				
			||||||
 | 
					            name="user",
 | 
				
			||||||
 | 
					            managers=[
 | 
				
			||||||
 | 
					                ("objects", authentik.core.models.UserManager()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="user",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "permissions": (
 | 
				
			||||||
 | 
					                    ("reset_user_password", "Reset Password"),
 | 
				
			||||||
 | 
					                    ("impersonate", "Can impersonate other users"),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                "verbose_name": "User",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Users",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="provider",
 | 
				
			||||||
 | 
					            name="name_temp",
 | 
				
			||||||
 | 
					            field=models.TextField(default=""),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-12 15:36
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.apps.registry import Apps
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import authentik.core.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def set_default_token_key(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Token = apps.get_model("authentik_core", "Token")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for token in Token.objects.using(db_alias).all():
 | 
				
			||||||
 | 
					        token.key = token.pk.hex
 | 
				
			||||||
 | 
					        token.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    replaces = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0012_auto_20201003_1737"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0013_auto_20201003_2132"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0014_auto_20201018_1158"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0015_application_icon"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0016_auto_20201202_2234"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_providers_saml", "0006_remove_samlprovider_name"),
 | 
				
			||||||
 | 
					        ("authentik_providers_oauth2", "0006_remove_oauth2provider_name"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0011_provider_name_temp"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.RenameField(
 | 
				
			||||||
 | 
					            model_name="provider",
 | 
				
			||||||
 | 
					            old_name="name_temp",
 | 
				
			||||||
 | 
					            new_name="name",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            name="identifier",
 | 
				
			||||||
 | 
					            field=models.TextField(default=""),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            name="intent",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("verification", "Intent Verification"),
 | 
				
			||||||
 | 
					                    ("api", "Intent Api"),
 | 
				
			||||||
 | 
					                    ("recovery", "Intent Recovery"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="verification",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterUniqueTogether(
 | 
				
			||||||
 | 
					            name="token",
 | 
				
			||||||
 | 
					            unique_together={("identifier", "user")},
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            name="key",
 | 
				
			||||||
 | 
					            field=models.TextField(default=authentik.core.models.default_token_key),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterUniqueTogether(
 | 
				
			||||||
 | 
					            name="token",
 | 
				
			||||||
 | 
					            unique_together=set(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            name="identifier",
 | 
				
			||||||
 | 
					            field=models.SlugField(max_length=255),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["key"], name="authentik_co_key_e45007_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["identifier"], name="authentik_co_identif_1a34a8_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=set_default_token_key,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="application",
 | 
				
			||||||
 | 
					            name="meta_icon_url",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="application",
 | 
				
			||||||
 | 
					            name="meta_icon",
 | 
				
			||||||
 | 
					            field=models.FileField(blank=True, default="", upload_to="application-icons/"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveIndex(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            name="authentik_co_key_e45007_idx",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveIndex(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            name="authentik_co_identif_1a34a8_idx",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RenameField(
 | 
				
			||||||
 | 
					            model_name="user",
 | 
				
			||||||
 | 
					            old_name="pb_groups",
 | 
				
			||||||
 | 
					            new_name="ak_groups",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["identifier"], name="authentik_c_identif_d9d032_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddIndex(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            index=models.Index(fields=["key"], name="authentik_c_key_f71355_idx"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,210 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-10 16:12
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import uuid
 | 
				
			||||||
 | 
					from os import environ
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.core.validators
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.apps.registry import Apps
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					from django.db.models import Count
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import authentik.core.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    from django.contrib.sessions.backends.cache import KEY_PREFIX
 | 
				
			||||||
 | 
					    from django.core.cache import cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    session_keys = cache.keys(KEY_PREFIX + "*")
 | 
				
			||||||
 | 
					    cache.delete_many(session_keys)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def fix_duplicates(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Token = apps.get_model("authentik_core", "token")
 | 
				
			||||||
 | 
					    identifiers = (
 | 
				
			||||||
 | 
					        Token.objects.using(db_alias)
 | 
				
			||||||
 | 
					        .values("identifier")
 | 
				
			||||||
 | 
					        .annotate(identifier_count=Count("identifier"))
 | 
				
			||||||
 | 
					        .filter(identifier_count__gt=1)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    for ident in identifiers:
 | 
				
			||||||
 | 
					        Token.objects.using(db_alias).filter(identifier=ident["identifier"]).delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_default_user_token(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    # We have to use a direct import here, otherwise we get an object manager error
 | 
				
			||||||
 | 
					    from authentik.core.models import Token, TokenIntents, User
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    akadmin = User.objects.using(db_alias).filter(username="akadmin")
 | 
				
			||||||
 | 
					    if not akadmin.exists():
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    if "AK_ADMIN_TOKEN" not in environ:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    Token.objects.using(db_alias).create(
 | 
				
			||||||
 | 
					        identifier="authentik-boostrap-token",
 | 
				
			||||||
 | 
					        user=akadmin.first(),
 | 
				
			||||||
 | 
					        intent=TokenIntents.INTENT_API,
 | 
				
			||||||
 | 
					        expiring=False,
 | 
				
			||||||
 | 
					        key=environ["AK_ADMIN_TOKEN"],
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    replaces = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0018_auto_20210330_1345"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0019_source_managed"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0020_source_user_matching_mode"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0021_alter_application_slug"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0022_authenticatedsession"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0023_alter_application_meta_launch_url"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0024_alter_token_identifier"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0025_alter_application_meta_icon"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0026_alter_application_meta_icon"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0027_bootstrap_token"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0028_alter_token_intent"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0017_managed"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="token",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "permissions": (("view_token_key", "View token's key"),),
 | 
				
			||||||
 | 
					                "verbose_name": "Token",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Tokens",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="source",
 | 
				
			||||||
 | 
					            name="managed",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                unique=True,
 | 
				
			||||||
 | 
					                verbose_name="Managed by authentik",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="source",
 | 
				
			||||||
 | 
					            name="user_matching_mode",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("identifier", "Use the source-specific identifier"),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "email_link",
 | 
				
			||||||
 | 
					                        "Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "email_deny",
 | 
				
			||||||
 | 
					                        "Use the user's email address, but deny enrollment when the email address already exists.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "username_link",
 | 
				
			||||||
 | 
					                        "Link to a user with identical username. Can have security implications when a username is used with another source.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "username_deny",
 | 
				
			||||||
 | 
					                        "Use the user's username, but deny enrollment when the username already exists.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="identifier",
 | 
				
			||||||
 | 
					                help_text="How the source determines if an existing user should be authenticated or a new user enrolled.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="application",
 | 
				
			||||||
 | 
					            name="slug",
 | 
				
			||||||
 | 
					            field=models.SlugField(
 | 
				
			||||||
 | 
					                help_text="Internal application name, used in URLs.", unique=True
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="AuthenticatedSession",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "expires",
 | 
				
			||||||
 | 
					                    models.DateTimeField(default=authentik.core.models.default_token_duration),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("expiring", models.BooleanField(default=True)),
 | 
				
			||||||
 | 
					                ("uuid", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
 | 
				
			||||||
 | 
					                ("session_key", models.CharField(max_length=40)),
 | 
				
			||||||
 | 
					                ("last_ip", models.TextField()),
 | 
				
			||||||
 | 
					                ("last_user_agent", models.TextField(blank=True)),
 | 
				
			||||||
 | 
					                ("last_used", models.DateTimeField(auto_now=True)),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "user",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "abstract": False,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=migrate_sessions,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="application",
 | 
				
			||||||
 | 
					            name="meta_launch_url",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                blank=True, default="", validators=[django.core.validators.URLValidator()]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=fix_duplicates,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            name="identifier",
 | 
				
			||||||
 | 
					            field=models.SlugField(max_length=255, unique=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="application",
 | 
				
			||||||
 | 
					            name="meta_icon",
 | 
				
			||||||
 | 
					            field=models.FileField(default=None, null=True, upload_to="application-icons/"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="application",
 | 
				
			||||||
 | 
					            name="meta_icon",
 | 
				
			||||||
 | 
					            field=models.FileField(
 | 
				
			||||||
 | 
					                default=None, max_length=500, null=True, upload_to="application-icons/"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="authenticatedsession",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Authenticated Session",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Authenticated Sessions",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=create_default_user_token,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="token",
 | 
				
			||||||
 | 
					            name="intent",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("verification", "Intent Verification"),
 | 
				
			||||||
 | 
					                    ("api", "Intent Api"),
 | 
				
			||||||
 | 
					                    ("recovery", "Intent Recovery"),
 | 
				
			||||||
 | 
					                    ("app_password", "Intent App Password"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="verification",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    (
 | 
					                    (
 | 
				
			||||||
                        "username_link",
 | 
					                        "username_link",
 | 
				
			||||||
                        "Link to a user with identical username address. Can have security implications when a username is used with another source.",
 | 
					                        "Link to a user with identical username. Can have security implications when a username is used with another source.",
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    (
 | 
					                    (
 | 
				
			||||||
                        "username_deny",
 | 
					                        "username_deny",
 | 
				
			||||||
 | 
				
			|||||||
@ -39,6 +39,8 @@ USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
 | 
				
			|||||||
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
 | 
					USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
 | 
				
			||||||
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
 | 
					USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
 | 
				
			||||||
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires"  # nosec
 | 
					USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires"  # nosec
 | 
				
			||||||
 | 
					USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
 | 
				
			||||||
 | 
					USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
 | 
				
			||||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
 | 
					USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GRAVATAR_URL = "https://secure.gravatar.com"
 | 
					GRAVATAR_URL = "https://secure.gravatar.com"
 | 
				
			||||||
@ -283,7 +285,7 @@ class SourceUserMatchingModes(models.TextChoices):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    USERNAME_LINK = "username_link", _(
 | 
					    USERNAME_LINK = "username_link", _(
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "Link to a user with identical username address. Can have security implications "
 | 
					            "Link to a user with identical username. Can have security implications "
 | 
				
			||||||
            "when a username is used with another source."
 | 
					            "when a username is used with another source."
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,7 @@ from authentik.flows.planner import (
 | 
				
			|||||||
    PLAN_CONTEXT_SSO,
 | 
					    PLAN_CONTEXT_SSO,
 | 
				
			||||||
    FlowPlanner,
 | 
					    FlowPlanner,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
 | 
					from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
 | 
				
			||||||
from authentik.lib.utils.urls import redirect_with_qs
 | 
					from authentik.lib.utils.urls import redirect_with_qs
 | 
				
			||||||
from authentik.policies.utils import delete_none_keys
 | 
					from authentik.policies.utils import delete_none_keys
 | 
				
			||||||
from authentik.stages.password import BACKEND_INBUILT
 | 
					from authentik.stages.password import BACKEND_INBUILT
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ from os import environ
 | 
				
			|||||||
from boto3.exceptions import Boto3Error
 | 
					from boto3.exceptions import Boto3Error
 | 
				
			||||||
from botocore.exceptions import BotoCoreError, ClientError
 | 
					from botocore.exceptions import BotoCoreError, ClientError
 | 
				
			||||||
from dbbackup.db.exceptions import CommandConnectorError
 | 
					from dbbackup.db.exceptions import CommandConnectorError
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
from django.contrib.humanize.templatetags.humanize import naturaltime
 | 
					from django.contrib.humanize.templatetags.humanize import naturaltime
 | 
				
			||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
 | 
					from django.contrib.sessions.backends.cache import KEY_PREFIX
 | 
				
			||||||
from django.core import management
 | 
					from django.core import management
 | 
				
			||||||
@ -15,7 +16,12 @@ from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
 | 
				
			|||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import AuthenticatedSession, ExpiringModel
 | 
					from authentik.core.models import AuthenticatedSession, ExpiringModel
 | 
				
			||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import (
 | 
				
			||||||
 | 
					    MonitoredTask,
 | 
				
			||||||
 | 
					    TaskResult,
 | 
				
			||||||
 | 
					    TaskResultStatus,
 | 
				
			||||||
 | 
					    prefill_task,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
from authentik.root.celery import CELERY_APP
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,6 +29,7 @@ LOGGER = get_logger()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
 | 
					@prefill_task()
 | 
				
			||||||
def clean_expired_models(self: MonitoredTask):
 | 
					def clean_expired_models(self: MonitoredTask):
 | 
				
			||||||
    """Remove expired objects"""
 | 
					    """Remove expired objects"""
 | 
				
			||||||
    messages = []
 | 
					    messages = []
 | 
				
			||||||
@ -49,23 +56,25 @@ def clean_expired_models(self: MonitoredTask):
 | 
				
			|||||||
    self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
 | 
					    self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def should_backup() -> bool:
 | 
				
			||||||
 | 
					    """Check if we should be doing backups"""
 | 
				
			||||||
 | 
					    if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup.bucket"):
 | 
				
			||||||
 | 
					        LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    if not CONFIG.y_bool("postgresql.backup.enabled"):
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    if settings.DEBUG:
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
 | 
					@prefill_task()
 | 
				
			||||||
def backup_database(self: MonitoredTask):  # pragma: no cover
 | 
					def backup_database(self: MonitoredTask):  # pragma: no cover
 | 
				
			||||||
    """Database backup"""
 | 
					    """Database backup"""
 | 
				
			||||||
    self.result_timeout_hours = 25
 | 
					    self.result_timeout_hours = 25
 | 
				
			||||||
    if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup"):
 | 
					    if not should_backup():
 | 
				
			||||||
        LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
 | 
					        self.set_status(TaskResult(TaskResultStatus.UNKNOWN, ["Backups are not configured."]))
 | 
				
			||||||
        self.set_status(
 | 
					 | 
				
			||||||
            TaskResult(
 | 
					 | 
				
			||||||
                TaskResultStatus.WARNING,
 | 
					 | 
				
			||||||
                [
 | 
					 | 
				
			||||||
                    (
 | 
					 | 
				
			||||||
                        "Skipping backup as authentik is running in Kubernetes "
 | 
					 | 
				
			||||||
                        "without S3 backups configured."
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        start = datetime.now()
 | 
					        start = datetime.now()
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
{% block head_before %}
 | 
					{% block head_before %}
 | 
				
			||||||
{{ block.super }}
 | 
					{{ block.super }}
 | 
				
			||||||
{% if flow.compatibility_mode %}
 | 
					{% if flow.compatibility_mode and not inspector %}
 | 
				
			||||||
<script>ShadyDOM = { force: !navigator.webdriver };</script>
 | 
					<script>ShadyDOM = { force: !navigator.webdriver };</script>
 | 
				
			||||||
{% endif %}
 | 
					{% endif %}
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
				
			|||||||
@ -61,7 +61,7 @@
 | 
				
			|||||||
                {% endfor %}
 | 
					                {% endfor %}
 | 
				
			||||||
                {% if tenant.branding_title != "authentik" %}
 | 
					                {% if tenant.branding_title != "authentik" %}
 | 
				
			||||||
                <li>
 | 
					                <li>
 | 
				
			||||||
                    <a href="https://goauthentik.io">
 | 
					                    <a href="https://goauthentik.io?utm_source=authentik">
 | 
				
			||||||
                        {% trans 'Powered by authentik' %}
 | 
					                        {% trans 'Powered by authentik' %}
 | 
				
			||||||
                    </a>
 | 
					                    </a>
 | 
				
			||||||
                </li>
 | 
					                </li>
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ from django.test import TestCase
 | 
				
			|||||||
from authentik.core.auth import TokenBackend
 | 
					from authentik.core.auth import TokenBackend
 | 
				
			||||||
from authentik.core.models import Token, TokenIntents, User
 | 
					from authentik.core.models import Token, TokenIntents, User
 | 
				
			||||||
from authentik.flows.planner import FlowPlan
 | 
					from authentik.flows.planner import FlowPlan
 | 
				
			||||||
from authentik.flows.views import SESSION_KEY_PLAN
 | 
					from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
				
			||||||
from authentik.lib.tests.utils import get_request
 | 
					from authentik.lib.tests.utils import get_request
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
from django.urls.base import reverse
 | 
					from django.urls.base import reverse
 | 
				
			||||||
from rest_framework.test import APITestCase
 | 
					from rest_framework.test import APITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, User
 | 
				
			||||||
from authentik.flows.models import Flow, FlowDesignation
 | 
					from authentik.flows.models import Flow, FlowDesignation
 | 
				
			||||||
from authentik.stages.email.models import EmailStage
 | 
					from authentik.stages.email.models import EmailStage
 | 
				
			||||||
from authentik.tenants.models import Tenant
 | 
					from authentik.tenants.models import Tenant
 | 
				
			||||||
@ -15,6 +15,34 @@ class TestUsersAPI(APITestCase):
 | 
				
			|||||||
        self.admin = User.objects.get(username="akadmin")
 | 
					        self.admin = User.objects.get(username="akadmin")
 | 
				
			||||||
        self.user = User.objects.create(username="test-user")
 | 
					        self.user = User.objects.create(username="test-user")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_update_self(self):
 | 
				
			||||||
 | 
					        """Test update_self"""
 | 
				
			||||||
 | 
					        self.client.force_login(self.admin)
 | 
				
			||||||
 | 
					        response = self.client.put(
 | 
				
			||||||
 | 
					            reverse("authentik_api:user-update-self"), data={"username": "foo", "name": "foo"}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_update_self_username_denied(self):
 | 
				
			||||||
 | 
					        """Test update_self"""
 | 
				
			||||||
 | 
					        self.admin.attributes[USER_ATTRIBUTE_CHANGE_USERNAME] = False
 | 
				
			||||||
 | 
					        self.admin.save()
 | 
				
			||||||
 | 
					        self.client.force_login(self.admin)
 | 
				
			||||||
 | 
					        response = self.client.put(
 | 
				
			||||||
 | 
					            reverse("authentik_api:user-update-self"), data={"username": "foo", "name": "foo"}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_update_self_email_denied(self):
 | 
				
			||||||
 | 
					        """Test update_self"""
 | 
				
			||||||
 | 
					        self.admin.attributes[USER_ATTRIBUTE_CHANGE_EMAIL] = False
 | 
				
			||||||
 | 
					        self.admin.save()
 | 
				
			||||||
 | 
					        self.client.force_login(self.admin)
 | 
				
			||||||
 | 
					        response = self.client.put(
 | 
				
			||||||
 | 
					            reverse("authentik_api:user-update-self"), data={"email": "foo", "name": "foo"}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_metrics(self):
 | 
					    def test_metrics(self):
 | 
				
			||||||
        """Test user's metrics"""
 | 
					        """Test user's metrics"""
 | 
				
			||||||
        self.client.force_login(self.admin)
 | 
					        self.client.force_login(self.admin)
 | 
				
			||||||
 | 
				
			|||||||
@ -14,4 +14,5 @@ class FlowInterfaceView(TemplateView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
 | 
					    def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
 | 
				
			||||||
        kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
 | 
					        kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
 | 
				
			||||||
 | 
					        kwargs["inspector"] = "inspector" in self.request.GET
 | 
				
			||||||
        return super().get_context_data(**kwargs)
 | 
					        return super().get_context_data(**kwargs)
 | 
				
			||||||
 | 
				
			|||||||
@ -99,6 +99,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
 | 
				
			|||||||
            "private_key_available",
 | 
					            "private_key_available",
 | 
				
			||||||
            "certificate_download_url",
 | 
					            "certificate_download_url",
 | 
				
			||||||
            "private_key_download_url",
 | 
					            "private_key_download_url",
 | 
				
			||||||
 | 
					            "managed",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        extra_kwargs = {
 | 
					        extra_kwargs = {
 | 
				
			||||||
            "key_data": {"write_only": True},
 | 
					            "key_data": {"write_only": True},
 | 
				
			||||||
@ -134,7 +135,7 @@ class CertificateKeyPairFilter(FilterSet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        model = CertificateKeyPair
 | 
					        model = CertificateKeyPair
 | 
				
			||||||
        fields = ["name"]
 | 
					        fields = ["name", "managed"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
 | 
					class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,6 @@
 | 
				
			|||||||
"""authentik crypto app config"""
 | 
					"""authentik crypto app config"""
 | 
				
			||||||
 | 
					from importlib import import_module
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.apps import AppConfig
 | 
					from django.apps import AppConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -8,3 +10,6 @@ class AuthentikCryptoConfig(AppConfig):
 | 
				
			|||||||
    name = "authentik.crypto"
 | 
					    name = "authentik.crypto"
 | 
				
			||||||
    label = "authentik_crypto"
 | 
					    label = "authentik_crypto"
 | 
				
			||||||
    verbose_name = "authentik Crypto"
 | 
					    verbose_name = "authentik Crypto"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def ready(self):
 | 
				
			||||||
 | 
					        import_module("authentik.crypto.managed")
 | 
				
			||||||
 | 
				
			|||||||
@ -24,16 +24,17 @@ class CertificateBuilder:
 | 
				
			|||||||
        self.__builder = None
 | 
					        self.__builder = None
 | 
				
			||||||
        self.__certificate = None
 | 
					        self.__certificate = None
 | 
				
			||||||
        self.common_name = "authentik Self-signed Certificate"
 | 
					        self.common_name = "authentik Self-signed Certificate"
 | 
				
			||||||
 | 
					        self.cert = CertificateKeyPair()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self) -> Optional[CertificateKeyPair]:
 | 
					    def save(self) -> Optional[CertificateKeyPair]:
 | 
				
			||||||
        """Save generated certificate as model"""
 | 
					        """Save generated certificate as model"""
 | 
				
			||||||
        if not self.__certificate:
 | 
					        if not self.__certificate:
 | 
				
			||||||
            raise ValueError("Certificated hasn't been built yet")
 | 
					            raise ValueError("Certificated hasn't been built yet")
 | 
				
			||||||
        return CertificateKeyPair.objects.create(
 | 
					        self.cert.name = self.common_name
 | 
				
			||||||
            name=self.common_name,
 | 
					        self.cert.certificate_data = self.certificate
 | 
				
			||||||
            certificate_data=self.certificate,
 | 
					        self.cert.key_data = self.private_key
 | 
				
			||||||
            key_data=self.private_key,
 | 
					        self.cert.save()
 | 
				
			||||||
        )
 | 
					        return self.cert
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def build(
 | 
					    def build(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										40
									
								
								authentik/crypto/managed.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								authentik/crypto/managed.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					"""Crypto managed objects"""
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.crypto.builder import CertificateBuilder
 | 
				
			||||||
 | 
					from authentik.crypto.models import CertificateKeyPair
 | 
				
			||||||
 | 
					from authentik.managed.manager import ObjectManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MANAGED_KEY = "goauthentik.io/crypto/jwt-managed"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CryptoManager(ObjectManager):
 | 
				
			||||||
 | 
					    """Crypto managed objects"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _create(self, cert: Optional[CertificateKeyPair] = None):
 | 
				
			||||||
 | 
					        builder = CertificateBuilder()
 | 
				
			||||||
 | 
					        builder.common_name = "goauthentik.io"
 | 
				
			||||||
 | 
					        builder.build(
 | 
				
			||||||
 | 
					            subject_alt_names=["goauthentik.io"],
 | 
				
			||||||
 | 
					            validity_days=360,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        if not cert:
 | 
				
			||||||
 | 
					            cert = CertificateKeyPair()
 | 
				
			||||||
 | 
					        cert.certificate_data = builder.certificate
 | 
				
			||||||
 | 
					        cert.key_data = builder.private_key
 | 
				
			||||||
 | 
					        cert.name = "authentik Internal JWT Certificate"
 | 
				
			||||||
 | 
					        cert.managed = MANAGED_KEY
 | 
				
			||||||
 | 
					        cert.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def reconcile(self):
 | 
				
			||||||
 | 
					        certs = CertificateKeyPair.objects.filter(managed=MANAGED_KEY)
 | 
				
			||||||
 | 
					        if not certs.exists():
 | 
				
			||||||
 | 
					            self._create()
 | 
				
			||||||
 | 
					            return []
 | 
				
			||||||
 | 
					        cert: CertificateKeyPair = certs.first()
 | 
				
			||||||
 | 
					        now = datetime.now()
 | 
				
			||||||
 | 
					        if now < cert.certificate.not_valid_before or now > cert.certificate.not_valid_after:
 | 
				
			||||||
 | 
					            self._create(cert)
 | 
				
			||||||
 | 
					            return []
 | 
				
			||||||
 | 
					        return []
 | 
				
			||||||
@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-09 17:05
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_crypto", "0002_create_self_signed_kp"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="certificatekeypair",
 | 
				
			||||||
 | 
					            name="managed",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                unique=True,
 | 
				
			||||||
 | 
					                verbose_name="Managed by authentik",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -13,9 +13,10 @@ from django.db import models
 | 
				
			|||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.lib.models import CreatedUpdatedModel
 | 
					from authentik.lib.models import CreatedUpdatedModel
 | 
				
			||||||
 | 
					from authentik.managed.models import ManagedModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CertificateKeyPair(CreatedUpdatedModel):
 | 
					class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
 | 
				
			||||||
    """CertificateKeyPair that can be used for signing or encrypting if `key_data`
 | 
					    """CertificateKeyPair that can be used for signing or encrypting if `key_data`
 | 
				
			||||||
    is set, otherwise it can be used to verify remote data."""
 | 
					    is set, otherwise it can be used to verify remote data."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,10 @@
 | 
				
			|||||||
"""NotificationTransport API Views"""
 | 
					"""NotificationTransport API Views"""
 | 
				
			||||||
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from drf_spectacular.types import OpenApiTypes
 | 
					from drf_spectacular.types import OpenApiTypes
 | 
				
			||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
					from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
 | 
					from rest_framework.exceptions import ValidationError
 | 
				
			||||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
 | 
					from rest_framework.fields import CharField, ListField, SerializerMethodField
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
@ -29,6 +32,14 @@ class NotificationTransportSerializer(ModelSerializer):
 | 
				
			|||||||
        """Return selected mode with a UI Label"""
 | 
					        """Return selected mode with a UI Label"""
 | 
				
			||||||
        return TransportMode(instance.mode).label
 | 
					        return TransportMode(instance.mode).label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
 | 
				
			||||||
 | 
					        """Ensure the required fields are set."""
 | 
				
			||||||
 | 
					        mode = attrs.get("mode")
 | 
				
			||||||
 | 
					        if mode in [TransportMode.WEBHOOK, TransportMode.WEBHOOK_SLACK]:
 | 
				
			||||||
 | 
					            if "webhook_url" not in attrs or attrs.get("webhook_url", "") == "":
 | 
				
			||||||
 | 
					                raise ValidationError("Webhook URL may not be empty.")
 | 
				
			||||||
 | 
					        return attrs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = NotificationTransport
 | 
					        model = NotificationTransport
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,831 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-10 16:01
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import uuid
 | 
				
			||||||
 | 
					from datetime import timedelta
 | 
				
			||||||
 | 
					from typing import Iterable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.core.validators
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.apps.registry import Apps
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import authentik.events.models
 | 
				
			||||||
 | 
					from authentik.events.models import EventAction, NotificationSeverity, TransportMode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    Event = apps.get_model("authentik_events", "Event")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    for event in Event.objects.all():
 | 
				
			||||||
 | 
					        event.delete()
 | 
				
			||||||
 | 
					        # Because event objects cannot be updated, we have to re-create them
 | 
				
			||||||
 | 
					        event.pk = None
 | 
				
			||||||
 | 
					        event.user_json = authentik.events.models.get_user(event.user) if event.user else {}
 | 
				
			||||||
 | 
					        event._state.adding = True
 | 
				
			||||||
 | 
					        event.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def notify_configuration_error(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Group = apps.get_model("authentik_core", "Group")
 | 
				
			||||||
 | 
					    PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
 | 
				
			||||||
 | 
					    EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
 | 
				
			||||||
 | 
					    NotificationRule = apps.get_model("authentik_events", "NotificationRule")
 | 
				
			||||||
 | 
					    NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    admin_group = (
 | 
				
			||||||
 | 
					        Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-match-configuration-error",
 | 
				
			||||||
 | 
					        defaults={"action": EventAction.CONFIGURATION_ERROR},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-notify-configuration-error",
 | 
				
			||||||
 | 
					        defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.transports.set(
 | 
				
			||||||
 | 
					        NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.save()
 | 
				
			||||||
 | 
					    PolicyBinding.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        target=trigger,
 | 
				
			||||||
 | 
					        policy=policy,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            "order": 0,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def notify_update(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Group = apps.get_model("authentik_core", "Group")
 | 
				
			||||||
 | 
					    PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
 | 
				
			||||||
 | 
					    EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
 | 
				
			||||||
 | 
					    NotificationRule = apps.get_model("authentik_events", "NotificationRule")
 | 
				
			||||||
 | 
					    NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    admin_group = (
 | 
				
			||||||
 | 
					        Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-match-update",
 | 
				
			||||||
 | 
					        defaults={"action": EventAction.UPDATE_AVAILABLE},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-notify-update",
 | 
				
			||||||
 | 
					        defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.transports.set(
 | 
				
			||||||
 | 
					        NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.save()
 | 
				
			||||||
 | 
					    PolicyBinding.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        target=trigger,
 | 
				
			||||||
 | 
					        policy=policy,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            "order": 0,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def notify_exception(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Group = apps.get_model("authentik_core", "Group")
 | 
				
			||||||
 | 
					    PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding")
 | 
				
			||||||
 | 
					    EventMatcherPolicy = apps.get_model("authentik_policies_event_matcher", "EventMatcherPolicy")
 | 
				
			||||||
 | 
					    NotificationRule = apps.get_model("authentik_events", "NotificationRule")
 | 
				
			||||||
 | 
					    NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    admin_group = (
 | 
				
			||||||
 | 
					        Group.objects.using(db_alias).filter(name="authentik Admins", is_superuser=True).first()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    policy_policy_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-match-policy-exception",
 | 
				
			||||||
 | 
					        defaults={"action": EventAction.POLICY_EXCEPTION},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    policy_pm_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-match-property-mapping-exception",
 | 
				
			||||||
 | 
					        defaults={"action": EventAction.PROPERTY_MAPPING_EXCEPTION},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger, _ = NotificationRule.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-notify-exception",
 | 
				
			||||||
 | 
					        defaults={"group": admin_group, "severity": NotificationSeverity.ALERT},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.transports.set(
 | 
				
			||||||
 | 
					        NotificationTransport.objects.using(db_alias).filter(name="default-email-transport")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    trigger.save()
 | 
				
			||||||
 | 
					    PolicyBinding.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        target=trigger,
 | 
				
			||||||
 | 
					        policy=policy_policy_exc,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            "order": 0,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    PolicyBinding.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        target=trigger,
 | 
				
			||||||
 | 
					        policy=policy_pm_exc,
 | 
				
			||||||
 | 
					        defaults={
 | 
				
			||||||
 | 
					            "order": 1,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def transport_email_global(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    NotificationTransport = apps.get_model("authentik_events", "NotificationTransport")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    NotificationTransport.objects.using(db_alias).update_or_create(
 | 
				
			||||||
 | 
					        name="default-email-transport",
 | 
				
			||||||
 | 
					        defaults={"mode": TransportMode.EMAIL},
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def token_view_to_secret_view(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    from authentik.events.models import EventAction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Event = apps.get_model("authentik_events", "Event")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    events = Event.objects.using(db_alias).filter(action="token_view")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for event in events:
 | 
				
			||||||
 | 
					        event.context["secret"] = event.context.pop("token")
 | 
				
			||||||
 | 
					        event.action = EventAction.SECRET_VIEW
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Event.objects.using(db_alias).bulk_update(events, ["context", "action"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Taken from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
 | 
				
			||||||
 | 
					def progress_bar(
 | 
				
			||||||
 | 
					    iterable: Iterable,
 | 
				
			||||||
 | 
					    prefix="Writing: ",
 | 
				
			||||||
 | 
					    suffix=" finished",
 | 
				
			||||||
 | 
					    decimals=1,
 | 
				
			||||||
 | 
					    length=100,
 | 
				
			||||||
 | 
					    fill="█",
 | 
				
			||||||
 | 
					    print_end="\r",
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Call in a loop to create terminal progress bar
 | 
				
			||||||
 | 
					    @params:
 | 
				
			||||||
 | 
					        iteration   - Required  : current iteration (Int)
 | 
				
			||||||
 | 
					        total       - Required  : total iterations (Int)
 | 
				
			||||||
 | 
					        prefix      - Optional  : prefix string (Str)
 | 
				
			||||||
 | 
					        suffix      - Optional  : suffix string (Str)
 | 
				
			||||||
 | 
					        decimals    - Optional  : positive number of decimals in percent complete (Int)
 | 
				
			||||||
 | 
					        length      - Optional  : character length of bar (Int)
 | 
				
			||||||
 | 
					        fill        - Optional  : bar fill character (Str)
 | 
				
			||||||
 | 
					        print_end   - Optional  : end character (e.g. "\r", "\r\n") (Str)
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    total = len(iterable)
 | 
				
			||||||
 | 
					    if total < 1:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def print_progress_bar(iteration):
 | 
				
			||||||
 | 
					        """Progress Bar Printing Function"""
 | 
				
			||||||
 | 
					        percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
 | 
				
			||||||
 | 
					        filledLength = int(length * iteration // total)
 | 
				
			||||||
 | 
					        bar = fill * filledLength + "-" * (length - filledLength)
 | 
				
			||||||
 | 
					        print(f"\r{prefix} |{bar}| {percent}% {suffix}", end=print_end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Initial Call
 | 
				
			||||||
 | 
					    print_progress_bar(0)
 | 
				
			||||||
 | 
					    # Update Progress Bar
 | 
				
			||||||
 | 
					    for i, item in enumerate(iterable):
 | 
				
			||||||
 | 
					        yield item
 | 
				
			||||||
 | 
					        print_progress_bar(i + 1)
 | 
				
			||||||
 | 
					    # Print New Line on Complete
 | 
				
			||||||
 | 
					    print()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Event = apps.get_model("authentik_events", "event")
 | 
				
			||||||
 | 
					    all_events = Event.objects.using(db_alias).all()
 | 
				
			||||||
 | 
					    if all_events.count() < 1:
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print("\nAdding expiry to events, this might take a couple of minutes...")
 | 
				
			||||||
 | 
					    for event in progress_bar(all_events):
 | 
				
			||||||
 | 
					        event.expires = event.created + timedelta(days=365)
 | 
				
			||||||
 | 
					        event.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    replaces = [
 | 
				
			||||||
 | 
					        ("authentik_events", "0001_initial"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0002_auto_20200918_2116"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0003_auto_20200917_1155"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0004_auto_20200921_1829"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0005_auto_20201005_2139"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0006_auto_20201017_2024"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0007_auto_20201215_0939"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0008_auto_20201220_1651"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0009_auto_20201227_1210"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0010_notification_notificationtransport_notificationrule"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0011_notification_rules_default_v1"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0012_auto_20210202_1821"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0013_auto_20210209_1657"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0014_expiry"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0015_alter_event_action"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0016_add_tenant"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0017_alter_event_action"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0018_auto_20210911_2217"),
 | 
				
			||||||
 | 
					        ("authentik_events", "0019_alter_notificationtransport_webhook_url"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_policies", "0004_policy_execution_logging"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0016_auto_20201202_2234"),
 | 
				
			||||||
 | 
					        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0028_alter_token_intent"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="Event",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "event_uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "action",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("LOGIN", "login"),
 | 
				
			||||||
 | 
					                            ("LOGIN_FAILED", "login_failed"),
 | 
				
			||||||
 | 
					                            ("LOGOUT", "logout"),
 | 
				
			||||||
 | 
					                            ("AUTHORIZE_APPLICATION", "authorize_application"),
 | 
				
			||||||
 | 
					                            ("SUSPICIOUS_REQUEST", "suspicious_request"),
 | 
				
			||||||
 | 
					                            ("SIGN_UP", "sign_up"),
 | 
				
			||||||
 | 
					                            ("PASSWORD_RESET", "password_reset"),
 | 
				
			||||||
 | 
					                            ("INVITE_CREATED", "invitation_created"),
 | 
				
			||||||
 | 
					                            ("INVITE_USED", "invitation_used"),
 | 
				
			||||||
 | 
					                            ("IMPERSONATION_STARTED", "impersonation_started"),
 | 
				
			||||||
 | 
					                            ("IMPERSONATION_ENDED", "impersonation_ended"),
 | 
				
			||||||
 | 
					                            ("CUSTOM", "custom"),
 | 
				
			||||||
 | 
					                        ]
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("date", models.DateTimeField(auto_now_add=True)),
 | 
				
			||||||
 | 
					                ("app", models.TextField()),
 | 
				
			||||||
 | 
					                ("context", models.JSONField(blank=True, default=dict)),
 | 
				
			||||||
 | 
					                ("client_ip", models.GenericIPAddressField(null=True)),
 | 
				
			||||||
 | 
					                ("created", models.DateTimeField(auto_now_add=True)),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "user",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        null=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.SET_NULL,
 | 
				
			||||||
 | 
					                        to=settings.AUTH_USER_MODEL,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("user_json", models.JSONField(default=dict)),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Event",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Events",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=convert_user_to_json,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="user",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RenameField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            old_name="user_json",
 | 
				
			||||||
 | 
					            new_name="user",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("sign_up", "Sign Up"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("invitation_created", "Invite Created"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("invitation_created", "Invite Created"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="date",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("token_view", "Token View"),
 | 
				
			||||||
 | 
					                    ("invitation_created", "Invite Created"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("token_view", "Token View"),
 | 
				
			||||||
 | 
					                    ("invitation_created", "Invite Created"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("token_view", "Token View"),
 | 
				
			||||||
 | 
					                    ("invitation_created", "Invite Created"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("token_view", "Token View"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="NotificationTransport",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("name", models.TextField(unique=True)),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "mode",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("webhook", "Generic Webhook"),
 | 
				
			||||||
 | 
					                            ("webhook_slack", "Slack Webhook (Slack/Discord)"),
 | 
				
			||||||
 | 
					                            ("email", "Email"),
 | 
				
			||||||
 | 
					                        ]
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("webhook_url", models.TextField(blank=True)),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Notification Transport",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Notification Transports",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="NotificationRule",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "policybindingmodel_ptr",
 | 
				
			||||||
 | 
					                    models.OneToOneField(
 | 
				
			||||||
 | 
					                        auto_created=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        parent_link=True,
 | 
				
			||||||
 | 
					                        primary_key=True,
 | 
				
			||||||
 | 
					                        serialize=False,
 | 
				
			||||||
 | 
					                        to="authentik_policies.policybindingmodel",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("name", models.TextField(unique=True)),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "severity",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        choices=[("notice", "Notice"), ("warning", "Warning"), ("alert", "Alert")],
 | 
				
			||||||
 | 
					                        default="notice",
 | 
				
			||||||
 | 
					                        help_text="Controls which severity level the created notifications will have.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "group",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        blank=True,
 | 
				
			||||||
 | 
					                        help_text="Define which group of users this notification should be sent and shown to. If left empty, Notification won't ben sent.",
 | 
				
			||||||
 | 
					                        null=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.SET_NULL,
 | 
				
			||||||
 | 
					                        to="authentik_core.group",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "transports",
 | 
				
			||||||
 | 
					                    models.ManyToManyField(
 | 
				
			||||||
 | 
					                        help_text="Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.",
 | 
				
			||||||
 | 
					                        to="authentik_events.NotificationTransport",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Notification Rule",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Notification Rules",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            bases=("authentik_policies.policybindingmodel",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="Notification",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "severity",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        choices=[("notice", "Notice"), ("warning", "Warning"), ("alert", "Alert")]
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("body", models.TextField()),
 | 
				
			||||||
 | 
					                ("created", models.DateTimeField(auto_now_add=True)),
 | 
				
			||||||
 | 
					                ("seen", models.BooleanField(default=False)),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "event",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        blank=True,
 | 
				
			||||||
 | 
					                        null=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.SET_NULL,
 | 
				
			||||||
 | 
					                        to="authentik_events.event",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "user",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Notification",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Notifications",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=transport_email_global,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=notify_configuration_error,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=notify_update,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=notify_exception,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="notificationtransport",
 | 
				
			||||||
 | 
					            name="send_once",
 | 
				
			||||||
 | 
					            field=models.BooleanField(
 | 
				
			||||||
 | 
					                default=False,
 | 
				
			||||||
 | 
					                help_text="Only send notification once, for example when sending a webhook into a chat channel.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("token_view", "Token View"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("system_task_execution", "System Task Execution"),
 | 
				
			||||||
 | 
					                    ("system_task_exception", "System Task Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("secret_view", "Secret View"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("system_task_execution", "System Task Execution"),
 | 
				
			||||||
 | 
					                    ("system_task_exception", "System Task Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=token_view_to_secret_view,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="expires",
 | 
				
			||||||
 | 
					            field=models.DateTimeField(default=authentik.events.models.default_event_duration),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="expiring",
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=update_expires,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("secret_view", "Secret View"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("system_task_execution", "System Task Execution"),
 | 
				
			||||||
 | 
					                    ("system_task_exception", "System Task Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("email_sent", "Email Sent"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="tenant",
 | 
				
			||||||
 | 
					            field=models.JSONField(blank=True, default=authentik.events.models.default_tenant),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("secret_view", "Secret View"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("system_task_execution", "System Task Execution"),
 | 
				
			||||||
 | 
					                    ("system_task_exception", "System Task Exception"),
 | 
				
			||||||
 | 
					                    ("system_exception", "System Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("email_sent", "Email Sent"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="event",
 | 
				
			||||||
 | 
					            name="action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("login", "Login"),
 | 
				
			||||||
 | 
					                    ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                    ("logout", "Logout"),
 | 
				
			||||||
 | 
					                    ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                    ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                    ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                    ("secret_view", "Secret View"),
 | 
				
			||||||
 | 
					                    ("secret_rotate", "Secret Rotate"),
 | 
				
			||||||
 | 
					                    ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                    ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                    ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                    ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                    ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                    ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                    ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                    ("system_task_execution", "System Task Execution"),
 | 
				
			||||||
 | 
					                    ("system_task_exception", "System Task Exception"),
 | 
				
			||||||
 | 
					                    ("system_exception", "System Exception"),
 | 
				
			||||||
 | 
					                    ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                    ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                    ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                    ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                    ("email_sent", "Email Sent"),
 | 
				
			||||||
 | 
					                    ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                    ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="NotificationWebhookMapping",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "propertymapping_ptr",
 | 
				
			||||||
 | 
					                    models.OneToOneField(
 | 
				
			||||||
 | 
					                        auto_created=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        parent_link=True,
 | 
				
			||||||
 | 
					                        primary_key=True,
 | 
				
			||||||
 | 
					                        serialize=False,
 | 
				
			||||||
 | 
					                        to="authentik_core.propertymapping",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Notification Webhook Mapping",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Notification Webhook Mappings",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            bases=("authentik_core.propertymapping",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="notificationtransport",
 | 
				
			||||||
 | 
					            name="webhook_mapping",
 | 
				
			||||||
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.SET_DEFAULT,
 | 
				
			||||||
 | 
					                to="authentik_events.notificationwebhookmapping",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="notificationtransport",
 | 
				
			||||||
 | 
					            name="webhook_url",
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.7 on 2021-10-04 15:31
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.core.validators
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_events", "0018_auto_20210911_2217"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="notificationtransport",
 | 
				
			||||||
 | 
					            name="webhook_url",
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Optional, Type, Union
 | 
				
			|||||||
from uuid import uuid4
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.core.validators import URLValidator
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from django.http.request import QueryDict
 | 
					from django.http.request import QueryDict
 | 
				
			||||||
@ -223,7 +224,7 @@ class NotificationTransport(models.Model):
 | 
				
			|||||||
    name = models.TextField(unique=True)
 | 
					    name = models.TextField(unique=True)
 | 
				
			||||||
    mode = models.TextField(choices=TransportMode.choices)
 | 
					    mode = models.TextField(choices=TransportMode.choices)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    webhook_url = models.TextField(blank=True)
 | 
					    webhook_url = models.TextField(blank=True, validators=[URLValidator()])
 | 
				
			||||||
    webhook_mapping = models.ForeignKey(
 | 
					    webhook_mapping = models.ForeignKey(
 | 
				
			||||||
        "NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
 | 
					        "NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,9 @@ from typing import Any, Optional
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from celery import Task
 | 
					from celery import Task
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from prometheus_client import Gauge
 | 
					from prometheus_client import Gauge
 | 
				
			||||||
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.lib.utils.errors import exception_to_string
 | 
					from authentik.lib.utils.errors import exception_to_string
 | 
				
			||||||
@ -18,6 +20,8 @@ GAUGE_TASKS = Gauge(
 | 
				
			|||||||
    ["task_name", "task_uid", "status"],
 | 
					    ["task_name", "task_uid", "status"],
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TaskResultStatus(Enum):
 | 
					class TaskResultStatus(Enum):
 | 
				
			||||||
    """Possible states of tasks"""
 | 
					    """Possible states of tasks"""
 | 
				
			||||||
@ -25,6 +29,7 @@ class TaskResultStatus(Enum):
 | 
				
			|||||||
    SUCCESSFUL = 1
 | 
					    SUCCESSFUL = 1
 | 
				
			||||||
    WARNING = 2
 | 
					    WARNING = 2
 | 
				
			||||||
    ERROR = 4
 | 
					    ERROR = 4
 | 
				
			||||||
 | 
					    UNKNOWN = 8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
@ -76,7 +81,7 @@ class TaskInfo:
 | 
				
			|||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def by_name(name: str) -> Optional["TaskInfo"]:
 | 
					    def by_name(name: str) -> Optional["TaskInfo"]:
 | 
				
			||||||
        """Get TaskInfo Object by name"""
 | 
					        """Get TaskInfo Object by name"""
 | 
				
			||||||
        return cache.get(f"task_{name}")
 | 
					        return cache.get(f"task_{name}", None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self):
 | 
					    def delete(self):
 | 
				
			||||||
        """Delete task info from cache"""
 | 
					        """Delete task info from cache"""
 | 
				
			||||||
@ -107,6 +112,30 @@ class TaskInfo:
 | 
				
			|||||||
        cache.set(key, self, timeout=timeout_hours * 60 * 60)
 | 
					        cache.set(key, self, timeout=timeout_hours * 60 * 60)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def prefill_task():
 | 
				
			||||||
 | 
					    """Ensure a task's details are always in cache, so it can always be triggered via API"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def inner_wrap(func):
 | 
				
			||||||
 | 
					        status = TaskInfo.by_name(func.__name__)
 | 
				
			||||||
 | 
					        if status:
 | 
				
			||||||
 | 
					            return func
 | 
				
			||||||
 | 
					        TaskInfo(
 | 
				
			||||||
 | 
					            task_name=func.__name__,
 | 
				
			||||||
 | 
					            task_description=func.__doc__,
 | 
				
			||||||
 | 
					            result=TaskResult(TaskResultStatus.UNKNOWN, messages=[_("Task has not been run yet.")]),
 | 
				
			||||||
 | 
					            task_call_module=func.__module__,
 | 
				
			||||||
 | 
					            task_call_func=func.__name__,
 | 
				
			||||||
 | 
					            # We don't have real values for these attributes but they cannot be null
 | 
				
			||||||
 | 
					            start_timestamp=default_timer(),
 | 
				
			||||||
 | 
					            finish_timestamp=default_timer(),
 | 
				
			||||||
 | 
					            finish_time=datetime.now(),
 | 
				
			||||||
 | 
					        ).save(86400)
 | 
				
			||||||
 | 
					        LOGGER.debug("prefilled task", task_name=func.__name__)
 | 
				
			||||||
 | 
					        return func
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return inner_wrap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MonitoredTask(Task):
 | 
					class MonitoredTask(Task):
 | 
				
			||||||
    """Task which can save its state to the cache"""
 | 
					    """Task which can save its state to the cache"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,7 @@ from authentik.core.signals import password_changed
 | 
				
			|||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.events.tasks import event_notification_handler
 | 
					from authentik.events.tasks import event_notification_handler
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
 | 
					from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
 | 
				
			||||||
from authentik.flows.views import SESSION_KEY_PLAN
 | 
					from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
				
			||||||
from authentik.stages.invitation.models import Invitation
 | 
					from authentik.stages.invitation.models import Invitation
 | 
				
			||||||
from authentik.stages.invitation.signals import invitation_used
 | 
					from authentik.stages.invitation.signals import invitation_used
 | 
				
			||||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 | 
					from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
 | 
				
			||||||
 | 
				
			|||||||
@ -98,7 +98,9 @@ def notification_transport(self: MonitoredTask, notification_pk: int, transport_
 | 
				
			|||||||
        notification: Notification = Notification.objects.filter(pk=notification_pk).first()
 | 
					        notification: Notification = Notification.objects.filter(pk=notification_pk).first()
 | 
				
			||||||
        if not notification:
 | 
					        if not notification:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        transport: NotificationTransport = NotificationTransport.objects.get(pk=transport_pk)
 | 
					        transport = NotificationTransport.objects.filter(pk=transport_pk).first()
 | 
				
			||||||
 | 
					        if not transport:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
        transport.send(notification)
 | 
					        transport.send(notification)
 | 
				
			||||||
        self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
 | 
					        self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
 | 
				
			||||||
    except NotificationTransportError as exc:
 | 
					    except NotificationTransportError as exc:
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,13 @@ from django.urls import reverse
 | 
				
			|||||||
from rest_framework.test import APITestCase
 | 
					from rest_framework.test import APITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.events.models import Event, EventAction, Notification, NotificationSeverity
 | 
					from authentik.events.models import (
 | 
				
			||||||
 | 
					    Event,
 | 
				
			||||||
 | 
					    EventAction,
 | 
				
			||||||
 | 
					    Notification,
 | 
				
			||||||
 | 
					    NotificationSeverity,
 | 
				
			||||||
 | 
					    TransportMode,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestEventsAPI(APITestCase):
 | 
					class TestEventsAPI(APITestCase):
 | 
				
			||||||
@ -41,3 +47,23 @@ class TestEventsAPI(APITestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        notification.refresh_from_db()
 | 
					        notification.refresh_from_db()
 | 
				
			||||||
        self.assertTrue(notification.seen)
 | 
					        self.assertTrue(notification.seen)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_transport(self):
 | 
				
			||||||
 | 
					        """Test transport API"""
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_api:notificationtransport-list"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "name": "foo-with",
 | 
				
			||||||
 | 
					                "mode": TransportMode.WEBHOOK,
 | 
				
			||||||
 | 
					                "webhook_url": "http://foo.com",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 201)
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_api:notificationtransport-list"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "name": "foo-without",
 | 
				
			||||||
 | 
					                "mode": TransportMode.WEBHOOK,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
				
			|||||||
@ -77,7 +77,7 @@ def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:
 | 
				
			|||||||
    final_dict = {}
 | 
					    final_dict = {}
 | 
				
			||||||
    for key, value in source.items():
 | 
					    for key, value in source.items():
 | 
				
			||||||
        if is_dataclass(value):
 | 
					        if is_dataclass(value):
 | 
				
			||||||
            # Because asdict calls `copy.deepcopy(obj)` on everything thats not tuple/dict,
 | 
					            # Because asdict calls `copy.deepcopy(obj)` on everything that's not tuple/dict,
 | 
				
			||||||
            # and deepcopy doesn't work with HttpRequests (neither django nor rest_framework).
 | 
					            # and deepcopy doesn't work with HttpRequests (neither django nor rest_framework).
 | 
				
			||||||
            # Currently, the only dataclass that actually holds an http request is a PolicyRequest
 | 
					            # Currently, the only dataclass that actually holds an http request is a PolicyRequest
 | 
				
			||||||
            if isinstance(value, PolicyRequest):
 | 
					            if isinstance(value, PolicyRequest):
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cach
 | 
				
			|||||||
from authentik.flows.transfer.common import DataclassEncoder
 | 
					from authentik.flows.transfer.common import DataclassEncoder
 | 
				
			||||||
from authentik.flows.transfer.exporter import FlowExporter
 | 
					from authentik.flows.transfer.exporter import FlowExporter
 | 
				
			||||||
from authentik.flows.transfer.importer import FlowImporter
 | 
					from authentik.flows.transfer.importer import FlowImporter
 | 
				
			||||||
from authentik.flows.views import SESSION_KEY_PLAN
 | 
					from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
 | 
				
			||||||
from authentik.lib.views import bad_request_message
 | 
					from authentik.lib.views import bad_request_message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
@ -108,6 +108,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
    queryset = Flow.objects.all()
 | 
					    queryset = Flow.objects.all()
 | 
				
			||||||
    serializer_class = FlowSerializer
 | 
					    serializer_class = FlowSerializer
 | 
				
			||||||
    lookup_field = "slug"
 | 
					    lookup_field = "slug"
 | 
				
			||||||
 | 
					    ordering = ["slug", "name"]
 | 
				
			||||||
    search_fields = ["name", "slug", "designation", "title"]
 | 
					    search_fields = ["name", "slug", "designation", "title"]
 | 
				
			||||||
    filterset_fields = ["flow_uuid", "name", "slug", "designation"]
 | 
					    filterset_fields = ["flow_uuid", "name", "slug", "designation"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -333,6 +334,9 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
    # pylint: disable=unused-argument
 | 
					    # pylint: disable=unused-argument
 | 
				
			||||||
    def execute(self, request: Request, slug: str):
 | 
					    def execute(self, request: Request, slug: str):
 | 
				
			||||||
        """Execute flow for current user"""
 | 
					        """Execute flow for current user"""
 | 
				
			||||||
 | 
					        # Because we pre-plan the flow here, and not in the planner, we need to manually clear
 | 
				
			||||||
 | 
					        # the history of the inspector
 | 
				
			||||||
 | 
					        request.session[SESSION_KEY_HISTORY] = []
 | 
				
			||||||
        flow: Flow = self.get_object()
 | 
					        flow: Flow = self.get_object()
 | 
				
			||||||
        planner = FlowPlanner(flow)
 | 
					        planner = FlowPlanner(flow)
 | 
				
			||||||
        planner.use_cache = False
 | 
					        planner.use_cache = False
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,180 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-10 16:08
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import uuid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    replaces = [
 | 
				
			||||||
 | 
					        ("authentik_flows", "0001_initial"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0003_auto_20200523_1133"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0006_auto_20200629_0857"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0007_auto_20200703_2059"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_policies", "0001_initial"),
 | 
				
			||||||
 | 
					        ("authentik_policies", "0002_auto_20200528_1647"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="Flow",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "flow_uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("name", models.TextField()),
 | 
				
			||||||
 | 
					                ("slug", models.SlugField(unique=True)),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "designation",
 | 
				
			||||||
 | 
					                    models.CharField(
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("authentication", "Authentication"),
 | 
				
			||||||
 | 
					                            ("invalidation", "Invalidation"),
 | 
				
			||||||
 | 
					                            ("enrollment", "Enrollment"),
 | 
				
			||||||
 | 
					                            ("unenrollment", "Unrenollment"),
 | 
				
			||||||
 | 
					                            ("recovery", "Recovery"),
 | 
				
			||||||
 | 
					                            ("password_change", "Password Change"),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        max_length=100,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "pbm",
 | 
				
			||||||
 | 
					                    models.OneToOneField(
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        parent_link=True,
 | 
				
			||||||
 | 
					                        related_name="+",
 | 
				
			||||||
 | 
					                        to="authentik_policies.policybindingmodel",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Flow",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Flows",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            bases=("authentik_policies.policybindingmodel",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="Stage",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "stage_uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("name", models.TextField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="FlowStageBinding",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "policybindingmodel_ptr",
 | 
				
			||||||
 | 
					                    models.OneToOneField(
 | 
				
			||||||
 | 
					                        auto_created=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        parent_link=True,
 | 
				
			||||||
 | 
					                        to="authentik_policies.policybindingmodel",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "fsb_uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "re_evaluate_policies",
 | 
				
			||||||
 | 
					                    models.BooleanField(
 | 
				
			||||||
 | 
					                        default=False,
 | 
				
			||||||
 | 
					                        help_text="When this option is enabled, the planner will re-evaluate policies bound to this.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("order", models.IntegerField()),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "target",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.flow"
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "stage",
 | 
				
			||||||
 | 
					                    models.ForeignKey(
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.stage"
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Flow Stage Binding",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Flow Stage Bindings",
 | 
				
			||||||
 | 
					                "ordering": ["order", "target"],
 | 
				
			||||||
 | 
					                "unique_together": {("target", "stage", "order")},
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            bases=("authentik_policies.policybindingmodel",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="stages",
 | 
				
			||||||
 | 
					            field=models.ManyToManyField(
 | 
				
			||||||
 | 
					                blank=True, through="authentik_flows.FlowStageBinding", to="authentik_flows.Stage"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="designation",
 | 
				
			||||||
 | 
					            field=models.CharField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("authentication", "Authentication"),
 | 
				
			||||||
 | 
					                    ("authorization", "Authorization"),
 | 
				
			||||||
 | 
					                    ("invalidation", "Invalidation"),
 | 
				
			||||||
 | 
					                    ("enrollment", "Enrollment"),
 | 
				
			||||||
 | 
					                    ("unenrollment", "Unrenollment"),
 | 
				
			||||||
 | 
					                    ("recovery", "Recovery"),
 | 
				
			||||||
 | 
					                    ("password_change", "Password Change"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                max_length=100,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="designation",
 | 
				
			||||||
 | 
					            field=models.CharField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("authentication", "Authentication"),
 | 
				
			||||||
 | 
					                    ("authorization", "Authorization"),
 | 
				
			||||||
 | 
					                    ("invalidation", "Invalidation"),
 | 
				
			||||||
 | 
					                    ("enrollment", "Enrollment"),
 | 
				
			||||||
 | 
					                    ("unenrollment", "Unrenollment"),
 | 
				
			||||||
 | 
					                    ("recovery", "Recovery"),
 | 
				
			||||||
 | 
					                    ("stage_setup", "Stage Setup"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                max_length=100,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RenameField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            old_name="pbm",
 | 
				
			||||||
 | 
					            new_name="policybindingmodel_ptr",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="policybindingmodel_ptr",
 | 
				
			||||||
 | 
					            field=models.OneToOneField(
 | 
				
			||||||
 | 
					                auto_created=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                parent_link=True,
 | 
				
			||||||
 | 
					                to="authentik_policies.policybindingmodel",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,171 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-10 16:08
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.apps.registry import Apps
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import authentik.lib.models
 | 
				
			||||||
 | 
					from authentik.flows.models import FlowDesignation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def update_flow_designation(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    Flow = apps.get_model("authentik_flows", "Flow")
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for flow in Flow.objects.using(db_alias).all():
 | 
				
			||||||
 | 
					        if flow.designation == "stage_setup":
 | 
				
			||||||
 | 
					            flow.designation = FlowDesignation.STAGE_CONFIGURATION
 | 
				
			||||||
 | 
					            flow.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# First stage for default-source-enrollment flow (prompt stage)
 | 
				
			||||||
 | 
					# needs to have its policy re-evaluated
 | 
				
			||||||
 | 
					def update_default_source_enrollment_flow_binding(
 | 
				
			||||||
 | 
					    apps: Apps, schema_editor: BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    Flow = apps.get_model("authentik_flows", "Flow")
 | 
				
			||||||
 | 
					    FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    flows = Flow.objects.using(db_alias).filter(slug="default-source-enrollment")
 | 
				
			||||||
 | 
					    if not flows.exists():
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    flow = flows.first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    binding = FlowStageBinding.objects.get(target=flow, order=0)
 | 
				
			||||||
 | 
					    binding.re_evaluate_policies = True
 | 
				
			||||||
 | 
					    binding.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    replaces = [
 | 
				
			||||||
 | 
					        ("authentik_flows", "0012_auto_20200908_1542"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0013_auto_20200924_1605"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0014_auto_20200925_2332"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0015_flowstagebinding_evaluate_on_plan"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0016_auto_20201202_1307"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0017_auto_20210329_1334"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_flows", "0011_flow_title"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flowstagebinding",
 | 
				
			||||||
 | 
					            name="stage",
 | 
				
			||||||
 | 
					            field=authentik.lib.models.InheritanceForeignKey(
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.CASCADE, to="authentik_flows.stage"
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="stage",
 | 
				
			||||||
 | 
					            name="name",
 | 
				
			||||||
 | 
					            field=models.TextField(unique=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="designation",
 | 
				
			||||||
 | 
					            field=models.CharField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("authentication", "Authentication"),
 | 
				
			||||||
 | 
					                    ("authorization", "Authorization"),
 | 
				
			||||||
 | 
					                    ("invalidation", "Invalidation"),
 | 
				
			||||||
 | 
					                    ("enrollment", "Enrollment"),
 | 
				
			||||||
 | 
					                    ("unenrollment", "Unrenollment"),
 | 
				
			||||||
 | 
					                    ("recovery", "Recovery"),
 | 
				
			||||||
 | 
					                    ("stage_configuration", "Stage Configuration"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                max_length=100,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=update_flow_designation,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="flowstagebinding",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "ordering": ["target", "order"],
 | 
				
			||||||
 | 
					                "verbose_name": "Flow Stage Binding",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Flow Stage Bindings",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flowstagebinding",
 | 
				
			||||||
 | 
					            name="re_evaluate_policies",
 | 
				
			||||||
 | 
					            field=models.BooleanField(
 | 
				
			||||||
 | 
					                default=False,
 | 
				
			||||||
 | 
					                help_text="When this option is enabled, the planner will re-evaluate policies bound to this binding.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=update_default_source_enrollment_flow_binding,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flowstagebinding",
 | 
				
			||||||
 | 
					            name="re_evaluate_policies",
 | 
				
			||||||
 | 
					            field=models.BooleanField(
 | 
				
			||||||
 | 
					                default=False, help_text="Evaluate policies when the Stage is present to the user."
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="flowstagebinding",
 | 
				
			||||||
 | 
					            name="evaluate_on_plan",
 | 
				
			||||||
 | 
					            field=models.BooleanField(
 | 
				
			||||||
 | 
					                default=True,
 | 
				
			||||||
 | 
					                help_text="Evaluate policies during the Flow planning process. Disable this for input-based policies.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="background",
 | 
				
			||||||
 | 
					            field=models.FileField(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                default="../static/dist/assets/images/flow_background.jpg",
 | 
				
			||||||
 | 
					                help_text="Background shown during execution",
 | 
				
			||||||
 | 
					                upload_to="flow-backgrounds/",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="designation",
 | 
				
			||||||
 | 
					            field=models.CharField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("authentication", "Authentication"),
 | 
				
			||||||
 | 
					                    ("authorization", "Authorization"),
 | 
				
			||||||
 | 
					                    ("invalidation", "Invalidation"),
 | 
				
			||||||
 | 
					                    ("enrollment", "Enrollment"),
 | 
				
			||||||
 | 
					                    ("unenrollment", "Unrenollment"),
 | 
				
			||||||
 | 
					                    ("recovery", "Recovery"),
 | 
				
			||||||
 | 
					                    ("stage_configuration", "Stage Configuration"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                help_text="Decides what this Flow is used for. For example, the Authentication flow is redirect to when an un-authenticated user visits authentik.",
 | 
				
			||||||
 | 
					                max_length=100,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="slug",
 | 
				
			||||||
 | 
					            field=models.SlugField(help_text="Visible in the URL.", unique=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="title",
 | 
				
			||||||
 | 
					            field=models.TextField(help_text="Shown as the Title in Flow pages."),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="flow",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "permissions": [
 | 
				
			||||||
 | 
					                    ("export_flow", "Can export a Flow"),
 | 
				
			||||||
 | 
					                    ("view_flow_cache", "View Flow's cache metrics"),
 | 
				
			||||||
 | 
					                    ("clear_flow_cache", "Clear Flow's cache metrics"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                "verbose_name": "Flow",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Flows",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-10 16:10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    replaces = [
 | 
				
			||||||
 | 
					        ("authentik_flows", "0019_alter_flow_background"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0020_flow_compatibility_mode"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0021_flowstagebinding_invalid_response_action"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0022_alter_flowstagebinding_invalid_response_action"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0023_alter_flow_background"),
 | 
				
			||||||
 | 
					        ("authentik_flows", "0024_alter_flow_compatibility_mode"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_flows", "0018_oob_flows"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="background",
 | 
				
			||||||
 | 
					            field=models.FileField(
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Background shown during execution",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                upload_to="flow-backgrounds/",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="flowstagebinding",
 | 
				
			||||||
 | 
					            name="invalid_response_action",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("retry", "Retry"),
 | 
				
			||||||
 | 
					                    ("restart", "Restart"),
 | 
				
			||||||
 | 
					                    ("restart_with_context", "Restart With Context"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="retry",
 | 
				
			||||||
 | 
					                help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="background",
 | 
				
			||||||
 | 
					            field=models.FileField(
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Background shown during execution",
 | 
				
			||||||
 | 
					                max_length=500,
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                upload_to="flow-backgrounds/",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="flow",
 | 
				
			||||||
 | 
					            name="compatibility_mode",
 | 
				
			||||||
 | 
					            field=models.BooleanField(
 | 
				
			||||||
 | 
					                default=False,
 | 
				
			||||||
 | 
					                help_text="Enable compatibility mode, increases compatibility with password managers on mobile devices.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -57,11 +57,11 @@ class FlowPlan:
 | 
				
			|||||||
    markers: list[StageMarker] = field(default_factory=list)
 | 
					    markers: list[StageMarker] = field(default_factory=list)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
 | 
					    def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
 | 
				
			||||||
        """Append `stage` to all stages, optionall with stage marker"""
 | 
					        """Append `stage` to all stages, optionally with stage marker"""
 | 
				
			||||||
        return self.append(FlowStageBinding(stage=stage), marker)
 | 
					        return self.append(FlowStageBinding(stage=stage), marker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
 | 
					    def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
 | 
				
			||||||
        """Append `stage` to all stages, optionall with stage marker"""
 | 
					        """Append `stage` to all stages, optionally with stage marker"""
 | 
				
			||||||
        self.bindings.append(binding)
 | 
					        self.bindings.append(binding)
 | 
				
			||||||
        self.markers.append(marker or StageMarker())
 | 
					        self.markers.append(marker or StageMarker())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
"""authentik flow signals"""
 | 
					"""authentik flow signals"""
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from django.db.models.signals import post_save
 | 
					from django.db.models.signals import post_save, pre_delete
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -15,6 +15,7 @@ def delete_cache_prefix(prefix: str) -> int:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(post_save)
 | 
					@receiver(post_save)
 | 
				
			||||||
 | 
					@receiver(pre_delete)
 | 
				
			||||||
# pylint: disable=unused-argument
 | 
					# pylint: disable=unused-argument
 | 
				
			||||||
def invalidate_flow_cache(sender, instance, **_):
 | 
					def invalidate_flow_cache(sender, instance, **_):
 | 
				
			||||||
    """Invalidate flow cache when flow is updated"""
 | 
					    """Invalidate flow cache when flow is updated"""
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@ from authentik.flows.challenge import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
from authentik.flows.models import InvalidResponseAction
 | 
					from authentik.flows.models import InvalidResponseAction
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
 | 
					from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
 | 
				
			||||||
from authentik.flows.views import FlowExecutorView
 | 
					from authentik.flows.views.executor import FlowExecutorView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
 | 
					PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ from authentik.flows.markers import ReevaluateMarker, StageMarker
 | 
				
			|||||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction
 | 
					from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction
 | 
				
			||||||
from authentik.flows.planner import FlowPlan, FlowPlanner
 | 
					from authentik.flows.planner import FlowPlan, FlowPlanner
 | 
				
			||||||
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
 | 
					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.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
from authentik.policies.dummy.models import DummyPolicy
 | 
					from authentik.policies.dummy.models import DummyPolicy
 | 
				
			||||||
from authentik.policies.models import PolicyBinding
 | 
					from authentik.policies.models import PolicyBinding
 | 
				
			||||||
@ -38,13 +38,13 @@ TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestFlowExecutor(APITestCase):
 | 
					class TestFlowExecutor(APITestCase):
 | 
				
			||||||
    """Test views logic"""
 | 
					    """Test executor"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
        self.request_factory = RequestFactory()
 | 
					        self.request_factory = RequestFactory()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @patch(
 | 
					    @patch(
 | 
				
			||||||
        "authentik.flows.views.to_stage_response",
 | 
					        "authentik.flows.views.executor.to_stage_response",
 | 
				
			||||||
        TO_STAGE_RESPONSE_MOCK,
 | 
					        TO_STAGE_RESPONSE_MOCK,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def test_existing_plan_diff_flow(self):
 | 
					    def test_existing_plan_diff_flow(self):
 | 
				
			||||||
@ -62,7 +62,7 @@ class TestFlowExecutor(APITestCase):
 | 
				
			|||||||
        session.save()
 | 
					        session.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cancel_mock = MagicMock()
 | 
					        cancel_mock = MagicMock()
 | 
				
			||||||
        with patch("authentik.flows.views.FlowExecutorView.cancel", cancel_mock):
 | 
					        with patch("authentik.flows.views.executor.FlowExecutorView.cancel", cancel_mock):
 | 
				
			||||||
            response = self.client.get(
 | 
					            response = self.client.get(
 | 
				
			||||||
                reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
					                reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
@ -70,7 +70,7 @@ class TestFlowExecutor(APITestCase):
 | 
				
			|||||||
            self.assertEqual(cancel_mock.call_count, 2)
 | 
					            self.assertEqual(cancel_mock.call_count, 2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @patch(
 | 
					    @patch(
 | 
				
			||||||
        "authentik.flows.views.to_stage_response",
 | 
					        "authentik.flows.views.executor.to_stage_response",
 | 
				
			||||||
        TO_STAGE_RESPONSE_MOCK,
 | 
					        TO_STAGE_RESPONSE_MOCK,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    @patch(
 | 
					    @patch(
 | 
				
			||||||
@ -105,7 +105,7 @@ class TestFlowExecutor(APITestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @patch(
 | 
					    @patch(
 | 
				
			||||||
        "authentik.flows.views.to_stage_response",
 | 
					        "authentik.flows.views.executor.to_stage_response",
 | 
				
			||||||
        TO_STAGE_RESPONSE_MOCK,
 | 
					        TO_STAGE_RESPONSE_MOCK,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def test_invalid_empty_flow(self):
 | 
					    def test_invalid_empty_flow(self):
 | 
				
			||||||
@ -124,7 +124,7 @@ class TestFlowExecutor(APITestCase):
 | 
				
			|||||||
        self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
 | 
					        self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @patch(
 | 
					    @patch(
 | 
				
			||||||
        "authentik.flows.views.to_stage_response",
 | 
					        "authentik.flows.views.executor.to_stage_response",
 | 
				
			||||||
        TO_STAGE_RESPONSE_MOCK,
 | 
					        TO_STAGE_RESPONSE_MOCK,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def test_invalid_flow_redirect(self):
 | 
					    def test_invalid_flow_redirect(self):
 | 
				
			||||||
@ -175,7 +175,7 @@ class TestFlowExecutor(APITestCase):
 | 
				
			|||||||
        self.assertEqual(len(plan.bindings), 1)
 | 
					        self.assertEqual(len(plan.bindings), 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @patch(
 | 
					    @patch(
 | 
				
			||||||
        "authentik.flows.views.to_stage_response",
 | 
					        "authentik.flows.views.executor.to_stage_response",
 | 
				
			||||||
        TO_STAGE_RESPONSE_MOCK,
 | 
					        TO_STAGE_RESPONSE_MOCK,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    def test_reevaluate_remove_last(self):
 | 
					    def test_reevaluate_remove_last(self):
 | 
				
			||||||
@ -438,7 +438,7 @@ class TestFlowExecutor(APITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # third request, this should trigger the re-evaluate
 | 
					        # third request, this should trigger the re-evaluate
 | 
				
			||||||
        # A get request will evaluate the policies and this will return stage 4
 | 
					        # A get request will evaluate the policies and this will return stage 4
 | 
				
			||||||
        # but it won't save it, hence we cant' check the plan
 | 
					        # but it won't save it, hence we can't check the plan
 | 
				
			||||||
        response = self.client.get(exec_url)
 | 
					        response = self.client.get(exec_url)
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
        self.assertJSONEqual(
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
							
								
								
									
										92
									
								
								authentik/flows/tests/test_inspector.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								authentik/flows/tests/test_inspector.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					"""Flow inspector tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from json import loads
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.test.client import RequestFactory
 | 
				
			||||||
 | 
					from django.urls.base import reverse
 | 
				
			||||||
 | 
					from rest_framework.test import APITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import User
 | 
				
			||||||
 | 
					from authentik.flows.challenge import ChallengeTypes
 | 
				
			||||||
 | 
					from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction
 | 
				
			||||||
 | 
					from authentik.stages.dummy.models import DummyStage
 | 
				
			||||||
 | 
					from authentik.stages.identification.models import IdentificationStage, UserFields
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestFlowInspector(APITestCase):
 | 
				
			||||||
 | 
					    """Test inspector"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        self.request_factory = RequestFactory()
 | 
				
			||||||
 | 
					        self.admin = User.objects.get(username="akadmin")
 | 
				
			||||||
 | 
					        self.client.force_login(self.admin)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test(self):
 | 
				
			||||||
 | 
					        """test inspector"""
 | 
				
			||||||
 | 
					        flow = Flow.objects.create(
 | 
				
			||||||
 | 
					            name="test-full",
 | 
				
			||||||
 | 
					            slug="test-full",
 | 
				
			||||||
 | 
					            designation=FlowDesignation.AUTHENTICATION,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Stage 1 is an identification stage
 | 
				
			||||||
 | 
					        ident_stage = IdentificationStage.objects.create(
 | 
				
			||||||
 | 
					            name="ident",
 | 
				
			||||||
 | 
					            user_fields=[UserFields.USERNAME],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        FlowStageBinding.objects.create(
 | 
				
			||||||
 | 
					            target=flow,
 | 
				
			||||||
 | 
					            stage=ident_stage,
 | 
				
			||||||
 | 
					            order=1,
 | 
				
			||||||
 | 
					            invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        FlowStageBinding.objects.create(
 | 
				
			||||||
 | 
					            target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
					            res.content,
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "component": "ak-stage-identification",
 | 
				
			||||||
 | 
					                "flow_info": {
 | 
				
			||||||
 | 
					                    "background": flow.background_url,
 | 
				
			||||||
 | 
					                    "cancel_url": reverse("authentik_flows:cancel"),
 | 
				
			||||||
 | 
					                    "title": "",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                "type": ChallengeTypes.NATIVE.value,
 | 
				
			||||||
 | 
					                "password_fields": False,
 | 
				
			||||||
 | 
					                "primary_action": "Log in",
 | 
				
			||||||
 | 
					                "sources": [],
 | 
				
			||||||
 | 
					                "user_fields": ["username"],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ins = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:flow-inspector", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        content = loads(ins.content)
 | 
				
			||||||
 | 
					        self.assertEqual(content["is_completed"], False)
 | 
				
			||||||
 | 
					        self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "ident")
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], "dummy2"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
 | 
					            {"uid_field": "akadmin"},
 | 
				
			||||||
 | 
					            follow=True,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ins = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:flow-inspector", kwargs={"flow_slug": flow.slug}),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        content = loads(ins.content)
 | 
				
			||||||
 | 
					        self.assertEqual(content["is_completed"], False)
 | 
				
			||||||
 | 
					        self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident")
 | 
				
			||||||
 | 
					        self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2")
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            content["current_plan"]["plan_context"]["pending_user"]["username"], "akadmin"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
@ -4,7 +4,7 @@ from typing import Callable, Type
 | 
				
			|||||||
from django.test import RequestFactory, TestCase
 | 
					from django.test import RequestFactory, TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.flows.stage import StageView
 | 
					from authentik.flows.stage import StageView
 | 
				
			||||||
from authentik.flows.views import FlowExecutorView
 | 
					from authentik.flows.views.executor import FlowExecutorView
 | 
				
			||||||
from authentik.lib.utils.reflection import all_subclasses
 | 
					from authentik.lib.utils.reflection import all_subclasses
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@ from django.urls import reverse
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from authentik.flows.models import Flow, FlowDesignation
 | 
					from authentik.flows.models import Flow, FlowDesignation
 | 
				
			||||||
from authentik.flows.planner import FlowPlan
 | 
					from authentik.flows.planner import FlowPlan
 | 
				
			||||||
from authentik.flows.views import SESSION_KEY_PLAN
 | 
					from authentik.flows.views.executor import SESSION_KEY_PLAN
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestHelperView(TestCase):
 | 
					class TestHelperView(TestCase):
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@ from authentik.lib.sentry import SentryIgnoredException
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
 | 
					def get_attrs(obj: SerializerModel) -> dict[str, Any]:
 | 
				
			||||||
    """Get object's attributes via their serializer, and covert it to a normal dict"""
 | 
					    """Get object's attributes via their serializer, and convert it to a normal dict"""
 | 
				
			||||||
    data = dict(obj.serializer(obj).data)
 | 
					    data = dict(obj.serializer(obj).data)
 | 
				
			||||||
    to_remove = (
 | 
					    to_remove = (
 | 
				
			||||||
        "policies",
 | 
					        "policies",
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@
 | 
				
			|||||||
from django.urls import path
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.flows.models import FlowDesignation
 | 
					from authentik.flows.models import FlowDesignation
 | 
				
			||||||
from authentik.flows.views import CancelView, ConfigureFlowInitView, ToDefaultFlow
 | 
					from authentik.flows.views.executor import CancelView, ConfigureFlowInitView, ToDefaultFlow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										0
									
								
								authentik/flows/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/flows/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -1,4 +1,5 @@
 | 
				
			|||||||
"""authentik multi-stage authentication engine"""
 | 
					"""authentik multi-stage authentication engine"""
 | 
				
			||||||
 | 
					from copy import deepcopy
 | 
				
			||||||
from traceback import format_tb
 | 
					from traceback import format_tb
 | 
				
			||||||
from typing import Any, Optional
 | 
					from typing import Any, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -52,6 +53,7 @@ NEXT_ARG_NAME = "next"
 | 
				
			|||||||
SESSION_KEY_PLAN = "authentik_flows_plan"
 | 
					SESSION_KEY_PLAN = "authentik_flows_plan"
 | 
				
			||||||
SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
 | 
					SESSION_KEY_APPLICATION_PRE = "authentik_flows_application_pre"
 | 
				
			||||||
SESSION_KEY_GET = "authentik_flows_get"
 | 
					SESSION_KEY_GET = "authentik_flows_get"
 | 
				
			||||||
 | 
					SESSION_KEY_HISTORY = "authentik_flows_history"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def challenge_types():
 | 
					def challenge_types():
 | 
				
			||||||
@ -126,12 +128,12 @@ class FlowExecutorView(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # pylint: disable=unused-argument, too-many-return-statements
 | 
					    # pylint: disable=unused-argument, too-many-return-statements
 | 
				
			||||||
    def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
 | 
					    def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
 | 
				
			||||||
        # Early check if theres an active Plan for the current session
 | 
					        # Early check if there's an active Plan for the current session
 | 
				
			||||||
        if SESSION_KEY_PLAN in self.request.session:
 | 
					        if SESSION_KEY_PLAN in self.request.session:
 | 
				
			||||||
            self.plan = self.request.session[SESSION_KEY_PLAN]
 | 
					            self.plan = self.request.session[SESSION_KEY_PLAN]
 | 
				
			||||||
            if self.plan.flow_pk != self.flow.pk.hex:
 | 
					            if self.plan.flow_pk != self.flow.pk.hex:
 | 
				
			||||||
                self._logger.warning(
 | 
					                self._logger.warning(
 | 
				
			||||||
                    "f(exec): Found existing plan for other flow, deleteing plan",
 | 
					                    "f(exec): Found existing plan for other flow, deleting plan",
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                # Existing plan is deleted from session and instance
 | 
					                # Existing plan is deleted from session and instance
 | 
				
			||||||
                self.plan = None
 | 
					                self.plan = None
 | 
				
			||||||
@ -140,6 +142,7 @@ class FlowExecutorView(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # Don't check session again as we've either already loaded the plan or we need to plan
 | 
					        # Don't check session again as we've either already loaded the plan or we need to plan
 | 
				
			||||||
        if not self.plan:
 | 
					        if not self.plan:
 | 
				
			||||||
 | 
					            request.session[SESSION_KEY_HISTORY] = []
 | 
				
			||||||
            self._logger.debug("f(exec): No active Plan found, initiating planner")
 | 
					            self._logger.debug("f(exec): No active Plan found, initiating planner")
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                self.plan = self._initiate_plan()
 | 
					                self.plan = self._initiate_plan()
 | 
				
			||||||
@ -321,6 +324,7 @@ class FlowExecutorView(APIView):
 | 
				
			|||||||
            "f(exec): Stage ok",
 | 
					            "f(exec): Stage ok",
 | 
				
			||||||
            stage_class=class_to_path(self.current_stage_view.__class__),
 | 
					            stage_class=class_to_path(self.current_stage_view.__class__),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        self.request.session.get(SESSION_KEY_HISTORY, []).append(deepcopy(self.plan))
 | 
				
			||||||
        self.plan.pop()
 | 
					        self.plan.pop()
 | 
				
			||||||
        self.request.session[SESSION_KEY_PLAN] = self.plan
 | 
					        self.request.session[SESSION_KEY_PLAN] = self.plan
 | 
				
			||||||
        if self.plan.bindings:
 | 
					        if self.plan.bindings:
 | 
				
			||||||
@ -368,6 +372,10 @@ class FlowExecutorView(APIView):
 | 
				
			|||||||
            SESSION_KEY_APPLICATION_PRE,
 | 
					            SESSION_KEY_APPLICATION_PRE,
 | 
				
			||||||
            SESSION_KEY_PLAN,
 | 
					            SESSION_KEY_PLAN,
 | 
				
			||||||
            SESSION_KEY_GET,
 | 
					            SESSION_KEY_GET,
 | 
				
			||||||
 | 
					            # We don't delete the history on purpose, as a user might
 | 
				
			||||||
 | 
					            # still be inspecting it.
 | 
				
			||||||
 | 
					            # It's only deleted on a fresh executions
 | 
				
			||||||
 | 
					            # SESSION_KEY_HISTORY,
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        for key in keys_to_delete:
 | 
					        for key in keys_to_delete:
 | 
				
			||||||
            if key in self.request.session:
 | 
					            if key in self.request.session:
 | 
				
			||||||
@ -433,7 +441,7 @@ class ToDefaultFlow(View):
 | 
				
			|||||||
            plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
 | 
					            plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
 | 
				
			||||||
            if plan.flow_pk != flow.pk.hex:
 | 
					            if plan.flow_pk != flow.pk.hex:
 | 
				
			||||||
                LOGGER.warning(
 | 
					                LOGGER.warning(
 | 
				
			||||||
                    "f(def): Found existing plan for other flow, deleteing plan",
 | 
					                    "f(def): Found existing plan for other flow, deleting plan",
 | 
				
			||||||
                    flow_slug=flow.slug,
 | 
					                    flow_slug=flow.slug,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                del self.request.session[SESSION_KEY_PLAN]
 | 
					                del self.request.session[SESSION_KEY_PLAN]
 | 
				
			||||||
							
								
								
									
										119
									
								
								authentik/flows/views/inspector.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								authentik/flows/views/inspector.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					"""Flow Inspector"""
 | 
				
			||||||
 | 
					from hashlib import sha256
 | 
				
			||||||
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.http.request import HttpRequest
 | 
				
			||||||
 | 
					from django.http.response import HttpResponse
 | 
				
			||||||
 | 
					from django.shortcuts import get_object_or_404
 | 
				
			||||||
 | 
					from django.utils.decorators import method_decorator
 | 
				
			||||||
 | 
					from django.views.decorators.clickjacking import xframe_options_sameorigin
 | 
				
			||||||
 | 
					from drf_spectacular.types import OpenApiTypes
 | 
				
			||||||
 | 
					from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
				
			||||||
 | 
					from rest_framework.fields import BooleanField, ListField, SerializerMethodField
 | 
				
			||||||
 | 
					from rest_framework.permissions import IsAdminUser
 | 
				
			||||||
 | 
					from rest_framework.request import Request
 | 
				
			||||||
 | 
					from rest_framework.response import Response
 | 
				
			||||||
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
 | 
					from structlog.stdlib import BoundLogger, get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.api.utils import PassiveSerializer
 | 
				
			||||||
 | 
					from authentik.events.utils import sanitize_dict
 | 
				
			||||||
 | 
					from authentik.flows.api.bindings import FlowStageBindingSerializer
 | 
				
			||||||
 | 
					from authentik.flows.models import Flow
 | 
				
			||||||
 | 
					from authentik.flows.planner import FlowPlan
 | 
				
			||||||
 | 
					from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FlowInspectorPlanSerializer(PassiveSerializer):
 | 
				
			||||||
 | 
					    """Serializer for an active FlowPlan"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    current_stage = SerializerMethodField()
 | 
				
			||||||
 | 
					    next_planned_stage = SerializerMethodField(required=False)
 | 
				
			||||||
 | 
					    plan_context = SerializerMethodField()
 | 
				
			||||||
 | 
					    session_id = SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_current_stage(self, plan: FlowPlan) -> FlowStageBindingSerializer:
 | 
				
			||||||
 | 
					        """Get the current stage"""
 | 
				
			||||||
 | 
					        return FlowStageBindingSerializer(instance=plan.bindings[0]).data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_next_planned_stage(self, plan: FlowPlan) -> FlowStageBindingSerializer:
 | 
				
			||||||
 | 
					        """Get the next planned stage"""
 | 
				
			||||||
 | 
					        if len(plan.bindings) < 2:
 | 
				
			||||||
 | 
					            return FlowStageBindingSerializer().data
 | 
				
			||||||
 | 
					        return FlowStageBindingSerializer(instance=plan.bindings[1]).data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_plan_context(self, plan: FlowPlan) -> dict[str, Any]:
 | 
				
			||||||
 | 
					        """Get the plan's context, sanitized"""
 | 
				
			||||||
 | 
					        return sanitize_dict(plan.context)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # pylint: disable=unused-argument
 | 
				
			||||||
 | 
					    def get_session_id(self, plan: FlowPlan) -> str:
 | 
				
			||||||
 | 
					        """Get a unique session ID"""
 | 
				
			||||||
 | 
					        request: Request = self.context["request"]
 | 
				
			||||||
 | 
					        return sha256(
 | 
				
			||||||
 | 
					            f"{request._request.session.session_key}-{settings.SECRET_KEY}".encode("ascii")
 | 
				
			||||||
 | 
					        ).hexdigest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class FlowInspectionSerializer(PassiveSerializer):
 | 
				
			||||||
 | 
					    """Serializer for inspect endpoint"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    plans = ListField(child=FlowInspectorPlanSerializer())
 | 
				
			||||||
 | 
					    current_plan = FlowInspectorPlanSerializer(required=False)
 | 
				
			||||||
 | 
					    is_completed = BooleanField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@method_decorator(xframe_options_sameorigin, name="dispatch")
 | 
				
			||||||
 | 
					class FlowInspectorView(APIView):
 | 
				
			||||||
 | 
					    """Flow inspector API"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    permission_classes = [IsAdminUser]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    flow: Flow
 | 
				
			||||||
 | 
					    _logger: BoundLogger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setup(self, request: HttpRequest, flow_slug: str):
 | 
				
			||||||
 | 
					        super().setup(request, flow_slug=flow_slug)
 | 
				
			||||||
 | 
					        self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
 | 
				
			||||||
 | 
					        self._logger = get_logger().bind(flow_slug=flow_slug)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # pylint: disable=unused-argument, too-many-return-statements
 | 
				
			||||||
 | 
					    def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
 | 
				
			||||||
 | 
					        if SESSION_KEY_HISTORY not in self.request.session:
 | 
				
			||||||
 | 
					            return HttpResponse(status=400)
 | 
				
			||||||
 | 
					        return super().dispatch(request, flow_slug=flow_slug)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema(
 | 
				
			||||||
 | 
					        responses={
 | 
				
			||||||
 | 
					            200: FlowInspectionSerializer(),
 | 
				
			||||||
 | 
					            400: OpenApiResponse(
 | 
				
			||||||
 | 
					                description="No flow plan in session."
 | 
				
			||||||
 | 
					            ),  # This error can be raised by the email stage
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        request=OpenApiTypes.NONE,
 | 
				
			||||||
 | 
					        operation_id="flows_inspector_get",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
				
			||||||
 | 
					        """Get current flow state and record it"""
 | 
				
			||||||
 | 
					        plans = []
 | 
				
			||||||
 | 
					        for plan in request.session[SESSION_KEY_HISTORY]:
 | 
				
			||||||
 | 
					            plan_serializer = FlowInspectorPlanSerializer(
 | 
				
			||||||
 | 
					                instance=plan, context={"request": request}
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            plans.append(plan_serializer.data)
 | 
				
			||||||
 | 
					        is_completed = False
 | 
				
			||||||
 | 
					        if SESSION_KEY_PLAN in request.session:
 | 
				
			||||||
 | 
					            current_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            current_plan = request.session[SESSION_KEY_HISTORY][-1]
 | 
				
			||||||
 | 
					            is_completed = True
 | 
				
			||||||
 | 
					        current_serializer = FlowInspectorPlanSerializer(
 | 
				
			||||||
 | 
					            instance=current_plan, context={"request": request}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = {
 | 
				
			||||||
 | 
					            "plans": plans,
 | 
				
			||||||
 | 
					            "current_plan": current_serializer.data,
 | 
				
			||||||
 | 
					            "is_completed": is_completed,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return Response(response)
 | 
				
			||||||
@ -5,6 +5,16 @@ postgresql:
 | 
				
			|||||||
  user: authentik
 | 
					  user: authentik
 | 
				
			||||||
  port: 5432
 | 
					  port: 5432
 | 
				
			||||||
  password: 'env://POSTGRES_PASSWORD'
 | 
					  password: 'env://POSTGRES_PASSWORD'
 | 
				
			||||||
 | 
					  backup:
 | 
				
			||||||
 | 
					    enabled: true
 | 
				
			||||||
 | 
					  s3_backup:
 | 
				
			||||||
 | 
					    access_key: ""
 | 
				
			||||||
 | 
					    secret_key: ""
 | 
				
			||||||
 | 
					    bucket: ""
 | 
				
			||||||
 | 
					    region: eu-central-1
 | 
				
			||||||
 | 
					    host: ""
 | 
				
			||||||
 | 
					    location: ""
 | 
				
			||||||
 | 
					    insecure_skip_verify: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
web:
 | 
					web:
 | 
				
			||||||
  listen: 0.0.0.0:9000
 | 
					  listen: 0.0.0.0:9000
 | 
				
			||||||
@ -54,8 +64,9 @@ outposts:
 | 
				
			|||||||
  # %(type)s: Outpost type; proxy, ldap, etc
 | 
					  # %(type)s: Outpost type; proxy, ldap, etc
 | 
				
			||||||
  # %(version)s: Current version; 2021.4.1
 | 
					  # %(version)s: Current version; 2021.4.1
 | 
				
			||||||
  # %(build_hash)s: Build hash if you're running a beta version
 | 
					  # %(build_hash)s: Build hash if you're running a beta version
 | 
				
			||||||
  docker_image_base: "ghcr.io/goauthentik/%(type)s:%(version)s"
 | 
					  container_image_base: env://AUTHENTIK_OUTPOSTS__DOCKER_IMAGE_BASE?goauthentik.io/%(type)s:%(version)s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					cookie_domain: null
 | 
				
			||||||
disable_update_check: false
 | 
					disable_update_check: false
 | 
				
			||||||
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
 | 
					avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
 | 
				
			||||||
geoip: "./GeoLite2-City.mmdb"
 | 
					geoip: "./GeoLite2-City.mmdb"
 | 
				
			||||||
@ -63,6 +74,6 @@ geoip: "./GeoLite2-City.mmdb"
 | 
				
			|||||||
# Can't currently be configured via environment variables, only yaml
 | 
					# Can't currently be configured via environment variables, only yaml
 | 
				
			||||||
footer_links:
 | 
					footer_links:
 | 
				
			||||||
  - name: Documentation
 | 
					  - name: Documentation
 | 
				
			||||||
    href: https://goauthentik.io/docs/
 | 
					    href: https://goauthentik.io/docs/?utm_source=authentik
 | 
				
			||||||
  - name: authentik Website
 | 
					  - name: authentik Website
 | 
				
			||||||
    href: https://goauthentik.io/
 | 
					    href: https://goauthentik.io/?utm_source=authentik
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ class TestConfig(TestCase):
 | 
				
			|||||||
        config = ConfigLoader()
 | 
					        config = ConfigLoader()
 | 
				
			||||||
        environ["foo"] = "bar"
 | 
					        environ["foo"] = "bar"
 | 
				
			||||||
        self.assertEqual(config.parse_uri("env://foo"), "bar")
 | 
					        self.assertEqual(config.parse_uri("env://foo"), "bar")
 | 
				
			||||||
        self.assertEqual(config.parse_uri("env://fo?bar"), "bar")
 | 
					        self.assertEqual(config.parse_uri("env://foo?bar"), "bar")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_uri_file(self):
 | 
					    def test_uri_file(self):
 | 
				
			||||||
        """Test URI parsing (file load)"""
 | 
					        """Test URI parsing (file load)"""
 | 
				
			||||||
 | 
				
			|||||||
@ -27,7 +27,7 @@ class TestHTTP(TestCase):
 | 
				
			|||||||
        token = Token.objects.create(
 | 
					        token = Token.objects.create(
 | 
				
			||||||
            identifier="test", user=self.user, intent=TokenIntents.INTENT_API
 | 
					            identifier="test", user=self.user, intent=TokenIntents.INTENT_API
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # Invalid, non-existant token
 | 
					        # Invalid, non-existent token
 | 
				
			||||||
        request = self.factory.get(
 | 
					        request = self.factory.get(
 | 
				
			||||||
            "/",
 | 
					            "/",
 | 
				
			||||||
            **{
 | 
					            **{
 | 
				
			||||||
@ -36,7 +36,7 @@ class TestHTTP(TestCase):
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(get_client_ip(request), "127.0.0.1")
 | 
					        self.assertEqual(get_client_ip(request), "127.0.0.1")
 | 
				
			||||||
        # Invalid, user doesn't have permisions
 | 
					        # Invalid, user doesn't have permissions
 | 
				
			||||||
        request = self.factory.get(
 | 
					        request = self.factory.get(
 | 
				
			||||||
            "/",
 | 
					            "/",
 | 
				
			||||||
            **{
 | 
					            **{
 | 
				
			||||||
 | 
				
			|||||||
@ -13,4 +13,4 @@ class AuthentikManagedConfig(AppConfig):
 | 
				
			|||||||
        from authentik.managed.tasks import managed_reconcile
 | 
					        from authentik.managed.tasks import managed_reconcile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # pyright: reportGeneralTypeIssues=false
 | 
					        # pyright: reportGeneralTypeIssues=false
 | 
				
			||||||
        managed_reconcile()  # pylint: disable=no-value-for-parameter
 | 
					        managed_reconcile.delay()  # pylint: disable=no-value-for-parameter
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,17 @@
 | 
				
			|||||||
from django.db import DatabaseError
 | 
					from django.db import DatabaseError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.tasks import CELERY_APP
 | 
					from authentik.core.tasks import CELERY_APP
 | 
				
			||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import (
 | 
				
			||||||
 | 
					    MonitoredTask,
 | 
				
			||||||
 | 
					    TaskResult,
 | 
				
			||||||
 | 
					    TaskResultStatus,
 | 
				
			||||||
 | 
					    prefill_task,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from authentik.managed.manager import ObjectManager
 | 
					from authentik.managed.manager import ObjectManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
 | 
					@prefill_task()
 | 
				
			||||||
def managed_reconcile(self: MonitoredTask):
 | 
					def managed_reconcile(self: MonitoredTask):
 | 
				
			||||||
    """Run ObjectManager to ensure objects are up-to-date"""
 | 
					    """Run ObjectManager to ensure objects are up-to-date"""
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
 | 
				
			|||||||
@ -104,7 +104,7 @@ class OutpostConsumer(AuthJsonConsumer):
 | 
				
			|||||||
                expected=self.outpost.config.kubernetes_replicas,
 | 
					                expected=self.outpost.config.kubernetes_replicas,
 | 
				
			||||||
            ).inc()
 | 
					            ).inc()
 | 
				
			||||||
            LOGGER.debug(
 | 
					            LOGGER.debug(
 | 
				
			||||||
                "added outpost instace to cache",
 | 
					                "added outpost instance to cache",
 | 
				
			||||||
                outpost=self.outpost,
 | 
					                outpost=self.outpost,
 | 
				
			||||||
                instance_uuid=self.last_uid,
 | 
					                instance_uuid=self.last_uid,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
				
			|||||||
@ -69,7 +69,10 @@ class BaseController:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_container_image(self) -> str:
 | 
					    def get_container_image(self) -> str:
 | 
				
			||||||
        """Get container image to use for this outpost"""
 | 
					        """Get container image to use for this outpost"""
 | 
				
			||||||
        image_name_template: str = CONFIG.y("outposts.docker_image_base")
 | 
					        if self.outpost.config.container_image is not None:
 | 
				
			||||||
 | 
					            return self.outpost.config.container_image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        image_name_template: str = CONFIG.y("outposts.container_image_base")
 | 
				
			||||||
        return image_name_template % {
 | 
					        return image_name_template % {
 | 
				
			||||||
            "type": self.outpost.type,
 | 
					            "type": self.outpost.type,
 | 
				
			||||||
            "version": __version__,
 | 
					            "version": __version__,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@
 | 
				
			|||||||
from time import sleep
 | 
					from time import sleep
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.utils.text import slugify
 | 
				
			||||||
from docker import DockerClient
 | 
					from docker import DockerClient
 | 
				
			||||||
from docker.errors import DockerException, NotFound
 | 
					from docker.errors import DockerException, NotFound
 | 
				
			||||||
from docker.models.containers import Container
 | 
					from docker.models.containers import Container
 | 
				
			||||||
@ -28,6 +29,17 @@ class DockerController(BaseController):
 | 
				
			|||||||
        except ServiceConnectionInvalid as exc:
 | 
					        except ServiceConnectionInvalid as exc:
 | 
				
			||||||
            raise ControllerException from exc
 | 
					            raise ControllerException from exc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def name(self) -> str:
 | 
				
			||||||
 | 
					        """Get the name of the object this reconciler manages"""
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            self.outpost.config.object_naming_template
 | 
				
			||||||
 | 
					            % {
 | 
				
			||||||
 | 
					                "name": slugify(self.outpost.name),
 | 
				
			||||||
 | 
					                "uuid": self.outpost.uuid.hex,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ).lower()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _get_labels(self) -> dict[str, str]:
 | 
					    def _get_labels(self) -> dict[str, str]:
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            "io.goauthentik.outpost-uuid": self.outpost.pk.hex,
 | 
					            "io.goauthentik.outpost-uuid": self.outpost.pk.hex,
 | 
				
			||||||
@ -38,6 +50,7 @@ class DockerController(BaseController):
 | 
				
			|||||||
            "AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
 | 
					            "AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
 | 
				
			||||||
            "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure).lower(),
 | 
					            "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure).lower(),
 | 
				
			||||||
            "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
					            "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
				
			||||||
 | 
					            "AUTHENTIK_HOST_BROWSER": self.outpost.config.authentik_host_browser,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _comp_env(self, container: Container) -> bool:
 | 
					    def _comp_env(self, container: Container) -> bool:
 | 
				
			||||||
@ -75,9 +88,12 @@ class DockerController(BaseController):
 | 
				
			|||||||
        #   {'HostIp': '0.0.0.0', 'HostPort': '389'},
 | 
					        #   {'HostIp': '0.0.0.0', 'HostPort': '389'},
 | 
				
			||||||
        #   {'HostIp': '::', 'HostPort': '389'}
 | 
					        #   {'HostIp': '::', 'HostPort': '389'}
 | 
				
			||||||
        # ]}
 | 
					        # ]}
 | 
				
			||||||
 | 
					        # If no ports are mapped (either mapping disabled, or host network)
 | 
				
			||||||
 | 
					        if not container.ports:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
        for port in self.deployment_ports:
 | 
					        for port in self.deployment_ports:
 | 
				
			||||||
            key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
 | 
					            key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
 | 
				
			||||||
            if key not in container.ports:
 | 
					            if not container.ports.get(key, None):
 | 
				
			||||||
                return True
 | 
					                return True
 | 
				
			||||||
            host_matching = False
 | 
					            host_matching = False
 | 
				
			||||||
            for host_port in container.ports[key]:
 | 
					            for host_port in container.ports[key]:
 | 
				
			||||||
@ -86,27 +102,37 @@ class DockerController(BaseController):
 | 
				
			|||||||
                return True
 | 
					                return True
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _get_container(self) -> tuple[Container, bool]:
 | 
					    def try_pull_image(self):
 | 
				
			||||||
        container_name = f"authentik-proxy-{self.outpost.uuid.hex}"
 | 
					        """Try to pull the image needed for this outpost based on the CONFIG
 | 
				
			||||||
 | 
					        `outposts.container_image_base`, but fall back to known-good images"""
 | 
				
			||||||
 | 
					        image = self.get_container_image()
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return self.client.containers.get(container_name), False
 | 
					            self.client.images.pull(image)
 | 
				
			||||||
 | 
					        except DockerException:
 | 
				
			||||||
 | 
					            image = f"goauthentik.io/{self.outpost.type}:latest"
 | 
				
			||||||
 | 
					            self.client.images.pull(image)
 | 
				
			||||||
 | 
					        return image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_container(self) -> tuple[Container, bool]:
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return self.client.containers.get(self.name), False
 | 
				
			||||||
        except NotFound:
 | 
					        except NotFound:
 | 
				
			||||||
            self.logger.info("(Re-)creating container...")
 | 
					            self.logger.info("(Re-)creating container...")
 | 
				
			||||||
            image_name = self.get_container_image()
 | 
					            image_name = self.try_pull_image()
 | 
				
			||||||
            self.client.images.pull(image_name)
 | 
					 | 
				
			||||||
            container_args = {
 | 
					            container_args = {
 | 
				
			||||||
                "image": image_name,
 | 
					                "image": image_name,
 | 
				
			||||||
                "name": container_name,
 | 
					                "name": self.name,
 | 
				
			||||||
                "detach": True,
 | 
					                "detach": True,
 | 
				
			||||||
                "ports": {
 | 
					 | 
				
			||||||
                    f"{port.inner_port or port.port}/{port.protocol.lower()}": port.port
 | 
					 | 
				
			||||||
                    for port in self.deployment_ports
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                "environment": self._get_env(),
 | 
					                "environment": self._get_env(),
 | 
				
			||||||
                "labels": self._get_labels(),
 | 
					                "labels": self._get_labels(),
 | 
				
			||||||
                "restart_policy": {"Name": "unless-stopped"},
 | 
					                "restart_policy": {"Name": "unless-stopped"},
 | 
				
			||||||
                "network": self.outpost.config.docker_network,
 | 
					                "network": self.outpost.config.docker_network,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            if self.outpost.config.docker_map_ports:
 | 
				
			||||||
 | 
					                container_args["ports"] = {
 | 
				
			||||||
 | 
					                    f"{port.inner_port or port.port}/{port.protocol.lower()}": str(port.port)
 | 
				
			||||||
 | 
					                    for port in self.deployment_ports
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            if settings.TEST:
 | 
					            if settings.TEST:
 | 
				
			||||||
                del container_args["ports"]
 | 
					                del container_args["ports"]
 | 
				
			||||||
                del container_args["network"]
 | 
					                del container_args["network"]
 | 
				
			||||||
@ -116,12 +142,23 @@ class DockerController(BaseController):
 | 
				
			|||||||
                True,
 | 
					                True,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _migrate_container_name(self):
 | 
				
			||||||
 | 
					        """Migrate 2021.9 to 2021.10+"""
 | 
				
			||||||
 | 
					        old_name = f"authentik-proxy-{self.outpost.uuid.hex}"
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            old_container: Container = self.client.containers.get(old_name)
 | 
				
			||||||
 | 
					            old_container.kill()
 | 
				
			||||||
 | 
					            old_container.remove()
 | 
				
			||||||
 | 
					        except NotFound:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # pylint: disable=too-many-return-statements
 | 
					    # pylint: disable=too-many-return-statements
 | 
				
			||||||
    def up(self, depth=1):
 | 
					    def up(self, depth=1):
 | 
				
			||||||
        if self.outpost.managed == MANAGED_OUTPOST:
 | 
					        if self.outpost.managed == MANAGED_OUTPOST:
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
        if depth >= 10:
 | 
					        if depth >= 10:
 | 
				
			||||||
            raise ControllerException("Giving up since we exceeded recursion limit.")
 | 
					            raise ControllerException("Giving up since we exceeded recursion limit.")
 | 
				
			||||||
 | 
					        self._migrate_container_name()
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            container, has_been_created = self._get_container()
 | 
					            container, has_been_created = self._get_container()
 | 
				
			||||||
            if has_been_created:
 | 
					            if has_been_created:
 | 
				
			||||||
@ -129,12 +166,12 @@ class DockerController(BaseController):
 | 
				
			|||||||
                return None
 | 
					                return None
 | 
				
			||||||
            # Check if the container is out of date, delete it and retry
 | 
					            # Check if the container is out of date, delete it and retry
 | 
				
			||||||
            if len(container.image.tags) > 0:
 | 
					            if len(container.image.tags) > 0:
 | 
				
			||||||
                tag: str = container.image.tags[0]
 | 
					                should_image = self.try_pull_image()
 | 
				
			||||||
                if tag != self.get_container_image():
 | 
					                if should_image not in container.image.tags:
 | 
				
			||||||
                    self.logger.info(
 | 
					                    self.logger.info(
 | 
				
			||||||
                        "Container has mismatched image, re-creating...",
 | 
					                        "Container has mismatched image, re-creating...",
 | 
				
			||||||
                        has=tag,
 | 
					                        has=container.image.tags,
 | 
				
			||||||
                        should=self.get_container_image(),
 | 
					                        should=should_image,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    self.down()
 | 
					                    self.down()
 | 
				
			||||||
                    return self.up(depth + 1)
 | 
					                    return self.up(depth + 1)
 | 
				
			||||||
@ -215,6 +252,7 @@ class DockerController(BaseController):
 | 
				
			|||||||
                        "AUTHENTIK_HOST": self.outpost.config.authentik_host,
 | 
					                        "AUTHENTIK_HOST": self.outpost.config.authentik_host,
 | 
				
			||||||
                        "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
 | 
					                        "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
 | 
				
			||||||
                        "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
					                        "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
				
			||||||
 | 
					                        "AUTHENTIK_HOST_BROWSER": self.outpost.config.authentik_host_browser,
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    "labels": self._get_labels(),
 | 
					                    "labels": self._get_labels(),
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
"""Base Kubernetes Reconciler"""
 | 
					"""Base Kubernetes Reconciler"""
 | 
				
			||||||
from typing import TYPE_CHECKING, Generic, TypeVar
 | 
					from typing import TYPE_CHECKING, Generic, Optional, TypeVar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.utils.text import slugify
 | 
					from django.utils.text import slugify
 | 
				
			||||||
from kubernetes.client import V1ObjectMeta
 | 
					from kubernetes.client import V1ObjectMeta
 | 
				
			||||||
@ -10,7 +10,7 @@ from structlog.stdlib import get_logger
 | 
				
			|||||||
from urllib3.exceptions import HTTPError
 | 
					from urllib3.exceptions import HTTPError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik import __version__
 | 
					from authentik import __version__
 | 
				
			||||||
from authentik.lib.sentry import SentryIgnoredException
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
 | 
				
			||||||
from authentik.outposts.managed import MANAGED_OUTPOST
 | 
					from authentik.outposts.managed import MANAGED_OUTPOST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
@ -20,18 +20,6 @@ if TYPE_CHECKING:
 | 
				
			|||||||
T = TypeVar("T", V1Pod, V1Deployment)
 | 
					T = TypeVar("T", V1Pod, V1Deployment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReconcileTrigger(SentryIgnoredException):
 | 
					 | 
				
			||||||
    """Base trigger raised by child classes to notify us"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NeedsRecreate(ReconcileTrigger):
 | 
					 | 
				
			||||||
    """Exception to trigger a complete recreate of the Kubernetes Object"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NeedsUpdate(ReconcileTrigger):
 | 
					 | 
				
			||||||
    """Exception to trigger an update to the Kubernetes Object"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class KubernetesObjectReconciler(Generic[T]):
 | 
					class KubernetesObjectReconciler(Generic[T]):
 | 
				
			||||||
    """Base Kubernetes Reconciler, handles the basic logic."""
 | 
					    """Base Kubernetes Reconciler, handles the basic logic."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -82,7 +70,25 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
                raise exc
 | 
					                raise exc
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                self.reconcile(current, reference)
 | 
					                self.reconcile(current, reference)
 | 
				
			||||||
 | 
					        except NeedsUpdate:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                self.update(current, reference)
 | 
				
			||||||
 | 
					                self.logger.debug("Updating")
 | 
				
			||||||
 | 
					            except (OpenApiException, HTTPError) as exc:
 | 
				
			||||||
 | 
					                # pylint: disable=no-member
 | 
				
			||||||
 | 
					                if isinstance(exc, ApiException) and exc.status == 422:
 | 
				
			||||||
 | 
					                    self.logger.debug("Failed to update current, triggering re-create")
 | 
				
			||||||
 | 
					                    self._recreate(current=current, reference=reference)
 | 
				
			||||||
 | 
					                    return
 | 
				
			||||||
 | 
					                self.logger.debug("Other unhandled error", exc=exc)
 | 
				
			||||||
 | 
					                raise exc
 | 
				
			||||||
        except NeedsRecreate:
 | 
					        except NeedsRecreate:
 | 
				
			||||||
 | 
					            self._recreate(current=current, reference=reference)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self.logger.debug("Object is up-to-date.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _recreate(self, reference: T, current: Optional[T] = None):
 | 
				
			||||||
 | 
					        """Recreate object"""
 | 
				
			||||||
        self.logger.debug("Recreate requested")
 | 
					        self.logger.debug("Recreate requested")
 | 
				
			||||||
        if current:
 | 
					        if current:
 | 
				
			||||||
            self.logger.debug("Deleted old")
 | 
					            self.logger.debug("Deleted old")
 | 
				
			||||||
@ -91,11 +97,6 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
            self.logger.debug("No old found, creating")
 | 
					            self.logger.debug("No old found, creating")
 | 
				
			||||||
        self.logger.debug("Creating")
 | 
					        self.logger.debug("Creating")
 | 
				
			||||||
        self.create(reference)
 | 
					        self.create(reference)
 | 
				
			||||||
        except NeedsUpdate:
 | 
					 | 
				
			||||||
            self.logger.debug("Updating")
 | 
					 | 
				
			||||||
            self.update(current, reference)
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            self.logger.debug("Object is up-to-date.")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def down(self):
 | 
					    def down(self):
 | 
				
			||||||
        """Delete object if found"""
 | 
					        """Delete object if found"""
 | 
				
			||||||
@ -109,7 +110,7 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
        except (OpenApiException, HTTPError) as exc:
 | 
					        except (OpenApiException, HTTPError) as exc:
 | 
				
			||||||
            # pylint: disable=no-member
 | 
					            # pylint: disable=no-member
 | 
				
			||||||
            if isinstance(exc, ApiException) and exc.status == 404:
 | 
					            if isinstance(exc, ApiException) and exc.status == 404:
 | 
				
			||||||
                self.logger.debug("Failed to get current, assuming non-existant")
 | 
					                self.logger.debug("Failed to get current, assuming non-existent")
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
            self.logger.debug("Other unhandled error", exc=exc)
 | 
					            self.logger.debug("Other unhandled error", exc=exc)
 | 
				
			||||||
            raise exc
 | 
					            raise exc
 | 
				
			||||||
@ -129,7 +130,7 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def retrieve(self) -> T:
 | 
					    def retrieve(self) -> T:
 | 
				
			||||||
        """API Wrapper to retrive object"""
 | 
					        """API Wrapper to retrieve object"""
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, reference: T):
 | 
					    def delete(self, reference: T):
 | 
				
			||||||
@ -150,6 +151,8 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
                "app.kubernetes.io/version": __version__,
 | 
					                "app.kubernetes.io/version": __version__,
 | 
				
			||||||
                "app.kubernetes.io/managed-by": "goauthentik.io",
 | 
					                "app.kubernetes.io/managed-by": "goauthentik.io",
 | 
				
			||||||
                "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
 | 
					                "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
 | 
				
			||||||
 | 
					                "goauthentik.io/outpost-type": str(self.controller.outpost.type),
 | 
				
			||||||
 | 
					                "goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            **kwargs,
 | 
					            **kwargs,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
"""Kubernetes Deployment Reconciler"""
 | 
					"""Kubernetes Deployment Reconciler"""
 | 
				
			||||||
from typing import TYPE_CHECKING
 | 
					from typing import TYPE_CHECKING
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.utils.text import slugify
 | 
				
			||||||
from kubernetes.client import (
 | 
					from kubernetes.client import (
 | 
				
			||||||
    AppsV1Api,
 | 
					    AppsV1Api,
 | 
				
			||||||
    V1Container,
 | 
					    V1Container,
 | 
				
			||||||
@ -11,13 +12,16 @@ from kubernetes.client import (
 | 
				
			|||||||
    V1EnvVarSource,
 | 
					    V1EnvVarSource,
 | 
				
			||||||
    V1LabelSelector,
 | 
					    V1LabelSelector,
 | 
				
			||||||
    V1ObjectMeta,
 | 
					    V1ObjectMeta,
 | 
				
			||||||
 | 
					    V1ObjectReference,
 | 
				
			||||||
    V1PodSpec,
 | 
					    V1PodSpec,
 | 
				
			||||||
    V1PodTemplateSpec,
 | 
					    V1PodTemplateSpec,
 | 
				
			||||||
    V1SecretKeySelector,
 | 
					    V1SecretKeySelector,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
 | 
					from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.utils import compare_ports
 | 
				
			||||||
from authentik.outposts.models import Outpost
 | 
					from authentik.outposts.models import Outpost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
@ -35,7 +39,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
        self.outpost = self.controller.outpost
 | 
					        self.outpost = self.controller.outpost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reconcile(self, current: V1Deployment, reference: V1Deployment):
 | 
					    def reconcile(self, current: V1Deployment, reference: V1Deployment):
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					        compare_ports(
 | 
				
			||||||
 | 
					            current.spec.template.spec.containers[0].ports,
 | 
				
			||||||
 | 
					            reference.spec.template.spec.containers[0].ports,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        if current.spec.replicas != reference.spec.replicas:
 | 
					        if current.spec.replicas != reference.spec.replicas:
 | 
				
			||||||
            raise NeedsUpdate()
 | 
					            raise NeedsUpdate()
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
@ -43,6 +50,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
            != reference.spec.template.spec.containers[0].image
 | 
					            != reference.spec.template.spec.containers[0].image
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            raise NeedsUpdate()
 | 
					            raise NeedsUpdate()
 | 
				
			||||||
 | 
					        super().reconcile(current, reference)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_pod_meta(self) -> dict[str, str]:
 | 
					    def get_pod_meta(self) -> dict[str, str]:
 | 
				
			||||||
        """Get common object metadata"""
 | 
					        """Get common object metadata"""
 | 
				
			||||||
@ -50,6 +58,8 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
            "app.kubernetes.io/name": "authentik-outpost",
 | 
					            "app.kubernetes.io/name": "authentik-outpost",
 | 
				
			||||||
            "app.kubernetes.io/managed-by": "goauthentik.io",
 | 
					            "app.kubernetes.io/managed-by": "goauthentik.io",
 | 
				
			||||||
            "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
 | 
					            "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
 | 
				
			||||||
 | 
					            "goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
 | 
				
			||||||
 | 
					            "goauthentik.io/outpost-type": str(self.controller.outpost.type),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_reference_object(self) -> V1Deployment:
 | 
					    def get_reference_object(self) -> V1Deployment:
 | 
				
			||||||
@ -66,6 +76,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
        meta = self.get_object_meta(name=self.name)
 | 
					        meta = self.get_object_meta(name=self.name)
 | 
				
			||||||
        image_name = self.controller.get_container_image()
 | 
					        image_name = self.controller.get_container_image()
 | 
				
			||||||
 | 
					        image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets
 | 
				
			||||||
        return V1Deployment(
 | 
					        return V1Deployment(
 | 
				
			||||||
            metadata=meta,
 | 
					            metadata=meta,
 | 
				
			||||||
            spec=V1DeploymentSpec(
 | 
					            spec=V1DeploymentSpec(
 | 
				
			||||||
@ -74,6 +85,9 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
                template=V1PodTemplateSpec(
 | 
					                template=V1PodTemplateSpec(
 | 
				
			||||||
                    metadata=V1ObjectMeta(labels=self.get_pod_meta()),
 | 
					                    metadata=V1ObjectMeta(labels=self.get_pod_meta()),
 | 
				
			||||||
                    spec=V1PodSpec(
 | 
					                    spec=V1PodSpec(
 | 
				
			||||||
 | 
					                        image_pull_secrets=[
 | 
				
			||||||
 | 
					                            V1ObjectReference(name=secret) for secret in image_pull_secrets
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
                        containers=[
 | 
					                        containers=[
 | 
				
			||||||
                            V1Container(
 | 
					                            V1Container(
 | 
				
			||||||
                                name=str(self.outpost.type),
 | 
					                                name=str(self.outpost.type),
 | 
				
			||||||
@ -89,6 +103,15 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
                                            )
 | 
					                                            )
 | 
				
			||||||
                                        ),
 | 
					                                        ),
 | 
				
			||||||
                                    ),
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    V1EnvVar(
 | 
				
			||||||
 | 
					                                        name="AUTHENTIK_HOST_BROWSER",
 | 
				
			||||||
 | 
					                                        value_from=V1EnvVarSource(
 | 
				
			||||||
 | 
					                                            secret_key_ref=V1SecretKeySelector(
 | 
				
			||||||
 | 
					                                                name=self.name,
 | 
				
			||||||
 | 
					                                                key="authentik_host_browser",
 | 
				
			||||||
 | 
					                                            )
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
                                    V1EnvVar(
 | 
					                                    V1EnvVar(
 | 
				
			||||||
                                        name="AUTHENTIK_TOKEN",
 | 
					                                        name="AUTHENTIK_TOKEN",
 | 
				
			||||||
                                        value_from=V1EnvVarSource(
 | 
					                                        value_from=V1EnvVarSource(
 | 
				
			||||||
@ -109,7 +132,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
                                    ),
 | 
					                                    ),
 | 
				
			||||||
                                ],
 | 
					                                ],
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                        ]
 | 
					                        ],
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,8 @@ from typing import TYPE_CHECKING
 | 
				
			|||||||
from kubernetes.client import CoreV1Api, V1Secret
 | 
					from kubernetes.client import CoreV1Api, V1Secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
 | 
					from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
					    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
				
			||||||
@ -26,7 +27,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
				
			|||||||
    def reconcile(self, current: V1Secret, reference: V1Secret):
 | 
					    def reconcile(self, current: V1Secret, reference: V1Secret):
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					        super().reconcile(current, reference)
 | 
				
			||||||
        for key in reference.data.keys():
 | 
					        for key in reference.data.keys():
 | 
				
			||||||
            if current.data[key] != reference.data[key]:
 | 
					            if key not in current.data or current.data[key] != reference.data[key]:
 | 
				
			||||||
                raise NeedsUpdate()
 | 
					                raise NeedsUpdate()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_reference_object(self) -> V1Secret:
 | 
					    def get_reference_object(self) -> V1Secret:
 | 
				
			||||||
@ -40,6 +41,9 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
				
			|||||||
                    str(self.controller.outpost.config.authentik_host_insecure)
 | 
					                    str(self.controller.outpost.config.authentik_host_insecure)
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                "token": b64string(self.controller.outpost.token.key),
 | 
					                "token": b64string(self.controller.outpost.token.key),
 | 
				
			||||||
 | 
					                "authentik_host_browser": b64string(
 | 
				
			||||||
 | 
					                    self.controller.outpost.config.authentik_host_browser
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,8 +4,9 @@ from typing import TYPE_CHECKING
 | 
				
			|||||||
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
 | 
					from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsRecreate
 | 
					from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 | 
				
			||||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
 | 
					from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.utils import compare_ports
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
					    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
				
			||||||
@ -19,12 +20,12 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
 | 
				
			|||||||
        self.api = CoreV1Api(controller.client)
 | 
					        self.api = CoreV1Api(controller.client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reconcile(self, current: V1Service, reference: V1Service):
 | 
					    def reconcile(self, current: V1Service, reference: V1Service):
 | 
				
			||||||
 | 
					        compare_ports(current.spec.ports, reference.spec.ports)
 | 
				
			||||||
 | 
					        # run the base reconcile last, as that will probably raise NeedsUpdate
 | 
				
			||||||
 | 
					        # after an authentik update. However the ports might have also changed during
 | 
				
			||||||
 | 
					        # the update, so this causes the service to be re-created with higher
 | 
				
			||||||
 | 
					        # priority than being updated.
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					        super().reconcile(current, reference)
 | 
				
			||||||
        if len(current.spec.ports) != len(reference.spec.ports):
 | 
					 | 
				
			||||||
            raise NeedsRecreate()
 | 
					 | 
				
			||||||
        for port in reference.spec.ports:
 | 
					 | 
				
			||||||
            if port not in current.spec.ports:
 | 
					 | 
				
			||||||
                raise NeedsRecreate()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_reference_object(self) -> V1Service:
 | 
					    def get_reference_object(self) -> V1Service:
 | 
				
			||||||
        """Get deployment object for outpost"""
 | 
					        """Get deployment object for outpost"""
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								authentik/outposts/controllers/k8s/triggers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								authentik/outposts/controllers/k8s/triggers.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					"""exceptions used by the kubernetes reconciler to trigger updates"""
 | 
				
			||||||
 | 
					from authentik.lib.sentry import SentryIgnoredException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReconcileTrigger(SentryIgnoredException):
 | 
				
			||||||
 | 
					    """Base trigger raised by child classes to notify us"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NeedsRecreate(ReconcileTrigger):
 | 
				
			||||||
 | 
					    """Exception to trigger a complete recreate of the Kubernetes Object"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NeedsUpdate(ReconcileTrigger):
 | 
				
			||||||
 | 
					    """Exception to trigger an update to the Kubernetes Object"""
 | 
				
			||||||
@ -1,8 +1,11 @@
 | 
				
			|||||||
"""k8s utils"""
 | 
					"""k8s utils"""
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from kubernetes.client.models.v1_container_port import V1ContainerPort
 | 
				
			||||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
 | 
					from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsRecreate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_namespace() -> str:
 | 
					def get_namespace() -> str:
 | 
				
			||||||
    """Get the namespace if we're running in a pod, otherwise default to default"""
 | 
					    """Get the namespace if we're running in a pod, otherwise default to default"""
 | 
				
			||||||
@ -11,3 +14,12 @@ def get_namespace() -> str:
 | 
				
			|||||||
        with open(path, "r", encoding="utf8") as _namespace_file:
 | 
					        with open(path, "r", encoding="utf8") as _namespace_file:
 | 
				
			||||||
            return _namespace_file.read()
 | 
					            return _namespace_file.read()
 | 
				
			||||||
    return "default"
 | 
					    return "default"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def compare_ports(current: list[V1ContainerPort], reference: list[V1ContainerPort]):
 | 
				
			||||||
 | 
					    """Compare ports of a list"""
 | 
				
			||||||
 | 
					    if len(current) != len(reference):
 | 
				
			||||||
 | 
					        raise NeedsRecreate()
 | 
				
			||||||
 | 
					    for port in reference:
 | 
				
			||||||
 | 
					        if port not in current:
 | 
				
			||||||
 | 
					            raise NeedsRecreate()
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,340 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-10 16:18
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import uuid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.apps.registry import Apps
 | 
				
			||||||
 | 
					from django.core.exceptions import FieldError
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import authentik.lib.models
 | 
				
			||||||
 | 
					import authentik.outposts.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def fix_missing_token_identifier(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    User = apps.get_model("authentik_core", "User")
 | 
				
			||||||
 | 
					    Token = apps.get_model("authentik_core", "Token")
 | 
				
			||||||
 | 
					    from authentik.outposts.models import Outpost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for outpost in Outpost.objects.using(schema_editor.connection.alias).all().only("pk"):
 | 
				
			||||||
 | 
					        user_identifier = outpost.user_identifier
 | 
				
			||||||
 | 
					        users = User.objects.filter(username=user_identifier)
 | 
				
			||||||
 | 
					        if not users.exists():
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        tokens = Token.objects.filter(user=users.first())
 | 
				
			||||||
 | 
					        for token in tokens:
 | 
				
			||||||
 | 
					            if token.identifier != outpost.token_identifier:
 | 
				
			||||||
 | 
					                token.identifier = outpost.token_identifier
 | 
				
			||||||
 | 
					                token.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    Outpost = apps.get_model("authentik_outposts", "Outpost")
 | 
				
			||||||
 | 
					    DockerServiceConnection = apps.get_model("authentik_outposts", "DockerServiceConnection")
 | 
				
			||||||
 | 
					    KubernetesServiceConnection = apps.get_model(
 | 
				
			||||||
 | 
					        "authentik_outposts", "KubernetesServiceConnection"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    docker = DockerServiceConnection.objects.filter(local=True).first()
 | 
				
			||||||
 | 
					    k8s = KubernetesServiceConnection.objects.filter(local=True).first()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        for outpost in Outpost.objects.using(db_alias).all().exclude(deployment_type="custom"):
 | 
				
			||||||
 | 
					            if outpost.deployment_type == "kubernetes":
 | 
				
			||||||
 | 
					                outpost.service_connection = k8s
 | 
				
			||||||
 | 
					            elif outpost.deployment_type == "docker":
 | 
				
			||||||
 | 
					                outpost.service_connection = docker
 | 
				
			||||||
 | 
					            outpost.save()
 | 
				
			||||||
 | 
					    except FieldError:
 | 
				
			||||||
 | 
					        # This is triggered during e2e tests when this function is called on an already-upgraded
 | 
				
			||||||
 | 
					        # schema
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def remove_pb_prefix_users(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
 | 
					    alias = schema_editor.connection.alias
 | 
				
			||||||
 | 
					    User = apps.get_model("authentik_core", "User")
 | 
				
			||||||
 | 
					    Outpost = apps.get_model("authentik_outposts", "Outpost")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for outpost in Outpost.objects.using(alias).all():
 | 
				
			||||||
 | 
					        matching = User.objects.using(alias).filter(username=f"pb-outpost-{outpost.uuid.hex}")
 | 
				
			||||||
 | 
					        if matching.exists():
 | 
				
			||||||
 | 
					            matching.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    replaces = [
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0001_initial"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0002_auto_20200826_1306"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0003_auto_20200827_2108"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0004_auto_20200830_1056"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0005_auto_20200909_1733"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0006_auto_20201003_2239"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0007_remove_outpost_channels"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0008_auto_20201014_1547"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0009_fix_missing_token_identifier"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0010_service_connection"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0011_docker_tls_auth"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0012_service_connection_non_unique"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0013_auto_20201203_2009"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0014_auto_20201213_1407"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0015_auto_20201224_1206"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0016_alter_outpost_type"),
 | 
				
			||||||
 | 
					        ("authentik_outposts", "0017_outpost_managed"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_core", "0014_auto_20201018_1158"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0016_auto_20201202_2234"),
 | 
				
			||||||
 | 
					        ("authentik_crypto", "0002_create_self_signed_kp"),
 | 
				
			||||||
 | 
					        ("authentik_core", "0008_auto_20200824_1532"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="Outpost",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("name", models.TextField()),
 | 
				
			||||||
 | 
					                ("providers", models.ManyToManyField(to="authentik_core.Provider")),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "_config",
 | 
				
			||||||
 | 
					                    models.JSONField(default=authentik.outposts.models.default_outpost_config),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("type", models.TextField(choices=[("proxy", "Proxy")], default="proxy")),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "deployment_type",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("kubernetes", "Kubernetes"),
 | 
				
			||||||
 | 
					                            ("docker", "Docker"),
 | 
				
			||||||
 | 
					                            ("custom", "Custom"),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        default="custom",
 | 
				
			||||||
 | 
					                        help_text="Select between authentik-managed deployment types or a custom deployment.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=fix_missing_token_identifier,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="OutpostServiceConnection",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "uuid",
 | 
				
			||||||
 | 
					                    models.UUIDField(
 | 
				
			||||||
 | 
					                        default=uuid.uuid4, editable=False, primary_key=True, serialize=False
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("name", models.TextField()),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "local",
 | 
				
			||||||
 | 
					                    models.BooleanField(
 | 
				
			||||||
 | 
					                        default=False,
 | 
				
			||||||
 | 
					                        help_text="If enabled, use the local connection. Required Docker socket/Kubernetes Integration",
 | 
				
			||||||
 | 
					                        unique=True,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="DockerServiceConnection",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "outpostserviceconnection_ptr",
 | 
				
			||||||
 | 
					                    models.OneToOneField(
 | 
				
			||||||
 | 
					                        auto_created=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        parent_link=True,
 | 
				
			||||||
 | 
					                        primary_key=True,
 | 
				
			||||||
 | 
					                        serialize=False,
 | 
				
			||||||
 | 
					                        to="authentik_outposts.outpostserviceconnection",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("url", models.TextField()),
 | 
				
			||||||
 | 
					                ("tls", models.BooleanField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            bases=("authentik_outposts.outpostserviceconnection",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="KubernetesServiceConnection",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "outpostserviceconnection_ptr",
 | 
				
			||||||
 | 
					                    models.OneToOneField(
 | 
				
			||||||
 | 
					                        auto_created=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        parent_link=True,
 | 
				
			||||||
 | 
					                        primary_key=True,
 | 
				
			||||||
 | 
					                        serialize=False,
 | 
				
			||||||
 | 
					                        to="authentik_outposts.outpostserviceconnection",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                ("kubeconfig", models.JSONField()),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            bases=("authentik_outposts.outpostserviceconnection",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="outpost",
 | 
				
			||||||
 | 
					            name="service_connection",
 | 
				
			||||||
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Select Service-Connection authentik should use to manage this outpost. Leave empty if authentik should not handle the deployment.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.SET_DEFAULT,
 | 
				
			||||||
 | 
					                to="authentik_outposts.outpostserviceconnection",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=migrate_to_service_connection,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="outpost",
 | 
				
			||||||
 | 
					            name="deployment_type",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="dockerserviceconnection",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Docker Service-Connection",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Docker Service-Connections",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="kubernetesserviceconnection",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Kubernetes Service-Connection",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Kubernetes Service-Connections",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="outpost",
 | 
				
			||||||
 | 
					            name="service_connection",
 | 
				
			||||||
 | 
					            field=authentik.lib.models.InheritanceForeignKey(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Select Service-Connection authentik should use to manage this outpost. Leave empty if authentik should not handle the deployment.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.SET_DEFAULT,
 | 
				
			||||||
 | 
					                to="authentik_outposts.outpostserviceconnection",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterModelOptions(
 | 
				
			||||||
 | 
					            name="outpostserviceconnection",
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Outpost Service-Connection",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Outpost Service-Connections",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="kubernetesserviceconnection",
 | 
				
			||||||
 | 
					            name="kubeconfig",
 | 
				
			||||||
 | 
					            field=models.JSONField(
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Paste your kubeconfig here. authentik will automatically use the currently selected context.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            preserve_default=False,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RemoveField(
 | 
				
			||||||
 | 
					            model_name="dockerserviceconnection",
 | 
				
			||||||
 | 
					            name="tls",
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="dockerserviceconnection",
 | 
				
			||||||
 | 
					            name="tls_authentication",
 | 
				
			||||||
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Certificate/Key used for authentication. Can be left empty for no authentication.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.SET_DEFAULT,
 | 
				
			||||||
 | 
					                related_name="+",
 | 
				
			||||||
 | 
					                to="authentik_crypto.certificatekeypair",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="dockerserviceconnection",
 | 
				
			||||||
 | 
					            name="tls_verification",
 | 
				
			||||||
 | 
					            field=models.ForeignKey(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="CA which the endpoint's Certificate is verified against. Can be left empty for no validation.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                on_delete=django.db.models.deletion.SET_DEFAULT,
 | 
				
			||||||
 | 
					                related_name="+",
 | 
				
			||||||
 | 
					                to="authentik_crypto.certificatekeypair",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="outpostserviceconnection",
 | 
				
			||||||
 | 
					            name="local",
 | 
				
			||||||
 | 
					            field=models.BooleanField(
 | 
				
			||||||
 | 
					                default=False,
 | 
				
			||||||
 | 
					                help_text="If enabled, use the local connection. Required Docker socket/Kubernetes Integration",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=remove_pb_prefix_users,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
					            code=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."
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="kubernetesserviceconnection",
 | 
				
			||||||
 | 
					            name="kubeconfig",
 | 
				
			||||||
 | 
					            field=models.JSONField(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                help_text="Paste your kubeconfig here. authentik will automatically use the currently selected context.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="outpost",
 | 
				
			||||||
 | 
					            name="type",
 | 
				
			||||||
 | 
					            field=models.TextField(choices=[("proxy", "Proxy"), ("ldap", "Ldap")], default="proxy"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="outpost",
 | 
				
			||||||
 | 
					            name="managed",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                default=None,
 | 
				
			||||||
 | 
					                help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
 | 
				
			||||||
 | 
					                null=True,
 | 
				
			||||||
 | 
					                unique=True,
 | 
				
			||||||
 | 
					                verbose_name="Managed by authentik",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -64,6 +64,7 @@ class OutpostConfig:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    authentik_host: str = ""
 | 
					    authentik_host: str = ""
 | 
				
			||||||
    authentik_host_insecure: bool = False
 | 
					    authentik_host_insecure: bool = False
 | 
				
			||||||
 | 
					    authentik_host_browser: str = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log_level: str = CONFIG.y("log_level")
 | 
					    log_level: str = CONFIG.y("log_level")
 | 
				
			||||||
    error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
 | 
					    error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
 | 
				
			||||||
@ -71,6 +72,9 @@ class OutpostConfig:
 | 
				
			|||||||
    object_naming_template: str = field(default="ak-outpost-%(name)s")
 | 
					    object_naming_template: str = field(default="ak-outpost-%(name)s")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    docker_network: Optional[str] = field(default=None)
 | 
					    docker_network: Optional[str] = field(default=None)
 | 
				
			||||||
 | 
					    docker_map_ports: bool = field(default=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    container_image: Optional[str] = field(default=None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    kubernetes_replicas: int = field(default=1)
 | 
					    kubernetes_replicas: int = field(default=1)
 | 
				
			||||||
    kubernetes_namespace: str = field(default_factory=get_namespace)
 | 
					    kubernetes_namespace: str = field(default_factory=get_namespace)
 | 
				
			||||||
@ -78,6 +82,7 @@ class OutpostConfig:
 | 
				
			|||||||
    kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
 | 
					    kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
 | 
				
			||||||
    kubernetes_service_type: str = field(default="ClusterIP")
 | 
					    kubernetes_service_type: str = field(default="ClusterIP")
 | 
				
			||||||
    kubernetes_disabled_components: list[str] = field(default_factory=list)
 | 
					    kubernetes_disabled_components: list[str] = field(default_factory=list)
 | 
				
			||||||
 | 
					    kubernetes_image_pull_secrets: Optional[list[str]] = field(default_factory=list)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OutpostModel(Model):
 | 
					class OutpostModel(Model):
 | 
				
			||||||
@ -339,19 +344,8 @@ class Outpost(ManagedModel):
 | 
				
			|||||||
        """Username for service user"""
 | 
					        """Username for service user"""
 | 
				
			||||||
        return f"ak-outpost-{self.uuid.hex}"
 | 
					        return f"ak-outpost-{self.uuid.hex}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    def build_user_permissions(self, user: User):
 | 
				
			||||||
    def user(self) -> User:
 | 
					        """Create per-object and global permissions for outpost service-account"""
 | 
				
			||||||
        """Get/create user with access to all required objects"""
 | 
					 | 
				
			||||||
        users = User.objects.filter(username=self.user_identifier)
 | 
					 | 
				
			||||||
        if not users.exists():
 | 
					 | 
				
			||||||
            user: User = User.objects.create(username=self.user_identifier)
 | 
					 | 
				
			||||||
            user.set_unusable_password()
 | 
					 | 
				
			||||||
            user.save()
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            user = users.first()
 | 
					 | 
				
			||||||
        user.attributes[USER_ATTRIBUTE_SA] = True
 | 
					 | 
				
			||||||
        user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
 | 
					 | 
				
			||||||
        user.save()
 | 
					 | 
				
			||||||
        # To ensure the user only has the correct permissions, we delete all of them and re-add
 | 
					        # To ensure the user only has the correct permissions, we delete all of them and re-add
 | 
				
			||||||
        # the ones the user needs
 | 
					        # the ones the user needs
 | 
				
			||||||
        with transaction.atomic():
 | 
					        with transaction.atomic():
 | 
				
			||||||
@ -395,6 +389,23 @@ class Outpost(ManagedModel):
 | 
				
			|||||||
            "Updated service account's permissions",
 | 
					            "Updated service account's permissions",
 | 
				
			||||||
            perms=UserObjectPermission.objects.filter(user=user),
 | 
					            perms=UserObjectPermission.objects.filter(user=user),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def user(self) -> User:
 | 
				
			||||||
 | 
					        """Get/create user with access to all required objects"""
 | 
				
			||||||
 | 
					        users = User.objects.filter(username=self.user_identifier)
 | 
				
			||||||
 | 
					        should_create_user = not users.exists()
 | 
				
			||||||
 | 
					        if should_create_user:
 | 
				
			||||||
 | 
					            user: User = User.objects.create(username=self.user_identifier)
 | 
				
			||||||
 | 
					            user.set_unusable_password()
 | 
				
			||||||
 | 
					            user.save()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            user = users.first()
 | 
				
			||||||
 | 
					        user.attributes[USER_ATTRIBUTE_SA] = True
 | 
				
			||||||
 | 
					        user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
 | 
				
			||||||
 | 
					        user.save()
 | 
				
			||||||
 | 
					        if should_create_user:
 | 
				
			||||||
 | 
					            self.build_user_permissions(user)
 | 
				
			||||||
        return user
 | 
					        return user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,12 @@ from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
 | 
				
			|||||||
from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
 | 
					from kubernetes.config.kube_config import KUBE_CONFIG_DEFAULT_LOCATION
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import (
 | 
				
			||||||
 | 
					    MonitoredTask,
 | 
				
			||||||
 | 
					    TaskResult,
 | 
				
			||||||
 | 
					    TaskResultStatus,
 | 
				
			||||||
 | 
					    prefill_task,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from authentik.lib.utils.reflection import path_to_class
 | 
					from authentik.lib.utils.reflection import path_to_class
 | 
				
			||||||
from authentik.outposts.controllers.base import BaseController, ControllerException
 | 
					from authentik.outposts.controllers.base import BaseController, ControllerException
 | 
				
			||||||
from authentik.outposts.models import (
 | 
					from authentik.outposts.models import (
 | 
				
			||||||
@ -71,6 +76,7 @@ def outpost_service_connection_state(connection_pk: Any):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
 | 
					@prefill_task()
 | 
				
			||||||
def outpost_service_connection_monitor(self: MonitoredTask):
 | 
					def outpost_service_connection_monitor(self: MonitoredTask):
 | 
				
			||||||
    """Regularly check the state of Outpost Service Connections"""
 | 
					    """Regularly check the state of Outpost Service Connections"""
 | 
				
			||||||
    connections = OutpostServiceConnection.objects.all()
 | 
					    connections = OutpostServiceConnection.objects.all()
 | 
				
			||||||
@ -120,12 +126,14 @@ def outpost_controller(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
 | 
					@prefill_task()
 | 
				
			||||||
def outpost_token_ensurer(self: MonitoredTask):
 | 
					def outpost_token_ensurer(self: MonitoredTask):
 | 
				
			||||||
    """Periodically ensure that all Outposts have valid Service Accounts
 | 
					    """Periodically ensure that all Outposts have valid Service Accounts
 | 
				
			||||||
    and Tokens"""
 | 
					    and Tokens"""
 | 
				
			||||||
    all_outposts = Outpost.objects.all()
 | 
					    all_outposts = Outpost.objects.all()
 | 
				
			||||||
    for outpost in all_outposts:
 | 
					    for outpost in all_outposts:
 | 
				
			||||||
        _ = outpost.token
 | 
					        _ = outpost.token
 | 
				
			||||||
 | 
					        outpost.build_user_permissions(outpost.user)
 | 
				
			||||||
    self.set_status(
 | 
					    self.set_status(
 | 
				
			||||||
        TaskResult(
 | 
					        TaskResult(
 | 
				
			||||||
            TaskResultStatus.SUCCESSFUL,
 | 
					            TaskResultStatus.SUCCESSFUL,
 | 
				
			||||||
@ -181,7 +189,7 @@ def outpost_post_save(model_class: str, model_pk: Any):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def outpost_send_update(model_instace: Model):
 | 
					def outpost_send_update(model_instace: Model):
 | 
				
			||||||
    """Send outpost update to all registered outposts, irregardless to which authentik
 | 
					    """Send outpost update to all registered outposts, regardless to which authentik
 | 
				
			||||||
    instance they are connected"""
 | 
					    instance they are connected"""
 | 
				
			||||||
    channel_layer = get_channel_layer()
 | 
					    channel_layer = get_channel_layer()
 | 
				
			||||||
    if isinstance(model_instace, OutpostModel):
 | 
					    if isinstance(model_instace, OutpostModel):
 | 
				
			||||||
@ -196,7 +204,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
 | 
				
			|||||||
    # Ensure token again, because this function is called when anything related to an
 | 
					    # Ensure token again, because this function is called when anything related to an
 | 
				
			||||||
    # OutpostModel is saved, so we can be sure permissions are right
 | 
					    # OutpostModel is saved, so we can be sure permissions are right
 | 
				
			||||||
    _ = outpost.token
 | 
					    _ = outpost.token
 | 
				
			||||||
    _ = outpost.user
 | 
					    outpost.build_user_permissions(outpost.user)
 | 
				
			||||||
    if not layer:  # pragma: no cover
 | 
					    if not layer:  # pragma: no cover
 | 
				
			||||||
        layer = get_channel_layer()
 | 
					        layer = get_channel_layer()
 | 
				
			||||||
    for state in OutpostState.for_outpost(outpost):
 | 
					    for state in OutpostState.for_outpost(outpost):
 | 
				
			||||||
@ -208,7 +216,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
 | 
				
			|||||||
@CELERY_APP.task()
 | 
					@CELERY_APP.task()
 | 
				
			||||||
def outpost_local_connection():
 | 
					def outpost_local_connection():
 | 
				
			||||||
    """Checks the local environment and create Service connections."""
 | 
					    """Checks the local environment and create Service connections."""
 | 
				
			||||||
    # Explicitly check against token filename, as thats
 | 
					    # Explicitly check against token filename, as that's
 | 
				
			||||||
    # only present when the integration is enabled
 | 
					    # only present when the integration is enabled
 | 
				
			||||||
    if Path(SERVICE_TOKEN_FILENAME).exists():
 | 
					    if Path(SERVICE_TOKEN_FILENAME).exists():
 | 
				
			||||||
        LOGGER.debug("Detected in-cluster Kubernetes Config")
 | 
					        LOGGER.debug("Detected in-cluster Kubernetes Config")
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,8 @@
 | 
				
			|||||||
from typing import OrderedDict
 | 
					from typing import OrderedDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.core.exceptions import ObjectDoesNotExist
 | 
					from django.core.exceptions import ObjectDoesNotExist
 | 
				
			||||||
 | 
					from django_filters.filters import BooleanFilter, ModelMultipleChoiceFilter
 | 
				
			||||||
 | 
					from django_filters.filterset import FilterSet
 | 
				
			||||||
from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError
 | 
					from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
@ -96,6 +98,22 @@ class PolicyBindingSerializer(ModelSerializer):
 | 
				
			|||||||
        return attrs
 | 
					        return attrs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PolicyBindingFilter(FilterSet):
 | 
				
			||||||
 | 
					    """Filter for PolicyBindings"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    target_in = ModelMultipleChoiceFilter(
 | 
				
			||||||
 | 
					        field_name="target__pbm_uuid",
 | 
				
			||||||
 | 
					        to_field_name="pbm_uuid",
 | 
				
			||||||
 | 
					        queryset=PolicyBindingModel.objects.select_subclasses(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    policy__isnull = BooleanFilter("policy", "isnull")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        model = PolicyBinding
 | 
				
			||||||
 | 
					        fields = ["policy", "policy__isnull", "target", "target_in", "enabled", "order", "timeout"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
 | 
					class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
 | 
				
			||||||
    """PolicyBinding Viewset"""
 | 
					    """PolicyBinding Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -105,5 +123,6 @@ class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        .prefetch_related("policy")
 | 
					        .prefetch_related("policy")
 | 
				
			||||||
    )  # prefetching policy so we resolve the subclass
 | 
					    )  # prefetching policy so we resolve the subclass
 | 
				
			||||||
    serializer_class = PolicyBindingSerializer
 | 
					    serializer_class = PolicyBindingSerializer
 | 
				
			||||||
    filterset_fields = ["policy", "target", "enabled", "order", "timeout"]
 | 
					 | 
				
			||||||
    search_fields = ["policy__name"]
 | 
					    search_fields = ["policy__name"]
 | 
				
			||||||
 | 
					    filterset_class = PolicyBindingFilter
 | 
				
			||||||
 | 
					    ordering = ["target", "order"]
 | 
				
			||||||
 | 
				
			|||||||
@ -87,6 +87,7 @@ class PolicyViewSet(
 | 
				
			|||||||
        "promptstage": ["isnull"],
 | 
					        "promptstage": ["isnull"],
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    search_fields = ["name"]
 | 
					    search_fields = ["name"]
 | 
				
			||||||
 | 
					    ordering = ["name"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):  # pragma: no cover
 | 
					    def get_queryset(self):  # pragma: no cover
 | 
				
			||||||
        return Policy.objects.select_subclasses().prefetch_related("bindings", "promptstage_set")
 | 
					        return Policy.objects.select_subclasses().prefetch_related("bindings", "promptstage_set")
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,173 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-10 16:11
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.db.models.deletion
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    replaces = [
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0001_initial"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0002_auto_20201230_2046"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0003_auto_20210110_1907"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0004_auto_20210112_2158"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0005_auto_20210202_1821"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0006_auto_20210203_1134"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0007_auto_20210209_1657"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0008_auto_20210213_1640"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0009_auto_20210215_2159"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0010_auto_20210222_1821"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0011_auto_20210302_0856"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0012_auto_20210323_1339"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0013_alter_eventmatcherpolicy_app"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0014_alter_eventmatcherpolicy_app"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0015_alter_eventmatcherpolicy_app"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0016_alter_eventmatcherpolicy_action"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0017_alter_eventmatcherpolicy_action"),
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0018_alter_eventmatcherpolicy_action"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    initial = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_policies", "0004_policy_execution_logging"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.CreateModel(
 | 
				
			||||||
 | 
					            name="EventMatcherPolicy",
 | 
				
			||||||
 | 
					            fields=[
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "policy_ptr",
 | 
				
			||||||
 | 
					                    models.OneToOneField(
 | 
				
			||||||
 | 
					                        auto_created=True,
 | 
				
			||||||
 | 
					                        on_delete=django.db.models.deletion.CASCADE,
 | 
				
			||||||
 | 
					                        parent_link=True,
 | 
				
			||||||
 | 
					                        primary_key=True,
 | 
				
			||||||
 | 
					                        serialize=False,
 | 
				
			||||||
 | 
					                        to="authentik_policies.policy",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "action",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        blank=True,
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("login", "Login"),
 | 
				
			||||||
 | 
					                            ("login_failed", "Login Failed"),
 | 
				
			||||||
 | 
					                            ("logout", "Logout"),
 | 
				
			||||||
 | 
					                            ("user_write", "User Write"),
 | 
				
			||||||
 | 
					                            ("suspicious_request", "Suspicious Request"),
 | 
				
			||||||
 | 
					                            ("password_set", "Password Set"),
 | 
				
			||||||
 | 
					                            ("secret_view", "Secret View"),
 | 
				
			||||||
 | 
					                            ("secret_rotate", "Secret Rotate"),
 | 
				
			||||||
 | 
					                            ("invitation_used", "Invite Used"),
 | 
				
			||||||
 | 
					                            ("authorize_application", "Authorize Application"),
 | 
				
			||||||
 | 
					                            ("source_linked", "Source Linked"),
 | 
				
			||||||
 | 
					                            ("impersonation_started", "Impersonation Started"),
 | 
				
			||||||
 | 
					                            ("impersonation_ended", "Impersonation Ended"),
 | 
				
			||||||
 | 
					                            ("policy_execution", "Policy Execution"),
 | 
				
			||||||
 | 
					                            ("policy_exception", "Policy Exception"),
 | 
				
			||||||
 | 
					                            ("property_mapping_exception", "Property Mapping Exception"),
 | 
				
			||||||
 | 
					                            ("system_task_execution", "System Task Execution"),
 | 
				
			||||||
 | 
					                            ("system_task_exception", "System Task Exception"),
 | 
				
			||||||
 | 
					                            ("system_exception", "System Exception"),
 | 
				
			||||||
 | 
					                            ("configuration_error", "Configuration Error"),
 | 
				
			||||||
 | 
					                            ("model_created", "Model Created"),
 | 
				
			||||||
 | 
					                            ("model_updated", "Model Updated"),
 | 
				
			||||||
 | 
					                            ("model_deleted", "Model Deleted"),
 | 
				
			||||||
 | 
					                            ("email_sent", "Email Sent"),
 | 
				
			||||||
 | 
					                            ("update_available", "Update Available"),
 | 
				
			||||||
 | 
					                            ("custom_", "Custom Prefix"),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        help_text="Match created events with this action type. When left empty, all action types will be matched.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "client_ip",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        blank=True,
 | 
				
			||||||
 | 
					                        help_text="Matches Event's Client IP (strict matching, for network matching use an Expression Policy)",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                (
 | 
				
			||||||
 | 
					                    "app",
 | 
				
			||||||
 | 
					                    models.TextField(
 | 
				
			||||||
 | 
					                        blank=True,
 | 
				
			||||||
 | 
					                        choices=[
 | 
				
			||||||
 | 
					                            ("authentik.admin", "authentik Admin"),
 | 
				
			||||||
 | 
					                            ("authentik.api", "authentik API"),
 | 
				
			||||||
 | 
					                            ("authentik.events", "authentik Events"),
 | 
				
			||||||
 | 
					                            ("authentik.crypto", "authentik Crypto"),
 | 
				
			||||||
 | 
					                            ("authentik.flows", "authentik Flows"),
 | 
				
			||||||
 | 
					                            ("authentik.outposts", "authentik Outpost"),
 | 
				
			||||||
 | 
					                            ("authentik.lib", "authentik lib"),
 | 
				
			||||||
 | 
					                            ("authentik.policies", "authentik Policies"),
 | 
				
			||||||
 | 
					                            ("authentik.policies.dummy", "authentik Policies.Dummy"),
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                "authentik.policies.event_matcher",
 | 
				
			||||||
 | 
					                                "authentik Policies.Event Matcher",
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            ("authentik.policies.expiry", "authentik Policies.Expiry"),
 | 
				
			||||||
 | 
					                            ("authentik.policies.expression", "authentik Policies.Expression"),
 | 
				
			||||||
 | 
					                            ("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
 | 
				
			||||||
 | 
					                            ("authentik.policies.password", "authentik Policies.Password"),
 | 
				
			||||||
 | 
					                            ("authentik.policies.reputation", "authentik Policies.Reputation"),
 | 
				
			||||||
 | 
					                            ("authentik.providers.proxy", "authentik Providers.Proxy"),
 | 
				
			||||||
 | 
					                            ("authentik.providers.ldap", "authentik Providers.LDAP"),
 | 
				
			||||||
 | 
					                            ("authentik.providers.oauth2", "authentik Providers.OAuth2"),
 | 
				
			||||||
 | 
					                            ("authentik.providers.saml", "authentik Providers.SAML"),
 | 
				
			||||||
 | 
					                            ("authentik.recovery", "authentik Recovery"),
 | 
				
			||||||
 | 
					                            ("authentik.sources.ldap", "authentik Sources.LDAP"),
 | 
				
			||||||
 | 
					                            ("authentik.sources.oauth", "authentik Sources.OAuth"),
 | 
				
			||||||
 | 
					                            ("authentik.sources.plex", "authentik Sources.Plex"),
 | 
				
			||||||
 | 
					                            ("authentik.sources.saml", "authentik Sources.SAML"),
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                "authentik.stages.authenticator_duo",
 | 
				
			||||||
 | 
					                                "authentik Stages.Authenticator.Duo",
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                "authentik.stages.authenticator_static",
 | 
				
			||||||
 | 
					                                "authentik Stages.Authenticator.Static",
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                "authentik.stages.authenticator_totp",
 | 
				
			||||||
 | 
					                                "authentik Stages.Authenticator.TOTP",
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                "authentik.stages.authenticator_validate",
 | 
				
			||||||
 | 
					                                "authentik Stages.Authenticator.Validate",
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            (
 | 
				
			||||||
 | 
					                                "authentik.stages.authenticator_webauthn",
 | 
				
			||||||
 | 
					                                "authentik Stages.Authenticator.WebAuthn",
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            ("authentik.stages.captcha", "authentik Stages.Captcha"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.consent", "authentik Stages.Consent"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.deny", "authentik Stages.Deny"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.dummy", "authentik Stages.Dummy"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.email", "authentik Stages.Email"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.identification", "authentik Stages.Identification"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.invitation", "authentik Stages.User Invitation"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.password", "authentik Stages.Password"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.prompt", "authentik Stages.Prompt"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.user_delete", "authentik Stages.User Delete"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.user_login", "authentik Stages.User Login"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.user_logout", "authentik Stages.User Logout"),
 | 
				
			||||||
 | 
					                            ("authentik.stages.user_write", "authentik Stages.User Write"),
 | 
				
			||||||
 | 
					                            ("authentik.tenants", "authentik Tenants"),
 | 
				
			||||||
 | 
					                            ("authentik.core", "authentik Core"),
 | 
				
			||||||
 | 
					                            ("authentik.managed", "authentik Managed"),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        default="",
 | 
				
			||||||
 | 
					                        help_text="Match events created by selected application. When left empty, all applications are matched.",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            options={
 | 
				
			||||||
 | 
					                "verbose_name": "Event Matcher Policy",
 | 
				
			||||||
 | 
					                "verbose_name_plural": "Event Matcher Policies",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            bases=("authentik_policies.policy",),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -0,0 +1,79 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.8 on 2021-10-09 17:43
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_policies_event_matcher", "0018_alter_eventmatcherpolicy_action"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="eventmatcherpolicy",
 | 
				
			||||||
 | 
					            name="app",
 | 
				
			||||||
 | 
					            field=models.TextField(
 | 
				
			||||||
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                choices=[
 | 
				
			||||||
 | 
					                    ("authentik.admin", "authentik Admin"),
 | 
				
			||||||
 | 
					                    ("authentik.api", "authentik API"),
 | 
				
			||||||
 | 
					                    ("authentik.crypto", "authentik Crypto"),
 | 
				
			||||||
 | 
					                    ("authentik.events", "authentik Events"),
 | 
				
			||||||
 | 
					                    ("authentik.flows", "authentik Flows"),
 | 
				
			||||||
 | 
					                    ("authentik.lib", "authentik lib"),
 | 
				
			||||||
 | 
					                    ("authentik.outposts", "authentik Outpost"),
 | 
				
			||||||
 | 
					                    ("authentik.policies.dummy", "authentik Policies.Dummy"),
 | 
				
			||||||
 | 
					                    ("authentik.policies.event_matcher", "authentik Policies.Event Matcher"),
 | 
				
			||||||
 | 
					                    ("authentik.policies.expiry", "authentik Policies.Expiry"),
 | 
				
			||||||
 | 
					                    ("authentik.policies.expression", "authentik Policies.Expression"),
 | 
				
			||||||
 | 
					                    ("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
 | 
				
			||||||
 | 
					                    ("authentik.policies.password", "authentik Policies.Password"),
 | 
				
			||||||
 | 
					                    ("authentik.policies.reputation", "authentik Policies.Reputation"),
 | 
				
			||||||
 | 
					                    ("authentik.policies", "authentik Policies"),
 | 
				
			||||||
 | 
					                    ("authentik.providers.ldap", "authentik Providers.LDAP"),
 | 
				
			||||||
 | 
					                    ("authentik.providers.oauth2", "authentik Providers.OAuth2"),
 | 
				
			||||||
 | 
					                    ("authentik.providers.proxy", "authentik Providers.Proxy"),
 | 
				
			||||||
 | 
					                    ("authentik.providers.saml", "authentik Providers.SAML"),
 | 
				
			||||||
 | 
					                    ("authentik.recovery", "authentik Recovery"),
 | 
				
			||||||
 | 
					                    ("authentik.sources.ldap", "authentik Sources.LDAP"),
 | 
				
			||||||
 | 
					                    ("authentik.sources.oauth", "authentik Sources.OAuth"),
 | 
				
			||||||
 | 
					                    ("authentik.sources.plex", "authentik Sources.Plex"),
 | 
				
			||||||
 | 
					                    ("authentik.sources.saml", "authentik Sources.SAML"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.authenticator_duo", "authentik Stages.Authenticator.Duo"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.authenticator_sms", "authentik Stages.Authenticator.SMS"),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "authentik.stages.authenticator_static",
 | 
				
			||||||
 | 
					                        "authentik Stages.Authenticator.Static",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    ("authentik.stages.authenticator_totp", "authentik Stages.Authenticator.TOTP"),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "authentik.stages.authenticator_validate",
 | 
				
			||||||
 | 
					                        "authentik Stages.Authenticator.Validate",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "authentik.stages.authenticator_webauthn",
 | 
				
			||||||
 | 
					                        "authentik Stages.Authenticator.WebAuthn",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    ("authentik.stages.captcha", "authentik Stages.Captcha"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.consent", "authentik Stages.Consent"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.deny", "authentik Stages.Deny"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.dummy", "authentik Stages.Dummy"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.email", "authentik Stages.Email"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.identification", "authentik Stages.Identification"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.invitation", "authentik Stages.User Invitation"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.password", "authentik Stages.Password"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.prompt", "authentik Stages.Prompt"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.user_delete", "authentik Stages.User Delete"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.user_login", "authentik Stages.User Login"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.user_logout", "authentik Stages.User Logout"),
 | 
				
			||||||
 | 
					                    ("authentik.stages.user_write", "authentik Stages.User Write"),
 | 
				
			||||||
 | 
					                    ("authentik.tenants", "authentik Tenants"),
 | 
				
			||||||
 | 
					                    ("authentik.core", "authentik Core"),
 | 
				
			||||||
 | 
					                    ("authentik.managed", "authentik Managed"),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					                default="",
 | 
				
			||||||
 | 
					                help_text="Match events created by selected application. When left empty, all applications are matched.",
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -65,6 +65,7 @@ class TestPasswordPolicyFlow(APITestCase):
 | 
				
			|||||||
                        "placeholder": "PASSWORD_PLACEHOLDER",
 | 
					                        "placeholder": "PASSWORD_PLACEHOLDER",
 | 
				
			||||||
                        "required": True,
 | 
					                        "required": True,
 | 
				
			||||||
                        "type": "password",
 | 
					                        "type": "password",
 | 
				
			||||||
 | 
					                        "sub_text": "",
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
                "flow_info": {
 | 
					                "flow_info": {
 | 
				
			||||||
 | 
				
			|||||||
@ -46,7 +46,7 @@ def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PolicyProcess(PROCESS_CLASS):
 | 
					class PolicyProcess(PROCESS_CLASS):
 | 
				
			||||||
    """Evaluate a single policy within a seprate process"""
 | 
					    """Evaluate a single policy within a separate process"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    connection: Connection
 | 
					    connection: Connection
 | 
				
			||||||
    binding: PolicyBinding
 | 
					    binding: PolicyBinding
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,7 @@ def update_score(request: HttpRequest, username: str, amount: int):
 | 
				
			|||||||
@receiver(user_login_failed)
 | 
					@receiver(user_login_failed)
 | 
				
			||||||
# pylint: disable=unused-argument
 | 
					# pylint: disable=unused-argument
 | 
				
			||||||
def handle_failed_login(sender, request, credentials, **_):
 | 
					def handle_failed_login(sender, request, credentials, **_):
 | 
				
			||||||
    """Lower Score for failed loging attempts"""
 | 
					    """Lower Score for failed login attempts"""
 | 
				
			||||||
    if "username" in credentials:
 | 
					    if "username" in credentials:
 | 
				
			||||||
        update_score(request, credentials.get("username"), -1)
 | 
					        update_score(request, credentials.get("username"), -1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,12 @@
 | 
				
			|||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import (
 | 
				
			||||||
 | 
					    MonitoredTask,
 | 
				
			||||||
 | 
					    TaskResult,
 | 
				
			||||||
 | 
					    TaskResultStatus,
 | 
				
			||||||
 | 
					    prefill_task,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from authentik.policies.reputation.models import IPReputation, UserReputation
 | 
					from authentik.policies.reputation.models import IPReputation, UserReputation
 | 
				
			||||||
from authentik.policies.reputation.signals import CACHE_KEY_IP_PREFIX, CACHE_KEY_USER_PREFIX
 | 
					from authentik.policies.reputation.signals import CACHE_KEY_IP_PREFIX, CACHE_KEY_USER_PREFIX
 | 
				
			||||||
from authentik.root.celery import CELERY_APP
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
@ -11,6 +16,7 @@ LOGGER = get_logger()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
 | 
					@prefill_task()
 | 
				
			||||||
def save_ip_reputation(self: MonitoredTask):
 | 
					def save_ip_reputation(self: MonitoredTask):
 | 
				
			||||||
    """Save currently cached reputation to database"""
 | 
					    """Save currently cached reputation to database"""
 | 
				
			||||||
    objects_to_update = []
 | 
					    objects_to_update = []
 | 
				
			||||||
@ -24,6 +30,7 @@ def save_ip_reputation(self: MonitoredTask):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
 | 
					@prefill_task()
 | 
				
			||||||
def save_user_reputation(self: MonitoredTask):
 | 
					def save_user_reputation(self: MonitoredTask):
 | 
				
			||||||
    """Save currently cached reputation to database"""
 | 
					    """Save currently cached reputation to database"""
 | 
				
			||||||
    objects_to_update = []
 | 
					    objects_to_update = []
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ from authentik.policies.types import PolicyRequest
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def clear_policy_cache():
 | 
					def clear_policy_cache():
 | 
				
			||||||
    """Ensure no policy-related keys are stil cached"""
 | 
					    """Ensure no policy-related keys are still cached"""
 | 
				
			||||||
    keys = cache.keys("policy_*")
 | 
					    keys = cache.keys("policy_*")
 | 
				
			||||||
    cache.delete(keys)
 | 
					    cache.delete(keys)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user