Compare commits
	
		
			148 Commits
		
	
	
		
			version/20
			...
			version-20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fe5d22ce6c | |||
| 0e30b6ee55 | |||
| 6cbba45291 | |||
| ba023a3bba | |||
| 6c805bcf32 | |||
| bc7d5042df | |||
| de3e1c3dbc | |||
| 3c6aac5435 | |||
| eeb755ab7d | |||
| 70d0dd51a5 | |||
| 073dd8b560 | |||
| b5d2924d46 | |||
| 597e279f34 | |||
| fc28def83d | |||
| f6efdfded4 | |||
| 91312496e0 | |||
| b557b4337d | |||
| bfde186aa0 | |||
| 2bd75dd1a9 | |||
| 27ab31a9b0 | |||
| 44a8b737d9 | |||
| b939ee7a09 | |||
| 0bae550520 | |||
| b5cc2f2bda | |||
| 9ad4cf1db9 | |||
| 9dbafaaea2 | |||
| 2db8b07578 | |||
| 7c1a7bfd9d | |||
| b7ef076798 | |||
| 37c29a073e | |||
| 0c288ea64b | |||
| 2476475174 | |||
| 71913c8164 | |||
| 6ec8432217 | |||
| 7a12c0e4d1 | |||
| 23a7eba16b | |||
| 3ba84a8e8b | |||
| 75476217a0 | |||
| 7771c0b905 | |||
| 3378e82ec7 | |||
| 126e43dea4 | |||
| f725009530 | |||
| 70d1e3a0cb | |||
| e751ce1220 | |||
| e09a27cf87 | |||
| 06fbf44724 | |||
| 200e409d91 | |||
| 5e5854e256 | |||
| 3df8bcfc9c | |||
| e76c14f9e0 | |||
| 6b6748b1c7 | |||
| d92d8e6dbb | |||
| c2b9dc5c75 | |||
| 5c1d27de2b | |||
| 6ab9e7cd68 | |||
| 3ef56e9ec1 | |||
| 6d8d157772 | |||
| cadd466eec | |||
| 3fea0c1e49 | |||
| 4c58201adc | |||
| 4fb4e72624 | |||
| 276d8fe5cf | |||
| 92ce5f0931 | |||
| 7fea20375f | |||
| d4d4034d2c | |||
| f0db408699 | |||
| 5e200655d9 | |||
| d5d1f2a645 | |||
| cc5cc43baa | |||
| e512f085db | |||
| f323c01bd8 | |||
| f56cacb406 | |||
| eaecd31e9f | |||
| 36989d82e1 | |||
| 50777d9022 | |||
| a15571bd3e | |||
| 26fd66d831 | |||
| 0be873025a | |||
| 28ada49910 | |||
| 4fc8e61f8c | |||
| 7d26ea1a9c | |||
| 3a58dc62e1 | |||
| 71fe7bc827 | |||
| 933336c38b | |||
| 371feb9a31 | |||
| 95a2fd3c9e | |||
| 17cb76c334 | |||
| 88f0dfc8cc | |||
| f82aada23b | |||
| ecaee92634 | |||
| 89252ec47b | |||
| f0f25ab291 | |||
| e4d0fec15a | |||
| 6b10baf086 | |||
| f148b5d341 | |||
| 1471ff8940 | |||
| d9a6ec2ac0 | |||
| 5745ffa0a8 | |||
| b26202db35 | |||
| 6318577a51 | |||
| 6a2cd45847 | |||
| ef5cea2c01 | |||
| 69f4d54bae | |||
| b1eec5a7d2 | |||
| 1b8271d767 | |||
| 3e9f5ec5ef | |||
| 63f57b6a77 | |||
| a016f99450 | |||
| adc18b2991 | |||
| e37a326b95 | |||
| 048467e97d | |||
| cc2cd6919f | |||
| 0c6e781e5b | |||
| 7294d8fca5 | |||
| 16ec5680b4 | |||
| 87920fb1d7 | |||
| 523b96a6d2 | |||
| 45731d8069 | |||
| e872371970 | |||
| 08e8cf850a | |||
| b1ed2154ac | |||
| 7ef2aa3eb9 | |||
| 160139813d | |||
| 582ad92c76 | |||
| f61736e3d1 | |||
| eb02c96281 | |||
| 8619552920 | |||
| 6237352e25 | |||
| 2d8b4f543b | |||
| 8542dc10ab | |||
| c55b63337c | |||
| 12ddee3bb6 | |||
| dc41d0af27 | |||
| 3323b50036 | |||
| 8acb15a7fd | |||
| f601e04b38 | |||
| f50529cb5b | |||
| 3f1b6f9ed4 | |||
| f1ab0f4314 | |||
| 4d1129f385 | |||
| 03ac9c6e16 | |||
| c0839924f1 | |||
| 91e3aa760a | |||
| 5c0681d57b | |||
| c4f72c2bc1 | |||
| e92f9836e3 | |||
| 3818dc834b | |||
| cda011a049 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2021.8.1 | current_version = 2021.8.5 | ||||||
| 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>.*) | ||||||
| @ -23,7 +23,7 @@ values = | |||||||
|  |  | ||||||
| [bumpversion:file:schema.yml] | [bumpversion:file:schema.yml] | ||||||
|  |  | ||||||
| [bumpversion:file:.github/workflows/release.yml] | [bumpversion:file:.github/workflows/release-publish.yml] | ||||||
|  |  | ||||||
| [bumpversion:file:authentik/__init__.py] | [bumpversion:file:authentik/__init__.py] | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										302
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,302 @@ | |||||||
|  | name: authentik-ci-main | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - master | ||||||
|  |       - next | ||||||
|  |       - version-* | ||||||
|  |     paths-ignore: | ||||||
|  |       - website | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - master | ||||||
|  |  | ||||||
|  | env: | ||||||
|  |   POSTGRES_DB: authentik | ||||||
|  |   POSTGRES_USER: authentik | ||||||
|  |   POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77" | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   lint-pylint: | ||||||
|  |     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-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|  |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|  |         run: scripts/ci_prepare.sh | ||||||
|  |       - name: run pylint | ||||||
|  |         run: pipenv run pylint authentik tests lifecycle | ||||||
|  |   lint-black: | ||||||
|  |     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-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|  |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|  |         run: scripts/ci_prepare.sh | ||||||
|  |       - name: run black | ||||||
|  |         run: pipenv run black --check authentik tests lifecycle | ||||||
|  |   lint-isort: | ||||||
|  |     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-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|  |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|  |         run: scripts/ci_prepare.sh | ||||||
|  |       - name: run isort | ||||||
|  |         run: pipenv run isort --check authentik tests lifecycle | ||||||
|  |   lint-bandit: | ||||||
|  |     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-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|  |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|  |         run: scripts/ci_prepare.sh | ||||||
|  |       - name: run bandit | ||||||
|  |         run: pipenv run bandit -r authentik tests lifecycle | ||||||
|  |   lint-pyright: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-python@v2 | ||||||
|  |         with: | ||||||
|  |           python-version: '3.9' | ||||||
|  |       - uses: actions/setup-node@v2 | ||||||
|  |         with: | ||||||
|  |           node-version: '16' | ||||||
|  |       - name: prepare | ||||||
|  |         run: | | ||||||
|  |           scripts/ci_prepare.sh | ||||||
|  |           npm install -g pyright@1.1.136 | ||||||
|  |       - name: run bandit | ||||||
|  |         run: pipenv run pyright e2e lifecycle | ||||||
|  |   test-migrations: | ||||||
|  |     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-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|  |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|  |         run: scripts/ci_prepare.sh | ||||||
|  |       - name: run migrations | ||||||
|  |         run: pipenv run python -m lifecycle.migrate | ||||||
|  |   test-migrations-from-stable: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-python@v2 | ||||||
|  |         with: | ||||||
|  |           python-version: '3.9' | ||||||
|  |       - name: checkout stable | ||||||
|  |         run: | | ||||||
|  |           # Copy current, latest config to local | ||||||
|  |           cp authentik/lib/default.yml local.env.yml | ||||||
|  |           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-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|  |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|  |         run: scripts/ci_prepare.sh | ||||||
|  |       - name: run migrations to stable | ||||||
|  |         run: pipenv run python -m lifecycle.migrate | ||||||
|  |       - name: checkout current code | ||||||
|  |         run: | | ||||||
|  |           set -x | ||||||
|  |           git checkout $GITHUB_REF | ||||||
|  |           pipenv sync --dev | ||||||
|  |       - name: migrate to latest | ||||||
|  |         run: pipenv run python -m lifecycle.migrate | ||||||
|  |   test-unittest: | ||||||
|  |     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-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|  |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|  |         run: scripts/ci_prepare.sh | ||||||
|  |       - uses: testspace-com/setup-testspace@v1 | ||||||
|  |         with: | ||||||
|  |           domain: ${{github.repository_owner}} | ||||||
|  |       - name: run unittest | ||||||
|  |         run: | | ||||||
|  |           pipenv run make test | ||||||
|  |           pipenv run coverage xml | ||||||
|  |       - name: run testspace | ||||||
|  |         if: ${{ always() }} | ||||||
|  |         run: | | ||||||
|  |           testspace [unittest]unittest.xml --link=codecov | ||||||
|  |       - if: ${{ always() }} | ||||||
|  |         uses: codecov/codecov-action@v2 | ||||||
|  |   test-integration: | ||||||
|  |     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-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|  |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|  |         run: scripts/ci_prepare.sh | ||||||
|  |       - uses: testspace-com/setup-testspace@v1 | ||||||
|  |         with: | ||||||
|  |           domain: ${{github.repository_owner}} | ||||||
|  |       - name: Create k8s Kind Cluster | ||||||
|  |         uses: helm/kind-action@v1.2.0 | ||||||
|  |       - name: run integration | ||||||
|  |         run: | | ||||||
|  |           pipenv run make test-integration | ||||||
|  |           pipenv run coverage xml | ||||||
|  |       - name: run testspace | ||||||
|  |         if: ${{ always() }} | ||||||
|  |         run: | | ||||||
|  |           testspace [integration]unittest.xml --link=codecov | ||||||
|  |       - if: ${{ always() }} | ||||||
|  |         uses: codecov/codecov-action@v2 | ||||||
|  |   test-e2e: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-python@v2 | ||||||
|  |         with: | ||||||
|  |           python-version: '3.9' | ||||||
|  |       - uses: actions/setup-node@v2 | ||||||
|  |         with: | ||||||
|  |           node-version: '16' | ||||||
|  |           cache: 'npm' | ||||||
|  |           cache-dependency-path: web/package-lock.json | ||||||
|  |       - uses: testspace-com/setup-testspace@v1 | ||||||
|  |         with: | ||||||
|  |           domain: ${{github.repository_owner}} | ||||||
|  |       - id: cache-pipenv | ||||||
|  |         uses: actions/cache@v2.1.6 | ||||||
|  |         with: | ||||||
|  |           path: ~/.local/share/virtualenvs | ||||||
|  |           key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }} | ||||||
|  |       - name: prepare | ||||||
|  |         env: | ||||||
|  |           INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }} | ||||||
|  |         run: | | ||||||
|  |           scripts/ci_prepare.sh | ||||||
|  |           docker-compose -f tests/e2e/ci.docker-compose.yml up -d | ||||||
|  |       - id: cache-web | ||||||
|  |         uses: actions/cache@v2.1.6 | ||||||
|  |         with: | ||||||
|  |           path: web/dist | ||||||
|  |           key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }} | ||||||
|  |       - name: prepare web ui | ||||||
|  |         if: steps.cache-web.outputs.cache-hit != 'true' | ||||||
|  |         run: | | ||||||
|  |           cd web | ||||||
|  |           npm i | ||||||
|  |           npm run build | ||||||
|  |       - name: run e2e | ||||||
|  |         run: | | ||||||
|  |           pipenv run make test-e2e | ||||||
|  |           pipenv run coverage xml | ||||||
|  |       - name: run testspace | ||||||
|  |         if: ${{ always() }} | ||||||
|  |         run: | | ||||||
|  |           testspace [e2e]unittest.xml --link=codecov | ||||||
|  |       - if: ${{ always() }} | ||||||
|  |         uses: codecov/codecov-action@v2 | ||||||
|  |   build: | ||||||
|  |     needs: | ||||||
|  |       - lint-pylint | ||||||
|  |       - lint-black | ||||||
|  |       - lint-isort | ||||||
|  |       - lint-bandit | ||||||
|  |       - lint-pyright | ||||||
|  |       - test-migrations | ||||||
|  |       - test-migrations-from-stable | ||||||
|  |       - test-unittest | ||||||
|  |       - test-integration | ||||||
|  |       - test-e2e | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v1 | ||||||
|  |       - name: prepare variables | ||||||
|  |         id: ev | ||||||
|  |         env: | ||||||
|  |           DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }} | ||||||
|  |         run: | | ||||||
|  |           python ./scripts/gh_do_set_branch.py | ||||||
|  |       - name: Login to Container Registry | ||||||
|  |         uses: docker/login-action@v1 | ||||||
|  |         if: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||||
|  |         with: | ||||||
|  |           registry: beryju.org | ||||||
|  |           username: ${{ secrets.HARBOR_USERNAME }} | ||||||
|  |           password: ${{ secrets.HARBOR_PASSWORD }} | ||||||
|  |       - name: Building Docker Image | ||||||
|  |         uses: docker/build-push-action@v2 | ||||||
|  |         with: | ||||||
|  |           push: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||||
|  |           tags: | | ||||||
|  |             beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }} | ||||||
|  |             beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }} | ||||||
|  |           build-args: | | ||||||
|  |             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||||
							
								
								
									
										75
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | |||||||
|  | name: authentik-ci-outpost | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - master | ||||||
|  |       - next | ||||||
|  |       - version-* | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - master | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   lint-golint: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-go@v2 | ||||||
|  |         with: | ||||||
|  |           go-version: '^1.16.3' | ||||||
|  |       - name: Generate API | ||||||
|  |         run: | | ||||||
|  |           make gen-outpost | ||||||
|  |       - name: Run linter | ||||||
|  |         run: | | ||||||
|  |           # Create folder structure for go embeds | ||||||
|  |           mkdir -p web/dist | ||||||
|  |           mkdir -p website/help | ||||||
|  |           touch web/dist/test website/help/test | ||||||
|  |           docker run \ | ||||||
|  |             --rm \ | ||||||
|  |             -v $(pwd):/app \ | ||||||
|  |             -w /app \ | ||||||
|  |             golangci/golangci-lint:v1.39.0 \ | ||||||
|  |             golangci-lint run -v --timeout 200s | ||||||
|  |   build: | ||||||
|  |     needs: | ||||||
|  |       - lint-golint | ||||||
|  |     strategy: | ||||||
|  |       matrix: | ||||||
|  |         type: | ||||||
|  |           - proxy | ||||||
|  |           - ldap | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v1.2.0 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v1 | ||||||
|  |       - name: prepare variables | ||||||
|  |         id: ev | ||||||
|  |         env: | ||||||
|  |           DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }} | ||||||
|  |         run: | | ||||||
|  |           python ./scripts/gh_do_set_branch.py | ||||||
|  |       - name: Login to Container Registry | ||||||
|  |         uses: docker/login-action@v1 | ||||||
|  |         if: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||||
|  |         with: | ||||||
|  |           registry: beryju.org | ||||||
|  |           username: ${{ secrets.HARBOR_USERNAME }} | ||||||
|  |           password: ${{ secrets.HARBOR_PASSWORD }} | ||||||
|  |       - name: Building Docker Image | ||||||
|  |         uses: docker/build-push-action@v2 | ||||||
|  |         with: | ||||||
|  |           push: ${{ steps.ev.outputs.shouldBuild == 'true' }} | ||||||
|  |           tags: | | ||||||
|  |             beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }} | ||||||
|  |             beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }} | ||||||
|  |             beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }} | ||||||
|  |           file: ${{ matrix.type }}.Dockerfile | ||||||
|  |           platforms: linux/amd64,linux/arm64 | ||||||
|  |           build-args: | | ||||||
|  |             GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} | ||||||
							
								
								
									
										89
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | |||||||
|  | name: authentik-ci-web | ||||||
|  |  | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: | ||||||
|  |       - master | ||||||
|  |       - next | ||||||
|  |       - version-* | ||||||
|  |   pull_request: | ||||||
|  |     branches: | ||||||
|  |       - master | ||||||
|  |  | ||||||
|  | jobs: | ||||||
|  |   lint-eslint: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-node@v2 | ||||||
|  |         with: | ||||||
|  |           node-version: '16' | ||||||
|  |           cache: 'npm' | ||||||
|  |           cache-dependency-path: web/package-lock.json | ||||||
|  |       - run: | | ||||||
|  |           cd web | ||||||
|  |           npm install | ||||||
|  |       - name: Generate API | ||||||
|  |         run: make gen-web | ||||||
|  |       - name: Eslint | ||||||
|  |         run: | | ||||||
|  |           cd web | ||||||
|  |           npm run lint | ||||||
|  |   lint-prettier: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-node@v2 | ||||||
|  |         with: | ||||||
|  |           node-version: '16' | ||||||
|  |           cache: 'npm' | ||||||
|  |           cache-dependency-path: web/package-lock.json | ||||||
|  |       - run: | | ||||||
|  |           cd web | ||||||
|  |           npm install | ||||||
|  |       - name: Generate API | ||||||
|  |         run: make gen-web | ||||||
|  |       - name: prettier | ||||||
|  |         run: | | ||||||
|  |           cd web | ||||||
|  |           npm run prettier-check | ||||||
|  |   lint-lit-analyse: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-node@v2 | ||||||
|  |         with: | ||||||
|  |           node-version: '16' | ||||||
|  |           cache: 'npm' | ||||||
|  |           cache-dependency-path: web/package-lock.json | ||||||
|  |       - run: | | ||||||
|  |           cd web | ||||||
|  |           npm install | ||||||
|  |       - name: Generate API | ||||||
|  |         run: make gen-web | ||||||
|  |       - name: prettier | ||||||
|  |         run: | | ||||||
|  |           cd web | ||||||
|  |           npm run lit-analyse | ||||||
|  |   build: | ||||||
|  |     needs: | ||||||
|  |       - lint-eslint | ||||||
|  |       - lint-prettier | ||||||
|  |       - lint-lit-analyse | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-node@v2 | ||||||
|  |         with: | ||||||
|  |           node-version: '16' | ||||||
|  |           cache: 'npm' | ||||||
|  |           cache-dependency-path: web/package-lock.json | ||||||
|  |       - run: | | ||||||
|  |           cd web | ||||||
|  |           npm install | ||||||
|  |       - name: Generate API | ||||||
|  |         run: make gen-web | ||||||
|  |       - name: build | ||||||
|  |         run: | | ||||||
|  |           cd web | ||||||
|  |           npm run build | ||||||
| @ -33,14 +33,14 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           push: ${{ github.event_name == 'release' }} |           push: ${{ github.event_name == 'release' }} | ||||||
|           tags: | |           tags: | | ||||||
|             beryju/authentik:2021.8.1, |             beryju/authentik:2021.8.5, | ||||||
|             beryju/authentik:latest, |             beryju/authentik:latest, | ||||||
|             ghcr.io/goauthentik/server:2021.8.1, |             ghcr.io/goauthentik/server:2021.8.5, | ||||||
|             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.8.1', 'rc') }} |         if: ${{ github.event_name == 'release' && !contains('2021.8.5', '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 +75,14 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           push: ${{ github.event_name == 'release' }} |           push: ${{ github.event_name == 'release' }} | ||||||
|           tags: | |           tags: | | ||||||
|             beryju/authentik-proxy:2021.8.1, |             beryju/authentik-proxy:2021.8.5, | ||||||
|             beryju/authentik-proxy:latest, |             beryju/authentik-proxy:latest, | ||||||
|             ghcr.io/goauthentik/proxy:2021.8.1, |             ghcr.io/goauthentik/proxy:2021.8.5, | ||||||
|             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.8.1', 'rc') }} |         if: ${{ github.event_name == 'release' && !contains('2021.8.5', '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 +117,14 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           push: ${{ github.event_name == 'release' }} |           push: ${{ github.event_name == 'release' }} | ||||||
|           tags: | |           tags: | | ||||||
|             beryju/authentik-ldap:2021.8.1, |             beryju/authentik-ldap:2021.8.5, | ||||||
|             beryju/authentik-ldap:latest, |             beryju/authentik-ldap:latest, | ||||||
|             ghcr.io/goauthentik/ldap:2021.8.1, |             ghcr.io/goauthentik/ldap:2021.8.5, | ||||||
|             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.8.1', 'rc') }} |         if: ${{ github.event_name == 'release' && !contains('2021.8.5', '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 | ||||||
| @ -157,9 +157,9 @@ jobs: | |||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - name: Setup Node.js environment |       - name: Setup Node.js environment | ||||||
|         uses: actions/setup-node@v2.4.0 |         uses: actions/setup-node@v2 | ||||||
|         with: |         with: | ||||||
|           node-version: 12.x |           node-version: '16' | ||||||
|       - name: Build web api client and web ui |       - name: Build web api client and web ui | ||||||
|         run: | |         run: | | ||||||
|           export NODE_ENV=production |           export NODE_ENV=production | ||||||
| @ -175,7 +175,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.8.1 |           version: authentik@2021.8.5 | ||||||
|           environment: beryjuorg-prod |           environment: beryjuorg-prod | ||||||
|           sourcemaps: './web/dist' |           sourcemaps: './web/dist' | ||||||
|           url_prefix: '~/static/dist' |           url_prefix: '~/static/dist' | ||||||
							
								
								
									
										2
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/web-api-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -12,7 +12,7 @@ jobs: | |||||||
|       # Setup .npmrc file to publish to npm |       # Setup .npmrc file to publish to npm | ||||||
|       - uses: actions/setup-node@v2 |       - uses: actions/setup-node@v2 | ||||||
|         with: |         with: | ||||||
|           node-version: '16.x' |           node-version: '16' | ||||||
|           registry-url: 'https://registry.npmjs.org' |           registry-url: 'https://registry.npmjs.org' | ||||||
|       - name: Generate API Client |       - name: Generate API Client | ||||||
|         run: make gen-web |         run: make gen-web | ||||||
|  | |||||||
| @ -98,5 +98,6 @@ COPY --from=builder /work/authentik /authentik-proxy | |||||||
| USER authentik | USER authentik | ||||||
| ENV TMPDIR /dev/shm/ | ENV TMPDIR /dev/shm/ | ||||||
| ENV PYTHONUBUFFERED 1 | ENV PYTHONUBUFFERED 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" | ||||||
| ENTRYPOINT [ "/lifecycle/ak" ] | ENTRYPOINT [ "/lifecycle/ak" ] | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
									
									
									
									
								
							| @ -7,8 +7,6 @@ NPM_VERSION = $(shell python -m scripts.npm_version) | |||||||
| all: lint-fix lint test gen | all: lint-fix lint test gen | ||||||
|  |  | ||||||
| test-integration: | test-integration: | ||||||
| 	k3d cluster create || exit 0 |  | ||||||
| 	k3d kubeconfig write -o ~/.kube/config --overwrite |  | ||||||
| 	coverage run manage.py test -v 3 tests/integration | 	coverage run manage.py test -v 3 tests/integration | ||||||
|  |  | ||||||
| test-e2e: | test-e2e: | ||||||
| @ -61,7 +59,7 @@ gen-outpost: | |||||||
| 		-i /local/schema.yml \ | 		-i /local/schema.yml \ | ||||||
| 		-g go \ | 		-g go \ | ||||||
| 		-o /local/api \ | 		-o /local/api \ | ||||||
| 		--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true | 		--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false | ||||||
| 	rm -f api/go.mod api/go.sum | 	rm -f api/go.mod api/go.sum | ||||||
|  |  | ||||||
| gen: gen-build gen-clean gen-web gen-outpost | gen: gen-build gen-clean gen-web gen-outpost | ||||||
|  | |||||||
							
								
								
									
										178
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										178
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -122,19 +122,19 @@ | |||||||
|         }, |         }, | ||||||
|         "boto3": { |         "boto3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:4dc7e346e92c01e8a997daa58a4c990151841d2d2962067325d963f665c7287a", |                 "sha256:4df1085f5c24504a1b1a6584947f27b67c26eda123f29d3cecce9b2fd683e09b", | ||||||
|                 "sha256:79b7e6e0167def749352968ed6eb96954d9e2dd1dca8f297f122414753ce73a3" |                 "sha256:a7fccb61d95230322dd812629455df14167307c569077fa89d297eae73605ffb" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.18.29" |             "version": "==1.18.36" | ||||||
|         }, |         }, | ||||||
|         "botocore": { |         "botocore": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:1f16998b4f5a88e6844196feee7fa5eef6b36034d377f9845c7df12b8803b3be", |                 "sha256:5b9a7d30e44b8a0a2bbbde62ae01bf6c349017e836985a0248552b00bbce7fae", | ||||||
|                 "sha256:fec924f63b40bd29b522fa109ecbc45f16eedcbeb22b68c6c79773c22a552b16" |                 "sha256:e3e522fbe0bad1197aa7182451dc05f650310e77cf0a77749f6a5e82794c53de" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==1.21.29" |             "version": "==1.21.36" | ||||||
|         }, |         }, | ||||||
|         "cachetools": { |         "cachetools": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -359,11 +359,11 @@ | |||||||
|         }, |         }, | ||||||
|         "django": { |         "django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:7f92413529aa0e291f3be78ab19be31aefb1e1c9a52cd59e130f505f27a51f13", |                 "sha256:95b318319d6997bac3595517101ad9cc83fe5672ac498ba48d1a410f47afecd2", | ||||||
|                 "sha256:f27f8544c9d4c383bbe007c57e3235918e258364577373d4920e9162837be022" |                 "sha256:e93c93565005b37ddebf2396b4dc4b6913c1838baa82efdfb79acedd5816c240" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==3.2.6" |             "version": "==3.2.7" | ||||||
|         }, |         }, | ||||||
|         "django-dbbackup": { |         "django-dbbackup": { | ||||||
|             "git": "https://github.com/django-dbbackup/django-dbbackup.git", |             "git": "https://github.com/django-dbbackup/django-dbbackup.git", | ||||||
| @ -443,19 +443,19 @@ | |||||||
|         }, |         }, | ||||||
|         "docker": { |         "docker": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:3e8bc47534e0ca9331d72c32f2881bb13b93ded0bcdeab3c833fb7cf61c0a9a5", |                 "sha256:21ec4998e90dff7a7aaaa098ca8d839c7de412b89e6f6c30908372d58fecf663", | ||||||
|                 "sha256:fc961d622160e8021c10d1bcabc388c57d55fb1f917175afbe24af442e6879bd" |                 "sha256:9b17f0723d83c1f3418d2aa17bf90b24dbe97deda06208dd4262fa30a6ee87eb" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==5.0.0" |             "version": "==5.0.2" | ||||||
|         }, |         }, | ||||||
|         "drf-spectacular": { |         "drf-spectacular": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:5b1c27de127c86564be5a967a6fa195cfe161b552d98364282ae9e6ed3d75a85", |                 "sha256:47ef6ec8ff48ac8aede6ec12450a55fee381cf84de969ef1724dcde5a93de6b8", | ||||||
|                 "sha256:8588706c27f44adfbb3405bae9ef9cd6506f4b59d4cbd66c59780dce035602d9" |                 "sha256:d746b936cb4cddec380ea95bf91de6a6721777dfc42e0eea53b83c61a625e94e" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==0.18.0" |             "version": "==0.18.2" | ||||||
|         }, |         }, | ||||||
|         "duo-client": { |         "duo-client": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -490,11 +490,11 @@ | |||||||
|         }, |         }, | ||||||
|         "google-auth": { |         "google-auth": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:c012c8be7c442c8309ca8fa0876fef33f5fd977c467be1e1c1c2f721e8ebd73c", |                 "sha256:104475dc4d57bbae49017aea16fffbb763204fa2d6a70f1f3cc79962c1a383a4", | ||||||
|                 "sha256:ea1af050b3e06eb73e4470f704d23007307bc0e87c13e015f6b90460f1407bd3" |                 "sha256:cde472372e030e1e0bc64dac00fb53e6c095d7ab641f4281e2c995e85e205d8b" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==2.0.1" |             "version": "==2.0.2" | ||||||
|         }, |         }, | ||||||
|         "gunicorn": { |         "gunicorn": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1151,11 +1151,11 @@ | |||||||
|         }, |         }, | ||||||
|         "typing-extensions": { |         "typing-extensions": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", |                 "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", | ||||||
|                 "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", |                 "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", | ||||||
|                 "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" |                 "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" | ||||||
|             ], |             ], | ||||||
|             "version": "==3.10.0.0" |             "version": "==3.10.0.2" | ||||||
|         }, |         }, | ||||||
|         "ua-parser": { |         "ua-parser": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1294,20 +1294,20 @@ | |||||||
|         }, |         }, | ||||||
|         "xmlsec": { |         "xmlsec": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:23f209260b37bdc2fd96af837494c47dd1e67964f077442b63acd83c0f62e212", |                 "sha256:135724cdce60e6bbd072fca6f09a21f72e2cecc59eebb4eed7740c316ecabc7b", | ||||||
|                 "sha256:4fb38ab0bf3e47cbae136119674a869e09d61c939b510350f369c8ac46087373", |                 "sha256:1b4377f6d37ad714ba95a227ef40fb54ba1b22ef5170ce04c330fe45ee6ad184", | ||||||
|                 "sha256:705ab5b848afdf3a5c78b1322276054c885f44dc51601e14cb883a9c86cbe20f", |                 "sha256:2c86ac6ce570c9e04f04da0cd5e7d3db346e4b5b1d006311606368f17c756ef9", | ||||||
|                 "sha256:843d10bba4c480609da74ee11fff1ee0fc1c12821c656979f12a7a4ecb043e03", |                 "sha256:4e5f565de311afa33aaee4724566e685f951afe301212b6cf82f98cf9d8a1749", | ||||||
|                 "sha256:86d54b93f8278e2f0c504d0744e39a483c1c7ce9993f2ca70184cc7770faa982", |                 "sha256:9a2b8a780093b0fe8cecae53a81a8cd9edd50c08980d374c5317c91f065042d9", | ||||||
|                 "sha256:8922fba55a060ee81de4a7f5efc593c5bf121047763aecf0eead02e061c9d2db", |                 "sha256:ce9c681adbc87b4f06c2b16725d9b2edbdbd508117dae4288b5faf78c1406038", | ||||||
|                 "sha256:c7b49d4fce83186b89f7ce6cec765245d36a70d0acc2f3ed0ba95c735b3667da", |                 "sha256:d22da4d3dcc559fb2e54e782f39c9ddad5f8d5b356f86a79bbb80b0a45115c97", | ||||||
|                 "sha256:cd2eaaff7f31784a07dd99ce81fa767313df3ba1834faa4143ee2c07000cac7a", |                 "sha256:db3e18ca883c01bbe28c9f5197c66f676c9772cf2d85f667e6122fc4d0702225", | ||||||
|                 "sha256:dea5bef9b5830c36ccb7a68a0d94d49eaea4d03fbbd04179652bf661b7e6e30f", |                 "sha256:e4783f7814aa2a3e318385cce8ef87c82954b9a59535a48f67da4e2c21c08ce1", | ||||||
|                 "sha256:eadff662d89c80db409c69d82eb3e695e16d4a5e8ab56b5b22670a54e9c6ff20", |                 "sha256:f32e54065f0404ceff71388daa7fa7df10e1fb800051dfe302d63abb0acf0020", | ||||||
|                 "sha256:ee233d0bc27fb8f447ca2622b0de2ac2df45b8795f02ef263825912011fe4fe9" |                 "sha256:f5d242b1a19a36078608f5d7f4d561c5ca55cac8061a323a071c06275267dc19" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==1.3.11" |             "version": "==1.3.12" | ||||||
|         }, |         }, | ||||||
|         "yarl": { |         "yarl": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1420,11 +1420,11 @@ | |||||||
|         }, |         }, | ||||||
|         "astroid": { |         "astroid": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:b6c2d75cd7c2982d09e7d41d70213e863b3ba34d3bd4014e08f167cee966e99e", |                 "sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c", | ||||||
|                 "sha256:ecc50f9b3803ebf8ea19aa2c6df5622d8a5c31456a53c741d3be044d96ff0948" |                 "sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version ~= '3.6'", |             "markers": "python_version ~= '3.6'", | ||||||
|             "version": "==2.7.2" |             "version": "==2.7.3" | ||||||
|         }, |         }, | ||||||
|         "attrs": { |         "attrs": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1652,19 +1652,19 @@ | |||||||
|         }, |         }, | ||||||
|         "platformdirs": { |         "platformdirs": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c", |                 "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f", | ||||||
|                 "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e" |                 "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.6'", |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==2.2.0" |             "version": "==2.3.0" | ||||||
|         }, |         }, | ||||||
|         "pluggy": { |         "pluggy": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", |                 "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", | ||||||
|                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" |                 "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", |             "markers": "python_version >= '3.6'", | ||||||
|             "version": "==0.13.1" |             "version": "==1.0.0" | ||||||
|         }, |         }, | ||||||
|         "py": { |         "py": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1707,11 +1707,11 @@ | |||||||
|         }, |         }, | ||||||
|         "pytest": { |         "pytest": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", |                 "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", | ||||||
|                 "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" |                 "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "version": "==6.2.4" |             "version": "==6.2.5" | ||||||
|         }, |         }, | ||||||
|         "pytest-django": { |         "pytest-django": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @ -1758,49 +1758,49 @@ | |||||||
|         }, |         }, | ||||||
|         "regex": { |         "regex": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd", |                 "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468", | ||||||
|                 "sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642", |                 "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354", | ||||||
|                 "sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1", |                 "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308", | ||||||
|                 "sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321", |                 "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d", | ||||||
|                 "sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529", |                 "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc", | ||||||
|                 "sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36", |                 "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8", | ||||||
|                 "sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a", |                 "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797", | ||||||
|                 "sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30", |                 "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2", | ||||||
|                 "sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce", |                 "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13", | ||||||
|                 "sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376", |                 "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d", | ||||||
|                 "sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd", |                 "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a", | ||||||
|                 "sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586", |                 "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0", | ||||||
|                 "sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7", |                 "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73", | ||||||
|                 "sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9", |                 "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1", | ||||||
|                 "sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea", |                 "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed", | ||||||
|                 "sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94", |                 "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a", | ||||||
|                 "sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3", |                 "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b", | ||||||
|                 "sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f", |                 "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f", | ||||||
|                 "sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267", |                 "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256", | ||||||
|                 "sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc", |                 "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb", | ||||||
|                 "sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23", |                 "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2", | ||||||
|                 "sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882", |                 "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983", | ||||||
|                 "sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc", |                 "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb", | ||||||
|                 "sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe", |                 "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645", | ||||||
|                 "sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759", |                 "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8", | ||||||
|                 "sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456", |                 "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a", | ||||||
|                 "sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239", |                 "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906", | ||||||
|                 "sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb", |                 "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f", | ||||||
|                 "sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948", |                 "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c", | ||||||
|                 "sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0", |                 "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892", | ||||||
|                 "sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183", |                 "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0", | ||||||
|                 "sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92", |                 "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e", | ||||||
|                 "sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade", |                 "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e", | ||||||
|                 "sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044", |                 "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed", | ||||||
|                 "sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee", |                 "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c", | ||||||
|                 "sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033", |                 "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374", | ||||||
|                 "sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2", |                 "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd", | ||||||
|                 "sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5", |                 "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791", | ||||||
|                 "sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2", |                 "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a", | ||||||
|                 "sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504", |                 "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1", | ||||||
|                 "sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a" |                 "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759" | ||||||
|             ], |             ], | ||||||
|             "version": "==2021.8.21" |             "version": "==2021.8.28" | ||||||
|         }, |         }, | ||||||
|         "requests": { |         "requests": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							| @ -4,14 +4,15 @@ | |||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
| [](https://discord.gg/jg33eMhnj6) | [](https://discord.gg/jg33eMhnj6) | ||||||
| [](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6) | [](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml) | ||||||
| [](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6) | [](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml) | ||||||
|  | [](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml) | ||||||
| [](https://codecov.io/gh/goauthentik/authentik) | [](https://codecov.io/gh/goauthentik/authentik) | ||||||
|  | [](https://goauthentik.testspace.com/) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | [](https://www.transifex.com/beryjuorg/authentik/) | ||||||
| [Transifex](https://www.transifex.com/beryjuorg/authentik/) |  | ||||||
|  |  | ||||||
| ## What is authentik? | ## What is authentik? | ||||||
|  |  | ||||||
|  | |||||||
| @ -6,9 +6,8 @@ | |||||||
|  |  | ||||||
| | Version    | Supported          | | | Version    | Supported          | | ||||||
| | ---------- | ------------------ | | | ---------- | ------------------ | | ||||||
| | 2021.5.x   | :white_check_mark: | |  | ||||||
| | 2021.6.x   | :white_check_mark: | |  | ||||||
| | 2021.7.x   | :white_check_mark: | | | 2021.7.x   | :white_check_mark: | | ||||||
|  | | 2021.8.x   | :white_check_mark: | | ||||||
|  |  | ||||||
| ## Reporting a Vulnerability | ## Reporting a Vulnerability | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,3 +1,3 @@ | |||||||
| """authentik""" | """authentik""" | ||||||
| __version__ = "2021.8.1" | __version__ = "2021.8.5" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  | |||||||
| @ -1,8 +1,10 @@ | |||||||
| """authentik api urls""" | """authentik api urls""" | ||||||
| from django.urls import include, path | from django.urls import include, path | ||||||
|  |  | ||||||
| from authentik.api.v2.urls import urlpatterns as v2_urls | from authentik.api.v3.urls import urlpatterns as v3_urls | ||||||
|  |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path("v2beta/", include(v2_urls)), |     # Remove in 2022.1 | ||||||
|  |     path("v2beta/", include(v3_urls)), | ||||||
|  |     path("v3/", include(v3_urls)), | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -4,16 +4,44 @@ from json import loads | |||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.http.request import HttpRequest | from django.http.request import HttpRequest | ||||||
| from django.http.response import HttpResponse | from django.http.response import HttpResponse | ||||||
| from django.views.generic.base import View |  | ||||||
| from requests import post | from requests import post | ||||||
| from requests.exceptions import RequestException | from requests.exceptions import RequestException | ||||||
|  | from rest_framework.authentication import SessionAuthentication | ||||||
|  | from rest_framework.parsers import BaseParser | ||||||
|  | from rest_framework.permissions import AllowAny | ||||||
|  | from rest_framework.request import Request | ||||||
|  | from rest_framework.throttling import AnonRateThrottle | ||||||
|  | from rest_framework.views import APIView | ||||||
| 
 | 
 | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SentryTunnelView(View): | class PlainTextParser(BaseParser): | ||||||
|  |     """Plain text parser.""" | ||||||
|  | 
 | ||||||
|  |     media_type = "text/plain" | ||||||
|  | 
 | ||||||
|  |     def parse(self, stream, media_type=None, parser_context=None) -> str: | ||||||
|  |         """Simply return a string representing the body of the request.""" | ||||||
|  |         return stream.read() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class CsrfExemptSessionAuthentication(SessionAuthentication): | ||||||
|  |     """CSRF-exempt Session authentication""" | ||||||
|  | 
 | ||||||
|  |     def enforce_csrf(self, request: Request): | ||||||
|  |         return  # To not perform the csrf check previously happening | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class SentryTunnelView(APIView): | ||||||
|     """Sentry tunnel, to prevent ad blockers from blocking sentry""" |     """Sentry tunnel, to prevent ad blockers from blocking sentry""" | ||||||
| 
 | 
 | ||||||
|  |     serializer_class = None | ||||||
|  |     parser_classes = [PlainTextParser] | ||||||
|  |     throttle_classes = [AnonRateThrottle] | ||||||
|  |     permission_classes = [AllowAny] | ||||||
|  |     authentication_classes = [CsrfExemptSessionAuthentication] | ||||||
|  | 
 | ||||||
|     def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: |     def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||||
|         """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 | ||||||
| @ -1,6 +1,6 @@ | |||||||
| """api v2 urls""" | """api v3 urls""" | ||||||
| from django.urls import path | from django.urls import path | ||||||
| from django.views.decorators.csrf import csrf_exempt | from django.views.decorators.cache import cache_page | ||||||
| from drf_spectacular.views import SpectacularAPIView | from drf_spectacular.views import SpectacularAPIView | ||||||
| from rest_framework import routers | from rest_framework import routers | ||||||
| 
 | 
 | ||||||
| @ -10,8 +10,8 @@ from authentik.admin.api.system import SystemView | |||||||
| from authentik.admin.api.tasks import TaskViewSet | from authentik.admin.api.tasks import TaskViewSet | ||||||
| from authentik.admin.api.version import VersionView | from authentik.admin.api.version import VersionView | ||||||
| from authentik.admin.api.workers import WorkerView | from authentik.admin.api.workers import WorkerView | ||||||
| from authentik.api.v2.config import ConfigView | from authentik.api.v3.config import ConfigView | ||||||
| from authentik.api.v2.sentry import SentryTunnelView | from authentik.api.v3.sentry import SentryTunnelView | ||||||
| from authentik.api.views import APIBrowserView | from authentik.api.views import APIBrowserView | ||||||
| from authentik.core.api.applications import ApplicationViewSet | from authentik.core.api.applications import ApplicationViewSet | ||||||
| from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet | from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet | ||||||
| @ -225,7 +225,7 @@ urlpatterns = ( | |||||||
|             FlowExecutorView.as_view(), |             FlowExecutorView.as_view(), | ||||||
|             name="flow-executor", |             name="flow-executor", | ||||||
|         ), |         ), | ||||||
|         path("sentry/", csrf_exempt(SentryTunnelView.as_view()), name="sentry"), |         path("sentry/", SentryTunnelView.as_view(), name="sentry"), | ||||||
|         path("schema/", SpectacularAPIView.as_view(), name="schema"), |         path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"), | ||||||
|     ] |     ] | ||||||
| ) | ) | ||||||
| @ -67,7 +67,7 @@ class ApplicationSerializer(ModelSerializer): | |||||||
| class ApplicationViewSet(UsedByMixin, ModelViewSet): | class ApplicationViewSet(UsedByMixin, ModelViewSet): | ||||||
|     """Application Viewset""" |     """Application Viewset""" | ||||||
|  |  | ||||||
|     queryset = Application.objects.all() |     queryset = Application.objects.all().prefetch_related("provider") | ||||||
|     serializer_class = ApplicationSerializer |     serializer_class = ApplicationSerializer | ||||||
|     search_fields = [ |     search_fields = [ | ||||||
|         "name", |         "name", | ||||||
|  | |||||||
| @ -81,7 +81,7 @@ class GroupFilter(FilterSet): | |||||||
| class GroupViewSet(UsedByMixin, ModelViewSet): | class GroupViewSet(UsedByMixin, ModelViewSet): | ||||||
|     """Group Viewset""" |     """Group Viewset""" | ||||||
|  |  | ||||||
|     queryset = Group.objects.all() |     queryset = Group.objects.all().select_related("parent").prefetch_related("users") | ||||||
|     serializer_class = GroupSerializer |     serializer_class = GroupSerializer | ||||||
|     search_fields = ["name", "is_superuser"] |     search_fields = ["name", "is_superuser"] | ||||||
|     filterset_class = GroupFilter |     filterset_class = GroupFilter | ||||||
|  | |||||||
| @ -23,7 +23,7 @@ from authentik.managed.api import ManagedSerializer | |||||||
| class TokenSerializer(ManagedSerializer, ModelSerializer): | class TokenSerializer(ManagedSerializer, ModelSerializer): | ||||||
|     """Token Serializer""" |     """Token Serializer""" | ||||||
|  |  | ||||||
|     user_obj = UserSerializer(required=False) |     user_obj = UserSerializer(required=False, source="user") | ||||||
|  |  | ||||||
|     def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: |     def validate(self, attrs: dict[Any, str]) -> dict[Any, str]: | ||||||
|         """Ensure only API or App password tokens are created.""" |         """Ensure only API or App password tokens are created.""" | ||||||
|  | |||||||
| @ -1,8 +1,13 @@ | |||||||
| """Notification API Views""" | """Notification API Views""" | ||||||
| from django_filters.rest_framework import DjangoFilterBackend | from django_filters.rest_framework import DjangoFilterBackend | ||||||
|  | from drf_spectacular.types import OpenApiTypes | ||||||
|  | from drf_spectacular.utils import OpenApiResponse, extend_schema | ||||||
| from rest_framework import mixins | from rest_framework import mixins | ||||||
|  | from rest_framework.decorators import action | ||||||
| from rest_framework.fields import ReadOnlyField | from rest_framework.fields import ReadOnlyField | ||||||
| from rest_framework.filters import OrderingFilter, SearchFilter | from rest_framework.filters import OrderingFilter, SearchFilter | ||||||
|  | from rest_framework.request import Request | ||||||
|  | from rest_framework.response import Response | ||||||
| from rest_framework.serializers import ModelSerializer | from rest_framework.serializers import ModelSerializer | ||||||
| from rest_framework.viewsets import GenericViewSet | from rest_framework.viewsets import GenericViewSet | ||||||
|  |  | ||||||
| @ -53,3 +58,18 @@ class NotificationViewSet( | |||||||
|     ] |     ] | ||||||
|     permission_classes = [OwnerPermissions] |     permission_classes = [OwnerPermissions] | ||||||
|     filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] |     filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] | ||||||
|  |  | ||||||
|  |     @extend_schema( | ||||||
|  |         request=OpenApiTypes.NONE, | ||||||
|  |         responses={ | ||||||
|  |             204: OpenApiResponse(description="Marked tasks as read successfully."), | ||||||
|  |         }, | ||||||
|  |     ) | ||||||
|  |     @action(detail=False, methods=["post"]) | ||||||
|  |     def mark_all_seen(self, request: Request) -> Response: | ||||||
|  |         """Mark all the user's notifications as seen""" | ||||||
|  |         notifications = Notification.objects.filter(user=request.user) | ||||||
|  |         for notification in notifications: | ||||||
|  |             notification.seen = True | ||||||
|  |         Notification.objects.bulk_update(notifications, ["seen"]) | ||||||
|  |         return Response({}, status=204) | ||||||
|  | |||||||
| @ -1,10 +1,7 @@ | |||||||
| """authentik events app""" | """authentik events app""" | ||||||
| from datetime import timedelta |  | ||||||
| from importlib import import_module | from importlib import import_module | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
| from django.db import ProgrammingError |  | ||||||
| from django.utils.timezone import now |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthentikEventsConfig(AppConfig): | class AuthentikEventsConfig(AppConfig): | ||||||
| @ -16,12 +13,3 @@ class AuthentikEventsConfig(AppConfig): | |||||||
|  |  | ||||||
|     def ready(self): |     def ready(self): | ||||||
|         import_module("authentik.events.signals") |         import_module("authentik.events.signals") | ||||||
|         try: |  | ||||||
|             from authentik.events.models import Event |  | ||||||
|  |  | ||||||
|             date_from = now() - timedelta(days=1) |  | ||||||
|  |  | ||||||
|             for event in Event.objects.filter(created__gte=date_from): |  | ||||||
|                 event._set_prom_metrics() |  | ||||||
|         except ProgrammingError: |  | ||||||
|             pass |  | ||||||
|  | |||||||
| @ -10,7 +10,6 @@ from django.db import models | |||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from django.utils.timezone import now | from django.utils.timezone import now | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from prometheus_client import Gauge |  | ||||||
| from requests import RequestException, post | from requests import RequestException, post | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| @ -28,11 +27,6 @@ from authentik.tenants.models import Tenant | |||||||
| from authentik.tenants.utils import DEFAULT_TENANT | from authentik.tenants.utils import DEFAULT_TENANT | ||||||
|  |  | ||||||
| LOGGER = get_logger("authentik.events") | LOGGER = get_logger("authentik.events") | ||||||
| GAUGE_EVENTS = Gauge( |  | ||||||
|     "authentik_events", |  | ||||||
|     "Events in authentik", |  | ||||||
|     ["action", "user_username", "app", "client_ip"], |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def default_event_duration(): | def default_event_duration(): | ||||||
| @ -182,14 +176,6 @@ class Event(ExpiringModel): | |||||||
|             return |             return | ||||||
|         self.context["geo"] = city |         self.context["geo"] = city | ||||||
|  |  | ||||||
|     def _set_prom_metrics(self): |  | ||||||
|         GAUGE_EVENTS.labels( |  | ||||||
|             action=self.action, |  | ||||||
|             user_username=self.user.get("username"), |  | ||||||
|             app=self.app, |  | ||||||
|             client_ip=self.client_ip, |  | ||||||
|         ).set(self.created.timestamp()) |  | ||||||
|  |  | ||||||
|     def save(self, *args, **kwargs): |     def save(self, *args, **kwargs): | ||||||
|         if self._state.adding: |         if self._state.adding: | ||||||
|             LOGGER.debug( |             LOGGER.debug( | ||||||
| @ -200,7 +186,6 @@ class Event(ExpiringModel): | |||||||
|                 user=self.user, |                 user=self.user, | ||||||
|             ) |             ) | ||||||
|         super().save(*args, **kwargs) |         super().save(*args, **kwargs) | ||||||
|         self._set_prom_metrics() |  | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def summary(self) -> str: |     def summary(self) -> str: | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ from django.core.cache import cache | |||||||
| from prometheus_client import Gauge | from prometheus_client import Gauge | ||||||
|  |  | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.lib.utils.errors import exception_to_string | ||||||
|  |  | ||||||
| GAUGE_TASKS = Gauge( | GAUGE_TASKS = Gauge( | ||||||
|     "authentik_system_tasks", |     "authentik_system_tasks", | ||||||
| @ -174,9 +175,7 @@ class MonitoredTask(Task): | |||||||
|         ).save(self.result_timeout_hours) |         ).save(self.result_timeout_hours) | ||||||
|         Event.new( |         Event.new( | ||||||
|             EventAction.SYSTEM_TASK_EXCEPTION, |             EventAction.SYSTEM_TASK_EXCEPTION, | ||||||
|             message=( |             message=(f"Task {self.__name__} encountered an error: {exception_to_string(exc)}"), | ||||||
|                 f"Task {self.__name__} encountered an error: " "\n".join(self._result.messages) |  | ||||||
|             ), |  | ||||||
|         ).save() |         ).save() | ||||||
|         return super().on_failure(exc, task_id, args, kwargs, einfo=einfo) |         return super().on_failure(exc, task_id, args, kwargs, einfo=einfo) | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,15 +4,15 @@ 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 | from authentik.events.models import Event, EventAction, Notification, NotificationSeverity | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestEventsAPI(APITestCase): | class TestEventsAPI(APITestCase): | ||||||
|     """Test Event API""" |     """Test Event API""" | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         user = User.objects.get(username="akadmin") |         self.user = User.objects.get(username="akadmin") | ||||||
|         self.client.force_login(user) |         self.client.force_login(self.user) | ||||||
|  |  | ||||||
|     def test_top_n(self): |     def test_top_n(self): | ||||||
|         """Test top_per_user""" |         """Test top_per_user""" | ||||||
| @ -30,3 +30,14 @@ class TestEventsAPI(APITestCase): | |||||||
|             reverse("authentik_api:event-actions"), |             reverse("authentik_api:event-actions"), | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |     def test_notifications(self): | ||||||
|  |         """Test notifications""" | ||||||
|  |         notification = Notification.objects.create( | ||||||
|  |             user=self.user, severity=NotificationSeverity.ALERT, body="", seen=False | ||||||
|  |         ) | ||||||
|  |         self.client.post( | ||||||
|  |             reverse("authentik_api:notification-mark-all-seen"), | ||||||
|  |         ) | ||||||
|  |         notification.refresh_from_db() | ||||||
|  |         self.assertTrue(notification.seen) | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ class FlowPlanProcess(PROCESS_CLASS):  # pragma: no cover | |||||||
|         self.request = RequestFactory().get("/") |         self.request = RequestFactory().get("/") | ||||||
|  |  | ||||||
|     def run(self): |     def run(self): | ||||||
|  |         """Execute 1000 flow plans""" | ||||||
|         print(f"Proc {self.index} Running") |         print(f"Proc {self.index} Running") | ||||||
|  |  | ||||||
|         def test_inner(): |         def test_inner(): | ||||||
|  | |||||||
| @ -0,0 +1,21 @@ | |||||||
|  | # Generated by Django 3.2.6 on 2021-08-30 14:49 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_flows", "0023_alter_flow_background"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="flow", | ||||||
|  |             name="compatibility_mode", | ||||||
|  |             field=models.BooleanField( | ||||||
|  |                 default=False, | ||||||
|  |                 help_text="Enable compatibility mode, increases compatibility with password managers on mobile devices.", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -125,7 +125,7 @@ class Flow(SerializerModel, PolicyBindingModel): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     compatibility_mode = models.BooleanField( |     compatibility_mode = models.BooleanField( | ||||||
|         default=True, |         default=False, | ||||||
|         help_text=_( |         help_text=_( | ||||||
|             "Enable compatibility mode, increases compatibility with " |             "Enable compatibility mode, increases compatibility with " | ||||||
|             "password managers on mobile devices." |             "password managers on mobile devices." | ||||||
|  | |||||||
| @ -2,10 +2,10 @@ | |||||||
| from unittest.mock import MagicMock, PropertyMock, patch | from unittest.mock import MagicMock, PropertyMock, patch | ||||||
|  |  | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django.test import TestCase |  | ||||||
| from django.test.client import RequestFactory | from django.test.client import RequestFactory | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -37,7 +37,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse): | |||||||
| TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response) | TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestFlowExecutor(TestCase): | class TestFlowExecutor(APITestCase): | ||||||
|     """Test views logic""" |     """Test views logic""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| """flow views tests""" | """flow views tests""" | ||||||
| from django.test import Client, TestCase | from django.test import TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
| from authentik.flows.models import Flow, FlowDesignation | from authentik.flows.models import Flow, FlowDesignation | ||||||
| @ -10,9 +10,6 @@ from authentik.flows.views import SESSION_KEY_PLAN | |||||||
| class TestHelperView(TestCase): | class TestHelperView(TestCase): | ||||||
|     """Test helper views logic""" |     """Test helper views logic""" | ||||||
|  |  | ||||||
|     def setUp(self): |  | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|     def test_default_view(self): |     def test_default_view(self): | ||||||
|         """Test that ToDefaultFlow returns the expected URL""" |         """Test that ToDefaultFlow returns the expected URL""" | ||||||
|         flow = Flow.objects.filter( |         flow = Flow.objects.filter( | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ from authentik.core.channels import AuthJsonConsumer | |||||||
| from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState | from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState | ||||||
|  |  | ||||||
| GAUGE_OUTPOSTS_CONNECTED = Gauge( | GAUGE_OUTPOSTS_CONNECTED = Gauge( | ||||||
|     "authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid"] |     "authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid", "expected"] | ||||||
| ) | ) | ||||||
| GAUGE_OUTPOSTS_LAST_UPDATE = Gauge( | GAUGE_OUTPOSTS_LAST_UPDATE = Gauge( | ||||||
|     "authentik_outposts_last_update", |     "authentik_outposts_last_update", | ||||||
| @ -76,6 +76,7 @@ class OutpostConsumer(AuthJsonConsumer): | |||||||
|             GAUGE_OUTPOSTS_CONNECTED.labels( |             GAUGE_OUTPOSTS_CONNECTED.labels( | ||||||
|                 outpost=self.outpost.name, |                 outpost=self.outpost.name, | ||||||
|                 uid=self.last_uid, |                 uid=self.last_uid, | ||||||
|  |                 expected=self.outpost.config.kubernetes_replicas, | ||||||
|             ).dec() |             ).dec() | ||||||
|         LOGGER.debug( |         LOGGER.debug( | ||||||
|             "removed outpost instance from cache", |             "removed outpost instance from cache", | ||||||
| @ -100,6 +101,7 @@ class OutpostConsumer(AuthJsonConsumer): | |||||||
|             GAUGE_OUTPOSTS_CONNECTED.labels( |             GAUGE_OUTPOSTS_CONNECTED.labels( | ||||||
|                 outpost=self.outpost.name, |                 outpost=self.outpost.name, | ||||||
|                 uid=self.last_uid, |                 uid=self.last_uid, | ||||||
|  |                 expected=self.outpost.config.kubernetes_replicas, | ||||||
|             ).inc() |             ).inc() | ||||||
|             LOGGER.debug( |             LOGGER.debug( | ||||||
|                 "added outpost instace to cache", |                 "added outpost instace to cache", | ||||||
|  | |||||||
| @ -29,7 +29,9 @@ class DockerController(BaseController): | |||||||
|             raise ControllerException from exc |             raise ControllerException from exc | ||||||
|  |  | ||||||
|     def _get_labels(self) -> dict[str, str]: |     def _get_labels(self) -> dict[str, str]: | ||||||
|         return {} |         return { | ||||||
|  |             "io.goauthentik.outpost-uuid": self.outpost.pk.hex, | ||||||
|  |         } | ||||||
|  |  | ||||||
|     def _get_env(self) -> dict[str, str]: |     def _get_env(self) -> dict[str, str]: | ||||||
|         return { |         return { | ||||||
| @ -49,6 +51,17 @@ class DockerController(BaseController): | |||||||
|                 return True |                 return True | ||||||
|         return False |         return False | ||||||
|  |  | ||||||
|  |     def _comp_labels(self, container: Container) -> bool: | ||||||
|  |         """Check if container's labels is equal to what we would set. Return true if container needs | ||||||
|  |         to be rebuilt.""" | ||||||
|  |         should_be = self._get_labels() | ||||||
|  |         for key, expected_value in should_be.items(): | ||||||
|  |             if key not in container.labels: | ||||||
|  |                 return True | ||||||
|  |             if container.labels[key] != expected_value: | ||||||
|  |                 return True | ||||||
|  |         return False | ||||||
|  |  | ||||||
|     def _comp_ports(self, container: Container) -> bool: |     def _comp_ports(self, container: Container) -> bool: | ||||||
|         """Check that the container has the correct ports exposed. Return true if container needs |         """Check that the container has the correct ports exposed. Return true if container needs | ||||||
|         to be rebuilt.""" |         to be rebuilt.""" | ||||||
| @ -92,9 +105,11 @@ class DockerController(BaseController): | |||||||
|                 "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, | ||||||
|             } |             } | ||||||
|             if settings.TEST: |             if settings.TEST: | ||||||
|                 del container_args["ports"] |                 del container_args["ports"] | ||||||
|  |                 del container_args["network"] | ||||||
|                 container_args["network_mode"] = "host" |                 container_args["network_mode"] = "host" | ||||||
|             return ( |             return ( | ||||||
|                 self.client.containers.create(**container_args), |                 self.client.containers.create(**container_args), | ||||||
| @ -133,6 +148,11 @@ class DockerController(BaseController): | |||||||
|                 self.logger.info("Container has outdated config, re-creating...") |                 self.logger.info("Container has outdated config, re-creating...") | ||||||
|                 self.down() |                 self.down() | ||||||
|                 return self.up(depth + 1) |                 return self.up(depth + 1) | ||||||
|  |             # Check that container values match our values | ||||||
|  |             if self._comp_labels(container): | ||||||
|  |                 self.logger.info("Container has outdated labels, re-creating...") | ||||||
|  |                 self.down() | ||||||
|  |                 return self.up(depth + 1) | ||||||
|             if ( |             if ( | ||||||
|                 container.attrs.get("HostConfig", {}) |                 container.attrs.get("HostConfig", {}) | ||||||
|                 .get("RestartPolicy", {}) |                 .get("RestartPolicy", {}) | ||||||
|  | |||||||
| @ -3,10 +3,11 @@ from typing import TYPE_CHECKING, Generic, TypeVar | |||||||
|  |  | ||||||
| from django.utils.text import slugify | from django.utils.text import slugify | ||||||
| from kubernetes.client import V1ObjectMeta | from kubernetes.client import V1ObjectMeta | ||||||
|  | from kubernetes.client.exceptions import ApiException, OpenApiException | ||||||
| from kubernetes.client.models.v1_deployment import V1Deployment | from kubernetes.client.models.v1_deployment import V1Deployment | ||||||
| from kubernetes.client.models.v1_pod import V1Pod | from kubernetes.client.models.v1_pod import V1Pod | ||||||
| from kubernetes.client.rest import ApiException |  | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  | from urllib3.exceptions import HTTPError | ||||||
|  |  | ||||||
| from authentik import __version__ | from authentik import __version__ | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| @ -72,8 +73,9 @@ class KubernetesObjectReconciler(Generic[T]): | |||||||
|         try: |         try: | ||||||
|             try: |             try: | ||||||
|                 current = self.retrieve() |                 current = self.retrieve() | ||||||
|             except ApiException as exc: |             except (OpenApiException, HTTPError) as exc: | ||||||
|                 if exc.status == 404: |                 # pylint: disable=no-member | ||||||
|  |                 if isinstance(exc, ApiException) and exc.status == 404: | ||||||
|                     self.logger.debug("Failed to get current, triggering recreate") |                     self.logger.debug("Failed to get current, triggering recreate") | ||||||
|                     raise NeedsRecreate from exc |                     raise NeedsRecreate from exc | ||||||
|                 self.logger.debug("Other unhandled error", exc=exc) |                 self.logger.debug("Other unhandled error", exc=exc) | ||||||
| @ -104,8 +106,9 @@ class KubernetesObjectReconciler(Generic[T]): | |||||||
|             current = self.retrieve() |             current = self.retrieve() | ||||||
|             self.delete(current) |             self.delete(current) | ||||||
|             self.logger.debug("Removing") |             self.logger.debug("Removing") | ||||||
|         except ApiException as exc: |         except (OpenApiException, HTTPError) as exc: | ||||||
|             if exc.status == 404: |             # pylint: disable=no-member | ||||||
|  |             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-existant") | ||||||
|                 return |                 return | ||||||
|             self.logger.debug("Other unhandled error", exc=exc) |             self.logger.debug("Other unhandled error", exc=exc) | ||||||
|  | |||||||
| @ -3,8 +3,9 @@ from io import StringIO | |||||||
| from typing import Type | from typing import Type | ||||||
|  |  | ||||||
| from kubernetes.client.api_client import ApiClient | from kubernetes.client.api_client import ApiClient | ||||||
| from kubernetes.client.exceptions import ApiException | from kubernetes.client.exceptions import OpenApiException | ||||||
| from structlog.testing import capture_logs | from structlog.testing import capture_logs | ||||||
|  | from urllib3.exceptions import HTTPError | ||||||
| from yaml import dump_all | from yaml import dump_all | ||||||
|  |  | ||||||
| from authentik.outposts.controllers.base import BaseController, ControllerException | from authentik.outposts.controllers.base import BaseController, ControllerException | ||||||
| @ -12,7 +13,7 @@ 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.secret import SecretReconciler | from authentik.outposts.controllers.k8s.secret import SecretReconciler | ||||||
| from authentik.outposts.controllers.k8s.service import ServiceReconciler | from authentik.outposts.controllers.k8s.service import ServiceReconciler | ||||||
| from authentik.outposts.models import KubernetesServiceConnection, Outpost | from authentik.outposts.models import KubernetesServiceConnection, Outpost, ServiceConnectionInvalid | ||||||
|  |  | ||||||
|  |  | ||||||
| class KubernetesController(BaseController): | class KubernetesController(BaseController): | ||||||
| @ -40,7 +41,7 @@ class KubernetesController(BaseController): | |||||||
|                 reconciler = self.reconcilers[reconcile_key](self) |                 reconciler = self.reconcilers[reconcile_key](self) | ||||||
|                 reconciler.up() |                 reconciler.up() | ||||||
|  |  | ||||||
|         except ApiException as exc: |         except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc: | ||||||
|             raise ControllerException(str(exc)) from exc |             raise ControllerException(str(exc)) from exc | ||||||
|  |  | ||||||
|     def up_with_logs(self) -> list[str]: |     def up_with_logs(self) -> list[str]: | ||||||
| @ -55,7 +56,7 @@ class KubernetesController(BaseController): | |||||||
|                     reconciler.up() |                     reconciler.up() | ||||||
|                 all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] |                 all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] | ||||||
|             return all_logs |             return all_logs | ||||||
|         except ApiException as exc: |         except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc: | ||||||
|             raise ControllerException(str(exc)) from exc |             raise ControllerException(str(exc)) from exc | ||||||
|  |  | ||||||
|     def down(self): |     def down(self): | ||||||
| @ -65,7 +66,7 @@ class KubernetesController(BaseController): | |||||||
|                 self.logger.debug("Tearing down object", name=reconcile_key) |                 self.logger.debug("Tearing down object", name=reconcile_key) | ||||||
|                 reconciler.down() |                 reconciler.down() | ||||||
|  |  | ||||||
|         except ApiException as exc: |         except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc: | ||||||
|             raise ControllerException(str(exc)) from exc |             raise ControllerException(str(exc)) from exc | ||||||
|  |  | ||||||
|     def down_with_logs(self) -> list[str]: |     def down_with_logs(self) -> list[str]: | ||||||
| @ -80,7 +81,7 @@ class KubernetesController(BaseController): | |||||||
|                     reconciler.down() |                     reconciler.down() | ||||||
|                 all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] |                 all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] | ||||||
|             return all_logs |             return all_logs | ||||||
|         except ApiException as exc: |         except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc: | ||||||
|             raise ControllerException(str(exc)) from exc |             raise ControllerException(str(exc)) from exc | ||||||
|  |  | ||||||
|     def get_static_deployment(self) -> str: |     def get_static_deployment(self) -> str: | ||||||
|  | |||||||
| @ -56,6 +56,7 @@ class ServiceConnectionInvalid(SentryIgnoredException): | |||||||
|  |  | ||||||
|  |  | ||||||
| @dataclass | @dataclass | ||||||
|  | # pylint: disable=too-many-instance-attributes | ||||||
| class OutpostConfig: | class OutpostConfig: | ||||||
|     """Configuration an outpost uses to configure it self""" |     """Configuration an outpost uses to configure it self""" | ||||||
|  |  | ||||||
| @ -67,8 +68,10 @@ class OutpostConfig: | |||||||
|     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") | ||||||
|     error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer") |     error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer") | ||||||
|  |  | ||||||
|     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) | ||||||
|  |  | ||||||
|     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) | ||||||
|     kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) |     kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) | ||||||
| @ -362,7 +365,7 @@ class Outpost(ManagedModel): | |||||||
|                     ) |                     ) | ||||||
|                     try: |                     try: | ||||||
|                         assign_perm(code_name, user, model_or_perm) |                         assign_perm(code_name, user, model_or_perm) | ||||||
|                     except Permission.DoesNotExist as exc: |                     except (Permission.DoesNotExist, AttributeError) as exc: | ||||||
|                         LOGGER.warning( |                         LOGGER.warning( | ||||||
|                             "permission doesn't exist", |                             "permission doesn't exist", | ||||||
|                             code_name=code_name, |                             code_name=code_name, | ||||||
|  | |||||||
| @ -8,8 +8,11 @@ from structlog.stdlib import get_logger | |||||||
|  |  | ||||||
| from authentik.policies.models import Policy | from authentik.policies.models import Policy | ||||||
| from authentik.policies.types import PolicyRequest, PolicyResult | from authentik.policies.types import PolicyRequest, PolicyResult | ||||||
|  | from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | RE_LOWER = re.compile("[a-z]") | ||||||
|  | RE_UPPER = re.compile("[A-Z]") | ||||||
|  |  | ||||||
|  |  | ||||||
| class PasswordPolicy(Policy): | class PasswordPolicy(Policy): | ||||||
| @ -38,31 +41,42 @@ class PasswordPolicy(Policy): | |||||||
|         return "ak-policy-password-form" |         return "ak-policy-password-form" | ||||||
|  |  | ||||||
|     def passes(self, request: PolicyRequest) -> PolicyResult: |     def passes(self, request: PolicyRequest) -> PolicyResult: | ||||||
|         if self.password_field not in request.context: |         if ( | ||||||
|  |             self.password_field not in request.context | ||||||
|  |             and self.password_field not in request.context.get(PLAN_CONTEXT_PROMPT, {}) | ||||||
|  |         ): | ||||||
|             LOGGER.warning( |             LOGGER.warning( | ||||||
|                 "Password field not set in Policy Request", |                 "Password field not set in Policy Request", | ||||||
|                 field=self.password_field, |                 field=self.password_field, | ||||||
|                 fields=request.context.keys(), |                 fields=request.context.keys(), | ||||||
|  |                 prompt_fields=request.context.get(PLAN_CONTEXT_PROMPT, {}).keys(), | ||||||
|             ) |             ) | ||||||
|             return PolicyResult(False, _("Password not set in context")) |             return PolicyResult(False, _("Password not set in context")) | ||||||
|  |  | ||||||
|  |         if self.password_field in request.context: | ||||||
|             password = request.context[self.password_field] |             password = request.context[self.password_field] | ||||||
|  |         else: | ||||||
|  |             password = request.context[PLAN_CONTEXT_PROMPT][self.password_field] | ||||||
|  |  | ||||||
|         filter_regex = [] |         if len(password) < self.length_min: | ||||||
|         if self.amount_lowercase > 0: |             LOGGER.debug("password failed", reason="length") | ||||||
|             filter_regex.append(r"[a-z]{%d,}" % self.amount_lowercase) |             return PolicyResult(False, self.error_message) | ||||||
|         if self.amount_uppercase > 0: |  | ||||||
|             filter_regex.append(r"[A-Z]{%d,}" % self.amount_uppercase) |         if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase: | ||||||
|  |             LOGGER.debug("password failed", reason="amount_lowercase") | ||||||
|  |             return PolicyResult(False, self.error_message) | ||||||
|  |         if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_lowercase: | ||||||
|  |             LOGGER.debug("password failed", reason="amount_uppercase") | ||||||
|  |             return PolicyResult(False, self.error_message) | ||||||
|         if self.amount_symbols > 0: |         if self.amount_symbols > 0: | ||||||
|             filter_regex.append(r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols)) |             count = 0 | ||||||
|         full_regex = "|".join(filter_regex) |             for symbol in self.symbol_charset: | ||||||
|         LOGGER.debug("Built regex", regexp=full_regex) |                 count += password.count(symbol) | ||||||
|         result = bool(re.compile(full_regex).match(password)) |             if count < self.amount_symbols: | ||||||
|  |                 LOGGER.debug("password failed", reason="amount_symbols") | ||||||
|  |                 return PolicyResult(False, self.error_message) | ||||||
|  |  | ||||||
|         result = result and len(password) >= self.length_min |         return PolicyResult(True) | ||||||
|  |  | ||||||
|         if not result: |  | ||||||
|             return PolicyResult(result, self.error_message) |  | ||||||
|         return PolicyResult(result) |  | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,57 +0,0 @@ | |||||||
| """Password Policy tests""" |  | ||||||
| from django.test import TestCase |  | ||||||
| from guardian.shortcuts import get_anonymous_user |  | ||||||
|  |  | ||||||
| from authentik.policies.password.models import PasswordPolicy |  | ||||||
| from authentik.policies.types import PolicyRequest, PolicyResult |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestPasswordPolicy(TestCase): |  | ||||||
|     """Test Password Policy""" |  | ||||||
|  |  | ||||||
|     def test_invalid(self): |  | ||||||
|         """Test without password""" |  | ||||||
|         policy = PasswordPolicy.objects.create( |  | ||||||
|             name="test_invalid", |  | ||||||
|             amount_uppercase=1, |  | ||||||
|             amount_lowercase=2, |  | ||||||
|             amount_symbols=3, |  | ||||||
|             length_min=24, |  | ||||||
|             error_message="test message", |  | ||||||
|         ) |  | ||||||
|         request = PolicyRequest(get_anonymous_user()) |  | ||||||
|         result: PolicyResult = policy.passes(request) |  | ||||||
|         self.assertFalse(result.passing) |  | ||||||
|         self.assertEqual(result.messages[0], "Password not set in context") |  | ||||||
|  |  | ||||||
|     def test_false(self): |  | ||||||
|         """Failing password case""" |  | ||||||
|         policy = PasswordPolicy.objects.create( |  | ||||||
|             name="test_false", |  | ||||||
|             amount_uppercase=1, |  | ||||||
|             amount_lowercase=2, |  | ||||||
|             amount_symbols=3, |  | ||||||
|             length_min=24, |  | ||||||
|             error_message="test message", |  | ||||||
|         ) |  | ||||||
|         request = PolicyRequest(get_anonymous_user()) |  | ||||||
|         request.context["password"] = "test" |  | ||||||
|         result: PolicyResult = policy.passes(request) |  | ||||||
|         self.assertFalse(result.passing) |  | ||||||
|         self.assertEqual(result.messages, ("test message",)) |  | ||||||
|  |  | ||||||
|     def test_true(self): |  | ||||||
|         """Positive password case""" |  | ||||||
|         policy = PasswordPolicy.objects.create( |  | ||||||
|             name="test_true", |  | ||||||
|             amount_uppercase=1, |  | ||||||
|             amount_lowercase=2, |  | ||||||
|             amount_symbols=3, |  | ||||||
|             length_min=3, |  | ||||||
|             error_message="test message", |  | ||||||
|         ) |  | ||||||
|         request = PolicyRequest(get_anonymous_user()) |  | ||||||
|         request.context["password"] = "Test()!" |  | ||||||
|         result: PolicyResult = policy.passes(request) |  | ||||||
|         self.assertTrue(result.passing) |  | ||||||
|         self.assertEqual(result.messages, tuple()) |  | ||||||
							
								
								
									
										0
									
								
								authentik/policies/password/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/policies/password/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										80
									
								
								authentik/policies/password/tests/test_flows.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								authentik/policies/password/tests/test_flows.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | |||||||
|  | """Password flow tests""" | ||||||
|  | from django.urls.base import reverse | ||||||
|  | from django.utils.encoding import force_str | ||||||
|  | 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 | ||||||
|  | from authentik.policies.password.models import PasswordPolicy | ||||||
|  | from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestPasswordPolicyFlow(APITestCase): | ||||||
|  |     """Test Password Policy""" | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         self.user = User.objects.create(username="unittest", email="test@beryju.org") | ||||||
|  |  | ||||||
|  |         self.flow = Flow.objects.create( | ||||||
|  |             name="test-prompt", | ||||||
|  |             slug="test-prompt", | ||||||
|  |             designation=FlowDesignation.AUTHENTICATION, | ||||||
|  |         ) | ||||||
|  |         password_prompt = Prompt.objects.create( | ||||||
|  |             field_key="password", | ||||||
|  |             label="PASSWORD_LABEL", | ||||||
|  |             type=FieldTypes.PASSWORD, | ||||||
|  |             required=True, | ||||||
|  |             placeholder="PASSWORD_PLACEHOLDER", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.policy = PasswordPolicy.objects.create( | ||||||
|  |             name="test_true", | ||||||
|  |             amount_uppercase=1, | ||||||
|  |             amount_lowercase=2, | ||||||
|  |             amount_symbols=3, | ||||||
|  |             length_min=3, | ||||||
|  |             error_message="test message", | ||||||
|  |         ) | ||||||
|  |         stage = PromptStage.objects.create(name="prompt-stage") | ||||||
|  |         stage.validation_policies.set([self.policy]) | ||||||
|  |         stage.fields.set( | ||||||
|  |             [ | ||||||
|  |                 password_prompt, | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |         FlowStageBinding.objects.create(target=self.flow, stage=stage, order=2) | ||||||
|  |  | ||||||
|  |     def test_prompt_data(self): | ||||||
|  |         """Test policy attached to a prompt stage""" | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||||
|  |             {"password": "akadmin"}, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             force_str(response.content), | ||||||
|  |             { | ||||||
|  |                 "component": "ak-stage-prompt", | ||||||
|  |                 "fields": [ | ||||||
|  |                     { | ||||||
|  |                         "field_key": "password", | ||||||
|  |                         "label": "PASSWORD_LABEL", | ||||||
|  |                         "order": 0, | ||||||
|  |                         "placeholder": "PASSWORD_PLACEHOLDER", | ||||||
|  |                         "required": True, | ||||||
|  |                         "type": "password", | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 "flow_info": { | ||||||
|  |                     "background": self.flow.background_url, | ||||||
|  |                     "cancel_url": reverse("authentik_flows:cancel"), | ||||||
|  |                     "title": "", | ||||||
|  |                 }, | ||||||
|  |                 "response_errors": { | ||||||
|  |                     "non_field_errors": [{"code": "invalid", "string": self.policy.error_message}] | ||||||
|  |                 }, | ||||||
|  |                 "type": ChallengeTypes.NATIVE.value, | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
							
								
								
									
										68
									
								
								authentik/policies/password/tests/test_policy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								authentik/policies/password/tests/test_policy.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | """Password Policy tests""" | ||||||
|  | from django.test import TestCase | ||||||
|  | from guardian.shortcuts import get_anonymous_user | ||||||
|  |  | ||||||
|  | from authentik.lib.generators import generate_key | ||||||
|  | from authentik.policies.password.models import PasswordPolicy | ||||||
|  | from authentik.policies.types import PolicyRequest, PolicyResult | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestPasswordPolicy(TestCase): | ||||||
|  |     """Test Password Policy""" | ||||||
|  |  | ||||||
|  |     def setUp(self) -> None: | ||||||
|  |         self.policy = PasswordPolicy.objects.create( | ||||||
|  |             name="test_false", | ||||||
|  |             amount_uppercase=1, | ||||||
|  |             amount_lowercase=2, | ||||||
|  |             amount_symbols=3, | ||||||
|  |             length_min=24, | ||||||
|  |             error_message="test message", | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def test_invalid(self): | ||||||
|  |         """Test without password""" | ||||||
|  |         request = PolicyRequest(get_anonymous_user()) | ||||||
|  |         result: PolicyResult = self.policy.passes(request) | ||||||
|  |         self.assertFalse(result.passing) | ||||||
|  |         self.assertEqual(result.messages[0], "Password not set in context") | ||||||
|  |  | ||||||
|  |     def test_failed_length(self): | ||||||
|  |         """Password too short""" | ||||||
|  |         request = PolicyRequest(get_anonymous_user()) | ||||||
|  |         request.context["password"] = "test" | ||||||
|  |         result: PolicyResult = self.policy.passes(request) | ||||||
|  |         self.assertFalse(result.passing) | ||||||
|  |         self.assertEqual(result.messages, ("test message",)) | ||||||
|  |  | ||||||
|  |     def test_failed_lowercase(self): | ||||||
|  |         """not enough lowercase""" | ||||||
|  |         request = PolicyRequest(get_anonymous_user()) | ||||||
|  |         request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe" | ||||||
|  |         result: PolicyResult = self.policy.passes(request) | ||||||
|  |         self.assertFalse(result.passing) | ||||||
|  |         self.assertEqual(result.messages, ("test message",)) | ||||||
|  |  | ||||||
|  |     def test_failed_uppercase(self): | ||||||
|  |         """not enough uppercase""" | ||||||
|  |         request = PolicyRequest(get_anonymous_user()) | ||||||
|  |         request.context["password"] = "tttttttttttttttttttttttE" | ||||||
|  |         result: PolicyResult = self.policy.passes(request) | ||||||
|  |         self.assertFalse(result.passing) | ||||||
|  |         self.assertEqual(result.messages, ("test message",)) | ||||||
|  |  | ||||||
|  |     def test_failed_symbols(self): | ||||||
|  |         """not enough uppercase""" | ||||||
|  |         request = PolicyRequest(get_anonymous_user()) | ||||||
|  |         request.context["password"] = "TETETETETETETETETETETETETe!!!" | ||||||
|  |         result: PolicyResult = self.policy.passes(request) | ||||||
|  |         self.assertFalse(result.passing) | ||||||
|  |         self.assertEqual(result.messages, ("test message",)) | ||||||
|  |  | ||||||
|  |     def test_true(self): | ||||||
|  |         """Positive password case""" | ||||||
|  |         request = PolicyRequest(get_anonymous_user()) | ||||||
|  |         request.context["password"] = generate_key() + "ee!!!" | ||||||
|  |         result: PolicyResult = self.policy.passes(request) | ||||||
|  |         self.assertTrue(result.passing) | ||||||
|  |         self.assertEqual(result.messages, tuple()) | ||||||
| @ -29,7 +29,19 @@ class LDAPProviderViewSet(UsedByMixin, ModelViewSet): | |||||||
|  |  | ||||||
|     queryset = LDAPProvider.objects.all() |     queryset = LDAPProvider.objects.all() | ||||||
|     serializer_class = LDAPProviderSerializer |     serializer_class = LDAPProviderSerializer | ||||||
|     filterset_fields = "__all__" |     filterset_fields = { | ||||||
|  |         "application": ["isnull"], | ||||||
|  |         "name": ["iexact"], | ||||||
|  |         "authorization_flow__slug": ["iexact"], | ||||||
|  |         "base_dn": ["iexact"], | ||||||
|  |         "search_group__group_uuid": ["iexact"], | ||||||
|  |         "search_group__name": ["iexact"], | ||||||
|  |         "certificate__kp_uuid": ["iexact"], | ||||||
|  |         "certificate__name": ["iexact"], | ||||||
|  |         "tls_server_name": ["iexact"], | ||||||
|  |         "uid_start_number": ["iexact"], | ||||||
|  |         "gid_start_number": ["iexact"], | ||||||
|  |     } | ||||||
|     ordering = ["name"] |     ordering = ["name"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -80,7 +80,24 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet): | |||||||
|  |  | ||||||
|     queryset = ProxyProvider.objects.all() |     queryset = ProxyProvider.objects.all() | ||||||
|     serializer_class = ProxyProviderSerializer |     serializer_class = ProxyProviderSerializer | ||||||
|     filterset_fields = "__all__" |     filterset_fields = { | ||||||
|  |         "application": ["isnull"], | ||||||
|  |         "name": ["iexact"], | ||||||
|  |         "authorization_flow__slug": ["iexact"], | ||||||
|  |         "property_mappings": ["iexact"], | ||||||
|  |         "internal_host": ["iexact"], | ||||||
|  |         "external_host": ["iexact"], | ||||||
|  |         "internal_host_ssl_validation": ["iexact"], | ||||||
|  |         "certificate__kp_uuid": ["iexact"], | ||||||
|  |         "certificate__name": ["iexact"], | ||||||
|  |         "skip_path_regex": ["iexact"], | ||||||
|  |         "basic_auth_enabled": ["iexact"], | ||||||
|  |         "basic_auth_password_attribute": ["iexact"], | ||||||
|  |         "basic_auth_user_attribute": ["iexact"], | ||||||
|  |         "mode": ["iexact"], | ||||||
|  |         "redirect_uris": ["iexact"], | ||||||
|  |         "cookie_domain": ["iexact"], | ||||||
|  |     } | ||||||
|     ordering = ["name"] |     ordering = ["name"] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -22,13 +22,13 @@ class ProxyDockerController(DockerController): | |||||||
|         for proxy_provider in ProxyProvider.objects.filter(outpost__in=[self.outpost]): |         for proxy_provider in ProxyProvider.objects.filter(outpost__in=[self.outpost]): | ||||||
|             proxy_provider: ProxyProvider |             proxy_provider: ProxyProvider | ||||||
|             external_host_name = urlparse(proxy_provider.external_host) |             external_host_name = urlparse(proxy_provider.external_host) | ||||||
|             hosts.append(f"`{external_host_name}`") |             hosts.append(f"`{external_host_name.netloc}`") | ||||||
|         traefik_name = f"ak-outpost-{self.outpost.pk.hex}" |         traefik_name = f"ak-outpost-{self.outpost.pk.hex}" | ||||||
|         return { |         labels = super()._get_labels() | ||||||
|             "traefik.enable": "true", |         labels["traefik.enable"] = "true" | ||||||
|             f"traefik.http.routers.{traefik_name}-router.rule": f"Host({','.join(hosts)})", |         labels[f"traefik.http.routers.{traefik_name}-router.rule"] = f"Host({','.join(hosts)})" | ||||||
|             f"traefik.http.routers.{traefik_name}-router.tls": "true", |         labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true" | ||||||
|             f"traefik.http.routers.{traefik_name}-router.service": f"{traefik_name}-service", |         labels[f"traefik.http.routers.{traefik_name}-router.service"] = f"{traefik_name}-service" | ||||||
|             f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path": "/", |         labels[f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path"] = "/" | ||||||
|             f"traefik.http.services.{traefik_name}-service.loadbalancer.server.port": "4180", |         labels[f"traefik.http.services.{traefik_name}-service.loadbalancer.server.port"] = "4180" | ||||||
|         } |         return labels | ||||||
|  | |||||||
| @ -61,6 +61,7 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]): | |||||||
|         have_hosts.sort() |         have_hosts.sort() | ||||||
|  |  | ||||||
|         have_hosts_tls = [] |         have_hosts_tls = [] | ||||||
|  |         if current.spec.tls: | ||||||
|             for tls_config in current.spec.tls: |             for tls_config in current.spec.tls: | ||||||
|                 if tls_config and tls_config.hosts: |                 if tls_config and tls_config.hosts: | ||||||
|                     have_hosts_tls += tls_config.hosts |                     have_hosts_tls += tls_config.hosts | ||||||
|  | |||||||
| @ -96,6 +96,7 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]) | |||||||
|  |  | ||||||
|     def get_reference_object(self) -> TraefikMiddleware: |     def get_reference_object(self) -> TraefikMiddleware: | ||||||
|         """Get deployment object for outpost""" |         """Get deployment object for outpost""" | ||||||
|  |         port = 9000 if self.is_embedded else 4180 | ||||||
|         return TraefikMiddleware( |         return TraefikMiddleware( | ||||||
|             apiVersion=f"{CRD_GROUP}/{CRD_VERSION}", |             apiVersion=f"{CRD_GROUP}/{CRD_VERSION}", | ||||||
|             kind="Middleware", |             kind="Middleware", | ||||||
| @ -106,7 +107,7 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]) | |||||||
|             ), |             ), | ||||||
|             spec=TraefikMiddlewareSpec( |             spec=TraefikMiddlewareSpec( | ||||||
|                 forwardAuth=TraefikMiddlewareSpecForwardAuth( |                 forwardAuth=TraefikMiddlewareSpecForwardAuth( | ||||||
|                     address=f"http://{self.name}.{self.namespace}:4180/akprox/auth?traefik", |                     address=f"http://{self.name}.{self.namespace}:{port}/akprox/auth?traefik", | ||||||
|                     authResponseHeaders=[ |                     authResponseHeaders=[ | ||||||
|                         "Set-Cookie", |                         "Set-Cookie", | ||||||
|                         "X-Auth-Username", |                         "X-Auth-Username", | ||||||
|  | |||||||
| @ -19,22 +19,24 @@ class ASGILogger: | |||||||
|  |  | ||||||
|     app: ASGIApp |     app: ASGIApp | ||||||
|  |  | ||||||
|     status_code: int |  | ||||||
|     start: float |  | ||||||
|  |  | ||||||
|     def __init__(self, app: ASGIApp): |     def __init__(self, app: ASGIApp): | ||||||
|         self.app = app |         self.app = app | ||||||
|  |  | ||||||
|     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: |     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: | ||||||
|         content_length = 0 |         content_length = 0 | ||||||
|  |         status_code = 0 | ||||||
|         request_id = "" |         request_id = "" | ||||||
|  |         location = "" | ||||||
|  |         start = time() | ||||||
|  |  | ||||||
|         async def send_hooked(message: Message) -> None: |         async def send_hooked(message: Message) -> None: | ||||||
|             """Hooked send method, which records status code and content-length, and for the final |             """Hooked send method, which records status code and content-length, and for the final | ||||||
|             requests logs it""" |             requests logs it""" | ||||||
|  |  | ||||||
|             headers = dict(message.get("headers", [])) |             headers = dict(message.get("headers", [])) | ||||||
|             if "status" in message: |             if "status" in message: | ||||||
|                 self.status_code = message["status"] |                 nonlocal status_code | ||||||
|  |                 status_code = message["status"] | ||||||
|  |  | ||||||
|             if b"Content-Length" in headers: |             if b"Content-Length" in headers: | ||||||
|                 nonlocal content_length |                 nonlocal content_length | ||||||
| @ -43,14 +45,19 @@ class ASGILogger: | |||||||
|             if message["type"] == "http.response.start": |             if message["type"] == "http.response.start": | ||||||
|                 response_headers = dict(message["headers"]) |                 response_headers = dict(message["headers"]) | ||||||
|                 nonlocal request_id |                 nonlocal request_id | ||||||
|  |                 nonlocal location | ||||||
|                 request_id = response_headers.get(RESPONSE_HEADER_ID.encode(), b"").decode() |                 request_id = response_headers.get(RESPONSE_HEADER_ID.encode(), b"").decode() | ||||||
|  |                 location = response_headers.get(b"Location", b"").decode() | ||||||
|  |  | ||||||
|             if message["type"] == "http.response.body" and not message.get("more_body", True): |             if message["type"] == "http.response.body" and not message.get("more_body", True): | ||||||
|                 runtime = int((time() - self.start) * 1000) |                 nonlocal start | ||||||
|                 self.log(scope, runtime, content_length, request_id=request_id) |                 runtime = int((time() - start) * 1000) | ||||||
|  |                 kwargs = {"request_id": request_id} | ||||||
|  |                 if location != "": | ||||||
|  |                     kwargs["location"] = location | ||||||
|  |                 self.log(scope, runtime, content_length, status_code, **kwargs) | ||||||
|             await send(message) |             await send(message) | ||||||
|  |  | ||||||
|         self.start = time() |  | ||||||
|         if scope["type"] == "lifespan": |         if scope["type"] == "lifespan": | ||||||
|             # https://code.djangoproject.com/ticket/31508 |             # https://code.djangoproject.com/ticket/31508 | ||||||
|             # https://github.com/encode/uvicorn/issues/266 |             # https://github.com/encode/uvicorn/issues/266 | ||||||
| @ -68,7 +75,7 @@ class ASGILogger: | |||||||
|         # Check if header has multiple values, and use the first one |         # Check if header has multiple values, and use the first one | ||||||
|         return client_ip.split(", ")[0] |         return client_ip.split(", ")[0] | ||||||
|  |  | ||||||
|     def log(self, scope: Scope, content_length: int, runtime: float, **kwargs): |     def log(self, scope: Scope, content_length: int, runtime: float, status_code: int, **kwargs): | ||||||
|         """Outpot access logs in a structured format""" |         """Outpot access logs in a structured format""" | ||||||
|         host = self._get_ip(scope) |         host = self._get_ip(scope) | ||||||
|         query_string = "" |         query_string = "" | ||||||
| @ -79,7 +86,7 @@ class ASGILogger: | |||||||
|             host=host, |             host=host, | ||||||
|             method=scope.get("method", ""), |             method=scope.get("method", ""), | ||||||
|             scheme=scope.get("scheme", ""), |             scheme=scope.get("scheme", ""), | ||||||
|             status=self.status_code, |             status=status_code, | ||||||
|             size=content_length / 1000 if content_length > 0 else 0, |             size=content_length / 1000 if content_length > 0 else 0, | ||||||
|             runtime=runtime, |             runtime=runtime, | ||||||
|             **kwargs, |             **kwargs, | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ class SessionMiddleware(UpstreamSessionMiddleware): | |||||||
|             # Since go does not consider localhost with http a secure origin |             # Since go does not consider localhost with http a secure origin | ||||||
|             # we can't set the secure flag. |             # we can't set the secure flag. | ||||||
|             user_agent = request.META.get("HTTP_USER_AGENT", "") |             user_agent = request.META.get("HTTP_USER_AGENT", "") | ||||||
|             if user_agent.startswith("authentik-outpost@"): |             if user_agent.startswith("authentik-outpost@") or "safari" in user_agent.lower(): | ||||||
|                 return False |                 return False | ||||||
|             return True |             return True | ||||||
|         return False |         return False | ||||||
|  | |||||||
| @ -74,6 +74,7 @@ LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}" | |||||||
| SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}" | SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}" | ||||||
|  |  | ||||||
| AUTHENTICATION_BACKENDS = [ | AUTHENTICATION_BACKENDS = [ | ||||||
|  |     "django.contrib.auth.backends.ModelBackend", | ||||||
|     BACKEND_INBUILT, |     BACKEND_INBUILT, | ||||||
|     BACKEND_APP_PASSWORD, |     BACKEND_APP_PASSWORD, | ||||||
|     BACKEND_LDAP, |     BACKEND_LDAP, | ||||||
| @ -149,9 +150,20 @@ SPECTACULAR_SETTINGS = { | |||||||
|     "DESCRIPTION": "Making authentication simple.", |     "DESCRIPTION": "Making authentication simple.", | ||||||
|     "VERSION": __version__, |     "VERSION": __version__, | ||||||
|     "COMPONENT_SPLIT_REQUEST": True, |     "COMPONENT_SPLIT_REQUEST": True, | ||||||
|  |     "SCHEMA_PATH_PREFIX": "/api/v([0-9]+(beta)?)", | ||||||
|  |     "SCHEMA_PATH_PREFIX_TRIM": True, | ||||||
|  |     "SERVERS": [ | ||||||
|  |         { | ||||||
|  |             "url": "/api/v3/", | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             "url": "/api/v2beta/", | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|     "CONTACT": { |     "CONTACT": { | ||||||
|         "email": "hello@beryju.org", |         "email": "hello@beryju.org", | ||||||
|     }, |     }, | ||||||
|  |     "AUTHENTICATION_WHITELIST": ["authentik.api.authentication.TokenAuthentication"], | ||||||
|     "LICENSE": { |     "LICENSE": { | ||||||
|         "name": "GNU GPLv3", |         "name": "GNU GPLv3", | ||||||
|         "url": "https://github.com/goauthentik/authentik/blob/master/LICENSE", |         "url": "https://github.com/goauthentik/authentik/blob/master/LICENSE", | ||||||
| @ -179,6 +191,9 @@ REST_FRAMEWORK = { | |||||||
|         "rest_framework.filters.OrderingFilter", |         "rest_framework.filters.OrderingFilter", | ||||||
|         "rest_framework.filters.SearchFilter", |         "rest_framework.filters.SearchFilter", | ||||||
|     ], |     ], | ||||||
|  |     "DEFAULT_PARSER_CLASSES": [ | ||||||
|  |         "rest_framework.parsers.JSONParser", | ||||||
|  |     ], | ||||||
|     "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoObjectPermissions",), |     "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoObjectPermissions",), | ||||||
|     "DEFAULT_AUTHENTICATION_CLASSES": ( |     "DEFAULT_AUTHENTICATION_CLASSES": ( | ||||||
|         "authentik.api.authentication.TokenAuthentication", |         "authentik.api.authentication.TokenAuthentication", | ||||||
| @ -188,6 +203,7 @@ REST_FRAMEWORK = { | |||||||
|         "rest_framework.renderers.JSONRenderer", |         "rest_framework.renderers.JSONRenderer", | ||||||
|     ], |     ], | ||||||
|     "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", |     "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", | ||||||
|  |     "TEST_REQUEST_DEFAULT_FORMAT": "json", | ||||||
| } | } | ||||||
|  |  | ||||||
| REDIS_PROTOCOL_PREFIX = "redis://" | REDIS_PROTOCOL_PREFIX = "redis://" | ||||||
| @ -356,7 +372,7 @@ CELERY_RESULT_BACKEND = ( | |||||||
| # Database backup | # Database backup | ||||||
| DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" | DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" | ||||||
| DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"} | DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"} | ||||||
| DBBACKUP_FILENAME_TEMPLATE = "authentik-backup-{datetime}.sql" | DBBACKUP_FILENAME_TEMPLATE = f"authentik-backup-{__version__}-{{datetime}}.sql" | ||||||
| DBBACKUP_CONNECTOR_MAPPING = { | DBBACKUP_CONNECTOR_MAPPING = { | ||||||
|     "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector", |     "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector", | ||||||
| } | } | ||||||
| @ -371,6 +387,7 @@ if CONFIG.y("postgresql.s3_backup"): | |||||||
|         "default_acl": "private", |         "default_acl": "private", | ||||||
|         "endpoint_url": CONFIG.y("postgresql.s3_backup.host"), |         "endpoint_url": CONFIG.y("postgresql.s3_backup.host"), | ||||||
|         "location": CONFIG.y("postgresql.s3_backup.location", ""), |         "location": CONFIG.y("postgresql.s3_backup.location", ""), | ||||||
|  |         "verify": not CONFIG.y_bool("postgresql.s3_backup.insecure_skip_verify", False), | ||||||
|     } |     } | ||||||
|     j_print( |     j_print( | ||||||
|         "Database backup to S3 is configured", |         "Database backup to S3 is configured", | ||||||
|  | |||||||
| @ -2,17 +2,13 @@ | |||||||
| from base64 import b64encode | from base64 import b64encode | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.test import Client, TestCase | from django.test import TestCase | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestRoot(TestCase): | class TestRoot(TestCase): | ||||||
|     """Test root application""" |     """Test root application""" | ||||||
|  |  | ||||||
|     def setUp(self): |  | ||||||
|         super().setUp() |  | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|     def test_monitoring_error(self): |     def test_monitoring_error(self): | ||||||
|         """Test monitoring without any credentials""" |         """Test monitoring without any credentials""" | ||||||
|         response = self.client.get(reverse("metrics")) |         response = self.client.get(reverse("metrics")) | ||||||
|  | |||||||
| @ -43,7 +43,6 @@ class BaseOAuthClient: | |||||||
|             profile_url = self.source.profile_url |             profile_url = self.source.profile_url | ||||||
|         try: |         try: | ||||||
|             response = self.do_request("get", profile_url, token=token) |             response = self.do_request("get", profile_url, token=token) | ||||||
|             LOGGER.debug(response.text) |  | ||||||
|             response.raise_for_status() |             response.raise_for_status() | ||||||
|         except RequestException as exc: |         except RequestException as exc: | ||||||
|             LOGGER.warning("Unable to fetch user profile", exc=exc) |             LOGGER.warning("Unable to fetch user profile", exc=exc) | ||||||
|  | |||||||
| @ -65,7 +65,6 @@ class OAuth2Client(BaseOAuthClient): | |||||||
|                 data=args, |                 data=args, | ||||||
|                 headers=self._default_headers, |                 headers=self._default_headers, | ||||||
|             ) |             ) | ||||||
|             LOGGER.debug(response.text) |  | ||||||
|             response.raise_for_status() |             response.raise_for_status() | ||||||
|         except RequestException as exc: |         except RequestException as exc: | ||||||
|             LOGGER.warning("Unable to fetch access token", exc=exc) |             LOGGER.warning("Unable to fetch access token", exc=exc) | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| """Twitter Type tests""" | """Twitter Type tests""" | ||||||
| from django.test import Client, TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from authentik.sources.oauth.models import OAuthSource | from authentik.sources.oauth.models import OAuthSource | ||||||
| from authentik.sources.oauth.types.twitter import TwitterOAuthCallback | from authentik.sources.oauth.types.twitter import TwitterOAuthCallback | ||||||
| @ -92,7 +92,6 @@ class TestTypeGitHub(TestCase): | |||||||
|     """OAuth Source tests""" |     """OAuth Source tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         self.client = Client() |  | ||||||
|         self.source = OAuthSource.objects.create( |         self.source = OAuthSource.objects.create( | ||||||
|             name="test", |             name="test", | ||||||
|             slug="test", |             slug="test", | ||||||
|  | |||||||
| @ -36,7 +36,6 @@ class AzureADClient(OAuth2Client): | |||||||
|                 profile_url, |                 profile_url, | ||||||
|                 headers={"Authorization": f"{token['token_type']} {token['access_token']}"}, |                 headers={"Authorization": f"{token['token_type']} {token['access_token']}"}, | ||||||
|             ) |             ) | ||||||
|             LOGGER.debug(response.text) |  | ||||||
|             response.raise_for_status() |             response.raise_for_status() | ||||||
|         except RequestException as exc: |         except RequestException as exc: | ||||||
|             LOGGER.warning("Unable to fetch user profile", exc=exc) |             LOGGER.warning("Unable to fetch user profile", exc=exc) | ||||||
|  | |||||||
| @ -51,20 +51,30 @@ def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict | |||||||
|     # for the reasons outlined in the comment in webauthn_begin_activate. |     # for the reasons outlined in the comment in webauthn_begin_activate. | ||||||
|     request.session["challenge"] = challenge.rstrip("=") |     request.session["challenge"] = challenge.rstrip("=") | ||||||
|  |  | ||||||
|  |     assertion = {} | ||||||
|  |     user = device.user | ||||||
|  |  | ||||||
|  |     # We want all the user's WebAuthn devices and merge their challenges | ||||||
|  |     for user_device in WebAuthnDevice.objects.filter(user=device.user).order_by("name"): | ||||||
|         webauthn_user = WebAuthnUser( |         webauthn_user = WebAuthnUser( | ||||||
|         device.user.uid, |             user.uid, | ||||||
|         device.user.username, |             user.username, | ||||||
|         device.user.name, |             user.name, | ||||||
|         device.user.avatar, |             user.avatar, | ||||||
|         device.credential_id, |             user_device.credential_id, | ||||||
|         device.public_key, |             user_device.public_key, | ||||||
|         device.sign_count, |             user_device.sign_count, | ||||||
|         device.rp_id, |             user_device.rp_id, | ||||||
|  |         ) | ||||||
|  |         webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge) | ||||||
|  |         if assertion == {}: | ||||||
|  |             assertion = webauthn_assertion_options.assertion_dict | ||||||
|  |         else: | ||||||
|  |             assertion["allowCredentials"] += webauthn_assertion_options.assertion_dict.get( | ||||||
|  |                 "allowCredentials" | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|     webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge) |     return assertion | ||||||
|  |  | ||||||
|     return webauthn_assertion_options.assertion_dict |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_challenge_code(code: str, request: HttpRequest, user: User) -> str: | def validate_challenge_code(code: str, request: HttpRequest, user: User) -> str: | ||||||
|  | |||||||
| @ -20,8 +20,6 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate | |||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
| PER_DEVICE_CLASSES = [DeviceClasses.WEBAUTHN] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthenticatorValidationChallenge(WithUserInfoChallenge): | class AuthenticatorValidationChallenge(WithUserInfoChallenge): | ||||||
|     """Authenticator challenge""" |     """Authenticator challenge""" | ||||||
| @ -91,9 +89,9 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|             if device_class not in stage.device_classes: |             if device_class not in stage.device_classes: | ||||||
|                 LOGGER.debug("device class not allowed", device_class=device_class) |                 LOGGER.debug("device class not allowed", device_class=device_class) | ||||||
|                 continue |                 continue | ||||||
|             # Ensure only classes in PER_DEVICE_CLASSES are returned per device |             # Ensure only one challenge per device class | ||||||
|             # otherwise only return a single challenge |             # WebAuthn does another device loop to find all webuahtn devices | ||||||
|             if device_class in seen_classes and device_class not in PER_DEVICE_CLASSES: |             if device_class in seen_classes: | ||||||
|                 continue |                 continue | ||||||
|             if device_class not in seen_classes: |             if device_class not in seen_classes: | ||||||
|                 seen_classes.append(device_class) |                 seen_classes.append(device_class) | ||||||
|  | |||||||
| @ -1,12 +1,12 @@ | |||||||
| """Test validator stage""" | """Test validator stage""" | ||||||
| from unittest.mock import MagicMock, patch | from unittest.mock import MagicMock, patch | ||||||
|  |  | ||||||
| from django.test import TestCase |  | ||||||
| from django.test.client import RequestFactory | from django.test.client import RequestFactory | ||||||
| from django.urls.base import reverse | from django.urls.base import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
| from django_otp.plugins.otp_totp.models import TOTPDevice | from django_otp.plugins.otp_totp.models import TOTPDevice | ||||||
| from rest_framework.exceptions import ValidationError | from rest_framework.exceptions import ValidationError | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -26,7 +26,7 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice | |||||||
| from authentik.stages.identification.models import IdentificationStage, UserFields | from authentik.stages.identification.models import IdentificationStage, UserFields | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthenticatorValidateStageTests(TestCase): | class AuthenticatorValidateStageTests(APITestCase): | ||||||
|     """Test validator stage""" |     """Test validator stage""" | ||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """captcha tests""" | """captcha tests""" | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -16,13 +16,12 @@ RECAPTCHA_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI" | |||||||
| RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" | RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestCaptchaStage(TestCase): | class TestCaptchaStage(APITestCase): | ||||||
|     """Captcha tests""" |     """Captcha tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create_user(username="unittest", email="test@beryju.org") |         self.user = User.objects.create_user(username="unittest", email="test@beryju.org") | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-captcha", |             name="test-captcha", | ||||||
|  | |||||||
| @ -1,9 +1,9 @@ | |||||||
| """consent tests""" | """consent tests""" | ||||||
| from time import sleep | from time import sleep | ||||||
|  |  | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import Application, User | from authentik.core.models import Application, User | ||||||
| from authentik.core.tasks import clean_expired_models | from authentik.core.tasks import clean_expired_models | ||||||
| @ -15,7 +15,7 @@ from authentik.flows.views import SESSION_KEY_PLAN | |||||||
| from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent | from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestConsentStage(TestCase): | class TestConsentStage(APITestCase): | ||||||
|     """Consent tests""" |     """Consent tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
| @ -25,7 +25,6 @@ class TestConsentStage(TestCase): | |||||||
|             name="test-application", |             name="test-application", | ||||||
|             slug="test-application", |             slug="test-application", | ||||||
|         ) |         ) | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|     def test_always_required(self): |     def test_always_required(self): | ||||||
|         """Test always required consent""" |         """Test always required consent""" | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """deny tests""" | """deny tests""" | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -12,13 +12,12 @@ from authentik.flows.views import SESSION_KEY_PLAN | |||||||
| from authentik.stages.deny.models import DenyStage | from authentik.stages.deny.models import DenyStage | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUserDenyStage(TestCase): | class TestUserDenyStage(APITestCase): | ||||||
|     """Deny tests""" |     """Deny tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create(username="unittest", email="test@beryju.org") |         self.user = User.objects.create(username="unittest", email="test@beryju.org") | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-logout", |             name="test-logout", | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """dummy tests""" | """dummy tests""" | ||||||
| from django.test import TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -9,7 +9,7 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding | |||||||
| from authentik.stages.dummy.models import DummyStage | from authentik.stages.dummy.models import DummyStage | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestDummyStage(TestCase): | class TestDummyStage(APITestCase): | ||||||
|     """Dummy tests""" |     """Dummy tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|  | |||||||
| @ -4,8 +4,8 @@ from unittest.mock import MagicMock, patch | |||||||
|  |  | ||||||
| from django.core import mail | from django.core import mail | ||||||
| from django.core.mail.backends.locmem import EmailBackend | from django.core.mail.backends.locmem import EmailBackend | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
|  | 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 | from authentik.events.models import Event, EventAction | ||||||
| @ -16,13 +16,12 @@ from authentik.flows.views import SESSION_KEY_PLAN | |||||||
| from authentik.stages.email.models import EmailStage | from authentik.stages.email.models import EmailStage | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestEmailStageSending(TestCase): | class TestEmailStageSending(APITestCase): | ||||||
|     """Email tests""" |     """Email tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create_user(username="unittest", email="test@beryju.org") |         self.user = User.objects.create_user(username="unittest", email="test@beryju.org") | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-email", |             name="test-email", | ||||||
|  | |||||||
| @ -2,10 +2,10 @@ | |||||||
| from unittest.mock import MagicMock, patch | from unittest.mock import MagicMock, patch | ||||||
|  |  | ||||||
| from django.core import mail | from django.core import mail | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
| from django.utils.http import urlencode | from django.utils.http import urlencode | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import Token, User | from authentik.core.models import Token, User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -17,13 +17,12 @@ from authentik.stages.email.models import EmailStage | |||||||
| from authentik.stages.email.stage import QS_KEY_TOKEN | from authentik.stages.email.stage import QS_KEY_TOKEN | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestEmailStage(TestCase): | class TestEmailStage(APITestCase): | ||||||
|     """Email tests""" |     """Email tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create_user(username="unittest", email="test@beryju.org") |         self.user = User.objects.create_user(username="unittest", email="test@beryju.org") | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-email", |             name="test-email", | ||||||
|  | |||||||
| @ -96,7 +96,9 @@ class IdentificationChallengeResponse(ChallengeResponse): | |||||||
|             # No password stage select, don't validate the password |             # No password stage select, don't validate the password | ||||||
|             return attrs |             return attrs | ||||||
|  |  | ||||||
|         password = attrs["password"] |         password = attrs.get("password", None) | ||||||
|  |         if not password: | ||||||
|  |             LOGGER.warning("Password not set for ident+auth attempt") | ||||||
|         try: |         try: | ||||||
|             user = authenticate( |             user = authenticate( | ||||||
|                 self.stage.request, |                 self.stage.request, | ||||||
| @ -132,6 +134,9 @@ class IdentificationStageView(ChallengeStageView): | |||||||
|             else: |             else: | ||||||
|                 model_field += "__exact" |                 model_field += "__exact" | ||||||
|             query |= Q(**{model_field: uid_value}) |             query |= Q(**{model_field: uid_value}) | ||||||
|  |         if not query: | ||||||
|  |             LOGGER.debug("Empty user query", query=query) | ||||||
|  |             return None | ||||||
|         users = User.objects.filter(query, is_active=True) |         users = User.objects.filter(query, is_active=True) | ||||||
|         if users.exists(): |         if users.exists(): | ||||||
|             LOGGER.debug("Found user", user=users.first(), query=query) |             LOGGER.debug("Found user", user=users.first(), query=query) | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """identification tests""" | """identification tests""" | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -13,7 +13,7 @@ from authentik.stages.password import BACKEND_INBUILT | |||||||
| from authentik.stages.password.models import PasswordStage | from authentik.stages.password.models import PasswordStage | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestIdentificationStage(TestCase): | class TestIdentificationStage(APITestCase): | ||||||
|     """Identification tests""" |     """Identification tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
| @ -22,7 +22,6 @@ class TestIdentificationStage(TestCase): | |||||||
|         self.user = User.objects.create_user( |         self.user = User.objects.create_user( | ||||||
|             username="unittest", email="test@beryju.org", password=self.password |             username="unittest", email="test@beryju.org", password=self.password | ||||||
|         ) |         ) | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         # OAuthSource for the login view |         # OAuthSource for the login view | ||||||
|         source = OAuthSource.objects.create(name="test", slug="test") |         source = OAuthSource.objects.create(name="test", slug="test") | ||||||
| @ -137,6 +136,48 @@ class TestIdentificationStage(TestCase): | |||||||
|         ) |         ) | ||||||
|         self.assertEqual(response.status_code, 200) |         self.assertEqual(response.status_code, 200) | ||||||
|  |  | ||||||
|  |     def test_invalid_no_fields(self): | ||||||
|  |         """Test invalid with username (no user fields are enabled)""" | ||||||
|  |         self.stage.user_fields = [] | ||||||
|  |         self.stage.save() | ||||||
|  |         form_data = {"uid_field": self.user.username} | ||||||
|  |         response = self.client.post( | ||||||
|  |             reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), | ||||||
|  |             form_data, | ||||||
|  |         ) | ||||||
|  |         self.assertEqual(response.status_code, 200) | ||||||
|  |         self.assertJSONEqual( | ||||||
|  |             force_str(response.content), | ||||||
|  |             { | ||||||
|  |                 "type": ChallengeTypes.NATIVE.value, | ||||||
|  |                 "component": "ak-stage-identification", | ||||||
|  |                 "password_fields": False, | ||||||
|  |                 "primary_action": "Log in", | ||||||
|  |                 "response_errors": { | ||||||
|  |                     "non_field_errors": [ | ||||||
|  |                         {"code": "invalid", "string": "Failed to " "authenticate."} | ||||||
|  |                     ] | ||||||
|  |                 }, | ||||||
|  |                 "flow_info": { | ||||||
|  |                     "background": self.flow.background_url, | ||||||
|  |                     "cancel_url": reverse("authentik_flows:cancel"), | ||||||
|  |                     "title": "", | ||||||
|  |                 }, | ||||||
|  |                 "sources": [ | ||||||
|  |                     { | ||||||
|  |                         "challenge": { | ||||||
|  |                             "component": "xak-flow-redirect", | ||||||
|  |                             "to": "/source/oauth/login/test/", | ||||||
|  |                             "type": ChallengeTypes.REDIRECT.value, | ||||||
|  |                         }, | ||||||
|  |                         "icon_url": "/static/authentik/sources/.svg", | ||||||
|  |                         "name": "test", | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 "user_fields": [], | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def test_invalid_with_invalid_email(self): |     def test_invalid_with_invalid_email(self): | ||||||
|         """Test with invalid email (user doesn't exist) -> Will return to login form""" |         """Test with invalid email (user doesn't exist) -> Will return to login form""" | ||||||
|         form_data = {"uid_field": self.user.email + "test"} |         form_data = {"uid_field": self.user.email + "test"} | ||||||
|  | |||||||
| @ -0,0 +1,25 @@ | |||||||
|  | # Generated by Django 3.2.6 on 2021-09-01 12:11 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  | import authentik.core.models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_stages_invitation", "0004_invitation_single_use"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="invitation", | ||||||
|  |             name="expiring", | ||||||
|  |             field=models.BooleanField(default=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="invitation", | ||||||
|  |             name="expires", | ||||||
|  |             field=models.DateTimeField(default=authentik.core.models.default_token_duration), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -7,7 +7,7 @@ from django.utils.translation import gettext_lazy as _ | |||||||
| from django.views import View | from django.views import View | ||||||
| from rest_framework.serializers import BaseSerializer | from rest_framework.serializers import BaseSerializer | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import ExpiringModel, User | ||||||
| from authentik.flows.models import Stage | from authentik.flows.models import Stage | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -48,7 +48,7 @@ class InvitationStage(Stage): | |||||||
|         verbose_name_plural = _("Invitation Stages") |         verbose_name_plural = _("Invitation Stages") | ||||||
|  |  | ||||||
|  |  | ||||||
| class Invitation(models.Model): | class Invitation(ExpiringModel): | ||||||
|     """Single-use invitation link""" |     """Single-use invitation link""" | ||||||
|  |  | ||||||
|     invite_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) |     invite_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||||
| @ -59,7 +59,6 @@ class Invitation(models.Model): | |||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     created_by = models.ForeignKey(User, on_delete=models.CASCADE) |     created_by = models.ForeignKey(User, on_delete=models.CASCADE) | ||||||
|     expires = models.DateTimeField(default=None, blank=True, null=True) |  | ||||||
|     fixed_data = models.JSONField( |     fixed_data = models.JSONField( | ||||||
|         default=dict, |         default=dict, | ||||||
|         blank=True, |         blank=True, | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| """invitation tests""" | """invitation tests""" | ||||||
| from unittest.mock import MagicMock, patch | from unittest.mock import MagicMock, patch | ||||||
|  |  | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
| from django.utils.http import urlencode | from django.utils.http import urlencode | ||||||
| @ -21,13 +20,12 @@ from authentik.stages.password import BACKEND_INBUILT | |||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND | from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUserLoginStage(TestCase): | class TestUserLoginStage(APITestCase): | ||||||
|     """Login tests""" |     """Login tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create(username="unittest", email="test@beryju.org") |         self.user = User.objects.create(username="unittest", email="test@beryju.org") | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-invitation", |             name="test-invitation", | ||||||
|  | |||||||
| @ -32,9 +32,7 @@ PLAN_CONTEXT_METHOD_ARGS = "auth_method_args" | |||||||
| SESSION_INVALID_TRIES = "user_invalid_tries" | SESSION_INVALID_TRIES = "user_invalid_tries" | ||||||
|  |  | ||||||
|  |  | ||||||
| def authenticate( | def authenticate(request: HttpRequest, backends: list[str], **credentials: Any) -> Optional[User]: | ||||||
|     request: HttpRequest, backends: list[str], **credentials: dict[str, Any] |  | ||||||
| ) -> Optional[User]: |  | ||||||
|     """If the given credentials are valid, return a User object. |     """If the given credentials are valid, return a User object. | ||||||
|  |  | ||||||
|     Customized version of django's authenticate, which accepts a list of backends""" |     Customized version of django's authenticate, which accepts a list of backends""" | ||||||
|  | |||||||
| @ -2,9 +2,9 @@ | |||||||
| from unittest.mock import MagicMock, patch | from unittest.mock import MagicMock, patch | ||||||
|  |  | ||||||
| from django.core.exceptions import PermissionDenied | from django.core.exceptions import PermissionDenied | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -20,7 +20,7 @@ from authentik.stages.password.models import PasswordStage | |||||||
| MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test")) | MOCK_BACKEND_AUTHENTICATE = MagicMock(side_effect=PermissionDenied("test")) | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestPasswordStage(TestCase): | class TestPasswordStage(APITestCase): | ||||||
|     """Password tests""" |     """Password tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
| @ -29,7 +29,6 @@ class TestPasswordStage(TestCase): | |||||||
|         self.user = User.objects.create_user( |         self.user = User.objects.create_user( | ||||||
|             username="unittest", email="test@beryju.org", password=self.password |             username="unittest", email="test@beryju.org", password=self.password | ||||||
|         ) |         ) | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-password", |             name="test-password", | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| """Prompt tests""" | """Prompt tests""" | ||||||
| from unittest.mock import MagicMock, patch | from unittest.mock import MagicMock, patch | ||||||
|  |  | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
| from rest_framework.exceptions import ErrorDetail | from rest_framework.exceptions import ErrorDetail | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -17,13 +17,12 @@ from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage | |||||||
| from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse | from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT, PromptChallengeResponse | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestPromptStage(TestCase): | class TestPromptStage(APITestCase): | ||||||
|     """Prompt tests""" |     """Prompt tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create(username="unittest", email="test@beryju.org") |         self.user = User.objects.create(username="unittest", email="test@beryju.org") | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-prompt", |             name="test-prompt", | ||||||
| @ -97,7 +96,6 @@ class TestPromptStage(TestCase): | |||||||
|                 static_prompt, |                 static_prompt, | ||||||
|             ] |             ] | ||||||
|         ) |         ) | ||||||
|         self.stage.save() |  | ||||||
|  |  | ||||||
|         self.prompt_data = { |         self.prompt_data = { | ||||||
|             username_prompt.field_key: "test-username", |             username_prompt.field_key: "test-username", | ||||||
|  | |||||||
| @ -1,9 +1,9 @@ | |||||||
| """delete tests""" | """delete tests""" | ||||||
| from unittest.mock import patch | from unittest.mock import patch | ||||||
|  |  | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -15,14 +15,13 @@ from authentik.flows.views import SESSION_KEY_PLAN | |||||||
| from authentik.stages.user_delete.models import UserDeleteStage | from authentik.stages.user_delete.models import UserDeleteStage | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUserDeleteStage(TestCase): | class TestUserDeleteStage(APITestCase): | ||||||
|     """Delete tests""" |     """Delete tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.username = "qerqwerqrwqwerwq" |         self.username = "qerqwerqrwqwerwq" | ||||||
|         self.user = User.objects.create(username=self.username, email="test@beryju.org") |         self.user = User.objects.create(username=self.username, email="test@beryju.org") | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-delete", |             name="test-delete", | ||||||
|  | |||||||
| @ -2,9 +2,9 @@ | |||||||
| from time import sleep | from time import sleep | ||||||
| from unittest.mock import patch | from unittest.mock import patch | ||||||
|  |  | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -16,13 +16,12 @@ from authentik.flows.views import SESSION_KEY_PLAN | |||||||
| from authentik.stages.user_login.models import UserLoginStage | from authentik.stages.user_login.models import UserLoginStage | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUserLoginStage(TestCase): | class TestUserLoginStage(APITestCase): | ||||||
|     """Login tests""" |     """Login tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create(username="unittest", email="test@beryju.org") |         self.user = User.objects.create(username="unittest", email="test@beryju.org") | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-login", |             name="test-login", | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| """logout tests""" | """logout tests""" | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import User | from authentik.core.models import User | ||||||
| from authentik.flows.challenge import ChallengeTypes | from authentik.flows.challenge import ChallengeTypes | ||||||
| @ -14,13 +14,12 @@ from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND | |||||||
| from authentik.stages.user_logout.models import UserLogoutStage | from authentik.stages.user_logout.models import UserLogoutStage | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUserLogoutStage(TestCase): | class TestUserLogoutStage(APITestCase): | ||||||
|     """Logout tests""" |     """Logout tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.user = User.objects.create(username="unittest", email="test@beryju.org") |         self.user = User.objects.create(username="unittest", email="test@beryju.org") | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-logout", |             name="test-logout", | ||||||
|  | |||||||
| @ -3,9 +3,9 @@ import string | |||||||
| from random import SystemRandom | from random import SystemRandom | ||||||
| from unittest.mock import patch | from unittest.mock import patch | ||||||
|  |  | ||||||
| from django.test import Client, TestCase |  | ||||||
| from django.urls import reverse | from django.urls import reverse | ||||||
| from django.utils.encoding import force_str | from django.utils.encoding import force_str | ||||||
|  | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import USER_ATTRIBUTE_SOURCES, Source, User, UserSourceConnection | from authentik.core.models import USER_ATTRIBUTE_SOURCES, Source, User, UserSourceConnection | ||||||
| from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION | from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION | ||||||
| @ -19,13 +19,11 @@ from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT | |||||||
| from authentik.stages.user_write.models import UserWriteStage | from authentik.stages.user_write.models import UserWriteStage | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestUserWriteStage(TestCase): | class TestUserWriteStage(APITestCase): | ||||||
|     """Write tests""" |     """Write tests""" | ||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|         self.client = Client() |  | ||||||
|  |  | ||||||
|         self.flow = Flow.objects.create( |         self.flow = Flow.objects.create( | ||||||
|             name="test-write", |             name="test-write", | ||||||
|             slug="test-write", |             slug="test-write", | ||||||
|  | |||||||
| @ -1,120 +0,0 @@ | |||||||
| trigger: |  | ||||||
|   batch: true |  | ||||||
|   branches: |  | ||||||
|     include: |  | ||||||
|       - master |  | ||||||
|       - next |  | ||||||
|       - version-* |  | ||||||
|  |  | ||||||
| stages: |  | ||||||
|   - stage: generate |  | ||||||
|     jobs: |  | ||||||
|       - job: generate_api |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: GoTool@0 |  | ||||||
|             inputs: |  | ||||||
|               version: '1.16.3' |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: make gen-outpost |  | ||||||
|           - task: PublishPipelineArtifact@1 |  | ||||||
|             inputs: |  | ||||||
|               targetPath: 'api/' |  | ||||||
|               artifact: 'go_api_client' |  | ||||||
|               publishLocation: 'pipeline' |  | ||||||
|   - stage: lint |  | ||||||
|     jobs: |  | ||||||
|       - job: golint |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: GoTool@0 |  | ||||||
|             inputs: |  | ||||||
|               version: '1.16.3' |  | ||||||
|           - task: DownloadPipelineArtifact@2 |  | ||||||
|             inputs: |  | ||||||
|               buildType: 'current' |  | ||||||
|               artifactName: 'go_api_client' |  | ||||||
|               path: "api/" |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 mkdir -p web/dist |  | ||||||
|                 mkdir -p website/help |  | ||||||
|                 touch web/dist/test website/help/test |  | ||||||
|                 docker run \ |  | ||||||
|                   --rm \ |  | ||||||
|                   -v $(pwd):/app \ |  | ||||||
|                   -w /app \ |  | ||||||
|                   golangci/golangci-lint:v1.39.0 \ |  | ||||||
|                   golangci-lint run -v --timeout 200s |  | ||||||
|   - stage: build_docker |  | ||||||
|     jobs: |  | ||||||
|       - job: proxy_build_docker |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: GoTool@0 |  | ||||||
|             inputs: |  | ||||||
|               version: '1.16.3' |  | ||||||
|           - task: Bash@3 |  | ||||||
|             inputs: |  | ||||||
|               targetType: 'inline' |  | ||||||
|               script: | |  | ||||||
|                 python ./scripts/az_do_set_branch.py |  | ||||||
|           - task: Docker@2 |  | ||||||
|             inputs: |  | ||||||
|               containerRegistry: 'beryjuorg-harbor' |  | ||||||
|               repository: 'authentik/outpost-proxy' |  | ||||||
|               command: 'build' |  | ||||||
|               Dockerfile: 'proxy.Dockerfile' |  | ||||||
|               buildContext: '$(Build.SourcesDirectory)' |  | ||||||
|               tags: | |  | ||||||
|                 gh-$(branchName) |  | ||||||
|                 gh-$(branchName)-$(timestamp) |  | ||||||
|                 gh-$(Build.SourceVersion) |  | ||||||
|               arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)' |  | ||||||
|           - task: Docker@2 |  | ||||||
|             inputs: |  | ||||||
|               containerRegistry: 'beryjuorg-harbor' |  | ||||||
|               repository: 'authentik/outpost-proxy' |  | ||||||
|               command: 'push' |  | ||||||
|               tags: | |  | ||||||
|                 gh-$(branchName) |  | ||||||
|                 gh-$(branchName)-$(timestamp) |  | ||||||
|                 gh-$(Build.SourceVersion) |  | ||||||
|       - job: ldap_build_docker |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: GoTool@0 |  | ||||||
|             inputs: |  | ||||||
|               version: '1.16.3' |  | ||||||
|           - task: Bash@3 |  | ||||||
|             inputs: |  | ||||||
|               targetType: 'inline' |  | ||||||
|               script: | |  | ||||||
|                 python ./scripts/az_do_set_branch.py |  | ||||||
|           - task: Docker@2 |  | ||||||
|             inputs: |  | ||||||
|               containerRegistry: 'beryjuorg-harbor' |  | ||||||
|               repository: 'authentik/outpost-ldap' |  | ||||||
|               command: 'build' |  | ||||||
|               Dockerfile: 'ldap.Dockerfile' |  | ||||||
|               buildContext: '$(Build.SourcesDirectory)' |  | ||||||
|               tags: | |  | ||||||
|                 gh-$(branchName) |  | ||||||
|                 gh-$(branchName)-$(timestamp) |  | ||||||
|                 gh-$(Build.SourceVersion) |  | ||||||
|               arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)' |  | ||||||
|           - task: Docker@2 |  | ||||||
|             inputs: |  | ||||||
|               containerRegistry: 'beryjuorg-harbor' |  | ||||||
|               repository: 'authentik/outpost-ldap' |  | ||||||
|               command: 'push' |  | ||||||
|               tags: | |  | ||||||
|                 gh-$(branchName) |  | ||||||
|                 gh-$(branchName)-$(timestamp) |  | ||||||
|                 gh-$(Build.SourceVersion) |  | ||||||
| @ -1,426 +0,0 @@ | |||||||
| trigger: |  | ||||||
|   batch: true |  | ||||||
|   branches: |  | ||||||
|     include: |  | ||||||
|       - master |  | ||||||
|       - next |  | ||||||
|       - version-* |  | ||||||
|   paths: |  | ||||||
|     exclude: |  | ||||||
|       - website |  | ||||||
|       - outpost |  | ||||||
|  |  | ||||||
| resources: |  | ||||||
|   - repo: self |  | ||||||
|  |  | ||||||
| variables: |  | ||||||
|   - name: POSTGRES_DB |  | ||||||
|     value: authentik |  | ||||||
|   - name: POSTGRES_USER |  | ||||||
|     value: authentik |  | ||||||
|   - name: POSTGRES_PASSWORD |  | ||||||
|     value: "EK-5jnKfjrGRm<77" |  | ||||||
|   - group: coverage |  | ||||||
|  |  | ||||||
| stages: |  | ||||||
|   - stage: Lint_and_test |  | ||||||
|     jobs: |  | ||||||
|       - job: pylint |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: DockerCompose@0 |  | ||||||
|             displayName: Run services |  | ||||||
|             inputs: |  | ||||||
|               dockerComposeFile: 'scripts/ci.docker-compose.yml' |  | ||||||
|               action: 'Run services' |  | ||||||
|               buildImages: false |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 pipenv run python -m scripts.generate_ci_config |  | ||||||
|                 pipenv run pylint authentik tests lifecycle |  | ||||||
|       - job: black |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: pipenv run black --check authentik tests lifecycle |  | ||||||
|       - job: isort |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: pipenv run isort --check authentik tests lifecycle |  | ||||||
|       - job: bandit |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: pipenv run bandit -r authentik tests lifecycle |  | ||||||
|       - job: pyright |  | ||||||
|         pool: |  | ||||||
|           vmImage: ubuntu-latest |  | ||||||
|         steps: |  | ||||||
|           - task: UseNode@1 |  | ||||||
|             inputs: |  | ||||||
|               version: '12.x' |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: npm install -g pyright@1.1.136 |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: pipenv run pyright e2e lifecycle |  | ||||||
|       - job: migrations |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: DockerCompose@0 |  | ||||||
|             displayName: Run services |  | ||||||
|             inputs: |  | ||||||
|               dockerComposeFile: 'scripts/ci.docker-compose.yml' |  | ||||||
|               action: 'Run services' |  | ||||||
|               buildImages: false |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 pipenv run python -m scripts.generate_ci_config |  | ||||||
|                 pipenv run python -m lifecycle.migrate |  | ||||||
|       - job: migrations_from_previous_release |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.8' |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: DockerCompose@0 |  | ||||||
|             displayName: Run services |  | ||||||
|             inputs: |  | ||||||
|               dockerComposeFile: 'scripts/ci.docker-compose.yml' |  | ||||||
|               action: 'Run services' |  | ||||||
|               buildImages: false |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Prepare Last tagged release |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 # Copy current, latest config to local |  | ||||||
|                 cp authentik/lib/default.yml local.env.yml |  | ||||||
|                 git checkout $(git describe --abbrev=0 --match 'version/*') |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Migrate to last tagged release |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 pipenv run python -m scripts.generate_ci_config |  | ||||||
|                 pipenv run python -m lifecycle.migrate |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Install current branch |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 set -x |  | ||||||
|                 git checkout ${{ variables.branchName }} |  | ||||||
|                 pipenv sync --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Migrate to current branch |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 pipenv run python -m scripts.generate_ci_config |  | ||||||
|                 pipenv run python -m lifecycle.migrate |  | ||||||
|       - job: coverage_unittest |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: DockerCompose@0 |  | ||||||
|             displayName: Run services |  | ||||||
|             inputs: |  | ||||||
|               dockerComposeFile: 'scripts/ci.docker-compose.yml' |  | ||||||
|               action: 'Run services' |  | ||||||
|               buildImages: false |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Run full test suite |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 pipenv run python -m scripts.generate_ci_config |  | ||||||
|                 pipenv run make test |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 mkdir output-unittest |  | ||||||
|                 mv unittest.xml output-unittest/unittest.xml |  | ||||||
|                 mv .coverage output-unittest/coverage |  | ||||||
|           - task: PublishPipelineArtifact@1 |  | ||||||
|             inputs: |  | ||||||
|               targetPath: 'output-unittest/' |  | ||||||
|               artifact: 'coverage-unittest' |  | ||||||
|               publishLocation: 'pipeline' |  | ||||||
|       - job: coverage_integration |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: DockerCompose@0 |  | ||||||
|             displayName: Run services |  | ||||||
|             inputs: |  | ||||||
|               dockerComposeFile: 'scripts/ci.docker-compose.yml' |  | ||||||
|               action: 'Run services' |  | ||||||
|               buildImages: false |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Install K3d and prepare |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash |  | ||||||
|                 k3d cluster create |  | ||||||
|                 k3d kubeconfig write -o ~/.kube/config --overwrite |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Run full test suite |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 pipenv run python -m scripts.generate_ci_config |  | ||||||
|                 pipenv run make test-integration |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 mkdir output-integration |  | ||||||
|                 mv unittest.xml output-integration/unittest.xml |  | ||||||
|                 mv .coverage output-integration/coverage |  | ||||||
|           - task: PublishPipelineArtifact@1 |  | ||||||
|             inputs: |  | ||||||
|               targetPath: 'output-integration/' |  | ||||||
|               artifact: 'coverage-integration' |  | ||||||
|               publishLocation: 'pipeline' |  | ||||||
|       - job: coverage_e2e |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: NodeTool@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '16.x' |  | ||||||
|           - task: DockerCompose@0 |  | ||||||
|             displayName: Run services |  | ||||||
|             inputs: |  | ||||||
|               dockerComposeFile: 'scripts/ci.docker-compose.yml' |  | ||||||
|               action: 'Run services' |  | ||||||
|               buildImages: false |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev --python python3.9 |  | ||||||
|           - task: DockerCompose@0 |  | ||||||
|             displayName: Run ChromeDriver |  | ||||||
|             inputs: |  | ||||||
|               dockerComposeFile: 'tests/e2e/ci.docker-compose.yml' |  | ||||||
|               action: 'Run a specific service' |  | ||||||
|               serviceName: 'chrome' |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Build static files for e2e |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 cd web |  | ||||||
|                 npm i |  | ||||||
|                 npm run build |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Run full test suite |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 pipenv run python -m scripts.generate_ci_config |  | ||||||
|                 pipenv run make test-e2e |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             condition: always() |  | ||||||
|             displayName: Cleanup |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 docker stop $(docker ps -aq) |  | ||||||
|                 docker container prune -f |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             displayName: Prepare unittests and coverage for upload |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 mkdir output-e2e |  | ||||||
|                 mv unittest.xml output-e2e/unittest.xml |  | ||||||
|                 mv .coverage output-e2e/coverage |  | ||||||
|           - task: PublishPipelineArtifact@1 |  | ||||||
|             condition: failed() |  | ||||||
|             displayName: Upload screenshots if selenium tests fail |  | ||||||
|             inputs: |  | ||||||
|               targetPath: 'selenium_screenshots/' |  | ||||||
|               artifact: 'selenium screenshots' |  | ||||||
|               publishLocation: 'pipeline' |  | ||||||
|           - task: PublishPipelineArtifact@1 |  | ||||||
|             inputs: |  | ||||||
|               targetPath: 'output-e2e/' |  | ||||||
|               artifact: 'coverage-e2e' |  | ||||||
|               publishLocation: 'pipeline' |  | ||||||
|   - stage: test_combine |  | ||||||
|     jobs: |  | ||||||
|       - job: test_coverage_combine |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|           - task: DownloadPipelineArtifact@2 |  | ||||||
|             inputs: |  | ||||||
|               buildType: 'current' |  | ||||||
|               artifactName: 'coverage-e2e' |  | ||||||
|               path: "coverage-e2e/" |  | ||||||
|           - task: DownloadPipelineArtifact@2 |  | ||||||
|             inputs: |  | ||||||
|               buildType: 'current' |  | ||||||
|               artifactName: 'coverage-integration' |  | ||||||
|               path: "coverage-integration/" |  | ||||||
|           - task: DownloadPipelineArtifact@2 |  | ||||||
|             inputs: |  | ||||||
|               buildType: 'current' |  | ||||||
|               artifactName: 'coverage-unittest' |  | ||||||
|               path: "coverage-unittest/" |  | ||||||
|           - task: UsePythonVersion@0 |  | ||||||
|             inputs: |  | ||||||
|               versionSpec: '3.9' |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: | |  | ||||||
|                 sudo apt update |  | ||||||
|                 sudo apt install -y libxmlsec1-dev pkg-config |  | ||||||
|                 sudo pip install -U wheel pipenv |  | ||||||
|                 pipenv install --dev |  | ||||||
|                 pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage coverage-integration/coverage |  | ||||||
|                 pipenv run coverage xml |  | ||||||
|                 pipenv run coverage html |  | ||||||
|           - task: PublishCodeCoverageResults@1 |  | ||||||
|             inputs: |  | ||||||
|               codeCoverageTool: 'Cobertura' |  | ||||||
|               summaryFileLocation: 'coverage.xml' |  | ||||||
|               pathToSources: '$(System.DefaultWorkingDirectory)' |  | ||||||
|           - task: PublishTestResults@2 |  | ||||||
|             condition: succeededOrFailed() |  | ||||||
|             inputs: |  | ||||||
|               testResultsFormat: 'JUnit' |  | ||||||
|               testResultsFiles: | |  | ||||||
|                 coverage-e2e/unittest.xml |  | ||||||
|                 coverage-integration/unittest.xml |  | ||||||
|                 coverage-unittest/unittest.xml |  | ||||||
|               mergeTestResults: true |  | ||||||
|           - task: CmdLine@2 |  | ||||||
|             inputs: |  | ||||||
|               script: bash <(curl -s https://codecov.io/bash) |  | ||||||
|   - stage: Build |  | ||||||
|     jobs: |  | ||||||
|       - job: build_server |  | ||||||
|         pool: |  | ||||||
|           vmImage: 'ubuntu-latest' |  | ||||||
|         steps: |  | ||||||
|         - task: Bash@3 |  | ||||||
|           inputs: |  | ||||||
|             targetType: 'inline' |  | ||||||
|             script: | |  | ||||||
|               python ./scripts/az_do_set_branch.py |  | ||||||
|         - task: Docker@2 |  | ||||||
|           inputs: |  | ||||||
|             containerRegistry: 'beryjuorg-harbor' |  | ||||||
|             repository: 'authentik/server' |  | ||||||
|             command: 'build' |  | ||||||
|             Dockerfile: 'Dockerfile' |  | ||||||
|             tags: | |  | ||||||
|               gh-$(branchName) |  | ||||||
|               gh-$(branchName)-$(timestamp) |  | ||||||
|             arguments: '--build-arg GIT_BUILD_HASH=$(Build.SourceVersion)' |  | ||||||
|         - task: Docker@2 |  | ||||||
|           inputs: |  | ||||||
|             containerRegistry: 'beryjuorg-harbor' |  | ||||||
|             repository: 'authentik/server' |  | ||||||
|             command: 'push' |  | ||||||
|             tags: | |  | ||||||
|               gh-$(branchName) |  | ||||||
|               gh-$(branchName)-$(timestamp) |  | ||||||
| @ -60,7 +60,9 @@ func main() { | |||||||
| 	for { | 	for { | ||||||
| 		go attemptStartBackend(g) | 		go attemptStartBackend(g) | ||||||
| 		ws.Start() | 		ws.Start() | ||||||
|  | 		if !config.G.Web.DisableEmbeddedOutpost { | ||||||
| 			go attemptProxyStart(ws, u) | 			go attemptProxyStart(ws, u) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		<-ex | 		<-ex | ||||||
| 		running = false | 		running = false | ||||||
|  | |||||||
| @ -21,7 +21,7 @@ services: | |||||||
|     networks: |     networks: | ||||||
|       - internal |       - internal | ||||||
|   server: |   server: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.8.1} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.8.5} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -44,7 +44,7 @@ services: | |||||||
|       - "0.0.0.0:9000:9000" |       - "0.0.0.0:9000:9000" | ||||||
|       - "0.0.0.0:9443:9443" |       - "0.0.0.0:9443:9443" | ||||||
|   worker: |   worker: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.8.1} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.8.5} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     networks: |     networks: | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @ -10,7 +10,7 @@ require ( | |||||||
| 	github.com/go-ldap/ldap/v3 v3.4.1 | 	github.com/go-ldap/ldap/v3 v3.4.1 | ||||||
| 	github.com/go-openapi/analysis v0.20.1 // indirect | 	github.com/go-openapi/analysis v0.20.1 // indirect | ||||||
| 	github.com/go-openapi/errors v0.20.0 // indirect | 	github.com/go-openapi/errors v0.20.0 // indirect | ||||||
| 	github.com/go-openapi/runtime v0.19.30 | 	github.com/go-openapi/runtime v0.19.31 | ||||||
| 	github.com/go-openapi/strfmt v0.20.2 | 	github.com/go-openapi/strfmt v0.20.2 | ||||||
| 	github.com/go-openapi/swag v0.19.15 // indirect | 	github.com/go-openapi/swag v0.19.15 // indirect | ||||||
| 	github.com/go-openapi/validate v0.20.2 // indirect | 	github.com/go-openapi/validate v0.20.2 // indirect | ||||||
|  | |||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @ -205,8 +205,8 @@ github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29g | |||||||
| github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= | github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= | ||||||
| github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= | github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= | ||||||
| github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= | github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= | ||||||
| github.com/go-openapi/runtime v0.19.30 h1:bVDeSf4HU9EMth+lHD1EthaHe1SFoUVPaUvQtkGS9g8= | github.com/go-openapi/runtime v0.19.31 h1:GX+MgBxN12s/tQiHNJpvHDIoZiEXAz6j6Rqg0oJcnpg= | ||||||
| github.com/go-openapi/runtime v0.19.30/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= | github.com/go-openapi/runtime v0.19.31/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= | ||||||
| github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= | github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= | ||||||
| github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= | github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= | ||||||
| github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= | github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ type WebConfig struct { | |||||||
| 	Listen                 string `yaml:"listen"` | 	Listen                 string `yaml:"listen"` | ||||||
| 	ListenTLS              string `yaml:"listen_tls"` | 	ListenTLS              string `yaml:"listen_tls"` | ||||||
| 	LoadLocalFiles         bool   `yaml:"load_local_files" env:"AUTHENTIK_WEB_LOAD_LOCAL_FILES"` | 	LoadLocalFiles         bool   `yaml:"load_local_files" env:"AUTHENTIK_WEB_LOAD_LOCAL_FILES"` | ||||||
|  | 	DisableEmbeddedOutpost bool   `yaml:"disable_embedded_outpost" env:"AUTHENTIK_WEB__DISABLE_EMBEDDED_OUTPOST"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type PathsConfig struct { | type PathsConfig struct { | ||||||
|  | |||||||
| @ -17,4 +17,4 @@ func OutpostUserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD()) | 	return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD()) | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2021.8.1" | const VERSION = "2021.8.5" | ||||||
|  | |||||||
| @ -47,7 +47,7 @@ func NewAPIController(akURL url.URL, token string) *APIController { | |||||||
| 	config.Host = akURL.Host | 	config.Host = akURL.Host | ||||||
| 	config.Scheme = akURL.Scheme | 	config.Scheme = akURL.Scheme | ||||||
| 	config.HTTPClient = &http.Client{ | 	config.HTTPClient = &http.Client{ | ||||||
| 		Transport: NewTracingTransport(GetTLSTransport()), | 		Transport: NewTracingTransport(context.TODO(), GetTLSTransport()), | ||||||
| 	} | 	} | ||||||
| 	config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token)) | 	config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token)) | ||||||
|  |  | ||||||
| @ -107,8 +107,24 @@ func (a *APIController) Start() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (a *APIController) OnRefresh() error { | ||||||
|  | 	// Because we don't know the outpost UUID, we simply do a list and pick the first | ||||||
|  | 	// The service account this token belongs to should only have access to a single outpost | ||||||
|  | 	outposts, _, err := a.Client.OutpostsApi.OutpostsInstancesList(context.Background()).Execute() | ||||||
|  |  | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.WithError(err).Error("Failed to fetch outpost configuration") | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	outpost := outposts.Results[0] | ||||||
|  | 	doGlobalSetup(outpost.Config) | ||||||
|  |  | ||||||
|  | 	log.WithField("name", outpost.Name).Debug("Fetched outpost configuration") | ||||||
|  | 	return a.Server.Refresh() | ||||||
|  | } | ||||||
|  |  | ||||||
| func (a *APIController) StartBackgorundTasks() error { | func (a *APIController) StartBackgorundTasks() error { | ||||||
| 	err := a.Server.Refresh() | 	err := a.OnRefresh() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return errors.Wrap(err, "failed to run initial refresh") | 		return errors.Wrap(err, "failed to run initial refresh") | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -82,7 +82,7 @@ func (ac *APIController) startWSHandler() { | |||||||
| 		if wsMsg.Instruction == WebsocketInstructionTriggerUpdate { | 		if wsMsg.Instruction == WebsocketInstructionTriggerUpdate { | ||||||
| 			time.Sleep(ac.reloadOffset) | 			time.Sleep(ac.reloadOffset) | ||||||
| 			logger.Debug("Got update trigger...") | 			logger.Debug("Got update trigger...") | ||||||
| 			err := ac.Server.Refresh() | 			err := ac.OnRefresh() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				logger.WithError(err).Debug("Failed to update") | 				logger.WithError(err).Debug("Failed to update") | ||||||
| 			} | 			} | ||||||
| @ -118,7 +118,7 @@ func (ac *APIController) startIntervalUpdater() { | |||||||
| 	logger := ac.logger.WithField("loop", "interval-updater") | 	logger := ac.logger.WithField("loop", "interval-updater") | ||||||
| 	ticker := time.NewTicker(5 * time.Minute) | 	ticker := time.NewTicker(5 * time.Minute) | ||||||
| 	for ; true; <-ticker.C { | 	for ; true; <-ticker.C { | ||||||
| 		err := ac.Server.Refresh() | 		err := ac.OnRefresh() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logger.WithError(err).Debug("Failed to update") | 			logger.WithError(err).Debug("Failed to update") | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| package ak | package ak | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  |  | ||||||
| 	"github.com/getsentry/sentry-go" | 	"github.com/getsentry/sentry-go" | ||||||
| @ -8,14 +9,15 @@ import ( | |||||||
|  |  | ||||||
| type tracingTransport struct { | type tracingTransport struct { | ||||||
| 	inner http.RoundTripper | 	inner http.RoundTripper | ||||||
|  | 	ctx   context.Context | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewTracingTransport(inner http.RoundTripper) *tracingTransport { | func NewTracingTransport(ctx context.Context, inner http.RoundTripper) *tracingTransport { | ||||||
| 	return &tracingTransport{inner} | 	return &tracingTransport{inner, ctx} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (tt *tracingTransport) RoundTrip(r *http.Request) (*http.Response, error) { | func (tt *tracingTransport) RoundTrip(r *http.Request) (*http.Response, error) { | ||||||
| 	span := sentry.StartSpan(r.Context(), "authentik.go.http_request") | 	span := sentry.StartSpan(tt.ctx, "authentik.go.http_request") | ||||||
| 	span.SetTag("url", r.URL.String()) | 	span.SetTag("url", r.URL.String()) | ||||||
| 	span.SetTag("method", r.Method) | 	span.SetTag("method", r.Method) | ||||||
| 	defer span.Finish() | 	defer span.Finish() | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ import ( | |||||||
| 	"goauthentik.io/api" | 	"goauthentik.io/api" | ||||||
| 	"goauthentik.io/internal/constants" | 	"goauthentik.io/internal/constants" | ||||||
| 	"goauthentik.io/internal/outpost/ak" | 	"goauthentik.io/internal/outpost/ak" | ||||||
|  | 	"goauthentik.io/internal/utils" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type StageComponent string | type StageComponent string | ||||||
| @ -61,8 +62,10 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config | |||||||
| 	config.UserAgent = constants.OutpostUserAgent() | 	config.UserAgent = constants.OutpostUserAgent() | ||||||
| 	config.HTTPClient = &http.Client{ | 	config.HTTPClient = &http.Client{ | ||||||
| 		Jar:       jar, | 		Jar:       jar, | ||||||
| 		Transport: ak.NewTracingTransport(ak.GetTLSTransport()), | 		Transport: ak.NewTracingTransport(ctx, ak.GetTLSTransport()), | ||||||
| 	} | 	} | ||||||
|  | 	token := strings.Split(refConfig.DefaultHeader["Authorization"], " ")[1] | ||||||
|  | 	config.AddDefaultHeader(HeaderAuthentikOutpostToken, token) | ||||||
| 	apiClient := api.NewAPIClient(config) | 	apiClient := api.NewAPIClient(config) | ||||||
| 	return &FlowExecutor{ | 	return &FlowExecutor{ | ||||||
| 		Params:   url.Values{}, | 		Params:   url.Values{}, | ||||||
| @ -71,7 +74,7 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config | |||||||
| 		api:      apiClient, | 		api:      apiClient, | ||||||
| 		flowSlug: flowSlug, | 		flowSlug: flowSlug, | ||||||
| 		log:      l, | 		log:      l, | ||||||
| 		token:    strings.Split(refConfig.DefaultHeader["Authorization"], " ")[1], | 		token:    token, | ||||||
| 		sp:       rsp, | 		sp:       rsp, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @ -87,13 +90,7 @@ type ChallengeInt interface { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (fe *FlowExecutor) DelegateClientIP(a net.Addr) { | func (fe *FlowExecutor) DelegateClientIP(a net.Addr) { | ||||||
| 	host, _, err := net.SplitHostPort(a.String()) | 	fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, utils.GetIP(a)) | ||||||
| 	if err != nil { |  | ||||||
| 		fe.log.WithError(err).Warning("Failed to get remote IP") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, host) |  | ||||||
| 	fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikOutpostToken, fe.token) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) { | func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) { | ||||||
| @ -152,7 +149,9 @@ func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) { | |||||||
| 	responseReq := fe.api.FlowsApi.FlowsExecutorSolve(scsp.Context(), fe.flowSlug).Query(fe.Params.Encode()) | 	responseReq := fe.api.FlowsApi.FlowsExecutorSolve(scsp.Context(), fe.flowSlug).Query(fe.Params.Encode()) | ||||||
| 	switch ch.GetComponent() { | 	switch ch.GetComponent() { | ||||||
| 	case string(StageIdentification): | 	case string(StageIdentification): | ||||||
| 		responseReq = responseReq.FlowChallengeResponseRequest(api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewIdentificationChallengeResponseRequest(fe.getAnswer(StageIdentification)))) | 		r := api.NewIdentificationChallengeResponseRequest(fe.getAnswer(StageIdentification)) | ||||||
|  | 		r.SetPassword(fe.getAnswer(StagePassword)) | ||||||
|  | 		responseReq = responseReq.FlowChallengeResponseRequest(api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(r)) | ||||||
| 	case string(StagePassword): | 	case string(StagePassword): | ||||||
| 		responseReq = responseReq.FlowChallengeResponseRequest(api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword)))) | 		responseReq = responseReq.FlowChallengeResponseRequest(api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword)))) | ||||||
| 	case string(StageAuthenticatorValidate): | 	case string(StageAuthenticatorValidate): | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import ( | |||||||
| 	"github.com/google/uuid" | 	"github.com/google/uuid" | ||||||
| 	"github.com/nmcclain/ldap" | 	"github.com/nmcclain/ldap" | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
|  | 	"goauthentik.io/internal/utils" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type BindRequest struct { | type BindRequest struct { | ||||||
| @ -33,7 +34,7 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD | |||||||
| 		BindDN: bindDN, | 		BindDN: bindDN, | ||||||
| 		BindPW: bindPW, | 		BindPW: bindPW, | ||||||
| 		conn:   conn, | 		conn:   conn, | ||||||
| 		log:    ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", conn.RemoteAddr().String()), | 		log:    ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())), | ||||||
| 		id:     rid, | 		id:     rid, | ||||||
| 		ctx:    span.Context(), | 		ctx:    span.Context(), | ||||||
| 	} | 	} | ||||||
|  | |||||||
							
								
								
									
										32
									
								
								internal/outpost/ldap/close.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								internal/outpost/ldap/close.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | |||||||
|  | package ldap | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (ls *LDAPServer) Close(boundDN string, conn net.Conn) error { | ||||||
|  | 	for _, p := range ls.providers { | ||||||
|  | 		p.delayDeleteUserInfo(boundDN) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (pi *ProviderInstance) delayDeleteUserInfo(dn string) { | ||||||
|  | 	ticker := time.NewTicker(30 * time.Second) | ||||||
|  | 	quit := make(chan struct{}) | ||||||
|  | 	go func() { | ||||||
|  | 		for { | ||||||
|  | 			select { | ||||||
|  | 			case <-ticker.C: | ||||||
|  | 				pi.boundUsersMutex.Lock() | ||||||
|  | 				delete(pi.boundUsers, dn) | ||||||
|  | 				pi.boundUsersMutex.Unlock() | ||||||
|  | 				close(quit) | ||||||
|  | 			case <-quit: | ||||||
|  | 				ticker.Stop() | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | } | ||||||
| @ -4,7 +4,6 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/getsentry/sentry-go" | 	"github.com/getsentry/sentry-go" | ||||||
| 	goldap "github.com/go-ldap/ldap/v3" | 	goldap "github.com/go-ldap/ldap/v3" | ||||||
| @ -12,6 +11,7 @@ import ( | |||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
| 	"goauthentik.io/api" | 	"goauthentik.io/api" | ||||||
| 	"goauthentik.io/internal/outpost" | 	"goauthentik.io/internal/outpost" | ||||||
|  | 	"goauthentik.io/internal/utils" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ContextUserKey = "ak_user" | const ContextUserKey = "ak_user" | ||||||
| @ -37,7 +37,7 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) { | |||||||
| func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPResultCode, error) { | func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPResultCode, error) { | ||||||
| 	fe := outpost.NewFlowExecutor(req.ctx, pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{ | 	fe := outpost.NewFlowExecutor(req.ctx, pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{ | ||||||
| 		"bindDN":    req.BindDN, | 		"bindDN":    req.BindDN, | ||||||
| 		"client":    req.conn.RemoteAddr().String(), | 		"client":    utils.GetIP(req.conn.RemoteAddr()), | ||||||
| 		"requestId": req.id, | 		"requestId": req.id, | ||||||
| 	}) | 	}) | ||||||
| 	fe.DelegateClientIP(req.conn.RemoteAddr()) | 	fe.DelegateClientIP(req.conn.RemoteAddr()) | ||||||
| @ -83,7 +83,6 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes | |||||||
| 	} | 	} | ||||||
| 	uisp.Finish() | 	uisp.Finish() | ||||||
| 	defer pi.boundUsersMutex.Unlock() | 	defer pi.boundUsersMutex.Unlock() | ||||||
| 	pi.delayDeleteUserInfo(username) |  | ||||||
| 	return ldap.LDAPResultSuccess, nil | 	return ldap.LDAPResultSuccess, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -100,25 +99,6 @@ func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (pi *ProviderInstance) delayDeleteUserInfo(dn string) { |  | ||||||
| 	ticker := time.NewTicker(30 * time.Second) |  | ||||||
| 	quit := make(chan struct{}) |  | ||||||
| 	go func() { |  | ||||||
| 		for { |  | ||||||
| 			select { |  | ||||||
| 			case <-ticker.C: |  | ||||||
| 				pi.boundUsersMutex.Lock() |  | ||||||
| 				delete(pi.boundUsers, dn) |  | ||||||
| 				pi.boundUsersMutex.Unlock() |  | ||||||
| 				close(quit) |  | ||||||
| 			case <-quit: |  | ||||||
| 				ticker.Stop() |  | ||||||
| 				return |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (pi *ProviderInstance) TimerFlowCacheExpiry() { | func (pi *ProviderInstance) TimerFlowCacheExpiry() { | ||||||
| 	fe := outpost.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{}) | 	fe := outpost.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{}) | ||||||
| 	fe.Params.Add("goauthentik.io/outpost/ldap", "true") | 	fe.Params.Add("goauthentik.io/outpost/ldap", "true") | ||||||
|  | |||||||
| @ -83,5 +83,6 @@ func NewServer(ac *ak.APIController) *LDAPServer { | |||||||
| 	ls.defaultCert = &defaultCert | 	ls.defaultCert = &defaultCert | ||||||
| 	s.BindFunc("", ls) | 	s.BindFunc("", ls) | ||||||
| 	s.SearchFunc("", ls) | 	s.SearchFunc("", ls) | ||||||
|  | 	s.CloseFunc("", ls) | ||||||
| 	return ls | 	return ls | ||||||
| } | } | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import ( | |||||||
| 	"github.com/google/uuid" | 	"github.com/google/uuid" | ||||||
| 	"github.com/nmcclain/ldap" | 	"github.com/nmcclain/ldap" | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
|  | 	"goauthentik.io/internal/utils" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type SearchRequest struct { | type SearchRequest struct { | ||||||
| @ -35,7 +36,7 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n | |||||||
| 		SearchRequest: searchReq, | 		SearchRequest: searchReq, | ||||||
| 		BindDN:        bindDN, | 		BindDN:        bindDN, | ||||||
| 		conn:          conn, | 		conn:          conn, | ||||||
| 		log:           ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", conn.RemoteAddr().String()).WithField("filter", searchReq.Filter).WithField("baseDN", searchReq.BaseDN), | 		log:           ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())).WithField("filter", searchReq.Filter).WithField("baseDN", searchReq.BaseDN), | ||||||
| 		id:            rid, | 		id:            rid, | ||||||
| 		ctx:           span.Context(), | 		ctx:           span.Context(), | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ type providerBundle struct { | |||||||
| 	Host  string | 	Host  string | ||||||
|  |  | ||||||
| 	endSessionUrl string | 	endSessionUrl string | ||||||
|  | 	Mode          *api.ProxyMode | ||||||
|  |  | ||||||
| 	cert *tls.Certificate | 	cert *tls.Certificate | ||||||
|  |  | ||||||
| @ -38,8 +39,8 @@ func intToPointer(i int) *int { | |||||||
| func (pb *providerBundle) replaceLocal(url string) string { | func (pb *providerBundle) replaceLocal(url string) string { | ||||||
| 	if strings.HasPrefix(url, "http://localhost:8000") { | 	if strings.HasPrefix(url, "http://localhost:8000") { | ||||||
| 		authentikHost, c := pb.s.ak.Outpost.Config["authentik_host"] | 		authentikHost, c := pb.s.ak.Outpost.Config["authentik_host"] | ||||||
| 		if !c { | 		if !c || authentikHost == "" { | ||||||
| 			pb.log.Warning("Outpost has localhost API Connection but no authentik_host is configured.") | 			pb.log.Warning("Outpost has localhost/blank API Connection but no authentik_host is configured.") | ||||||
| 			return url | 			return url | ||||||
| 		} | 		} | ||||||
| 		f := strings.ReplaceAll(url, "http://localhost:8000", authentikHost.(string)) | 		f := strings.ReplaceAll(url, "http://localhost:8000", authentikHost.(string)) | ||||||
| @ -49,6 +50,10 @@ func (pb *providerBundle) replaceLocal(url string) string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.Options { | func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.Options { | ||||||
|  | 	// We need to save the mode in the bundle | ||||||
|  | 	// Since for the embedded outpost we only switch for fully proxy providers | ||||||
|  | 	pb.Mode = provider.Mode | ||||||
|  |  | ||||||
| 	externalHost, err := url.Parse(provider.ExternalHost) | 	externalHost, err := url.Parse(provider.ExternalHost) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.WithError(err).Warning("Failed to parse URL, skipping provider") | 		log.WithError(err).Warning("Failed to parse URL, skipping provider") | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ import ( | |||||||
| 	"github.com/oauth2-proxy/oauth2-proxy/providers" | 	"github.com/oauth2-proxy/oauth2-proxy/providers" | ||||||
| 	"goauthentik.io/api" | 	"goauthentik.io/api" | ||||||
| 	"goauthentik.io/internal/utils/web" | 	"goauthentik.io/internal/utils/web" | ||||||
|  | 	staticWeb "goauthentik.io/web" | ||||||
|  |  | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
| @ -121,7 +122,7 @@ func NewOAuthProxy(opts *options.Options, provider api.ProxyOutpostConfig, c *ht | |||||||
| 		redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) | 		redirectURL.Path = fmt.Sprintf("%s/callback", opts.ProxyPrefix) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logger.Printf("proxy instance configured for Client ID: %s", opts.ClientID) | 	logger.WithField("auth_url", opts.GetProvider().Data().LoginURL.String()).WithField("client_id", opts.ClientID).Info("proxy instance configured") | ||||||
|  |  | ||||||
| 	sessionChain := buildSessionChain(opts, sessionStore) | 	sessionChain := buildSessionChain(opts, sessionStore) | ||||||
|  |  | ||||||
| @ -255,11 +256,18 @@ func (p *OAuthProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | |||||||
| 		p.AuthenticateOnly(rw, req) | 		p.AuthenticateOnly(rw, req) | ||||||
| 	case path == p.UserInfoPath: | 	case path == p.UserInfoPath: | ||||||
| 		p.UserInfo(rw, req) | 		p.UserInfo(rw, req) | ||||||
|  | 	case strings.HasPrefix(path, fmt.Sprintf("%s/static", p.ProxyPrefix)): | ||||||
|  | 		p.ServeStatic(rw, req) | ||||||
| 	default: | 	default: | ||||||
| 		p.Proxy(rw, req) | 		p.Proxy(rw, req) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (p *OAuthProxy) ServeStatic(rw http.ResponseWriter, req *http.Request) { | ||||||
|  | 	staticFs := http.FileServer(http.FS(staticWeb.StaticDist)) | ||||||
|  | 	http.StripPrefix(fmt.Sprintf("%s/static", p.ProxyPrefix), staticFs).ServeHTTP(rw, req) | ||||||
|  | } | ||||||
|  |  | ||||||
| //UserInfo endpoint outputs session email and preferred username in JSON format | //UserInfo endpoint outputs session email and preferred username in JSON format | ||||||
| func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { | func (p *OAuthProxy) UserInfo(rw http.ResponseWriter, req *http.Request) { | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,25 +4,11 @@ import ( | |||||||
| 	"html/template" | 	"html/template" | ||||||
|  |  | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
|  | 	"goauthentik.io/internal/outpost/proxy/templates" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func getTemplates() *template.Template { | func getTemplates() *template.Template { | ||||||
| 	t, err := template.New("foo").Parse(`{{define "error.html"}} | 	t, err := template.New("foo").Parse(templates.ErrorTemplate) | ||||||
| <!DOCTYPE html> |  | ||||||
| <html lang="en" charset="utf-8"> |  | ||||||
| <head> |  | ||||||
| 	<title>{{.Title}}</title> |  | ||||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> |  | ||||||
| 	<style>* { font-family: sans-serif; }</style> |  | ||||||
| </head> |  | ||||||
| <body> |  | ||||||
| 	<h2>{{.Title}}</h2> |  | ||||||
| 	<p>{{.Message}}</p> |  | ||||||
| 	<hr> |  | ||||||
| 	<p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p> |  | ||||||
| 	<p>Powered by <a href="https://goauthentik.io">authentik</a></p> |  | ||||||
| </body> |  | ||||||
| </html>{{end}}`) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("failed parsing template %s", err) | 		log.Fatalf("failed parsing template %s", err) | ||||||
| 	} | 	} | ||||||
|  | |||||||
							
								
								
									
										65
									
								
								internal/outpost/proxy/templates/error.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								internal/outpost/proxy/templates/error.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | {{define "error.html"}}<!DOCTYPE html> | ||||||
|  |  | ||||||
|  | <html lang="en"> | ||||||
|  |     <head> | ||||||
|  |         <meta charset="UTF-8"> | ||||||
|  |         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||||
|  |         <title>{{.Title}}</title> | ||||||
|  |         <link rel="shortcut icon" type="image/png" href="/akprox/static/dist/assets/icons/icon.png"> | ||||||
|  |         <link rel="stylesheet" type="text/css" href="/akprox/static/dist/patternfly.min.css"> | ||||||
|  |         <link rel="stylesheet" type="text/css" href="/akprox/static/dist/authentik.css"> | ||||||
|  |         <style> | ||||||
|  |             .pf-c-background-image::before { | ||||||
|  |                 --ak-flow-background: url("/akprox/static/dist/assets/images/flow_background.jpg"); | ||||||
|  |             } | ||||||
|  |         </style> | ||||||
|  |     </head> | ||||||
|  |     <body> | ||||||
|  |         <div class="pf-c-background-image"> | ||||||
|  |             <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0"> | ||||||
|  |                 <filter id="image_overlay"> | ||||||
|  |                     <feColorMatrix in="SourceGraphic" type="matrix" values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0" /> | ||||||
|  |                     <feComponentTransfer color-interpolation-filters="sRGB" result="duotone"> | ||||||
|  |                         <feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR> | ||||||
|  |                         <feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG> | ||||||
|  |                         <feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB> | ||||||
|  |                         <feFuncA type="table" tableValues="0 1"></feFuncA> | ||||||
|  |                     </feComponentTransfer> | ||||||
|  |                 </filter> | ||||||
|  |             </svg> | ||||||
|  |         </div> | ||||||
|  |         <div class="pf-c-login"> | ||||||
|  |             <div class="ak-login-container"> | ||||||
|  |                 <header class="pf-c-login__header"> | ||||||
|  |                     <div class="pf-c-brand ak-brand"> | ||||||
|  |                         <img src="/akprox/static/dist/assets/icons/icon_left_brand.svg" alt="authentik icon" /> | ||||||
|  |                     </div> | ||||||
|  |                 </header> | ||||||
|  |                 <main class="pf-c-login__main"> | ||||||
|  |                     <header class="pf-c-login__main-header"> | ||||||
|  |                         <h1 class="pf-c-title pf-m-3xl"> | ||||||
|  |                             {{ .Title }} | ||||||
|  |                         </h1> | ||||||
|  |                     </header> | ||||||
|  |                     <div class="pf-c-login__main-body"> | ||||||
|  |                         {{ .Message }} | ||||||
|  |                     </div> | ||||||
|  |                     <div class="pf-c-login__main-body"> | ||||||
|  |                         <a href="/" class="pf-c-button pf-m-primary pf-m-block">Go to home</a> | ||||||
|  |                     </div> | ||||||
|  |                 </main> | ||||||
|  |                 <footer class="pf-c-login__footer"> | ||||||
|  |                     <p></p> | ||||||
|  |                     <ul class="pf-c-list pf-m-inline"> | ||||||
|  |                         <li> | ||||||
|  |                             <a href="https://goauthentik.io"> | ||||||
|  |                                 Powered by authentik | ||||||
|  |                             </a> | ||||||
|  |                         </li> | ||||||
|  |                     </ul> | ||||||
|  |                 </footer> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </body> | ||||||
|  | </html> | ||||||
|  | {{end}} | ||||||
							
								
								
									
										6
									
								
								internal/outpost/proxy/templates/templates.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								internal/outpost/proxy/templates/templates.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | package templates | ||||||
|  |  | ||||||
|  | import _ "embed" | ||||||
|  |  | ||||||
|  | //go:embed error.html | ||||||
|  | var ErrorTemplate string | ||||||
							
								
								
									
										13
									
								
								internal/utils/net.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								internal/utils/net.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | |||||||
|  | package utils | ||||||
|  |  | ||||||
|  | import "net" | ||||||
|  |  | ||||||
|  | func GetIP(addr net.Addr) string { | ||||||
|  | 	switch addr := addr.(type) { | ||||||
|  | 	case *net.UDPAddr: | ||||||
|  | 		return addr.IP.String() | ||||||
|  | 	case *net.TCPAddr: | ||||||
|  | 		return addr.IP.String() | ||||||
|  | 	} | ||||||
|  | 	return "" | ||||||
|  | } | ||||||
| @ -9,14 +9,15 @@ import ( | |||||||
| 	"goauthentik.io/internal/utils/web" | 	"goauthentik.io/internal/utils/web" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func loggingMiddleware(next http.Handler) http.Handler { | func loggingMiddleware(l *log.Entry) func(next http.Handler) http.Handler { | ||||||
|  | 	return func(next http.Handler) http.Handler { | ||||||
| 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
| 			span := sentry.StartSpan(r.Context(), "authentik.go.request") | 			span := sentry.StartSpan(r.Context(), "authentik.go.request") | ||||||
| 			before := time.Now() | 			before := time.Now() | ||||||
| 			// Call the next handler, which can be another middleware in the chain, or the final handler. | 			// Call the next handler, which can be another middleware in the chain, or the final handler. | ||||||
| 			next.ServeHTTP(w, r) | 			next.ServeHTTP(w, r) | ||||||
| 			after := time.Now() | 			after := time.Now() | ||||||
| 		log.WithFields(log.Fields{ | 			l.WithFields(log.Fields{ | ||||||
| 				"remote": r.RemoteAddr, | 				"remote": r.RemoteAddr, | ||||||
| 				"method": r.Method, | 				"method": r.Method, | ||||||
| 				"took":   after.Sub(before), | 				"took":   after.Sub(before), | ||||||
| @ -25,3 +26,4 @@ func loggingMiddleware(next http.Handler) http.Handler { | |||||||
| 			span.Finish() | 			span.Finish() | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  | } | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ type WebServer struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func NewWebServer() *WebServer { | func NewWebServer() *WebServer { | ||||||
|  | 	l := log.WithField("logger", "authentik.g.web") | ||||||
| 	mainHandler := mux.NewRouter() | 	mainHandler := mux.NewRouter() | ||||||
| 	if config.G.ErrorReporting.Enabled { | 	if config.G.ErrorReporting.Enabled { | ||||||
| 		mainHandler.Use(recoveryMiddleware()) | 		mainHandler.Use(recoveryMiddleware()) | ||||||
| @ -37,14 +38,14 @@ func NewWebServer() *WebServer { | |||||||
| 	mainHandler.Use(handlers.ProxyHeaders) | 	mainHandler.Use(handlers.ProxyHeaders) | ||||||
| 	mainHandler.Use(handlers.CompressHandler) | 	mainHandler.Use(handlers.CompressHandler) | ||||||
| 	logginRouter := mainHandler.NewRoute().Subrouter() | 	logginRouter := mainHandler.NewRoute().Subrouter() | ||||||
| 	logginRouter.Use(loggingMiddleware) | 	logginRouter.Use(loggingMiddleware(l)) | ||||||
|  |  | ||||||
| 	ws := &WebServer{ | 	ws := &WebServer{ | ||||||
| 		LegacyProxy: true, | 		LegacyProxy: true, | ||||||
|  |  | ||||||
| 		m:   mainHandler, | 		m:   mainHandler, | ||||||
| 		lh:  logginRouter, | 		lh:  logginRouter, | ||||||
| 		log: log.WithField("logger", "authentik.g.web"), | 		log: l, | ||||||
| 	} | 	} | ||||||
| 	ws.configureStatic() | 	ws.configureStatic() | ||||||
| 	ws.configureProxy() | 	ws.configureProxy() | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	