Compare commits
	
		
			213 Commits
		
	
	
		
			version-20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| eaad564e23 | |||
| 511a94975b | |||
| 015810a2fd | |||
| e70e6b84c2 | |||
| d0b9c9a26f | |||
| 3e403fa348 | |||
| 48f4a971ef | |||
| 6314be14ad | |||
| 1a072c6c39 | |||
| ef2eed0bdf | |||
| 91227b1e96 | |||
| 67d68629da | |||
| e875db8f66 | |||
| 055a76393d | |||
| 0754821628 | |||
| fca88d9896 | |||
| dfe0404c51 | |||
| fa61696b46 | |||
| e5773738f4 | |||
| cac8539d79 | |||
| cf600f6f26 | |||
| 49dfb4756e | |||
| 814758e2aa | |||
| 5c42dac5e2 | |||
| 88603fa4f7 | |||
| 0232c4e162 | |||
| 11753c1fe1 | |||
| f5cc6c67ec | |||
| 8b8ed3527a | |||
| 1aa0274e7c | |||
| ecd33ca0c1 | |||
| e93be0de9a | |||
| a5adc4f8ed | |||
| a6baed9753 | |||
| ceaf832e63 | |||
| a6b0b14685 | |||
| f679250edd | |||
| acc4de2235 | |||
| 56a8276dbf | |||
| 6dfe6edbef | |||
| 6af4bd0d9a | |||
| 7ee7f6bd6a | |||
| f8b8334010 | |||
| d4b65dc4b4 | |||
| e4bbd3b1c0 | |||
| 87de5e625d | |||
| efbe51673e | |||
| a95bea53ea | |||
| 6021fc0f52 | |||
| 1415b68ff4 | |||
| be6853ac52 | |||
| 7fd6be5abb | |||
| 91d6f572a5 | |||
| 016a9ce34e | |||
| 8adb95af7f | |||
| 1dc54775d8 | |||
| 370ef716b5 | |||
| 16e56ad9ca | |||
| b5b5a9eed3 | |||
| 8b22e7bcc3 | |||
| d48b5b9511 | |||
| 0eccaa3f1e | |||
| 67d550a80d | |||
| ebb5711c32 | |||
| 79ec872232 | |||
| 4284e14ff7 | |||
| 92a09779d0 | |||
| 14c621631d | |||
| c55f503b9b | |||
| a908cad976 | |||
| c2586557d8 | |||
| 01c80a82e2 | |||
| 0d47654651 | |||
| 1183095833 | |||
| c281b11bdc | |||
| 61fe45a58c | |||
| d43aab479c | |||
| 7f8383427a | |||
| a06d6cf33d | |||
| 5b7cb205c9 | |||
| 293a932d20 | |||
| fff901ff03 | |||
| f47c936295 | |||
| 88d5aec618 | |||
| 96ae68cf09 | |||
| 63b3434b6f | |||
| 947ecec02b | |||
| 1c2b452406 | |||
| 47777529ac | |||
| 949095c376 | |||
| 4b112c2799 | |||
| 291a2516b1 | |||
| 4dcfd021e2 | |||
| ca50848db3 | |||
| 0bb3e3c558 | |||
| e4b25809ab | |||
| 7bf932f8e2 | |||
| 99d04528b0 | |||
| e48d172036 | |||
| c2388137a8 | |||
| 650e2cbc38 | |||
| b32800ea71 | |||
| e1c0c0b20c | |||
| fe39e39dcd | |||
| 883f213b03 | |||
| 538996f617 | |||
| 2f4c92deb9 | |||
| ef335ec083 | |||
| 07b09df3fe | |||
| e70e031a1f | |||
| c7ba183dc0 | |||
| 3ed23a37ea | |||
| 3d724db0e3 | |||
| 2997542114 | |||
| 84b18fff96 | |||
| 1dce408c72 | |||
| e5ff47bf14 | |||
| b53bf331c3 | |||
| 90e9a8b34c | |||
| 845f842783 | |||
| 7397849c60 | |||
| 6dd46b5fc5 | |||
| 89ca79ed10 | |||
| 713bef895c | |||
| 925115e9ce | |||
| 42f5cf8c93 | |||
| 82cc1d536a | |||
| 08af2fd46b | |||
| 70e3b27a4d | |||
| 6a411d7960 | |||
| 33567b56d7 | |||
| 0c1954aeb7 | |||
| f4a6c70e98 | |||
| 5f198e7fe4 | |||
| d172d32817 | |||
| af3fb5c2cd | |||
| 885efb526e | |||
| 3bfb8b2cb2 | |||
| 9fc5ff4b77 | |||
| dd8b579dd6 | |||
| e12cbd8711 | |||
| 62d35f8f8c | |||
| 49be504c13 | |||
| edad55e51d | |||
| 38086fa8bb | |||
| c4f9a3e9a7 | |||
| 930df791bd | |||
| 9a6086634c | |||
| b68e65355a | |||
| 72d33a91dd | |||
| 7067e3d69a | |||
| 4db370d24e | |||
| 41e7b9b73f | |||
| 7f47f93e4e | |||
| 89abd44b76 | |||
| 14c7d8c4f4 | |||
| 525976a81b | |||
| 64a2126ea4 | |||
| 994c5882ab | |||
| ad64d51e85 | |||
| a184a7518a | |||
| 943fd80920 | |||
| 01bb18b8c4 | |||
| 94baaaa5a5 | |||
| 40b164ce94 | |||
| 1d7c7801e7 | |||
| 0db0a12ef3 | |||
| 8008aba450 | |||
| eaeab27004 | |||
| 111fbf119b | |||
| 300ad88447 | |||
| 92cc0c9c64 | |||
| 18ff803370 | |||
| 819af78e2b | |||
| 6338785ce1 | |||
| 973e151dff | |||
| fae6d83f27 | |||
| ed84fe0b8d | |||
| 1ee603403e | |||
| 7db7b7cc4d | |||
| 68a98cd86c | |||
| e758db5727 | |||
| 4d7d700afa | |||
| f9a5add01d | |||
| 2986b56389 | |||
| 58f79b525d | |||
| 0a1c0dae05 | |||
| e18ef8dab6 | |||
| 3cacc59bec | |||
| 4eea46d399 | |||
| 11e25617bd | |||
| 4817126811 | |||
| 0181361efa | |||
| 8ff8e1d5f7 | |||
| 19d5902a92 | |||
| 71dffb21a9 | |||
| bd283c506d | |||
| ef564e5f1a | |||
| 2543224c7c | |||
| 077eee9310 | |||
| d894eeaa67 | |||
| 452bfb39bf | |||
| 6b6702521f | |||
| c07b8d95d0 | |||
| bf347730b3 | |||
| ececfc3a30 | |||
| b76546de0c | |||
| 424d490a60 | |||
| 127dd85214 | |||
| 10570ac7f8 | |||
| dc5667b0b8 | |||
| ec9cacb610 | |||
| 0027dbc0e5 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2021.12.5 | current_version = 2022.1.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>.*) | ||||||
|  | |||||||
							
								
								
									
										26
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -40,7 +40,7 @@ jobs: | |||||||
|         uses: actions/cache@v2.1.7 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ~/.cache/pypoetry/virtualenvs |           path: ~/.cache/pypoetry/virtualenvs | ||||||
|           key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }} |           key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         env: |         env: | ||||||
|           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} | ||||||
| @ -56,7 +56,7 @@ jobs: | |||||||
|         uses: actions/cache@v2.1.7 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ~/.cache/pypoetry/virtualenvs |           path: ~/.cache/pypoetry/virtualenvs | ||||||
|           key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }} |           key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         env: |         env: | ||||||
|           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} | ||||||
| @ -79,7 +79,7 @@ jobs: | |||||||
|         uses: actions/cache@v2.1.7 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ~/.cache/pypoetry/virtualenvs |           path: ~/.cache/pypoetry/virtualenvs | ||||||
|           key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }} |           key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }} | ||||||
|       - name: checkout stable |       - name: checkout stable | ||||||
|         run: | |         run: | | ||||||
|           # Copy current, latest config to local |           # Copy current, latest config to local | ||||||
| @ -95,11 +95,7 @@ jobs: | |||||||
|           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} | ||||||
|         run: | |         run: | | ||||||
|           scripts/ci_prepare.sh |           scripts/ci_prepare.sh | ||||||
|           # Sync anyways since stable will have different dependencies |           # install anyways since stable will have different dependencies | ||||||
|           # TODO: Remove after next stable release |  | ||||||
|           if [[ -f "Pipfile.lock" ]]; then |  | ||||||
|             pipenv install --dev |  | ||||||
|           fi |  | ||||||
|           poetry install |           poetry install | ||||||
|       - name: run migrations to stable |       - name: run migrations to stable | ||||||
|         run: poetry run python -m lifecycle.migrate |         run: poetry run python -m lifecycle.migrate | ||||||
| @ -108,13 +104,7 @@ jobs: | |||||||
|           set -x |           set -x | ||||||
|           git fetch |           git fetch | ||||||
|           git reset --hard HEAD |           git reset --hard HEAD | ||||||
|           # TODO: Remove after next stable release |  | ||||||
|           rm -f poetry.lock |  | ||||||
|           git checkout $GITHUB_SHA |           git checkout $GITHUB_SHA | ||||||
|           # TODO: Remove after next stable release |  | ||||||
|           if [[ -f "Pipfile.lock" ]]; then |  | ||||||
|             pipenv install --dev |  | ||||||
|           fi |  | ||||||
|           poetry install |           poetry install | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         env: |         env: | ||||||
| @ -131,7 +121,7 @@ jobs: | |||||||
|         uses: actions/cache@v2.1.7 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ~/.cache/pypoetry/virtualenvs |           path: ~/.cache/pypoetry/virtualenvs | ||||||
|           key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }} |           key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         env: |         env: | ||||||
|           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} | ||||||
| @ -158,7 +148,7 @@ jobs: | |||||||
|         uses: actions/cache@v2.1.7 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ~/.cache/pypoetry/virtualenvs |           path: ~/.cache/pypoetry/virtualenvs | ||||||
|           key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }} |           key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         env: |         env: | ||||||
|           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} | ||||||
| @ -195,7 +185,7 @@ jobs: | |||||||
|         uses: actions/cache@v2.1.7 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ~/.cache/pypoetry/virtualenvs |           path: ~/.cache/pypoetry/virtualenvs | ||||||
|           key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }} |           key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         env: |         env: | ||||||
|           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} | ||||||
| @ -240,7 +230,7 @@ jobs: | |||||||
|         uses: actions/cache@v2.1.7 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ~/.cache/pypoetry/virtualenvs |           path: ~/.cache/pypoetry/virtualenvs | ||||||
|           key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }} |           key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         env: |         env: | ||||||
|           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} | ||||||
|  | |||||||
							
								
								
									
										18
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							| @ -28,11 +28,27 @@ jobs: | |||||||
|             --rm \ |             --rm \ | ||||||
|             -v $(pwd):/app \ |             -v $(pwd):/app \ | ||||||
|             -w /app \ |             -w /app \ | ||||||
|             golangci/golangci-lint:v1.39.0 \ |             golangci/golangci-lint:v1.43 \ | ||||||
|             golangci-lint run -v --timeout 200s |             golangci-lint run -v --timeout 200s | ||||||
|  |   test-unittest: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - uses: actions/checkout@v2 | ||||||
|  |       - uses: actions/setup-go@v2 | ||||||
|  |         with: | ||||||
|  |           go-version: "^1.17" | ||||||
|  |       - name: Get dependencies | ||||||
|  |         run: | | ||||||
|  |           go get github.com/axw/gocov/gocov | ||||||
|  |           go get github.com/AlekSi/gocov-xml | ||||||
|  |           go get github.com/jstemmer/go-junit-report | ||||||
|  |       - name: Go unittests | ||||||
|  |         run: | | ||||||
|  |           go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./... | go-junit-report > junit.xml | ||||||
|   ci-outpost-mark: |   ci-outpost-mark: | ||||||
|     needs: |     needs: | ||||||
|       - lint-golint |       - lint-golint | ||||||
|  |       - test-unittest | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - run: echo mark |       - run: echo mark | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -30,14 +30,14 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           push: ${{ github.event_name == 'release' }} |           push: ${{ github.event_name == 'release' }} | ||||||
|           tags: | |           tags: | | ||||||
|             beryju/authentik:2021.12.5, |             beryju/authentik:2022.1.5, | ||||||
|             beryju/authentik:latest, |             beryju/authentik:latest, | ||||||
|             ghcr.io/goauthentik/server:2021.12.5, |             ghcr.io/goauthentik/server:2022.1.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.12.5', 'rc') }} |         if: ${{ github.event_name == 'release' && !contains('2022.1.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 | ||||||
| @ -78,14 +78,14 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           push: ${{ github.event_name == 'release' }} |           push: ${{ github.event_name == 'release' }} | ||||||
|           tags: | |           tags: | | ||||||
|             beryju/authentik-${{ matrix.type }}:2021.12.5, |             beryju/authentik-${{ matrix.type }}:2022.1.5, | ||||||
|             beryju/authentik-${{ matrix.type }}:latest, |             beryju/authentik-${{ matrix.type }}:latest, | ||||||
|             ghcr.io/goauthentik/${{ matrix.type }}:2021.12.5, |             ghcr.io/goauthentik/${{ matrix.type }}:2022.1.5, | ||||||
|             ghcr.io/goauthentik/${{ matrix.type }}:latest |             ghcr.io/goauthentik/${{ matrix.type }}:latest | ||||||
|           file: ${{ matrix.type }}.Dockerfile |           file: ${{ matrix.type }}.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.12.5', 'rc') }} |         if: ${{ github.event_name == 'release' && !contains('2022.1.5', 'rc') }} | ||||||
|         run: | |         run: | | ||||||
|           docker pull beryju/authentik-${{ matrix.type }}:latest |           docker pull beryju/authentik-${{ matrix.type }}:latest | ||||||
|           docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable |           docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable | ||||||
| @ -170,7 +170,7 @@ jobs: | |||||||
|           SENTRY_PROJECT: authentik |           SENTRY_PROJECT: authentik | ||||||
|           SENTRY_URL: https://sentry.beryju.org |           SENTRY_URL: https://sentry.beryju.org | ||||||
|         with: |         with: | ||||||
|           version: authentik@2021.12.5 |           version: authentik@2022.1.5 | ||||||
|           environment: beryjuorg-prod |           environment: beryjuorg-prod | ||||||
|           sourcemaps: './web/dist' |           sourcemaps: './web/dist' | ||||||
|           url_prefix: '~/static/dist' |           url_prefix: '~/static/dist' | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/translation-compile.yml
									
									
									
									
										vendored
									
									
								
							| @ -26,7 +26,7 @@ jobs: | |||||||
|         uses: actions/cache@v2.1.7 |         uses: actions/cache@v2.1.7 | ||||||
|         with: |         with: | ||||||
|           path: ~/.cache/pypoetry/virtualenvs |           path: ~/.cache/pypoetry/virtualenvs | ||||||
|           key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }} |           key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }} | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         env: |         env: | ||||||
|           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} | ||||||
|  | |||||||
| @ -1 +0,0 @@ | |||||||
| 3.9.7 |  | ||||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -12,7 +12,8 @@ | |||||||
|         "totp", |         "totp", | ||||||
|         "webauthn", |         "webauthn", | ||||||
|         "traefik", |         "traefik", | ||||||
|         "passwordless" |         "passwordless", | ||||||
|  |         "kubernetes" | ||||||
|     ], |     ], | ||||||
|     "python.linting.pylintEnabled": true, |     "python.linting.pylintEnabled": true, | ||||||
|     "todo-tree.tree.showCountsInTree": true, |     "todo-tree.tree.showCountsInTree": true, | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ ENV NODE_ENV=production | |||||||
| RUN cd /work/web && npm i && npm run build | RUN cd /work/web && npm i && npm run build | ||||||
|  |  | ||||||
| # Stage 3: Build go proxy | # Stage 3: Build go proxy | ||||||
| FROM docker.io/golang:1.17.5-bullseye AS builder | FROM docker.io/golang:1.17.6-bullseye AS builder | ||||||
|  |  | ||||||
| WORKDIR /work | WORKDIR /work | ||||||
|  |  | ||||||
| @ -32,7 +32,7 @@ COPY ./go.sum /work/go.sum | |||||||
| RUN go build -o /work/authentik ./cmd/server/main.go | RUN go build -o /work/authentik ./cmd/server/main.go | ||||||
|  |  | ||||||
| # Stage 4: Run | # Stage 4: Run | ||||||
| FROM docker.io/python:3.10.1-slim-bullseye | FROM docker.io/python:3.10.2-slim-bullseye | ||||||
|  |  | ||||||
| LABEL org.opencontainers.image.url https://goauthentik.io | LABEL org.opencontainers.image.url https://goauthentik.io | ||||||
| LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info. | LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info. | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								Makefile
									
									
									
									
									
								
							| @ -15,6 +15,9 @@ test-e2e-provider: | |||||||
| test-e2e-rest: | test-e2e-rest: | ||||||
| 	coverage run manage.py test tests/e2e/test_flows* tests/e2e/test_source* | 	coverage run manage.py test tests/e2e/test_flows* tests/e2e/test_source* | ||||||
|  |  | ||||||
|  | test-go: | ||||||
|  | 	go test -timeout 0 -v -race -cover ./... | ||||||
|  |  | ||||||
| test: | test: | ||||||
| 	coverage run manage.py test authentik | 	coverage run manage.py test authentik | ||||||
| 	coverage html | 	coverage html | ||||||
|  | |||||||
| @ -57,4 +57,4 @@ DigitalOcean provides development and testing resources for authentik. | |||||||
|     </a> |     </a> | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
| Netlify hosts the [goauthentik.io](goauthentik.io) site. | Netlify hosts the [goauthentik.io](https://goauthentik.io) site. | ||||||
|  | |||||||
| @ -1,3 +1,19 @@ | |||||||
| """authentik""" | """authentik""" | ||||||
| __version__ = "2021.12.5" | from os import environ | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
|  | __version__ = "2022.1.5" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_build_hash(fallback: Optional[str] = None) -> str: | ||||||
|  |     """Get build hash""" | ||||||
|  |     return environ.get(ENV_GIT_HASH_KEY, fallback if fallback else "") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_full_version() -> str: | ||||||
|  |     """Get full version, with build hash appended""" | ||||||
|  |     version = __version__ | ||||||
|  |     if (build_hash := get_build_hash()) != "": | ||||||
|  |         version += "." + build_hash | ||||||
|  |     return version | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| """authentik administration overview""" | """authentik administration overview""" | ||||||
| from os import environ |  | ||||||
|  |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from drf_spectacular.utils import extend_schema | from drf_spectacular.utils import extend_schema | ||||||
| from packaging.version import parse | from packaging.version import parse | ||||||
| @ -10,7 +8,7 @@ from rest_framework.request import Request | |||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.views import APIView | from rest_framework.views import APIView | ||||||
|  |  | ||||||
| from authentik import ENV_GIT_HASH_KEY, __version__ | from authentik import __version__, get_build_hash | ||||||
| from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version | from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
|  |  | ||||||
| @ -25,7 +23,7 @@ class VersionSerializer(PassiveSerializer): | |||||||
|  |  | ||||||
|     def get_build_hash(self, _) -> str: |     def get_build_hash(self, _) -> str: | ||||||
|         """Get build hash, if version is not latest or released""" |         """Get build hash, if version is not latest or released""" | ||||||
|         return environ.get(ENV_GIT_HASH_KEY, "") |         return get_build_hash() | ||||||
|  |  | ||||||
|     def get_version_current(self, _) -> str: |     def get_version_current(self, _) -> str: | ||||||
|         """Get current version""" |         """Get current version""" | ||||||
|  | |||||||
| @ -1,4 +1,6 @@ | |||||||
| """authentik admin app config""" | """authentik admin app config""" | ||||||
|  | from importlib import import_module | ||||||
|  |  | ||||||
| from django.apps import AppConfig | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -13,3 +15,4 @@ class AuthentikAdminConfig(AppConfig): | |||||||
|         from authentik.admin.tasks import clear_update_notifications |         from authentik.admin.tasks import clear_update_notifications | ||||||
|  |  | ||||||
|         clear_update_notifications.delay() |         clear_update_notifications.delay() | ||||||
|  |         import_module("authentik.admin.signals") | ||||||
|  | |||||||
							
								
								
									
										23
									
								
								authentik/admin/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								authentik/admin/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | """admin signals""" | ||||||
|  | from django.dispatch import receiver | ||||||
|  |  | ||||||
|  | from authentik.admin.api.tasks import TaskInfo | ||||||
|  | from authentik.admin.api.workers import GAUGE_WORKERS | ||||||
|  | from authentik.root.celery import CELERY_APP | ||||||
|  | from authentik.root.monitoring import monitoring_set | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(monitoring_set) | ||||||
|  | # pylint: disable=unused-argument | ||||||
|  | def monitoring_set_workers(sender, **kwargs): | ||||||
|  |     """Set worker gauge""" | ||||||
|  |     count = len(CELERY_APP.control.ping(timeout=0.5)) | ||||||
|  |     GAUGE_WORKERS.set(count) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(monitoring_set) | ||||||
|  | # pylint: disable=unused-argument | ||||||
|  | def monitoring_set_tasks(sender, **kwargs): | ||||||
|  |     """Set task gauges""" | ||||||
|  |     for task in TaskInfo.all().values(): | ||||||
|  |         task.set_prom_metrics() | ||||||
| @ -1,6 +1,5 @@ | |||||||
| """authentik admin tasks""" | """authentik admin tasks""" | ||||||
| import re | import re | ||||||
| from os import environ |  | ||||||
|  |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.core.validators import URLValidator | from django.core.validators import URLValidator | ||||||
| @ -9,7 +8,7 @@ from prometheus_client import Info | |||||||
| from requests import RequestException | from requests import RequestException | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik import ENV_GIT_HASH_KEY, __version__ | from authentik import __version__, get_build_hash | ||||||
| from authentik.events.models import Event, EventAction, Notification | from authentik.events.models import Event, EventAction, Notification | ||||||
| from authentik.events.monitored_tasks import ( | from authentik.events.monitored_tasks import ( | ||||||
|     MonitoredTask, |     MonitoredTask, | ||||||
| @ -36,7 +35,7 @@ def _set_prom_info(): | |||||||
|         { |         { | ||||||
|             "version": __version__, |             "version": __version__, | ||||||
|             "latest": cache.get(VERSION_CACHE_KEY, ""), |             "latest": cache.get(VERSION_CACHE_KEY, ""), | ||||||
|             "build_hash": environ.get(ENV_GIT_HASH_KEY, ""), |             "build_hash": get_build_hash(), | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -30,7 +30,7 @@ function getCookie(name) { | |||||||
| window.addEventListener('DOMContentLoaded', (event) => { | window.addEventListener('DOMContentLoaded', (event) => { | ||||||
|     const rapidocEl = document.querySelector('rapi-doc'); |     const rapidocEl = document.querySelector('rapi-doc'); | ||||||
|     rapidocEl.addEventListener('before-try', (e) => { |     rapidocEl.addEventListener('before-try', (e) => { | ||||||
|         e.detail.request.headers.append('X-CSRFToken', getCookie("authentik_csrf")); |         e.detail.request.headers.append('X-authentik-CSRF', getCookie("authentik_csrf")); | ||||||
|     }); |     }); | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  | |||||||
| @ -4,7 +4,5 @@ from django.urls import include, path | |||||||
| from authentik.api.v3.urls import urlpatterns as v3_urls | from authentik.api.v3.urls import urlpatterns as v3_urls | ||||||
|  |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     # TODO: Remove in 2022.1 |  | ||||||
|     path("v2beta/", include(v3_urls)), |  | ||||||
|     path("v3/", include(v3_urls)), |     path("v3/", include(v3_urls)), | ||||||
| ] | ] | ||||||
|  | |||||||
| @ -80,7 +80,7 @@ class ConfigView(APIView): | |||||||
|         config = ConfigSerializer( |         config = ConfigSerializer( | ||||||
|             { |             { | ||||||
|                 "error_reporting": { |                 "error_reporting": { | ||||||
|                     "enabled": CONFIG.y("error_reporting.enabled"), |                     "enabled": CONFIG.y("error_reporting.enabled") and not settings.DEBUG, | ||||||
|                     "environment": CONFIG.y("error_reporting.environment"), |                     "environment": CONFIG.y("error_reporting.environment"), | ||||||
|                     "send_pii": CONFIG.y("error_reporting.send_pii"), |                     "send_pii": CONFIG.y("error_reporting.send_pii"), | ||||||
|                     "traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)), |                     "traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)), | ||||||
|  | |||||||
| @ -1,13 +1,16 @@ | |||||||
| """Application API Views""" | """Application API Views""" | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.db.models import QuerySet | from django.db.models import QuerySet | ||||||
| from django.http.response import HttpResponseBadRequest | from django.http.response import HttpResponseBadRequest | ||||||
| from django.shortcuts import get_object_or_404 | from django.shortcuts import get_object_or_404 | ||||||
|  | from django.utils.functional import SimpleLazyObject | ||||||
| from drf_spectacular.types import OpenApiTypes | from drf_spectacular.types import OpenApiTypes | ||||||
| from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema | from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema | ||||||
| from guardian.shortcuts import get_objects_for_user | from guardian.shortcuts import get_objects_for_user | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.fields import ReadOnlyField | from rest_framework.fields import ReadOnlyField, SerializerMethodField | ||||||
| from rest_framework.parsers import MultiPartParser | from rest_framework.parsers import MultiPartParser | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| @ -39,11 +42,22 @@ def user_app_cache_key(user_pk: str) -> str: | |||||||
| class ApplicationSerializer(ModelSerializer): | class ApplicationSerializer(ModelSerializer): | ||||||
|     """Application Serializer""" |     """Application Serializer""" | ||||||
|  |  | ||||||
|     launch_url = ReadOnlyField(source="get_launch_url") |     launch_url = SerializerMethodField() | ||||||
|     provider_obj = ProviderSerializer(source="get_provider", required=False) |     provider_obj = ProviderSerializer(source="get_provider", required=False) | ||||||
|  |  | ||||||
|     meta_icon = ReadOnlyField(source="get_meta_icon") |     meta_icon = ReadOnlyField(source="get_meta_icon") | ||||||
|  |  | ||||||
|  |     def get_launch_url(self, app: Application) -> Optional[str]: | ||||||
|  |         """Allow formatting of launch URL""" | ||||||
|  |         url = app.get_launch_url() | ||||||
|  |         if not url: | ||||||
|  |             return url | ||||||
|  |         user = self.context["request"].user | ||||||
|  |         if isinstance(user, SimpleLazyObject): | ||||||
|  |             user._setup() | ||||||
|  |             user = user._wrapped | ||||||
|  |         return url % user.__dict__ | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = Application |         model = Application | ||||||
|  | |||||||
| @ -1,10 +1,9 @@ | |||||||
| """Tokens API Viewset""" | """Tokens API Viewset""" | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from django.http.response import Http404 |  | ||||||
| from django_filters.rest_framework import DjangoFilterBackend | from django_filters.rest_framework import DjangoFilterBackend | ||||||
| from drf_spectacular.utils import OpenApiResponse, extend_schema | from drf_spectacular.utils import OpenApiResponse, extend_schema | ||||||
| from guardian.shortcuts import get_anonymous_user | from guardian.shortcuts import assign_perm, get_anonymous_user | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.exceptions import ValidationError | from rest_framework.exceptions import ValidationError | ||||||
| from rest_framework.fields import CharField | from rest_framework.fields import CharField | ||||||
| @ -96,10 +95,12 @@ class TokenViewSet(UsedByMixin, ModelViewSet): | |||||||
|  |  | ||||||
|     def perform_create(self, serializer: TokenSerializer): |     def perform_create(self, serializer: TokenSerializer): | ||||||
|         if not self.request.user.is_superuser: |         if not self.request.user.is_superuser: | ||||||
|             return serializer.save( |             instance = serializer.save( | ||||||
|                 user=self.request.user, |                 user=self.request.user, | ||||||
|                 expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True), |                 expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True), | ||||||
|             ) |             ) | ||||||
|  |             assign_perm("authentik_core.view_token_key", self.request.user, instance) | ||||||
|  |             return instance | ||||||
|         return super().perform_create(serializer) |         return super().perform_create(serializer) | ||||||
|  |  | ||||||
|     @permission_required("authentik_core.view_token_key") |     @permission_required("authentik_core.view_token_key") | ||||||
| @ -114,7 +115,5 @@ class TokenViewSet(UsedByMixin, ModelViewSet): | |||||||
|     def view_key(self, request: Request, identifier: str) -> Response: |     def view_key(self, request: Request, identifier: str) -> Response: | ||||||
|         """Return token key and log access""" |         """Return token key and log access""" | ||||||
|         token: Token = self.get_object() |         token: Token = self.get_object() | ||||||
|         if token.is_expired: |  | ||||||
|             raise Http404 |  | ||||||
|         Event.new(EventAction.SECRET_VIEW, secret=token).from_http(request)  # noqa # nosec |         Event.new(EventAction.SECRET_VIEW, secret=token).from_http(request)  # noqa # nosec | ||||||
|         return Response(TokenViewSerializer({"key": token.key}).data) |         return Response(TokenViewSerializer({"key": token.key}).data) | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| """authentik core signals""" | """authentik core signals""" | ||||||
| from typing import TYPE_CHECKING | from typing import TYPE_CHECKING | ||||||
|  |  | ||||||
|  | from django.apps import apps | ||||||
| from django.contrib.auth.signals import user_logged_in, user_logged_out | from django.contrib.auth.signals import user_logged_in, user_logged_out | ||||||
| from django.contrib.sessions.backends.cache import KEY_PREFIX | from django.contrib.sessions.backends.cache import KEY_PREFIX | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| @ -11,6 +12,8 @@ from django.dispatch import receiver | |||||||
| from django.http.request import HttpRequest | from django.http.request import HttpRequest | ||||||
| from prometheus_client import Gauge | from prometheus_client import Gauge | ||||||
|  |  | ||||||
|  | from authentik.root.monitoring import monitoring_set | ||||||
|  |  | ||||||
| # Arguments: user: User, password: str | # Arguments: user: User, password: str | ||||||
| password_changed = Signal() | password_changed = Signal() | ||||||
|  |  | ||||||
| @ -20,6 +23,17 @@ if TYPE_CHECKING: | |||||||
|     from authentik.core.models import AuthenticatedSession, User |     from authentik.core.models import AuthenticatedSession, User | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(monitoring_set) | ||||||
|  | # pylint: disable=unused-argument | ||||||
|  | def monitoring_set_models(sender, **kwargs): | ||||||
|  |     """set models gauges""" | ||||||
|  |     for model in apps.get_models(): | ||||||
|  |         GAUGE_MODELS.labels( | ||||||
|  |             model_name=model._meta.model_name, | ||||||
|  |             app=model._meta.app_label, | ||||||
|  |         ).set(model.objects.count()) | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(post_save) | @receiver(post_save) | ||||||
| # pylint: disable=unused-argument | # pylint: disable=unused-argument | ||||||
| def post_save_application(sender: type[Model], instance, created: bool, **_): | def post_save_application(sender: type[Model], instance, created: bool, **_): | ||||||
| @ -27,11 +41,6 @@ def post_save_application(sender: type[Model], instance, created: bool, **_): | |||||||
|     from authentik.core.api.applications import user_app_cache_key |     from authentik.core.api.applications import user_app_cache_key | ||||||
|     from authentik.core.models import Application |     from authentik.core.models import Application | ||||||
|  |  | ||||||
|     GAUGE_MODELS.labels( |  | ||||||
|         model_name=sender._meta.model_name, |  | ||||||
|         app=sender._meta.app_label, |  | ||||||
|     ).set(sender.objects.count()) |  | ||||||
|  |  | ||||||
|     if sender != Application: |     if sender != Application: | ||||||
|         return |         return | ||||||
|     if not created:  # pragma: no cover |     if not created:  # pragma: no cover | ||||||
|  | |||||||
| @ -13,7 +13,9 @@ class TestApplicationsAPI(APITestCase): | |||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         self.user = create_test_admin_user() |         self.user = create_test_admin_user() | ||||||
|         self.allowed = Application.objects.create(name="allowed", slug="allowed") |         self.allowed = Application.objects.create( | ||||||
|  |             name="allowed", slug="allowed", meta_launch_url="https://goauthentik.io/%(username)s" | ||||||
|  |         ) | ||||||
|         self.denied = Application.objects.create(name="denied", slug="denied") |         self.denied = Application.objects.create(name="denied", slug="denied") | ||||||
|         PolicyBinding.objects.create( |         PolicyBinding.objects.create( | ||||||
|             target=self.denied, |             target=self.denied, | ||||||
| @ -64,8 +66,8 @@ class TestApplicationsAPI(APITestCase): | |||||||
|                         "slug": "allowed", |                         "slug": "allowed", | ||||||
|                         "provider": None, |                         "provider": None, | ||||||
|                         "provider_obj": None, |                         "provider_obj": None, | ||||||
|                         "launch_url": None, |                         "launch_url": f"https://goauthentik.io/{self.user.username}", | ||||||
|                         "meta_launch_url": "", |                         "meta_launch_url": "https://goauthentik.io/%(username)s", | ||||||
|                         "meta_icon": None, |                         "meta_icon": None, | ||||||
|                         "meta_description": "", |                         "meta_description": "", | ||||||
|                         "meta_publisher": "", |                         "meta_publisher": "", | ||||||
| @ -100,8 +102,8 @@ class TestApplicationsAPI(APITestCase): | |||||||
|                         "slug": "allowed", |                         "slug": "allowed", | ||||||
|                         "provider": None, |                         "provider": None, | ||||||
|                         "provider_obj": None, |                         "provider_obj": None, | ||||||
|                         "launch_url": None, |                         "launch_url": f"https://goauthentik.io/{self.user.username}", | ||||||
|                         "meta_launch_url": "", |                         "meta_launch_url": "https://goauthentik.io/%(username)s", | ||||||
|                         "meta_icon": None, |                         "meta_icon": None, | ||||||
|                         "meta_description": "", |                         "meta_description": "", | ||||||
|                         "meta_publisher": "", |                         "meta_publisher": "", | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ class TestTokenAPI(APITestCase): | |||||||
|         self.assertEqual(token.user, self.user) |         self.assertEqual(token.user, self.user) | ||||||
|         self.assertEqual(token.intent, TokenIntents.INTENT_API) |         self.assertEqual(token.intent, TokenIntents.INTENT_API) | ||||||
|         self.assertEqual(token.expiring, True) |         self.assertEqual(token.expiring, True) | ||||||
|  |         self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token)) | ||||||
|  |  | ||||||
|     def test_token_create_invalid(self): |     def test_token_create_invalid(self): | ||||||
|         """Test token creation endpoint (invalid data)""" |         """Test token creation endpoint (invalid data)""" | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ from rest_framework.request import Request | |||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.serializers import ModelSerializer, ValidationError | from rest_framework.serializers import ModelSerializer, ValidationError | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.api.decorators import permission_required | from authentik.api.decorators import permission_required | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| @ -26,6 +27,8 @@ from authentik.crypto.managed import MANAGED_KEY | |||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  |  | ||||||
|  | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| class CertificateKeyPairSerializer(ModelSerializer): | class CertificateKeyPairSerializer(ModelSerializer): | ||||||
|     """CertificateKeyPair Serializer""" |     """CertificateKeyPair Serializer""" | ||||||
| @ -76,8 +79,11 @@ class CertificateKeyPairSerializer(ModelSerializer): | |||||||
|     def validate_certificate_data(self, value: str) -> str: |     def validate_certificate_data(self, value: str) -> str: | ||||||
|         """Verify that input is a valid PEM x509 Certificate""" |         """Verify that input is a valid PEM x509 Certificate""" | ||||||
|         try: |         try: | ||||||
|             load_pem_x509_certificate(value.encode("utf-8"), default_backend()) |             # Cast to string to fully load and parse certificate | ||||||
|         except ValueError: |             # Prevents issues like https://github.com/goauthentik/authentik/issues/2082 | ||||||
|  |             str(load_pem_x509_certificate(value.encode("utf-8"), default_backend())) | ||||||
|  |         except ValueError as exc: | ||||||
|  |             LOGGER.warning("Failed to load certificate", exc=exc) | ||||||
|             raise ValidationError("Unable to load certificate.") |             raise ValidationError("Unable to load certificate.") | ||||||
|         return value |         return value | ||||||
|  |  | ||||||
| @ -86,12 +92,17 @@ class CertificateKeyPairSerializer(ModelSerializer): | |||||||
|         # Since this field is optional, data can be empty. |         # Since this field is optional, data can be empty. | ||||||
|         if value != "": |         if value != "": | ||||||
|             try: |             try: | ||||||
|  |                 # Cast to string to fully load and parse certificate | ||||||
|  |                 # Prevents issues like https://github.com/goauthentik/authentik/issues/2082 | ||||||
|  |                 str( | ||||||
|                     load_pem_private_key( |                     load_pem_private_key( | ||||||
|                         str.encode("\n".join([x.strip() for x in value.split("\n")])), |                         str.encode("\n".join([x.strip() for x in value.split("\n")])), | ||||||
|                         password=None, |                         password=None, | ||||||
|                         backend=default_backend(), |                         backend=default_backend(), | ||||||
|                     ) |                     ) | ||||||
|             except (ValueError, TypeError): |                 ) | ||||||
|  |             except (ValueError, TypeError) as exc: | ||||||
|  |                 LOGGER.warning("Failed to load private key", exc=exc) | ||||||
|                 raise ValidationError("Unable to load private key (possibly encrypted?).") |                 raise ValidationError("Unable to load private key (possibly encrypted?).") | ||||||
|         return value |         return value | ||||||
|  |  | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ from typing import Any, Optional | |||||||
|  |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from prometheus_client import Histogram | from prometheus_client import Gauge, Histogram | ||||||
| from sentry_sdk.hub import Hub | from sentry_sdk.hub import Hub | ||||||
| from sentry_sdk.tracing import Span | from sentry_sdk.tracing import Span | ||||||
| from structlog.stdlib import BoundLogger, get_logger | from structlog.stdlib import BoundLogger, get_logger | ||||||
| @ -16,7 +16,6 @@ from authentik.flows.markers import ReevaluateMarker, StageMarker | |||||||
| from authentik.flows.models import Flow, FlowStageBinding, Stage | from authentik.flows.models import Flow, FlowStageBinding, Stage | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.policies.engine import PolicyEngine | from authentik.policies.engine import PolicyEngine | ||||||
| from authentik.root.monitoring import UpdatingGauge |  | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| PLAN_CONTEXT_PENDING_USER = "pending_user" | PLAN_CONTEXT_PENDING_USER = "pending_user" | ||||||
| @ -27,10 +26,9 @@ PLAN_CONTEXT_SOURCE = "source" | |||||||
| # Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan | # Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan | ||||||
| # was restored. | # was restored. | ||||||
| PLAN_CONTEXT_IS_RESTORED = "is_restored" | PLAN_CONTEXT_IS_RESTORED = "is_restored" | ||||||
| GAUGE_FLOWS_CACHED = UpdatingGauge( | GAUGE_FLOWS_CACHED = Gauge( | ||||||
|     "authentik_flows_cached", |     "authentik_flows_cached", | ||||||
|     "Cached flows", |     "Cached flows", | ||||||
|     update_func=lambda: len(cache.keys("flow_*") or []), |  | ||||||
| ) | ) | ||||||
| HIST_FLOWS_PLAN_TIME = Histogram( | HIST_FLOWS_PLAN_TIME = Histogram( | ||||||
|     "authentik_flows_plan_time", |     "authentik_flows_plan_time", | ||||||
| @ -171,7 +169,6 @@ class FlowPlanner: | |||||||
|             ) |             ) | ||||||
|             plan = self._build_plan(user, request, default_context) |             plan = self._build_plan(user, request, default_context) | ||||||
|             cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT) |             cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT) | ||||||
|             GAUGE_FLOWS_CACHED.update() |  | ||||||
|             if not plan.bindings and not self.allow_empty_flows: |             if not plan.bindings and not self.allow_empty_flows: | ||||||
|                 raise EmptyFlowException() |                 raise EmptyFlowException() | ||||||
|             return plan |             return plan | ||||||
|  | |||||||
| @ -4,6 +4,9 @@ from django.db.models.signals import post_save, pre_delete | |||||||
| from django.dispatch import receiver | from django.dispatch import receiver | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.flows.planner import GAUGE_FLOWS_CACHED | ||||||
|  | from authentik.root.monitoring import monitoring_set | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -14,6 +17,13 @@ def delete_cache_prefix(prefix: str) -> int: | |||||||
|     return len(keys) |     return len(keys) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(monitoring_set) | ||||||
|  | # pylint: disable=unused-argument | ||||||
|  | def monitoring_set_flows(sender, **kwargs): | ||||||
|  |     """set flow gauges""" | ||||||
|  |     GAUGE_FLOWS_CACHED.set(len(cache.keys("flow_*") or [])) | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(post_save) | @receiver(post_save) | ||||||
| @receiver(pre_delete) | @receiver(pre_delete) | ||||||
| # pylint: disable=unused-argument | # pylint: disable=unused-argument | ||||||
|  | |||||||
| @ -118,9 +118,12 @@ class ChallengeStageView(StageView): | |||||||
|         """Allow usage of placeholder in flow title.""" |         """Allow usage of placeholder in flow title.""" | ||||||
|         if not self.executor.plan: |         if not self.executor.plan: | ||||||
|             return self.executor.flow.title |             return self.executor.flow.title | ||||||
|  |         try: | ||||||
|             return self.executor.flow.title % { |             return self.executor.flow.title % { | ||||||
|                 "app": self.executor.plan.context.get(PLAN_CONTEXT_APPLICATION, "") |                 "app": self.executor.plan.context.get(PLAN_CONTEXT_APPLICATION, "") | ||||||
|             } |             } | ||||||
|  |         except ValueError: | ||||||
|  |             return self.executor.flow.title | ||||||
|  |  | ||||||
|     def _get_challenge(self, *args, **kwargs) -> Challenge: |     def _get_challenge(self, *args, **kwargs) -> Challenge: | ||||||
|         with Hub.current.start_span( |         with Hub.current.start_span( | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ postgresql: | |||||||
|   port: 5432 |   port: 5432 | ||||||
|   password: 'env://POSTGRES_PASSWORD' |   password: 'env://POSTGRES_PASSWORD' | ||||||
|   backup: |   backup: | ||||||
|     enabled: true |     enabled: false | ||||||
|   s3_backup: |   s3_backup: | ||||||
|     access_key: "" |     access_key: "" | ||||||
|     secret_key: "" |     secret_key: "" | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ class BaseEvaluator: | |||||||
|         self._globals = { |         self._globals = { | ||||||
|             "regex_match": BaseEvaluator.expr_regex_match, |             "regex_match": BaseEvaluator.expr_regex_match, | ||||||
|             "regex_replace": BaseEvaluator.expr_regex_replace, |             "regex_replace": BaseEvaluator.expr_regex_replace, | ||||||
|  |             "list_flatten": BaseEvaluator.expr_flatten, | ||||||
|             "ak_is_group_member": BaseEvaluator.expr_is_group_member, |             "ak_is_group_member": BaseEvaluator.expr_is_group_member, | ||||||
|             "ak_user_by": BaseEvaluator.expr_user_by, |             "ak_user_by": BaseEvaluator.expr_user_by, | ||||||
|             "ak_logger": get_logger(), |             "ak_logger": get_logger(), | ||||||
| @ -40,6 +41,15 @@ class BaseEvaluator: | |||||||
|         self._context = {} |         self._context = {} | ||||||
|         self._filename = "BaseEvalautor" |         self._filename = "BaseEvalautor" | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def expr_flatten(value: list[Any] | Any) -> Optional[Any]: | ||||||
|  |         """Flatten `value` if its a list""" | ||||||
|  |         if isinstance(value, list): | ||||||
|  |             if len(value) < 1: | ||||||
|  |                 return None | ||||||
|  |             return value[0] | ||||||
|  |         return value | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def expr_regex_match(value: Any, regex: str) -> bool: |     def expr_regex_match(value: Any, regex: str) -> bool: | ||||||
|         """Expression Filter to run re.search""" |         """Expression Filter to run re.search""" | ||||||
|  | |||||||
| @ -111,6 +111,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]: | |||||||
|             "django_redis.cache", |             "django_redis.cache", | ||||||
|             "celery.backends.redis", |             "celery.backends.redis", | ||||||
|             "celery.worker", |             "celery.worker", | ||||||
|  |             "paramiko.transport", | ||||||
|         ]: |         ]: | ||||||
|             return None |             return None | ||||||
|     LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None)) |     LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None)) | ||||||
|  | |||||||
| @ -1,5 +1,4 @@ | |||||||
| """http helpers""" | """http helpers""" | ||||||
| from os import environ |  | ||||||
| from typing import Any, Optional | from typing import Any, Optional | ||||||
|  |  | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| @ -7,7 +6,7 @@ from requests.sessions import Session | |||||||
| from sentry_sdk.hub import Hub | from sentry_sdk.hub import Hub | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik import ENV_GIT_HASH_KEY, __version__ | from authentik import get_full_version | ||||||
|  |  | ||||||
| OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP" | OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP" | ||||||
| OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN"  # nosec | OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN"  # nosec | ||||||
| @ -75,8 +74,7 @@ def get_client_ip(request: Optional[HttpRequest]) -> str: | |||||||
|  |  | ||||||
| def authentik_user_agent() -> str: | def authentik_user_agent() -> str: | ||||||
|     """Get a common user agent""" |     """Get a common user agent""" | ||||||
|     build = environ.get(ENV_GIT_HASH_KEY, "tagged") |     return f"authentik@{get_full_version()}" | ||||||
|     return f"authentik@{__version__} (build={build})" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_http_session() -> Session: | def get_http_session() -> Session: | ||||||
|  | |||||||
| @ -58,4 +58,6 @@ def get_env() -> str: | |||||||
|         return "compose" |         return "compose" | ||||||
|     if CONFIG.y_bool("debug"): |     if CONFIG.y_bool("debug"): | ||||||
|         return "dev" |         return "dev" | ||||||
|  |     if "AK_APPLIANCE" in os.environ: | ||||||
|  |         return os.environ["AK_APPLIANCE"] | ||||||
|     return "custom" |     return "custom" | ||||||
|  | |||||||
| @ -1,6 +1,4 @@ | |||||||
| """Outpost API Views""" | """Outpost API Views""" | ||||||
| from os import environ |  | ||||||
|  |  | ||||||
| from dacite.core import from_dict | from dacite.core import from_dict | ||||||
| from dacite.exceptions import DaciteError | from dacite.exceptions import DaciteError | ||||||
| from django_filters.filters import ModelMultipleChoiceFilter | from django_filters.filters import ModelMultipleChoiceFilter | ||||||
| @ -14,7 +12,7 @@ from rest_framework.response import Response | |||||||
| from rest_framework.serializers import JSONField, ModelSerializer, ValidationError | from rest_framework.serializers import JSONField, ModelSerializer, ValidationError | ||||||
| from rest_framework.viewsets import ModelViewSet | from rest_framework.viewsets import ModelViewSet | ||||||
|  |  | ||||||
| from authentik import ENV_GIT_HASH_KEY | from authentik import get_build_hash | ||||||
| from authentik.core.api.providers import ProviderSerializer | from authentik.core.api.providers import ProviderSerializer | ||||||
| from authentik.core.api.used_by import UsedByMixin | from authentik.core.api.used_by import UsedByMixin | ||||||
| from authentik.core.api.utils import PassiveSerializer, is_dict | from authentik.core.api.utils import PassiveSerializer, is_dict | ||||||
| @ -154,7 +152,7 @@ class OutpostViewSet(UsedByMixin, ModelViewSet): | |||||||
|                     "version_should": state.version_should, |                     "version_should": state.version_should, | ||||||
|                     "version_outdated": state.version_outdated, |                     "version_outdated": state.version_outdated, | ||||||
|                     "build_hash": state.build_hash, |                     "build_hash": state.build_hash, | ||||||
|                     "build_hash_should": environ.get(ENV_GIT_HASH_KEY, ""), |                     "build_hash_should": get_build_hash(), | ||||||
|                 } |                 } | ||||||
|             ) |             ) | ||||||
|         return Response(OutpostHealthSerializer(states, many=True).data) |         return Response(OutpostHealthSerializer(states, many=True).data) | ||||||
|  | |||||||
| @ -55,6 +55,10 @@ class OutpostConsumer(AuthJsonConsumer): | |||||||
|  |  | ||||||
|     first_msg = False |     first_msg = False | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |         self.logger = get_logger() | ||||||
|  |  | ||||||
|     def connect(self): |     def connect(self): | ||||||
|         super().connect() |         super().connect() | ||||||
|         uuid = self.scope["url_route"]["kwargs"]["pk"] |         uuid = self.scope["url_route"]["kwargs"]["pk"] | ||||||
| @ -65,7 +69,7 @@ class OutpostConsumer(AuthJsonConsumer): | |||||||
|         ) |         ) | ||||||
|         if not outpost: |         if not outpost: | ||||||
|             raise DenyConnection() |             raise DenyConnection() | ||||||
|         self.logger = get_logger().bind(outpost=outpost) |         self.logger = self.logger.bind(outpost=outpost) | ||||||
|         try: |         try: | ||||||
|             self.accept() |             self.accept() | ||||||
|         except RuntimeError as exc: |         except RuntimeError as exc: | ||||||
|  | |||||||
| @ -1,12 +1,11 @@ | |||||||
| """Base Controller""" | """Base Controller""" | ||||||
| from dataclasses import dataclass | from dataclasses import dataclass | ||||||
| from os import environ |  | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
| from structlog.testing import capture_logs | from structlog.testing import capture_logs | ||||||
|  |  | ||||||
| from authentik import ENV_GIT_HASH_KEY, __version__ | from authentik import __version__, get_build_hash | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.sentry import SentryIgnoredException | from authentik.lib.sentry import SentryIgnoredException | ||||||
| from authentik.outposts.models import ( | from authentik.outposts.models import ( | ||||||
| @ -102,5 +101,5 @@ class BaseController: | |||||||
|         return image_name_template % { |         return image_name_template % { | ||||||
|             "type": self.outpost.type, |             "type": self.outpost.type, | ||||||
|             "version": __version__, |             "version": __version__, | ||||||
|             "build_hash": environ.get(ENV_GIT_HASH_KEY, ""), |             "build_hash": get_build_hash(), | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ from docker import DockerClient as UpstreamDockerClient | |||||||
| from docker.errors import DockerException, NotFound | from docker.errors import DockerException, NotFound | ||||||
| from docker.models.containers import Container | from docker.models.containers import Container | ||||||
| from docker.utils.utils import kwargs_from_env | from docker.utils.utils import kwargs_from_env | ||||||
|  | from paramiko.ssh_exception import SSHException | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
| from yaml import safe_dump | from yaml import safe_dump | ||||||
|  |  | ||||||
| @ -49,10 +50,13 @@ class DockerClient(UpstreamDockerClient, BaseClient): | |||||||
|                     authentication_kp=connection.tls_authentication, |                     authentication_kp=connection.tls_authentication, | ||||||
|                 ) |                 ) | ||||||
|                 tls_config = self.tls.write() |                 tls_config = self.tls.write() | ||||||
|  |             try: | ||||||
|                 super().__init__( |                 super().__init__( | ||||||
|                     base_url=connection.url, |                     base_url=connection.url, | ||||||
|                     tls=tls_config, |                     tls=tls_config, | ||||||
|                 ) |                 ) | ||||||
|  |             except SSHException as exc: | ||||||
|  |                 raise ServiceConnectionInvalid from exc | ||||||
|         self.logger = get_logger() |         self.logger = get_logger() | ||||||
|         # Ensure the client actually works |         # Ensure the client actually works | ||||||
|         self.containers.list() |         self.containers.list() | ||||||
| @ -102,9 +106,12 @@ class DockerController(BaseController): | |||||||
|         ).lower() |         ).lower() | ||||||
|  |  | ||||||
|     def _get_labels(self) -> dict[str, str]: |     def _get_labels(self) -> dict[str, str]: | ||||||
|         return { |         labels = { | ||||||
|             "io.goauthentik.outpost-uuid": self.outpost.pk.hex, |             "io.goauthentik.outpost-uuid": self.outpost.pk.hex, | ||||||
|         } |         } | ||||||
|  |         if self.outpost.config.docker_labels: | ||||||
|  |             labels.update(self.outpost.config.docker_labels) | ||||||
|  |         return labels | ||||||
|  |  | ||||||
|     def _get_env(self) -> dict[str, str]: |     def _get_env(self) -> dict[str, str]: | ||||||
|         return { |         return { | ||||||
|  | |||||||
| @ -18,6 +18,7 @@ from kubernetes.client import ( | |||||||
|     V1SecretKeySelector, |     V1SecretKeySelector, | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | from authentik import __version__, get_full_version | ||||||
| from authentik.outposts.controllers.base import FIELD_MANAGER | from authentik.outposts.controllers.base import FIELD_MANAGER | ||||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler | from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler | ||||||
| from authentik.outposts.controllers.k8s.triggers import NeedsUpdate | from authentik.outposts.controllers.k8s.triggers import NeedsUpdate | ||||||
| @ -52,15 +53,18 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | |||||||
|             raise NeedsUpdate() |             raise NeedsUpdate() | ||||||
|         super().reconcile(current, reference) |         super().reconcile(current, reference) | ||||||
|  |  | ||||||
|     def get_pod_meta(self) -> dict[str, str]: |     def get_pod_meta(self, **kwargs) -> dict[str, str]: | ||||||
|         """Get common object metadata""" |         """Get common object metadata""" | ||||||
|         return { |         kwargs.update( | ||||||
|             "app.kubernetes.io/name": "authentik-outpost", |             { | ||||||
|  |                 "app.kubernetes.io/name": f"authentik-outpost-{self.outpost.type}", | ||||||
|                 "app.kubernetes.io/managed-by": "goauthentik.io", |                 "app.kubernetes.io/managed-by": "goauthentik.io", | ||||||
|                 "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex, |                 "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex, | ||||||
|                 "goauthentik.io/outpost-name": slugify(self.controller.outpost.name), |                 "goauthentik.io/outpost-name": slugify(self.controller.outpost.name), | ||||||
|                 "goauthentik.io/outpost-type": str(self.controller.outpost.type), |                 "goauthentik.io/outpost-type": str(self.controller.outpost.type), | ||||||
|             } |             } | ||||||
|  |         ) | ||||||
|  |         return kwargs | ||||||
|  |  | ||||||
|     def get_reference_object(self) -> V1Deployment: |     def get_reference_object(self) -> V1Deployment: | ||||||
|         """Get deployment object for outpost""" |         """Get deployment object for outpost""" | ||||||
| @ -77,13 +81,24 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]): | |||||||
|         meta = self.get_object_meta(name=self.name) |         meta = self.get_object_meta(name=self.name) | ||||||
|         image_name = self.controller.get_container_image() |         image_name = self.controller.get_container_image() | ||||||
|         image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets |         image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets | ||||||
|  |         version = get_full_version() | ||||||
|         return V1Deployment( |         return V1Deployment( | ||||||
|             metadata=meta, |             metadata=meta, | ||||||
|             spec=V1DeploymentSpec( |             spec=V1DeploymentSpec( | ||||||
|                 replicas=self.outpost.config.kubernetes_replicas, |                 replicas=self.outpost.config.kubernetes_replicas, | ||||||
|                 selector=V1LabelSelector(match_labels=self.get_pod_meta()), |                 selector=V1LabelSelector(match_labels=self.get_pod_meta()), | ||||||
|                 template=V1PodTemplateSpec( |                 template=V1PodTemplateSpec( | ||||||
|                     metadata=V1ObjectMeta(labels=self.get_pod_meta()), |                     metadata=V1ObjectMeta( | ||||||
|  |                         labels=self.get_pod_meta( | ||||||
|  |                             **{ | ||||||
|  |                                 # Support istio-specific labels, but also use the standard k8s | ||||||
|  |                                 # recommendations | ||||||
|  |                                 "app.kubernetes.io/version": version, | ||||||
|  |                                 "app": "authentik-outpost", | ||||||
|  |                                 "version": version, | ||||||
|  |                             } | ||||||
|  |                         ) | ||||||
|  |                     ), | ||||||
|                     spec=V1PodSpec( |                     spec=V1PodSpec( | ||||||
|                         image_pull_secrets=[ |                         image_pull_secrets=[ | ||||||
|                             V1ObjectReference(name=secret) for secret in image_pull_secrets |                             V1ObjectReference(name=secret) for secret in image_pull_secrets | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec | |||||||
| from authentik.outposts.controllers.base import FIELD_MANAGER | from authentik.outposts.controllers.base import FIELD_MANAGER | ||||||
| from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler | 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.triggers import NeedsUpdate | ||||||
| from authentik.outposts.controllers.k8s.utils import compare_ports | from authentik.outposts.controllers.k8s.utils import compare_ports | ||||||
|  |  | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
| @ -25,6 +26,8 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]): | |||||||
|         # after an authentik update. However the ports might have also changed during |         # after an authentik update. However the ports might have also changed during | ||||||
|         # the update, so this causes the service to be re-created with higher |         # the update, so this causes the service to be re-created with higher | ||||||
|         # priority than being updated. |         # priority than being updated. | ||||||
|  |         if current.spec.selector != reference.spec.selector: | ||||||
|  |             raise NeedsUpdate() | ||||||
|         super().reconcile(current, reference) |         super().reconcile(current, reference) | ||||||
|  |  | ||||||
|     def get_reference_object(self) -> V1Service: |     def get_reference_object(self) -> V1Service: | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  |  | ||||||
| from kubernetes.client.models.v1_container_port import V1ContainerPort | from kubernetes.client.models.v1_container_port import V1ContainerPort | ||||||
|  | from kubernetes.client.models.v1_service_port import V1ServicePort | ||||||
| from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME | from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME | ||||||
|  |  | ||||||
| from authentik.outposts.controllers.k8s.triggers import NeedsRecreate | from authentik.outposts.controllers.k8s.triggers import NeedsRecreate | ||||||
| @ -16,10 +17,31 @@ def get_namespace() -> str: | |||||||
|     return "default" |     return "default" | ||||||
|  |  | ||||||
|  |  | ||||||
| def compare_ports(current: list[V1ContainerPort], reference: list[V1ContainerPort]): | def compare_port( | ||||||
|  |     current: V1ServicePort | V1ContainerPort, reference: V1ServicePort | V1ContainerPort | ||||||
|  | ) -> bool: | ||||||
|  |     """Compare a single port""" | ||||||
|  |     if current.name != reference.name: | ||||||
|  |         return False | ||||||
|  |     if current.protocol != reference.protocol: | ||||||
|  |         return False | ||||||
|  |     if isinstance(current, V1ServicePort) and isinstance(reference, V1ServicePort): | ||||||
|  |         # We only care about the target port | ||||||
|  |         if current.target_port != reference.target_port: | ||||||
|  |             return False | ||||||
|  |     if isinstance(current, V1ContainerPort) and isinstance(reference, V1ContainerPort): | ||||||
|  |         # We only care about the target port | ||||||
|  |         if current.container_port != reference.container_port: | ||||||
|  |             return False | ||||||
|  |     return True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def compare_ports( | ||||||
|  |     current: list[V1ServicePort | V1ContainerPort], reference: list[V1ServicePort | V1ContainerPort] | ||||||
|  | ): | ||||||
|     """Compare ports of a list""" |     """Compare ports of a list""" | ||||||
|     if len(current) != len(reference): |     if len(current) != len(reference): | ||||||
|         raise NeedsRecreate() |         raise NeedsRecreate() | ||||||
|     for port in reference: |     for port in reference: | ||||||
|         if port not in current: |         if not any(compare_port(port, current_port) for current_port in current): | ||||||
|             raise NeedsRecreate() |             raise NeedsRecreate() | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| """Outpost models""" | """Outpost models""" | ||||||
| from dataclasses import asdict, dataclass, field | from dataclasses import asdict, dataclass, field | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from os import environ |  | ||||||
| from typing import Iterable, Optional | from typing import Iterable, Optional | ||||||
| from uuid import uuid4 | from uuid import uuid4 | ||||||
|  |  | ||||||
| @ -17,7 +16,7 @@ from model_utils.managers import InheritanceManager | |||||||
| from packaging.version import LegacyVersion, Version, parse | from packaging.version import LegacyVersion, Version, parse | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik import ENV_GIT_HASH_KEY, __version__ | from authentik import __version__, get_build_hash | ||||||
| from authentik.core.models import ( | from authentik.core.models import ( | ||||||
|     USER_ATTRIBUTE_CAN_OVERRIDE_IP, |     USER_ATTRIBUTE_CAN_OVERRIDE_IP, | ||||||
|     USER_ATTRIBUTE_SA, |     USER_ATTRIBUTE_SA, | ||||||
| @ -61,6 +60,7 @@ class OutpostConfig: | |||||||
|  |  | ||||||
|     docker_network: Optional[str] = field(default=None) |     docker_network: Optional[str] = field(default=None) | ||||||
|     docker_map_ports: bool = field(default=True) |     docker_map_ports: bool = field(default=True) | ||||||
|  |     docker_labels: Optional[dict[str, str]] = field(default=None) | ||||||
|  |  | ||||||
|     container_image: Optional[str] = field(default=None) |     container_image: Optional[str] = field(default=None) | ||||||
|  |  | ||||||
| @ -414,7 +414,7 @@ class OutpostState: | |||||||
|         """Check if outpost version matches our version""" |         """Check if outpost version matches our version""" | ||||||
|         if not self.version: |         if not self.version: | ||||||
|             return False |             return False | ||||||
|         if self.build_hash != environ.get(ENV_GIT_HASH_KEY, ""): |         if self.build_hash != get_build_hash(): | ||||||
|             return False |             return False | ||||||
|         return parse(self.version) < OUR_VERSION |         return parse(self.version) < OUR_VERSION | ||||||
|  |  | ||||||
|  | |||||||
| @ -77,8 +77,12 @@ def outpost_service_connection_state(connection_pk: Any): | |||||||
|         cls = DockerClient |         cls = DockerClient | ||||||
|     if isinstance(connection, KubernetesServiceConnection): |     if isinstance(connection, KubernetesServiceConnection): | ||||||
|         cls = KubernetesClient |         cls = KubernetesClient | ||||||
|  |     try: | ||||||
|         with cls(connection) as client: |         with cls(connection) as client: | ||||||
|             state = client.fetch_state() |             state = client.fetch_state() | ||||||
|  |     except ServiceConnectionInvalid as exc: | ||||||
|  |         LOGGER.warning("Failed to get client status", exc=exc) | ||||||
|  |         return | ||||||
|     cache.set(connection.state_key, state, timeout=None) |     cache.set(connection.state_key, state, timeout=None) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ from typing import Iterator, Optional | |||||||
|  |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.http import HttpRequest | from django.http import HttpRequest | ||||||
| from prometheus_client import Histogram | from prometheus_client import Gauge, Histogram | ||||||
| from sentry_sdk.hub import Hub | from sentry_sdk.hub import Hub | ||||||
| from sentry_sdk.tracing import Span | from sentry_sdk.tracing import Span | ||||||
| from structlog.stdlib import BoundLogger, get_logger | from structlog.stdlib import BoundLogger, get_logger | ||||||
| @ -14,13 +14,11 @@ from authentik.core.models import User | |||||||
| from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel, PolicyEngineMode | from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel, PolicyEngineMode | ||||||
| from authentik.policies.process import PolicyProcess, cache_key | from authentik.policies.process import PolicyProcess, cache_key | ||||||
| from authentik.policies.types import PolicyRequest, PolicyResult | from authentik.policies.types import PolicyRequest, PolicyResult | ||||||
| from authentik.root.monitoring import UpdatingGauge |  | ||||||
|  |  | ||||||
| CURRENT_PROCESS = current_process() | CURRENT_PROCESS = current_process() | ||||||
| GAUGE_POLICIES_CACHED = UpdatingGauge( | GAUGE_POLICIES_CACHED = Gauge( | ||||||
|     "authentik_policies_cached", |     "authentik_policies_cached", | ||||||
|     "Cached Policies", |     "Cached Policies", | ||||||
|     update_func=lambda: len(cache.keys("policy_*") or []), |  | ||||||
| ) | ) | ||||||
| HIST_POLICIES_BUILD_TIME = Histogram( | HIST_POLICIES_BUILD_TIME = Histogram( | ||||||
|     "authentik_policies_build_time", |     "authentik_policies_build_time", | ||||||
|  | |||||||
| @ -45,7 +45,7 @@ class HaveIBeenPwendPolicy(Policy): | |||||||
|                 fields=request.context.keys(), |                 fields=request.context.keys(), | ||||||
|             ) |             ) | ||||||
|             return PolicyResult(False, _("Password not set in context")) |             return PolicyResult(False, _("Password not set in context")) | ||||||
|         password = request.context[self.password_field] |         password = str(request.context[self.password_field]) | ||||||
|  |  | ||||||
|         pw_hash = sha1(password.encode("utf-8")).hexdigest()  # nosec |         pw_hash = sha1(password.encode("utf-8")).hexdigest()  # nosec | ||||||
|         url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}" |         url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}" | ||||||
|  | |||||||
| @ -5,10 +5,19 @@ from django.dispatch import receiver | |||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.api.applications import user_app_cache_key | from authentik.core.api.applications import user_app_cache_key | ||||||
|  | from authentik.policies.engine import GAUGE_POLICIES_CACHED | ||||||
|  | from authentik.root.monitoring import monitoring_set | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(monitoring_set) | ||||||
|  | # pylint: disable=unused-argument | ||||||
|  | def monitoring_set_policies(sender, **kwargs): | ||||||
|  |     """set policy gauges""" | ||||||
|  |     GAUGE_POLICIES_CACHED.set(len(cache.keys("policy_*") or [])) | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(post_save) | @receiver(post_save) | ||||||
| # pylint: disable=unused-argument | # pylint: disable=unused-argument | ||||||
| def invalidate_policy_cache(sender, instance, **_): | def invalidate_policy_cache(sender, instance, **_): | ||||||
|  | |||||||
| @ -99,7 +99,7 @@ class OAuthAuthorizationParams: | |||||||
|         # and POST request. |         # and POST request. | ||||||
|         query_dict = request.POST if request.method == "POST" else request.GET |         query_dict = request.POST if request.method == "POST" else request.GET | ||||||
|         state = query_dict.get("state") |         state = query_dict.get("state") | ||||||
|         redirect_uri = query_dict.get("redirect_uri", "") |         redirect_uri = query_dict.get("redirect_uri", "").lower() | ||||||
|  |  | ||||||
|         response_type = query_dict.get("response_type", "") |         response_type = query_dict.get("response_type", "") | ||||||
|         grant_type = None |         grant_type = None | ||||||
| @ -156,13 +156,20 @@ class OAuthAuthorizationParams: | |||||||
|         if not self.redirect_uri: |         if not self.redirect_uri: | ||||||
|             LOGGER.warning("Missing redirect uri.") |             LOGGER.warning("Missing redirect uri.") | ||||||
|             raise RedirectUriError("", allowed_redirect_urls) |             raise RedirectUriError("", allowed_redirect_urls) | ||||||
|         if len(allowed_redirect_urls) < 1: |  | ||||||
|  |         if self.provider.redirect_uris == "": | ||||||
|  |             LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri) | ||||||
|  |             self.provider.redirect_uris = self.redirect_uri | ||||||
|  |             self.provider.save() | ||||||
|  |             allowed_redirect_urls = self.provider.redirect_uris.split() | ||||||
|  |  | ||||||
|  |         if self.provider.redirect_uris == "*": | ||||||
|             LOGGER.warning( |             LOGGER.warning( | ||||||
|                 "Provider has no allowed redirect_uri set, allowing all.", |                 "Provider has wildcard allowed redirect_uri set, allowing all.", | ||||||
|                 allow=self.redirect_uri.lower(), |                 allow=self.redirect_uri, | ||||||
|             ) |             ) | ||||||
|             return |             return | ||||||
|         if self.redirect_uri.lower() not in [x.lower() for x in allowed_redirect_urls]: |         if self.redirect_uri not in [x.lower() for x in allowed_redirect_urls]: | ||||||
|             LOGGER.warning( |             LOGGER.warning( | ||||||
|                 "Invalid redirect uri", |                 "Invalid redirect uri", | ||||||
|                 redirect_uri=self.redirect_uri, |                 redirect_uri=self.redirect_uri, | ||||||
|  | |||||||
| @ -66,7 +66,7 @@ class TokenParams: | |||||||
|             provider=provider, |             provider=provider, | ||||||
|             client_id=client_id, |             client_id=client_id, | ||||||
|             client_secret=client_secret, |             client_secret=client_secret, | ||||||
|             redirect_uri=request.POST.get("redirect_uri", ""), |             redirect_uri=request.POST.get("redirect_uri", "").lower(), | ||||||
|             grant_type=request.POST.get("grant_type", ""), |             grant_type=request.POST.get("grant_type", ""), | ||||||
|             state=request.POST.get("state", ""), |             state=request.POST.get("state", ""), | ||||||
|             scope=request.POST.get("scope", "").split(), |             scope=request.POST.get("scope", "").split(), | ||||||
| @ -123,21 +123,23 @@ class TokenParams: | |||||||
|             LOGGER.warning("Invalid grant type", grant_type=self.grant_type) |             LOGGER.warning("Invalid grant type", grant_type=self.grant_type) | ||||||
|             raise TokenError("unsupported_grant_type") |             raise TokenError("unsupported_grant_type") | ||||||
|  |  | ||||||
|     def __post_init_code(self, raw_code): |     def __post_init_code(self, raw_code: str): | ||||||
|         if not raw_code: |         if not raw_code: | ||||||
|             LOGGER.warning("Missing authorization code") |             LOGGER.warning("Missing authorization code") | ||||||
|             raise TokenError("invalid_grant") |             raise TokenError("invalid_grant") | ||||||
|  |  | ||||||
|         allowed_redirect_urls = self.provider.redirect_uris.split() |         allowed_redirect_urls = self.provider.redirect_uris.split() | ||||||
|         if len(allowed_redirect_urls) < 1: |         if self.provider.redirect_uris == "*": | ||||||
|             LOGGER.warning( |             LOGGER.warning( | ||||||
|                 "Provider has no allowed redirect_uri set, allowing all.", |                 "Provider has wildcard allowed redirect_uri set, allowing all.", | ||||||
|                 allow=self.redirect_uri.lower(), |                 redirect=self.redirect_uri, | ||||||
|             ) |             ) | ||||||
|         elif self.redirect_uri.lower() not in [x.lower() for x in allowed_redirect_urls]: |         # At this point, no provider should have a blank redirect_uri, in case they do | ||||||
|  |         # this will check an empty array and raise an error | ||||||
|  |         elif self.redirect_uri not in [x.lower() for x in allowed_redirect_urls]: | ||||||
|             LOGGER.warning( |             LOGGER.warning( | ||||||
|                 "Invalid redirect uri", |                 "Invalid redirect uri", | ||||||
|                 uri=self.redirect_uri, |                 redirect=self.redirect_uri, | ||||||
|                 expected=self.provider.redirect_uris.split(), |                 expected=self.provider.redirect_uris.split(), | ||||||
|             ) |             ) | ||||||
|             raise TokenError("invalid_client") |             raise TokenError("invalid_client") | ||||||
|  | |||||||
| @ -23,10 +23,12 @@ class ProxyDockerController(DockerController): | |||||||
|             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.netloc}`") |             hosts.append(f"`{external_host_name.netloc}`") | ||||||
|         traefik_name = f"ak-outpost-{self.outpost.pk.hex}" |         traefik_name = self.name | ||||||
|         labels = super()._get_labels() |         labels = super()._get_labels() | ||||||
|         labels["traefik.enable"] = "true" |         labels["traefik.enable"] = "true" | ||||||
|         labels[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)}) && PathPrefix(`/akprox`)" | ||||||
|         labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true" |         labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true" | ||||||
|         labels[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" | ||||||
|         labels[ |         labels[ | ||||||
|  | |||||||
| @ -121,13 +121,6 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]) | |||||||
|                 forwardAuth=TraefikMiddlewareSpecForwardAuth( |                 forwardAuth=TraefikMiddlewareSpecForwardAuth( | ||||||
|                     address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik", |                     address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik", | ||||||
|                     authResponseHeaders=[ |                     authResponseHeaders=[ | ||||||
|                         # Legacy headers, remove after 2022.1 |  | ||||||
|                         "X-Auth-Username", |  | ||||||
|                         "X-Auth-Groups", |  | ||||||
|                         "X-Forwarded-Email", |  | ||||||
|                         "X-Forwarded-Preferred-Username", |  | ||||||
|                         "X-Forwarded-User", |  | ||||||
|                         # New headers, unique prefix |  | ||||||
|                         "X-authentik-username", |                         "X-authentik-username", | ||||||
|                         "X-authentik-groups", |                         "X-authentik-groups", | ||||||
|                         "X-authentik-email", |                         "X-authentik-email", | ||||||
|  | |||||||
| @ -15,6 +15,7 @@ from authentik.providers.saml.processors.request_parser import AuthNRequestParse | |||||||
| from authentik.sources.saml.exceptions import MismatchedRequestID | from authentik.sources.saml.exceptions import MismatchedRequestID | ||||||
| from authentik.sources.saml.models import SAMLSource | from authentik.sources.saml.models import SAMLSource | ||||||
| from authentik.sources.saml.processors.constants import ( | from authentik.sources.saml.processors.constants import ( | ||||||
|  |     SAML_BINDING_REDIRECT, | ||||||
|     SAML_NAME_ID_FORMAT_EMAIL, |     SAML_NAME_ID_FORMAT_EMAIL, | ||||||
|     SAML_NAME_ID_FORMAT_UNSPECIFIED, |     SAML_NAME_ID_FORMAT_UNSPECIFIED, | ||||||
| ) | ) | ||||||
| @ -98,6 +99,9 @@ class TestAuthNRequest(TestCase): | |||||||
|  |  | ||||||
|         # First create an AuthNRequest |         # First create an AuthNRequest | ||||||
|         request_proc = RequestProcessor(self.source, http_request, "test_state") |         request_proc = RequestProcessor(self.source, http_request, "test_state") | ||||||
|  |         auth_n = request_proc.get_auth_n() | ||||||
|  |         self.assertEqual(auth_n.attrib["ProtocolBinding"], SAML_BINDING_REDIRECT) | ||||||
|  |  | ||||||
|         request = request_proc.build_auth_n() |         request = request_proc.build_auth_n() | ||||||
|         # Now we check the ID and signature |         # Now we check the ID and signature | ||||||
|         parsed_request = AuthNRequestParser(self.provider).parse( |         parsed_request = AuthNRequestParser(self.provider).parse( | ||||||
|  | |||||||
| @ -1,37 +1,17 @@ | |||||||
| """Metrics view""" | """Metrics view""" | ||||||
| from base64 import b64encode | from base64 import b64encode | ||||||
| from typing import Callable |  | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db import connections | from django.db import connections | ||||||
| from django.db.utils import OperationalError | from django.db.utils import OperationalError | ||||||
|  | from django.dispatch import Signal | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django.views import View | from django.views import View | ||||||
| from django_prometheus.exports import ExportToDjangoView | from django_prometheus.exports import ExportToDjangoView | ||||||
| from django_redis import get_redis_connection | from django_redis import get_redis_connection | ||||||
| from prometheus_client import Gauge |  | ||||||
| from redis.exceptions import RedisError | from redis.exceptions import RedisError | ||||||
|  |  | ||||||
| from authentik.admin.api.workers import GAUGE_WORKERS | monitoring_set = Signal() | ||||||
| from authentik.events.monitored_tasks import TaskInfo |  | ||||||
| from authentik.root.celery import CELERY_APP |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class UpdatingGauge(Gauge): |  | ||||||
|     """Gauge which fetches its own value from an update function. |  | ||||||
|  |  | ||||||
|     Update function is called on instantiate""" |  | ||||||
|  |  | ||||||
|     def __init__(self, *args, update_func: Callable, **kwargs): |  | ||||||
|         super().__init__(*args, **kwargs) |  | ||||||
|         self._update_func = update_func |  | ||||||
|         self.update() |  | ||||||
|  |  | ||||||
|     def update(self): |  | ||||||
|         """Set value from update function""" |  | ||||||
|         val = self._update_func() |  | ||||||
|         if val: |  | ||||||
|             self.set(val) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class MetricsView(View): | class MetricsView(View): | ||||||
| @ -49,11 +29,7 @@ class MetricsView(View): | |||||||
|             response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"' |             response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"' | ||||||
|             return response |             return response | ||||||
|  |  | ||||||
|         count = len(CELERY_APP.control.ping(timeout=0.5)) |         monitoring_set.send_robust(self) | ||||||
|         GAUGE_WORKERS.set(count) |  | ||||||
|  |  | ||||||
|         for task in TaskInfo.all().values(): |  | ||||||
|             task.set_prom_metrics() |  | ||||||
|  |  | ||||||
|         return ExportToDjangoView(request) |         return ExportToDjangoView(request) | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,14 +1,4 @@ | |||||||
| """ | """root settings for authentik""" | ||||||
| Django settings for authentik project. |  | ||||||
|  |  | ||||||
| Generated by 'django-admin startproject' using Django 2.1.3. |  | ||||||
|  |  | ||||||
| For more information on this file, see |  | ||||||
| https://docs.djangoproject.com/en/2.1/topics/settings/ |  | ||||||
|  |  | ||||||
| For the full list of settings and their values, see |  | ||||||
| https://docs.djangoproject.com/en/2.1/ref/settings/ |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| import importlib | import importlib | ||||||
| import logging | import logging | ||||||
| @ -18,24 +8,22 @@ from hashlib import sha512 | |||||||
| from json import dumps | from json import dumps | ||||||
| from tempfile import gettempdir | from tempfile import gettempdir | ||||||
| from time import time | from time import time | ||||||
| from urllib.parse import quote | from urllib.parse import quote_plus | ||||||
|  |  | ||||||
| import structlog | import structlog | ||||||
| from celery.schedules import crontab | from celery.schedules import crontab | ||||||
| from sentry_sdk import init as sentry_init | from sentry_sdk import init as sentry_init | ||||||
| from sentry_sdk.api import set_tag | from sentry_sdk.api import set_tag | ||||||
| from sentry_sdk.integrations.boto3 import Boto3Integration |  | ||||||
| from sentry_sdk.integrations.celery import CeleryIntegration | from sentry_sdk.integrations.celery import CeleryIntegration | ||||||
| from sentry_sdk.integrations.django import DjangoIntegration | from sentry_sdk.integrations.django import DjangoIntegration | ||||||
| from sentry_sdk.integrations.redis import RedisIntegration | from sentry_sdk.integrations.redis import RedisIntegration | ||||||
| from sentry_sdk.integrations.threading import ThreadingIntegration | from sentry_sdk.integrations.threading import ThreadingIntegration | ||||||
|  |  | ||||||
| from authentik import ENV_GIT_HASH_KEY, __version__ | from authentik import ENV_GIT_HASH_KEY, __version__, get_build_hash | ||||||
| from authentik.core.middleware import structlog_add_request_id | from authentik.core.middleware import structlog_add_request_id | ||||||
| from authentik.lib.config import CONFIG | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.logging import add_process_id | from authentik.lib.logging import add_process_id | ||||||
| from authentik.lib.sentry import before_send | from authentik.lib.sentry import before_send | ||||||
| from authentik.lib.utils.http import get_http_session |  | ||||||
| from authentik.lib.utils.reflection import get_env | from authentik.lib.utils.reflection import get_env | ||||||
| from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP | from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP | ||||||
|  |  | ||||||
| @ -75,6 +63,7 @@ AUTH_USER_MODEL = "authentik_core.User" | |||||||
|  |  | ||||||
| _cookie_suffix = "_debug" if DEBUG else "" | _cookie_suffix = "_debug" if DEBUG else "" | ||||||
| CSRF_COOKIE_NAME = "authentik_csrf" | CSRF_COOKIE_NAME = "authentik_csrf" | ||||||
|  | CSRF_HEADER_NAME = "HTTP_X_AUTHENTIK_CSRF" | ||||||
| LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}" | LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}" | ||||||
| SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}" | SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}" | ||||||
| SESSION_COOKIE_DOMAIN = CONFIG.y("cookie_domain", None) | SESSION_COOKIE_DOMAIN = CONFIG.y("cookie_domain", None) | ||||||
| @ -164,9 +153,6 @@ SPECTACULAR_SETTINGS = { | |||||||
|         { |         { | ||||||
|             "url": "/api/v3/", |             "url": "/api/v3/", | ||||||
|         }, |         }, | ||||||
|         { |  | ||||||
|             "url": "/api/v2beta/", |  | ||||||
|         }, |  | ||||||
|     ], |     ], | ||||||
|     "CONTACT": { |     "CONTACT": { | ||||||
|         "email": "hello@beryju.org", |         "email": "hello@beryju.org", | ||||||
| @ -222,7 +208,7 @@ if CONFIG.y_bool("redis.tls", False): | |||||||
|     REDIS_CELERY_TLS_REQUIREMENTS = f"?ssl_cert_reqs={CONFIG.y('redis.tls_reqs')}" |     REDIS_CELERY_TLS_REQUIREMENTS = f"?ssl_cert_reqs={CONFIG.y('redis.tls_reqs')}" | ||||||
| _redis_url = ( | _redis_url = ( | ||||||
|     f"{REDIS_PROTOCOL_PREFIX}:" |     f"{REDIS_PROTOCOL_PREFIX}:" | ||||||
|     f"{quote(CONFIG.y('redis.password'))}@{quote(CONFIG.y('redis.host'))}:" |     f"{quote_plus(CONFIG.y('redis.password'))}@{quote_plus(CONFIG.y('redis.host'))}:" | ||||||
|     f"{int(CONFIG.y('redis.port'))}" |     f"{int(CONFIG.y('redis.port'))}" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -349,6 +335,7 @@ LOCALE_PATHS = ["./locale"] | |||||||
| # Celery settings | # Celery settings | ||||||
| # Add a 10 minute timeout to all Celery tasks. | # Add a 10 minute timeout to all Celery tasks. | ||||||
| CELERY_TASK_SOFT_TIME_LIMIT = 600 | CELERY_TASK_SOFT_TIME_LIMIT = 600 | ||||||
|  | CELERY_WORKER_MAX_TASKS_PER_CHILD = 50 | ||||||
| CELERY_BEAT_SCHEDULE = { | CELERY_BEAT_SCHEDULE = { | ||||||
|     "clean_expired_models": { |     "clean_expired_models": { | ||||||
|         "task": "authentik.core.tasks.clean_expired_models", |         "task": "authentik.core.tasks.clean_expired_models", | ||||||
| @ -357,7 +344,7 @@ CELERY_BEAT_SCHEDULE = { | |||||||
|     }, |     }, | ||||||
|     "db_backup": { |     "db_backup": { | ||||||
|         "task": "authentik.core.tasks.backup_database", |         "task": "authentik.core.tasks.backup_database", | ||||||
|         "schedule": crontab(hour="*/24"), |         "schedule": crontab(hour="*/24", minute=0), | ||||||
|         "options": {"queue": "authentik_scheduled"}, |         "options": {"queue": "authentik_scheduled"}, | ||||||
|     }, |     }, | ||||||
| } | } | ||||||
| @ -378,7 +365,7 @@ DBBACKUP_CONNECTOR_MAPPING = { | |||||||
|     "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector", |     "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector", | ||||||
| } | } | ||||||
| DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp"  # nosec | DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp"  # nosec | ||||||
| DBBACKUP_CLEANUP_KEEP = 30 | DBBACKUP_CLEANUP_KEEP = 10 | ||||||
| if CONFIG.y("postgresql.s3_backup.bucket", "") != "": | if CONFIG.y("postgresql.s3_backup.bucket", "") != "": | ||||||
|     DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" |     DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" | ||||||
|     DBBACKUP_STORAGE_OPTIONS = { |     DBBACKUP_STORAGE_OPTIONS = { | ||||||
| @ -398,10 +385,6 @@ if CONFIG.y("postgresql.s3_backup.bucket", "") != "": | |||||||
|  |  | ||||||
| # Sentry integration | # Sentry integration | ||||||
| SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8" | SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8" | ||||||
| # Default to empty string as that is what docker has |  | ||||||
| build_hash = os.environ.get(ENV_GIT_HASH_KEY, "") |  | ||||||
| if build_hash == "": |  | ||||||
|     build_hash = "tagged" |  | ||||||
|  |  | ||||||
| env = get_env() | env = get_env() | ||||||
| _ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False) | _ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False) | ||||||
| @ -413,7 +396,6 @@ if _ERROR_REPORTING: | |||||||
|             DjangoIntegration(transaction_style="function_name"), |             DjangoIntegration(transaction_style="function_name"), | ||||||
|             CeleryIntegration(), |             CeleryIntegration(), | ||||||
|             RedisIntegration(), |             RedisIntegration(), | ||||||
|             Boto3Integration(), |  | ||||||
|             ThreadingIntegration(propagate_hub=True), |             ThreadingIntegration(propagate_hub=True), | ||||||
|         ], |         ], | ||||||
|         before_send=before_send, |         before_send=before_send, | ||||||
| @ -422,35 +404,14 @@ if _ERROR_REPORTING: | |||||||
|         environment=CONFIG.y("error_reporting.environment", "customer"), |         environment=CONFIG.y("error_reporting.environment", "customer"), | ||||||
|         send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False), |         send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False), | ||||||
|     ) |     ) | ||||||
|     set_tag("authentik.build_hash", build_hash) |     set_tag("authentik.build_hash", get_build_hash("tagged")) | ||||||
|     set_tag("authentik.env", env) |     set_tag("authentik.env", env) | ||||||
|     set_tag("authentik.component", "backend") |     set_tag("authentik.component", "backend") | ||||||
|     set_tag("authentik.uuid", sha512(SECRET_KEY.encode("ascii")).hexdigest()[:16]) |     set_tag("authentik.uuid", sha512(str(SECRET_KEY).encode("ascii")).hexdigest()[:16]) | ||||||
|     j_print( |     j_print( | ||||||
|         "Error reporting is enabled", |         "Error reporting is enabled", | ||||||
|         env=CONFIG.y("error_reporting.environment", "customer"), |         env=CONFIG.y("error_reporting.environment", "customer"), | ||||||
|     ) |     ) | ||||||
| if not CONFIG.y_bool("disable_startup_analytics", False): |  | ||||||
|     should_send = env not in ["dev", "ci"] |  | ||||||
|     if should_send: |  | ||||||
|         try: |  | ||||||
|             get_http_session().post( |  | ||||||
|                 "https://goauthentik.io/api/event", |  | ||||||
|                 json={ |  | ||||||
|                     "domain": "authentik", |  | ||||||
|                     "name": "pageview", |  | ||||||
|                     "referrer": f"{__version__} ({build_hash})", |  | ||||||
|                     "url": f"http://localhost/{env}?utm_source={__version__}&utm_medium={env}", |  | ||||||
|                 }, |  | ||||||
|                 headers={ |  | ||||||
|                     "User-Agent": sha512(SECRET_KEY.encode("ascii")).hexdigest()[:16], |  | ||||||
|                     "Content-Type": "application/json", |  | ||||||
|                 }, |  | ||||||
|                 timeout=5, |  | ||||||
|             ) |  | ||||||
|         # pylint: disable=bare-except |  | ||||||
|         except:  # nosec |  | ||||||
|             pass |  | ||||||
|  |  | ||||||
| # Static files (CSS, JavaScript, Images) | # Static files (CSS, JavaScript, Images) | ||||||
| # https://docs.djangoproject.com/en/2.1/howto/static-files/ | # https://docs.djangoproject.com/en/2.1/howto/static-files/ | ||||||
|  | |||||||
| @ -35,21 +35,21 @@ class LDAPProviderManager(ObjectManager): | |||||||
|                 "goauthentik.io/sources/ldap/ms-userprincipalname", |                 "goauthentik.io/sources/ldap/ms-userprincipalname", | ||||||
|                 name="authentik default Active Directory Mapping: userPrincipalName", |                 name="authentik default Active Directory Mapping: userPrincipalName", | ||||||
|                 object_field="attributes.upn", |                 object_field="attributes.upn", | ||||||
|                 expression="return ldap.get('userPrincipalName')", |                 expression="return list_flatten(ldap.get('userPrincipalName'))", | ||||||
|             ), |             ), | ||||||
|             EnsureExists( |             EnsureExists( | ||||||
|                 LDAPPropertyMapping, |                 LDAPPropertyMapping, | ||||||
|                 "goauthentik.io/sources/ldap/ms-givenName", |                 "goauthentik.io/sources/ldap/ms-givenName", | ||||||
|                 name="authentik default Active Directory Mapping: givenName", |                 name="authentik default Active Directory Mapping: givenName", | ||||||
|                 object_field="attributes.givenName", |                 object_field="attributes.givenName", | ||||||
|                 expression="return ldap.get('givenName')", |                 expression="return list_flatten(ldap.get('givenName'))", | ||||||
|             ), |             ), | ||||||
|             EnsureExists( |             EnsureExists( | ||||||
|                 LDAPPropertyMapping, |                 LDAPPropertyMapping, | ||||||
|                 "goauthentik.io/sources/ldap/ms-sn", |                 "goauthentik.io/sources/ldap/ms-sn", | ||||||
|                 name="authentik default Active Directory Mapping: sn", |                 name="authentik default Active Directory Mapping: sn", | ||||||
|                 object_field="attributes.sn", |                 object_field="attributes.sn", | ||||||
|                 expression="return ldap.get('sn')", |                 expression="return list_flatten(ldap.get('sn'))", | ||||||
|             ), |             ), | ||||||
|             # OpenLDAP specific mappings |             # OpenLDAP specific mappings | ||||||
|             EnsureExists( |             EnsureExists( | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ from ldap3.core.exceptions import LDAPException | |||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||||
|  | from authentik.lib.utils.errors import exception_to_string | ||||||
| from authentik.lib.utils.reflection import class_to_path, path_to_class | from authentik.lib.utils.reflection import class_to_path, path_to_class | ||||||
| from authentik.root.celery import CELERY_APP | from authentik.root.celery import CELERY_APP | ||||||
| from authentik.sources.ldap.models import LDAPSource | from authentik.sources.ldap.models import LDAPSource | ||||||
| @ -52,5 +53,5 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str): | |||||||
|         ) |         ) | ||||||
|     except LDAPException as exc: |     except LDAPException as exc: | ||||||
|         # No explicit event is created here as .set_status with an error will do that |         # No explicit event is created here as .set_status with an error will do that | ||||||
|         LOGGER.debug(exc) |         LOGGER.warning(exception_to_string(exc)) | ||||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) |         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||||
|  | |||||||
| @ -18,6 +18,8 @@ from authentik.sources.saml.processors.constants import ( | |||||||
|     RSA_SHA256, |     RSA_SHA256, | ||||||
|     RSA_SHA384, |     RSA_SHA384, | ||||||
|     RSA_SHA512, |     RSA_SHA512, | ||||||
|  |     SAML_BINDING_POST, | ||||||
|  |     SAML_BINDING_REDIRECT, | ||||||
|     SAML_NAME_ID_FORMAT_EMAIL, |     SAML_NAME_ID_FORMAT_EMAIL, | ||||||
|     SAML_NAME_ID_FORMAT_PERSISTENT, |     SAML_NAME_ID_FORMAT_PERSISTENT, | ||||||
|     SAML_NAME_ID_FORMAT_TRANSIENT, |     SAML_NAME_ID_FORMAT_TRANSIENT, | ||||||
| @ -37,6 +39,15 @@ class SAMLBindingTypes(models.TextChoices): | |||||||
|     POST = "POST", _("POST Binding") |     POST = "POST", _("POST Binding") | ||||||
|     POST_AUTO = "POST_AUTO", _("POST Binding with auto-confirmation") |     POST_AUTO = "POST_AUTO", _("POST Binding with auto-confirmation") | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def uri(self) -> str: | ||||||
|  |         """Convert database field to URI""" | ||||||
|  |         return { | ||||||
|  |             SAMLBindingTypes.POST: SAML_BINDING_POST, | ||||||
|  |             SAMLBindingTypes.POST_AUTO: SAML_BINDING_POST, | ||||||
|  |             SAMLBindingTypes.REDIRECT: SAML_BINDING_REDIRECT, | ||||||
|  |         }[self] | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLNameIDPolicy(models.TextChoices): | class SAMLNameIDPolicy(models.TextChoices): | ||||||
|     """SAML NameID Policies""" |     """SAML NameID Policies""" | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ from lxml.etree import Element  # nosec | |||||||
| from authentik.providers.saml.utils import get_random_id | from authentik.providers.saml.utils import get_random_id | ||||||
| from authentik.providers.saml.utils.encoding import deflate_and_base64_encode | from authentik.providers.saml.utils.encoding import deflate_and_base64_encode | ||||||
| from authentik.providers.saml.utils.time import get_time_string | from authentik.providers.saml.utils.time import get_time_string | ||||||
| from authentik.sources.saml.models import SAMLSource | from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource | ||||||
| from authentik.sources.saml.processors.constants import ( | from authentik.sources.saml.processors.constants import ( | ||||||
|     DIGEST_ALGORITHM_TRANSLATION_MAP, |     DIGEST_ALGORITHM_TRANSLATION_MAP, | ||||||
|     NS_MAP, |     NS_MAP, | ||||||
| @ -62,7 +62,7 @@ class RequestProcessor: | |||||||
|         auth_n_request.attrib["Destination"] = self.source.sso_url |         auth_n_request.attrib["Destination"] = self.source.sso_url | ||||||
|         auth_n_request.attrib["ID"] = self.request_id |         auth_n_request.attrib["ID"] = self.request_id | ||||||
|         auth_n_request.attrib["IssueInstant"] = self.issue_instant |         auth_n_request.attrib["IssueInstant"] = self.issue_instant | ||||||
|         auth_n_request.attrib["ProtocolBinding"] = self.source.binding_type |         auth_n_request.attrib["ProtocolBinding"] = SAMLBindingTypes(self.source.binding_type).uri | ||||||
|         auth_n_request.attrib["Version"] = "2.0" |         auth_n_request.attrib["Version"] = "2.0" | ||||||
|         # Create issuer object |         # Create issuer object | ||||||
|         auth_n_request.append(self.get_issuer()) |         auth_n_request.append(self.get_issuer()) | ||||||
|  | |||||||
| @ -196,7 +196,10 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|         return super().get(request, *args, **kwargs) |         return super().get(request, *args, **kwargs) | ||||||
|  |  | ||||||
|     def get_challenge(self) -> AuthenticatorValidationChallenge: |     def get_challenge(self) -> AuthenticatorValidationChallenge: | ||||||
|         challenges = self.request.session["device_challenges"] |         challenges = self.request.session.get("device_challenges") | ||||||
|  |         if not challenges: | ||||||
|  |             LOGGER.debug("Authenticator Validation stage ran without challenges") | ||||||
|  |             return self.executor.stage_invalid() | ||||||
|         return AuthenticatorValidationChallenge( |         return AuthenticatorValidationChallenge( | ||||||
|             data={ |             data={ | ||||||
|                 "type": ChallengeTypes.NATIVE.value, |                 "type": ChallengeTypes.NATIVE.value, | ||||||
|  | |||||||
| @ -18,7 +18,12 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer): | |||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = AuthenticateWebAuthnStage |         model = AuthenticateWebAuthnStage | ||||||
|         fields = StageSerializer.Meta.fields + ["configure_flow", "user_verification"] |         fields = StageSerializer.Meta.fields + [ | ||||||
|  |             "configure_flow", | ||||||
|  |             "user_verification", | ||||||
|  |             "authenticator_attachment", | ||||||
|  |             "resident_key_requirement", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet): | class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet): | ||||||
|  | |||||||
| @ -0,0 +1,37 @@ | |||||||
|  | # Generated by Django 4.0.1 on 2022-01-12 21:48 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ( | ||||||
|  |             "authentik_stages_authenticator_webauthn", | ||||||
|  |             "0005_authenticatewebauthnstage_user_verification", | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="authenticatewebauthnstage", | ||||||
|  |             name="authenticator_attachment", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 choices=[("platform", "Platform"), ("cross-platform", "Cross Platform")], | ||||||
|  |                 default=None, | ||||||
|  |                 null=True, | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="authenticatewebauthnstage", | ||||||
|  |             name="resident_key_requirement", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 choices=[ | ||||||
|  |                     ("discouraged", "Discouraged"), | ||||||
|  |                     ("preferred", "Preferred"), | ||||||
|  |                     ("required", "Required"), | ||||||
|  |                 ], | ||||||
|  |                 default="preferred", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -31,6 +31,40 @@ class UserVerification(models.TextChoices): | |||||||
|     DISCOURAGED = "discouraged" |     DISCOURAGED = "discouraged" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ResidentKeyRequirement(models.TextChoices): | ||||||
|  |     """The Relying Party's preference for the authenticator to create a dedicated "client-side" | ||||||
|  |     credential for it. Requiring an authenticator to store a dedicated credential should not be | ||||||
|  |     done lightly due to the limited storage capacity of some types of authenticators. | ||||||
|  |  | ||||||
|  |     Members: | ||||||
|  |         `DISCOURAGED`: The authenticator should not create a dedicated credential | ||||||
|  |         `PREFERRED`: The authenticator can create and store a dedicated credential, but if it | ||||||
|  |             doesn't that's alright too | ||||||
|  |         `REQUIRED`: The authenticator MUST create a dedicated credential. If it cannot, the RP | ||||||
|  |             is prepared for an error to occur. | ||||||
|  |  | ||||||
|  |     https://www.w3.org/TR/webauthn-2/#enum-residentKeyRequirement | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     DISCOURAGED = "discouraged" | ||||||
|  |     PREFERRED = "preferred" | ||||||
|  |     REQUIRED = "required" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AuthenticatorAttachment(models.TextChoices): | ||||||
|  |     """How an authenticator is connected to the client/browser. | ||||||
|  |  | ||||||
|  |     Members: | ||||||
|  |         `PLATFORM`: A non-removable authenticator, like TouchID or Windows Hello | ||||||
|  |         `CROSS_PLATFORM`: A "roaming" authenticator, like a YubiKey | ||||||
|  |  | ||||||
|  |     https://www.w3.org/TR/webauthn-2/#enumdef-authenticatorattachment | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     PLATFORM = "platform" | ||||||
|  |     CROSS_PLATFORM = "cross-platform" | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthenticateWebAuthnStage(ConfigurableStage, Stage): | class AuthenticateWebAuthnStage(ConfigurableStage, Stage): | ||||||
|     """WebAuthn stage""" |     """WebAuthn stage""" | ||||||
|  |  | ||||||
| @ -38,6 +72,13 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage): | |||||||
|         choices=UserVerification.choices, |         choices=UserVerification.choices, | ||||||
|         default=UserVerification.PREFERRED, |         default=UserVerification.PREFERRED, | ||||||
|     ) |     ) | ||||||
|  |     resident_key_requirement = models.TextField( | ||||||
|  |         choices=ResidentKeyRequirement.choices, | ||||||
|  |         default=ResidentKeyRequirement.PREFERRED, | ||||||
|  |     ) | ||||||
|  |     authenticator_attachment = models.TextField( | ||||||
|  |         choices=AuthenticatorAttachment.choices, default=None, null=True | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def serializer(self) -> BaseSerializer: |     def serializer(self) -> BaseSerializer: | ||||||
|  | |||||||
| @ -13,7 +13,6 @@ from webauthn.helpers.structs import ( | |||||||
|     AuthenticatorSelectionCriteria, |     AuthenticatorSelectionCriteria, | ||||||
|     PublicKeyCredentialCreationOptions, |     PublicKeyCredentialCreationOptions, | ||||||
|     RegistrationCredential, |     RegistrationCredential, | ||||||
|     ResidentKeyRequirement, |  | ||||||
| ) | ) | ||||||
| from webauthn.registration.verify_registration_response import VerifiedRegistration | from webauthn.registration.verify_registration_response import VerifiedRegistration | ||||||
|  |  | ||||||
| @ -85,6 +84,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | |||||||
|         stage: AuthenticateWebAuthnStage = self.executor.current_stage |         stage: AuthenticateWebAuthnStage = self.executor.current_stage | ||||||
|         user = self.get_pending_user() |         user = self.get_pending_user() | ||||||
|  |  | ||||||
|  |         # library accepts none so we store null in the database, but if there is a value | ||||||
|  |         # set, cast it to string to ensure it's not a django class | ||||||
|  |         authenticator_attachment = stage.authenticator_attachment | ||||||
|  |         if authenticator_attachment: | ||||||
|  |             authenticator_attachment = str(authenticator_attachment) | ||||||
|  |  | ||||||
|         registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( |         registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( | ||||||
|             rp_id=get_rp_id(self.request), |             rp_id=get_rp_id(self.request), | ||||||
|             rp_name=self.request.tenant.branding_title, |             rp_name=self.request.tenant.branding_title, | ||||||
| @ -92,8 +97,9 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): | |||||||
|             user_name=user.username, |             user_name=user.username, | ||||||
|             user_display_name=user.name, |             user_display_name=user.name, | ||||||
|             authenticator_selection=AuthenticatorSelectionCriteria( |             authenticator_selection=AuthenticatorSelectionCriteria( | ||||||
|                 resident_key=ResidentKeyRequirement.PREFERRED, |                 resident_key=str(stage.resident_key_requirement), | ||||||
|                 user_verification=str(stage.user_verification), |                 user_verification=str(stage.user_verification), | ||||||
|  |                 authenticator_attachment=authenticator_attachment, | ||||||
|             ), |             ), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import ( | |||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
|  |  | ||||||
| 	"goauthentik.io/internal/common" | 	"goauthentik.io/internal/common" | ||||||
|  | 	"goauthentik.io/internal/debug" | ||||||
| 	"goauthentik.io/internal/outpost/ak" | 	"goauthentik.io/internal/outpost/ak" | ||||||
| 	"goauthentik.io/internal/outpost/ldap" | 	"goauthentik.io/internal/outpost/ldap" | ||||||
| ) | ) | ||||||
| @ -27,6 +28,7 @@ func main() { | |||||||
| 			log.FieldKeyTime: "timestamp", | 			log.FieldKeyTime: "timestamp", | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
|  | 	go debug.EnableDebugServer() | ||||||
| 	akURL, found := os.LookupEnv("AUTHENTIK_HOST") | 	akURL, found := os.LookupEnv("AUTHENTIK_HOST") | ||||||
| 	if !found { | 	if !found { | ||||||
| 		fmt.Println("env AUTHENTIK_HOST not set!") | 		fmt.Println("env AUTHENTIK_HOST not set!") | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import ( | |||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
|  |  | ||||||
| 	"goauthentik.io/internal/common" | 	"goauthentik.io/internal/common" | ||||||
|  | 	"goauthentik.io/internal/debug" | ||||||
| 	"goauthentik.io/internal/outpost/ak" | 	"goauthentik.io/internal/outpost/ak" | ||||||
| 	"goauthentik.io/internal/outpost/proxyv2" | 	"goauthentik.io/internal/outpost/proxyv2" | ||||||
| ) | ) | ||||||
| @ -32,6 +33,7 @@ func main() { | |||||||
| 			log.FieldKeyTime: "timestamp", | 			log.FieldKeyTime: "timestamp", | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
|  | 	go debug.EnableDebugServer() | ||||||
| 	akURL, found := os.LookupEnv("AUTHENTIK_HOST") | 	akURL, found := os.LookupEnv("AUTHENTIK_HOST") | ||||||
| 	if !found { | 	if !found { | ||||||
| 		fmt.Println("env AUTHENTIK_HOST not set!") | 		fmt.Println("env AUTHENTIK_HOST not set!") | ||||||
|  | |||||||
| @ -11,6 +11,7 @@ import ( | |||||||
| 	"goauthentik.io/internal/common" | 	"goauthentik.io/internal/common" | ||||||
| 	"goauthentik.io/internal/config" | 	"goauthentik.io/internal/config" | ||||||
| 	"goauthentik.io/internal/constants" | 	"goauthentik.io/internal/constants" | ||||||
|  | 	"goauthentik.io/internal/debug" | ||||||
| 	"goauthentik.io/internal/gounicorn" | 	"goauthentik.io/internal/gounicorn" | ||||||
| 	"goauthentik.io/internal/outpost/ak" | 	"goauthentik.io/internal/outpost/ak" | ||||||
| 	"goauthentik.io/internal/outpost/proxyv2" | 	"goauthentik.io/internal/outpost/proxyv2" | ||||||
| @ -28,6 +29,7 @@ func main() { | |||||||
| 			log.FieldKeyTime: "timestamp", | 			log.FieldKeyTime: "timestamp", | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
|  | 	go debug.EnableDebugServer() | ||||||
| 	l := log.WithField("logger", "authentik.root") | 	l := log.WithField("logger", "authentik.root") | ||||||
| 	config.DefaultConfig() | 	config.DefaultConfig() | ||||||
| 	err := config.LoadConfig("./authentik/lib/default.yml") | 	err := config.LoadConfig("./authentik/lib/default.yml") | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ services: | |||||||
|     image: redis:alpine |     image: redis:alpine | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|   server: |   server: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.12.5} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.5} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -35,10 +35,10 @@ services: | |||||||
|     env_file: |     env_file: | ||||||
|       - .env |       - .env | ||||||
|     ports: |     ports: | ||||||
|       - "0.0.0.0:9000:9000" |       - "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000" | ||||||
|       - "0.0.0.0:9443:9443" |       - "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443" | ||||||
|   worker: |   worker: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.12.5} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.5} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								go.mod
									
									
									
									
									
								
							| @ -9,10 +9,9 @@ require ( | |||||||
| 	github.com/garyburd/redigo v1.6.2 // indirect | 	github.com/garyburd/redigo v1.6.2 // indirect | ||||||
| 	github.com/getsentry/sentry-go v0.12.0 | 	github.com/getsentry/sentry-go v0.12.0 | ||||||
| 	github.com/go-ldap/ldap/v3 v3.4.1 | 	github.com/go-ldap/ldap/v3 v3.4.1 | ||||||
| 	github.com/go-openapi/runtime v0.21.0 | 	github.com/go-openapi/runtime v0.22.0 | ||||||
| 	github.com/go-openapi/strfmt v0.21.1 | 	github.com/go-openapi/strfmt v0.21.1 | ||||||
| 	github.com/golang-jwt/jwt v3.2.2+incompatible | 	github.com/golang-jwt/jwt v3.2.2+incompatible | ||||||
| 	github.com/golang/protobuf v1.5.2 // indirect |  | ||||||
| 	github.com/google/uuid v1.3.0 | 	github.com/google/uuid v1.3.0 | ||||||
| 	github.com/gorilla/handlers v1.5.1 | 	github.com/gorilla/handlers v1.5.1 | ||||||
| 	github.com/gorilla/mux v1.8.0 | 	github.com/gorilla/mux v1.8.0 | ||||||
| @ -26,10 +25,12 @@ require ( | |||||||
| 	github.com/pires/go-proxyproto v0.6.1 | 	github.com/pires/go-proxyproto v0.6.1 | ||||||
| 	github.com/pkg/errors v0.9.1 | 	github.com/pkg/errors v0.9.1 | ||||||
| 	github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect | 	github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect | ||||||
| 	github.com/prometheus/client_golang v1.11.0 | 	github.com/prometheus/client_golang v1.12.1 | ||||||
|  | 	github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b | ||||||
| 	github.com/sirupsen/logrus v1.8.1 | 	github.com/sirupsen/logrus v1.8.1 | ||||||
| 	goauthentik.io/api v0.2021124.9 | 	github.com/stretchr/testify v1.7.0 | ||||||
| 	golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 | 	goauthentik.io/api v0.2021125.1 | ||||||
|  | 	golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c | ||||||
| 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c | 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c | ||||||
| 	google.golang.org/appengine v1.6.7 // indirect | 	google.golang.org/appengine v1.6.7 // indirect | ||||||
| 	gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b | 	gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b | ||||||
|  | |||||||
							
								
								
									
										32
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								go.sum
									
									
									
									
									
								
							| @ -70,8 +70,9 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce | |||||||
| github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= | ||||||
| github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= | ||||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||||
| github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= |  | ||||||
| github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||||
|  | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= | ||||||
|  | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||||
| github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= | ||||||
| github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= | ||||||
| github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= | ||||||
| @ -182,8 +183,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.21.0 h1:giZ8eT26R+/rx6RX2MkYjZPY8vPYVKDhP/mOazrQHzM= | github.com/go-openapi/runtime v0.22.0 h1:vY2D0u807kkcwidaj0YJuq4zyAWQnjLNDpJcVBrUFNs= | ||||||
| github.com/go-openapi/runtime v0.21.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs= | github.com/go-openapi/runtime v0.22.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs= | ||||||
| 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= | ||||||
| @ -360,6 +361,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV | |||||||
| github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||||
| github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||||
| github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||||
|  | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= | ||||||
| github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= | ||||||
| github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= | ||||||
| github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | ||||||
| @ -427,6 +429,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ | |||||||
| github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||||
| github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= | ||||||
|  | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= | ||||||
| github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= | ||||||
| github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= | github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= | ||||||
| github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= | ||||||
| @ -467,8 +470,9 @@ github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac/go.mod h1:hoL | |||||||
| github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= | ||||||
| github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= | ||||||
| github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= | ||||||
| github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= |  | ||||||
| github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= | ||||||
|  | github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= | ||||||
|  | github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= | ||||||
| github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= | ||||||
| github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||||
| github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||||
| @ -476,13 +480,17 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2 | |||||||
| github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||||
| github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= | ||||||
| github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= | ||||||
| github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= |  | ||||||
| github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= | ||||||
|  | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= | ||||||
|  | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= | ||||||
| github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= | ||||||
| github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= | ||||||
| github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= | ||||||
| github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= |  | ||||||
| github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= | ||||||
|  | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= | ||||||
|  | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= | ||||||
|  | github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc= | ||||||
|  | github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= | ||||||
| github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | ||||||
| github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | ||||||
| github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | ||||||
| @ -562,8 +570,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= | |||||||
| go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= | ||||||
| go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= | ||||||
| go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= | ||||||
| goauthentik.io/api v0.2021124.9 h1:l6kzTggi8lt+Vmm5js7oxp9U/bXoOEIl4565bUeepuM= | goauthentik.io/api v0.2021125.1 h1:Ja00N1D1wjqFiR90JV8nDayhmm39uLudzsJhwCjoQJ4= | ||||||
| goauthentik.io/api v0.2021124.9/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE= | goauthentik.io/api v0.2021125.1/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE= | ||||||
| golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||||
| golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= | ||||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
| @ -655,6 +663,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v | |||||||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||||
| golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= | ||||||
| golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= | golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= | ||||||
|  | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||||
| golang.org/x/net v0.0.0-20211008194852-3b03d305991f h1:1scJEYZBaF48BaG6tYbtxmLcXqwYGSfGcMoStTqkkIw= | golang.org/x/net v0.0.0-20211008194852-3b03d305991f h1:1scJEYZBaF48BaG6tYbtxmLcXqwYGSfGcMoStTqkkIw= | ||||||
| golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= | ||||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||||
| @ -663,8 +672,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr | |||||||
| golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
| golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= | ||||||
| golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= | ||||||
| golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY= | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI= | ||||||
| golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= | ||||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| @ -731,8 +740,9 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc | |||||||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw= |  | ||||||
| golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= | ||||||
|  | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||||
| golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
|  | |||||||
| @ -5,16 +5,24 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func BUILD() string { | func BUILD(def string) string { | ||||||
| 	build := os.Getenv("GIT_BUILD_HASH") | 	build := os.Getenv("GIT_BUILD_HASH") | ||||||
| 	if build == "" { | 	if build == "" { | ||||||
| 		return "tagged" | 		return def | ||||||
| 	} | 	} | ||||||
| 	return build | 	return build | ||||||
| } | } | ||||||
|  |  | ||||||
| func OutpostUserAgent() string { | func FullVersion() string { | ||||||
| 	return fmt.Sprintf("authentik-outpost@%s (build=%s)", VERSION, BUILD()) | 	ver := VERSION | ||||||
|  | 	if b := BUILD(""); b != "" { | ||||||
|  | 		ver = fmt.Sprintf("%s.%s", ver, b) | ||||||
|  | 	} | ||||||
|  | 	return ver | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2021.12.5" | func OutpostUserAgent() string { | ||||||
|  | 	return fmt.Sprintf("authentik-outpost@%s", FullVersion()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const VERSION = "2022.1.5" | ||||||
|  | |||||||
							
								
								
									
										24
									
								
								internal/debug/debug.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								internal/debug/debug.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | |||||||
|  | package debug | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/pprof" | ||||||
|  | 	"os" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	log "github.com/sirupsen/logrus" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func EnableDebugServer() { | ||||||
|  | 	l := log.WithField("logger", "authentik.go_debugger") | ||||||
|  | 	if deb := os.Getenv("AUTHENTIK_DEBUG"); strings.ToLower(deb) != "true" { | ||||||
|  | 		l.Info("not enabling debug server, set `AUTHENTIK_DEBUG` to `true` to enable it.") | ||||||
|  | 	} | ||||||
|  | 	h := http.NewServeMux() | ||||||
|  | 	h.HandleFunc("/debug/pprof/", pprof.Index) | ||||||
|  | 	h.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) | ||||||
|  | 	h.HandleFunc("/debug/pprof/profile", pprof.Profile) | ||||||
|  | 	h.HandleFunc("/debug/pprof/symbol", pprof.Symbol) | ||||||
|  | 	h.HandleFunc("/debug/pprof/trace", pprof.Trace) | ||||||
|  | 	l.Println(http.ListenAndServe("0.0.0.0:9900", nil)) | ||||||
|  | } | ||||||
| @ -171,7 +171,7 @@ func (a *APIController) StartBackgorundTasks() error { | |||||||
| 		"outpost_type": a.Server.Type(), | 		"outpost_type": a.Server.Type(), | ||||||
| 		"uuid":         a.instanceUUID.String(), | 		"uuid":         a.instanceUUID.String(), | ||||||
| 		"version":      constants.VERSION, | 		"version":      constants.VERSION, | ||||||
| 		"build":        constants.BUILD(), | 		"build":        constants.BUILD("tagged"), | ||||||
| 	}).Set(1) | 	}).Set(1) | ||||||
| 	go func() { | 	go func() { | ||||||
| 		a.logger.Debug("Starting WS Handler...") | 		a.logger.Debug("Starting WS Handler...") | ||||||
|  | |||||||
| @ -51,7 +51,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error { | |||||||
| 		Instruction: WebsocketInstructionHello, | 		Instruction: WebsocketInstructionHello, | ||||||
| 		Args: map[string]interface{}{ | 		Args: map[string]interface{}{ | ||||||
| 			"version":   constants.VERSION, | 			"version":   constants.VERSION, | ||||||
| 			"buildHash": constants.BUILD(), | 			"buildHash": constants.BUILD("tagged"), | ||||||
| 			"uuid":      ac.instanceUUID.String(), | 			"uuid":      ac.instanceUUID.String(), | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| @ -151,7 +151,7 @@ func (ac *APIController) startWSHandler() { | |||||||
| 					"outpost_type": ac.Server.Type(), | 					"outpost_type": ac.Server.Type(), | ||||||
| 					"uuid":         ac.instanceUUID.String(), | 					"uuid":         ac.instanceUUID.String(), | ||||||
| 					"version":      constants.VERSION, | 					"version":      constants.VERSION, | ||||||
| 					"build":        constants.BUILD(), | 					"build":        constants.BUILD("tagged"), | ||||||
| 				}).SetToCurrentTime() | 				}).SetToCurrentTime() | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| @ -165,7 +165,7 @@ func (ac *APIController) startWSHealth() { | |||||||
| 			Instruction: WebsocketInstructionHello, | 			Instruction: WebsocketInstructionHello, | ||||||
| 			Args: map[string]interface{}{ | 			Args: map[string]interface{}{ | ||||||
| 				"version":   constants.VERSION, | 				"version":   constants.VERSION, | ||||||
| 				"buildHash": constants.BUILD(), | 				"buildHash": constants.BUILD("tagged"), | ||||||
| 				"uuid":      ac.instanceUUID.String(), | 				"uuid":      ac.instanceUUID.String(), | ||||||
| 			}, | 			}, | ||||||
| 		} | 		} | ||||||
| @ -205,7 +205,7 @@ func (ac *APIController) startIntervalUpdater() { | |||||||
| 				"outpost_type": ac.Server.Type(), | 				"outpost_type": ac.Server.Type(), | ||||||
| 				"uuid":         ac.instanceUUID.String(), | 				"uuid":         ac.instanceUUID.String(), | ||||||
| 				"version":      constants.VERSION, | 				"version":      constants.VERSION, | ||||||
| 				"build":        constants.BUILD(), | 				"build":        constants.BUILD("tagged"), | ||||||
| 			}).SetToCurrentTime() | 			}).SetToCurrentTime() | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -34,7 +34,7 @@ func doGlobalSetup(outpost api.Outpost, globalConfig api.Config) { | |||||||
| 	} else { | 	} else { | ||||||
| 		l.Debug("Managed outpost, not setting global log level") | 		l.Debug("Managed outpost, not setting global log level") | ||||||
| 	} | 	} | ||||||
| 	l.WithField("hash", constants.BUILD()).WithField("version", constants.VERSION).Info("Starting authentik outpost") | 	l.WithField("hash", constants.BUILD("tagged")).WithField("version", constants.VERSION).Info("Starting authentik outpost") | ||||||
|  |  | ||||||
| 	if globalConfig.ErrorReporting.Enabled { | 	if globalConfig.ErrorReporting.Enabled { | ||||||
| 		dsn := "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8" | 		dsn := "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8" | ||||||
|  | |||||||
							
								
								
									
										66
									
								
								internal/outpost/ak/test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								internal/outpost/ak/test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | |||||||
|  | package ak | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/rand" | ||||||
|  | 	"net/http" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"github.com/gorilla/securecookie" | ||||||
|  | 	log "github.com/sirupsen/logrus" | ||||||
|  | 	"goauthentik.io/api" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestSecret() string { | ||||||
|  | 	return base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func MockConfig() api.Config { | ||||||
|  | 	return *api.NewConfig( | ||||||
|  | 		*api.NewErrorReportingConfig(false, "test", false, 0.0), | ||||||
|  | 		[]api.CapabilitiesEnum{}, | ||||||
|  | 		100, | ||||||
|  | 		100, | ||||||
|  | 		100, | ||||||
|  | 		100, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func MockAK(outpost api.Outpost, globalConfig api.Config) *APIController { | ||||||
|  | 	config := api.NewConfiguration() | ||||||
|  | 	config.HTTPClient = &http.Client{ | ||||||
|  | 		Transport: GetTLSTransport(), | ||||||
|  | 	} | ||||||
|  | 	token := TestSecret() | ||||||
|  | 	config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token)) | ||||||
|  |  | ||||||
|  | 	// create the API client, with the transport | ||||||
|  | 	apiClient := api.NewAPIClient(config) | ||||||
|  |  | ||||||
|  | 	log := log.WithField("logger", "authentik.outpost.ak-api-controller") | ||||||
|  |  | ||||||
|  | 	log.WithField("name", outpost.Name).Debug("Fetched outpost configuration") | ||||||
|  |  | ||||||
|  | 	log.Debug("Fetched global configuration") | ||||||
|  |  | ||||||
|  | 	// doGlobalSetup is called by the OnRefresh handler, which ticks on start | ||||||
|  | 	// doGlobalSetup(outpost, akConfig) | ||||||
|  |  | ||||||
|  | 	ac := &APIController{ | ||||||
|  | 		Client:       apiClient, | ||||||
|  | 		GlobalConfig: globalConfig, | ||||||
|  |  | ||||||
|  | 		token:  token, | ||||||
|  | 		logger: log, | ||||||
|  |  | ||||||
|  | 		reloadOffset:        time.Duration(rand.Intn(10)) * time.Second, | ||||||
|  | 		instanceUUID:        uuid.New(), | ||||||
|  | 		Outpost:             outpost, | ||||||
|  | 		wsBackoffMultiplier: 1, | ||||||
|  | 		refreshHandlers:     make([]func(), 0), | ||||||
|  | 	} | ||||||
|  | 	ac.logger.WithField("offset", ac.reloadOffset.String()).Debug("HA Reload offset") | ||||||
|  | 	return ac | ||||||
|  | } | ||||||
| @ -16,6 +16,7 @@ import ( | |||||||
| 	"goauthentik.io/internal/outpost/ldap/flags" | 	"goauthentik.io/internal/outpost/ldap/flags" | ||||||
| 	"goauthentik.io/internal/outpost/ldap/metrics" | 	"goauthentik.io/internal/outpost/ldap/metrics" | ||||||
| 	"goauthentik.io/internal/outpost/ldap/server" | 	"goauthentik.io/internal/outpost/ldap/server" | ||||||
|  | 	"goauthentik.io/internal/outpost/ldap/utils" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ContextUserKey = "ak_user" | const ContextUserKey = "ak_user" | ||||||
| @ -35,7 +36,7 @@ func NewDirectBinder(si server.LDAPServerInstance) *DirectBinder { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (db *DirectBinder) GetUsername(dn string) (string, error) { | func (db *DirectBinder) GetUsername(dn string) (string, error) { | ||||||
| 	if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(db.si.GetBaseDN())) { | 	if !utils.HasSuffixNoCase(dn, db.si.GetBaseDN()) { | ||||||
| 		return "", errors.New("invalid base DN") | 		return "", errors.New("invalid base DN") | ||||||
| 	} | 	} | ||||||
| 	dns, err := goldap.ParseDN(dn) | 	dns, err := goldap.ParseDN(dn) | ||||||
|  | |||||||
| @ -13,10 +13,6 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry { | |||||||
|  |  | ||||||
| 	attrs = utils.EnsureAttributes(attrs, map[string][]string{ | 	attrs = utils.EnsureAttributes(attrs, map[string][]string{ | ||||||
| 		"memberOf":                      pi.GroupsForUser(u), | 		"memberOf":                      pi.GroupsForUser(u), | ||||||
| 		// Old fields for backwards compatibility |  | ||||||
| 		"accountStatus": {utils.BoolToString(*u.IsActive)}, |  | ||||||
| 		"superuser":     {utils.BoolToString(u.IsSuperuser)}, |  | ||||||
| 		// End old fields |  | ||||||
| 		"goauthentik.io/ldap/active":    {utils.BoolToString(*u.IsActive)}, | 		"goauthentik.io/ldap/active":    {utils.BoolToString(*u.IsActive)}, | ||||||
| 		"goauthentik.io/ldap/superuser": {utils.BoolToString(u.IsSuperuser)}, | 		"goauthentik.io/ldap/superuser": {utils.BoolToString(u.IsSuperuser)}, | ||||||
| 		"cn":                            {u.Username}, | 		"cn":                            {u.Username}, | ||||||
|  | |||||||
| @ -124,7 +124,7 @@ func (pi *ProviderInstance) GetBaseEntry() *ldap.Entry { | |||||||
| 			}, | 			}, | ||||||
| 			{ | 			{ | ||||||
| 				Name:   "vendorVersion", | 				Name:   "vendorVersion", | ||||||
| 				Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s (build %s)", constants.VERSION, constants.BUILD())}, | 				Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s", constants.FullVersion())}, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| @ -140,26 +140,26 @@ func (pi *ProviderInstance) GetNeededObjects(scope int, baseDN string, filterOC | |||||||
| 	// If our requested base DN doesn't match any of the container DNs, then | 	// If our requested base DN doesn't match any of the container DNs, then | ||||||
| 	// we're probably loading a user or group. If it does, then make sure our | 	// we're probably loading a user or group. If it does, then make sure our | ||||||
| 	// scope will eventually take us to users or groups. | 	// scope will eventually take us to users or groups. | ||||||
| 	if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.UserDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetUserOCs()) { | 	if (strings.EqualFold(baseDN, pi.BaseDN) || utils.HasSuffixNoCase(baseDN, pi.UserDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetUserOCs()) { | ||||||
| 		if baseDN != pi.UserDN && baseDN != pi.BaseDN || | 		if baseDN != pi.UserDN && baseDN != pi.BaseDN || | ||||||
| 			baseDN == pi.BaseDN && scope > 1 || | 			strings.EqualFold(baseDN, pi.BaseDN) && scope > 1 || | ||||||
| 			baseDN == pi.UserDN && scope > 0 { | 			strings.EqualFold(baseDN, pi.UserDN) && scope > 0 { | ||||||
| 			needUsers = true | 			needUsers = true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.GroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetGroupOCs()) { | 	if (strings.EqualFold(baseDN, pi.BaseDN) || utils.HasSuffixNoCase(baseDN, pi.GroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetGroupOCs()) { | ||||||
| 		if baseDN != pi.GroupDN && baseDN != pi.BaseDN || | 		if baseDN != pi.GroupDN && baseDN != pi.BaseDN || | ||||||
| 			baseDN == pi.BaseDN && scope > 1 || | 			strings.EqualFold(baseDN, pi.BaseDN) && scope > 1 || | ||||||
| 			baseDN == pi.GroupDN && scope > 0 { | 			strings.EqualFold(baseDN, pi.GroupDN) && scope > 0 { | ||||||
| 			needGroups = true | 			needGroups = true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.VirtualGroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetVirtualGroupOCs()) { | 	if (strings.EqualFold(baseDN, pi.BaseDN) || utils.HasSuffixNoCase(baseDN, pi.VirtualGroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetVirtualGroupOCs()) { | ||||||
| 		if baseDN != pi.VirtualGroupDN && baseDN != pi.BaseDN || | 		if baseDN != pi.VirtualGroupDN && baseDN != pi.BaseDN || | ||||||
| 			baseDN == pi.BaseDN && scope > 1 || | 			strings.EqualFold(baseDN, pi.BaseDN) && scope > 1 || | ||||||
| 			baseDN == pi.VirtualGroupDN && scope > 0 { | 			strings.EqualFold(baseDN, pi.VirtualGroupDN) && scope > 0 { | ||||||
| 			needUsers = true | 			needUsers = true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -44,7 +44,7 @@ func (ds *DirectSearcher) SearchBase(req *search.Request, authz bool) (ldap.Serv | |||||||
| 					}, | 					}, | ||||||
| 					{ | 					{ | ||||||
| 						Name:   "vendorVersion", | 						Name:   "vendorVersion", | ||||||
| 						Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s (build %s)", constants.VERSION, constants.BUILD())}, | 						Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s", constants.FullVersion())}, | ||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
|  | |||||||
| @ -36,7 +36,7 @@ func NewDirectSearcher(si server.LDAPServerInstance) *DirectSearcher { | |||||||
|  |  | ||||||
| func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) { | func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) { | ||||||
| 	accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access") | 	accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access") | ||||||
| 	baseDN := strings.ToLower(ds.si.GetBaseDN()) | 	baseDN := ds.si.GetBaseDN() | ||||||
|  |  | ||||||
| 	filterOC, err := ldap.GetFilterObjectClass(req.Filter) | 	filterOC, err := ldap.GetFilterObjectClass(req.Filter) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -59,7 +59,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		}).Inc() | 		}).Inc() | ||||||
| 		return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN) | 		return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN) | ||||||
| 	} | 	} | ||||||
| 	if !strings.HasSuffix(req.BindDN, ","+baseDN) { | 	if !utils.HasSuffixNoCase(req.BindDN, ","+baseDN) { | ||||||
| 		metrics.RequestsRejected.With(prometheus.Labels{ | 		metrics.RequestsRejected.With(prometheus.Labels{ | ||||||
| 			"outpost_name": ds.si.GetOutpostName(), | 			"outpost_name": ds.si.GetOutpostName(), | ||||||
| 			"type":         "search", | 			"type":         "search", | ||||||
| @ -105,7 +105,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 	scope := req.SearchRequest.Scope | 	scope := req.SearchRequest.Scope | ||||||
| 	needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, filterOC) | 	needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, filterOC) | ||||||
|  |  | ||||||
| 	if scope >= 0 && req.BaseDN == baseDN { | 	if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) { | ||||||
| 		if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) { | 		if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) { | ||||||
| 			entries = append(entries, ds.si.GetBaseEntry()) | 			entries = append(entries, ds.si.GetBaseEntry()) | ||||||
| 		} | 		} | ||||||
| @ -209,8 +209,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err | 		return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseUserDN())) { | 	if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseUserDN())) { | ||||||
| 		singleu := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseUserDN()) | 		singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseUserDN()) | ||||||
|  |  | ||||||
| 		if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | 		if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | ||||||
| 			entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseUserDN(), constants.OUUsers)) | 			entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseUserDN(), constants.OUUsers)) | ||||||
| @ -220,7 +220,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) { | 		if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) { | ||||||
| 			for _, u := range *users { | 			for _, u := range *users { | ||||||
| 				entry := ds.si.UserEntry(u) | 				entry := ds.si.UserEntry(u) | ||||||
| 				if req.BaseDN == entry.DN || !singleu { | 				if strings.EqualFold(req.BaseDN, entry.DN) || !singleu { | ||||||
| 					entries = append(entries, entry) | 					entries = append(entries, entry) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @ -229,8 +229,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		scope += 1 // Return the scope to what it was before we descended | 		scope += 1 // Return the scope to what it was before we descended | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseGroupDN())) { | 	if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseGroupDN())) { | ||||||
| 		singleg := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseGroupDN()) | 		singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseGroupDN()) | ||||||
|  |  | ||||||
| 		if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | 		if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | ||||||
| 			entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseGroupDN(), constants.OUGroups)) | 			entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseGroupDN(), constants.OUGroups)) | ||||||
| @ -240,7 +240,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) { | 		if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) { | ||||||
| 			for _, g := range *groups { | 			for _, g := range *groups { | ||||||
| 				entry := group.FromAPIGroup(g, ds.si).Entry() | 				entry := group.FromAPIGroup(g, ds.si).Entry() | ||||||
| 				if req.BaseDN == entry.DN || !singleg { | 				if strings.EqualFold(req.BaseDN, entry.DN) || !singleg { | ||||||
| 					entries = append(entries, entry) | 					entries = append(entries, entry) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @ -249,8 +249,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		scope += 1 // Return the scope to what it was before we descended | 		scope += 1 // Return the scope to what it was before we descended | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) { | 	if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) { | ||||||
| 		singlevg := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN()) | 		singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN()) | ||||||
|  |  | ||||||
| 		if !singlevg || utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | 		if !singlevg || utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | ||||||
| 			entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups)) | 			entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups)) | ||||||
| @ -260,7 +260,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) { | 		if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) { | ||||||
| 			for _, u := range *users { | 			for _, u := range *users { | ||||||
| 				entry := group.FromAPIUser(u, ds.si).Entry() | 				entry := group.FromAPIUser(u, ds.si).Entry() | ||||||
| 				if req.BaseDN == entry.DN || !singlevg { | 				if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg { | ||||||
| 					entries = append(entries, entry) | 					entries = append(entries, entry) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | |||||||
| @ -39,7 +39,7 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher { | |||||||
|  |  | ||||||
| func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) { | func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) { | ||||||
| 	accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access") | 	accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access") | ||||||
| 	baseDN := strings.ToLower(ms.si.GetBaseDN()) | 	baseDN := ms.si.GetBaseDN() | ||||||
|  |  | ||||||
| 	filterOC, err := ldap.GetFilterObjectClass(req.Filter) | 	filterOC, err := ldap.GetFilterObjectClass(req.Filter) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -62,7 +62,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		}).Inc() | 		}).Inc() | ||||||
| 		return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN) | 		return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN) | ||||||
| 	} | 	} | ||||||
| 	if !strings.HasSuffix(req.BindDN, ","+baseDN) { | 	if !utils.HasSuffixNoCase(req.BindDN, ","+baseDN) { | ||||||
| 		metrics.RequestsRejected.With(prometheus.Labels{ | 		metrics.RequestsRejected.With(prometheus.Labels{ | ||||||
| 			"outpost_name": ms.si.GetOutpostName(), | 			"outpost_name": ms.si.GetOutpostName(), | ||||||
| 			"type":         "search", | 			"type":         "search", | ||||||
| @ -92,7 +92,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 	scope := req.SearchRequest.Scope | 	scope := req.SearchRequest.Scope | ||||||
| 	needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, filterOC) | 	needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, filterOC) | ||||||
|  |  | ||||||
| 	if scope >= 0 && req.BaseDN == baseDN { | 	if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) { | ||||||
| 		if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) { | 		if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) { | ||||||
| 			entries = append(entries, ms.si.GetBaseEntry()) | 			entries = append(entries, ms.si.GetBaseEntry()) | ||||||
| 		} | 		} | ||||||
| @ -139,7 +139,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 				// as a member. | 				// as a member. | ||||||
| 				for _, u := range g.UsersObj { | 				for _, u := range g.UsersObj { | ||||||
| 					if flags.UserPk == u.Pk { | 					if flags.UserPk == u.Pk { | ||||||
| 						// TODO: Is there a better way to clone this object? | 						//TODO: Is there a better way to clone this object? | ||||||
| 						fg := api.NewGroup(g.Pk, g.Name, g.Parent, g.ParentName, []int32{flags.UserPk}, []api.GroupMember{u}) | 						fg := api.NewGroup(g.Pk, g.Name, g.Parent, g.ParentName, []int32{flags.UserPk}, []api.GroupMember{u}) | ||||||
| 						fg.SetAttributes(*g.Attributes) | 						fg.SetAttributes(*g.Attributes) | ||||||
| 						fg.SetIsSuperuser(*g.IsSuperuser) | 						fg.SetIsSuperuser(*g.IsSuperuser) | ||||||
| @ -155,8 +155,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err | 		return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseUserDN())) { | 	if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseUserDN())) { | ||||||
| 		singleu := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseUserDN()) | 		singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseUserDN()) | ||||||
|  |  | ||||||
| 		if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | 		if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | ||||||
| 			entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseUserDN(), constants.OUUsers)) | 			entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseUserDN(), constants.OUUsers)) | ||||||
| @ -166,7 +166,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) { | 		if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) { | ||||||
| 			for _, u := range *users { | 			for _, u := range *users { | ||||||
| 				entry := ms.si.UserEntry(u) | 				entry := ms.si.UserEntry(u) | ||||||
| 				if req.BaseDN == entry.DN || !singleu { | 				if strings.EqualFold(req.BaseDN, entry.DN) || !singleu { | ||||||
| 					entries = append(entries, entry) | 					entries = append(entries, entry) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @ -175,8 +175,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		scope += 1 // Return the scope to what it was before we descended | 		scope += 1 // Return the scope to what it was before we descended | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseGroupDN())) { | 	if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseGroupDN())) { | ||||||
| 		singleg := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseGroupDN()) | 		singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseGroupDN()) | ||||||
|  |  | ||||||
| 		if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | 		if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | ||||||
| 			entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseGroupDN(), constants.OUGroups)) | 			entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseGroupDN(), constants.OUGroups)) | ||||||
| @ -185,7 +185,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
|  |  | ||||||
| 		if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) { | 		if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) { | ||||||
| 			for _, g := range groups { | 			for _, g := range groups { | ||||||
| 				if req.BaseDN == g.DN || !singleg { | 				if strings.EqualFold(req.BaseDN, g.DN) || !singleg { | ||||||
| 					entries = append(entries, g.Entry()) | 					entries = append(entries, g.Entry()) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| @ -194,8 +194,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		scope += 1 // Return the scope to what it was before we descended | 		scope += 1 // Return the scope to what it was before we descended | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) { | 	if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) { | ||||||
| 		singlevg := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN()) | 		singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN()) | ||||||
|  |  | ||||||
| 		if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | 		if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) { | ||||||
| 			entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups)) | 			entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups)) | ||||||
| @ -205,7 +205,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | |||||||
| 		if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) { | 		if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) { | ||||||
| 			for _, u := range *users { | 			for _, u := range *users { | ||||||
| 				entry := group.FromAPIUser(u, ms.si).Entry() | 				entry := group.FromAPIUser(u, ms.si).Entry() | ||||||
| 				if req.BaseDN == entry.DN || !singlevg { | 				if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg { | ||||||
| 					entries = append(entries, entry) | 					entries = append(entries, entry) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | |||||||
| @ -26,7 +26,6 @@ type Request struct { | |||||||
| func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Request, *sentry.Span) { | func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Request, *sentry.Span) { | ||||||
| 	rid := uuid.New().String() | 	rid := uuid.New().String() | ||||||
| 	bindDN = strings.ToLower(bindDN) | 	bindDN = strings.ToLower(bindDN) | ||||||
| 	searchReq.BaseDN = strings.ToLower(searchReq.BaseDN) |  | ||||||
| 	span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search")) | 	span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search")) | ||||||
| 	span.Description = fmt.Sprintf("%s (%s)", searchReq.BaseDN, ldap.ScopeMap[searchReq.Scope]) | 	span.Description = fmt.Sprintf("%s (%s)", searchReq.BaseDN, ldap.ScopeMap[searchReq.Scope]) | ||||||
| 	span.SetTag("request_uid", rid) | 	span.SetTag("request_uid", rid) | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package utils | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"reflect" | 	"reflect" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/nmcclain/ldap" | 	"github.com/nmcclain/ldap" | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
| @ -117,3 +118,7 @@ func GetContainerEntry(filterOC string, dn string, ou string) *ldap.Entry { | |||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func HasSuffixNoCase(s1 string, s2 string) bool { | ||||||
|  | 	return strings.HasSuffix(strings.ToLower(s1), strings.ToLower(s2)) | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,6 +1,8 @@ | |||||||
| package utils | package utils | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	goldap "github.com/go-ldap/ldap/v3" | 	goldap "github.com/go-ldap/ldap/v3" | ||||||
| 	ber "github.com/nmcclain/asn1-ber" | 	ber "github.com/nmcclain/asn1-ber" | ||||||
| 	"github.com/nmcclain/ldap" | 	"github.com/nmcclain/ldap" | ||||||
| @ -41,7 +43,7 @@ func parseFilterForGroupSingle(req api.ApiCoreGroupsListRequest, f *ber.Packet) | |||||||
| 	// Switch on type of the value, then check the key | 	// Switch on type of the value, then check the key | ||||||
| 	switch vv := v.(type) { | 	switch vv := v.(type) { | ||||||
| 	case string: | 	case string: | ||||||
| 		switch k { | 		switch strings.ToLower(k.(string)) { | ||||||
| 		case "cn": | 		case "cn": | ||||||
| 			return req.Name(vv), false | 			return req.Name(vv), false | ||||||
| 		case "member": | 		case "member": | ||||||
| @ -54,14 +56,14 @@ func parseFilterForGroupSingle(req api.ApiCoreGroupsListRequest, f *ber.Packet) | |||||||
| 			username := userDN.RDNs[0].Attributes[0].Value | 			username := userDN.RDNs[0].Attributes[0].Value | ||||||
| 			// If the DN's first ou is virtual-groups, ignore this filter | 			// If the DN's first ou is virtual-groups, ignore this filter | ||||||
| 			if len(userDN.RDNs) > 1 { | 			if len(userDN.RDNs) > 1 { | ||||||
| 				if userDN.RDNs[1].Attributes[0].Value == constants.OUVirtualGroups || userDN.RDNs[1].Attributes[0].Value == constants.OUGroups { | 				if strings.EqualFold(userDN.RDNs[1].Attributes[0].Value, constants.OUVirtualGroups) || strings.EqualFold(userDN.RDNs[1].Attributes[0].Value, constants.OUGroups) { | ||||||
| 					// Since we know we're not filtering anything, skip this request | 					// Since we know we're not filtering anything, skip this request | ||||||
| 					return req, true | 					return req, true | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			return req.MembersByUsername([]string{username}), false | 			return req.MembersByUsername([]string{username}), false | ||||||
| 		} | 		} | ||||||
| 	// TODO: Support int | 	//TODO: Support int | ||||||
| 	default: | 	default: | ||||||
| 		return req, false | 		return req, false | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -66,7 +66,7 @@ func parseFilterForUserSingle(req api.ApiCoreUsersListRequest, f *ber.Packet) (a | |||||||
| 			} | 			} | ||||||
| 			return req.GroupsByName([]string{name}), false | 			return req.GroupsByName([]string{name}), false | ||||||
| 		} | 		} | ||||||
| 	// TODO: Support int | 	//TODO: Support int | ||||||
| 	default: | 	default: | ||||||
| 		return req, false | 		return req, false | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -46,6 +46,7 @@ type Application struct { | |||||||
|  |  | ||||||
| 	log *log.Entry | 	log *log.Entry | ||||||
| 	mux *mux.Router | 	mux *mux.Router | ||||||
|  | 	ak  *ak.APIController | ||||||
|  |  | ||||||
| 	errorTemplates *template.Template | 	errorTemplates *template.Template | ||||||
| } | } | ||||||
| @ -84,7 +85,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore | |||||||
| 	mux := mux.NewRouter() | 	mux := mux.NewRouter() | ||||||
| 	a := &Application{ | 	a := &Application{ | ||||||
| 		Host:           externalHost.Host, | 		Host:           externalHost.Host, | ||||||
| 		log:            log.WithField("logger", "authentik.outpost.proxy.bundle").WithField("provider", p.Name), | 		log:            muxLogger, | ||||||
| 		outpostName:    ak.Outpost.Name, | 		outpostName:    ak.Outpost.Name, | ||||||
| 		endpint:        endpoint, | 		endpint:        endpoint, | ||||||
| 		oauthConfig:    oauth2Config, | 		oauthConfig:    oauth2Config, | ||||||
| @ -93,6 +94,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore | |||||||
| 		httpClient:     c, | 		httpClient:     c, | ||||||
| 		mux:            mux, | 		mux:            mux, | ||||||
| 		errorTemplates: templates.GetTemplates(), | 		errorTemplates: templates.GetTemplates(), | ||||||
|  | 		ak:             ak, | ||||||
| 	} | 	} | ||||||
| 	a.sessions = a.getStore(p) | 	a.sessions = a.getStore(p) | ||||||
| 	mux.Use(web.NewLoggingHandler(muxLogger, func(l *log.Entry, r *http.Request) *log.Entry { | 	mux.Use(web.NewLoggingHandler(muxLogger, func(l *log.Entry, r *http.Request) *log.Entry { | ||||||
| @ -172,7 +174,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore | |||||||
| 		for _, regex := range strings.Split(*p.SkipPathRegex, "\n") { | 		for _, regex := range strings.Split(*p.SkipPathRegex, "\n") { | ||||||
| 			re, err := regexp.Compile(regex) | 			re, err := regexp.Compile(regex) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				// TODO: maybe create event for this? | 				//TODO: maybe create event for this? | ||||||
| 				a.log.WithError(err).Warning("failed to compile SkipPathRegex") | 				a.log.WithError(err).Warning("failed to compile SkipPathRegex") | ||||||
| 				continue | 				continue | ||||||
| 			} else { | 			} else { | ||||||
| @ -187,12 +189,16 @@ func (a *Application) Mode() api.ProxyMode { | |||||||
| 	return *a.proxyConfig.Mode | 	return *a.proxyConfig.Mode | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (a *Application) ProxyConfig() api.ProxyOutpostConfig { | ||||||
|  | 	return a.proxyConfig | ||||||
|  | } | ||||||
|  |  | ||||||
| func (a *Application) ServeHTTP(rw http.ResponseWriter, r *http.Request) { | func (a *Application) ServeHTTP(rw http.ResponseWriter, r *http.Request) { | ||||||
| 	a.mux.ServeHTTP(rw, r) | 	a.mux.ServeHTTP(rw, r) | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) { | func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) { | ||||||
| 	// TODO: Token revocation | 	//TODO: Token revocation | ||||||
| 	s, err := a.sessions.Get(r, constants.SeesionName) | 	s, err := a.sessions.Get(r, constants.SeesionName) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		http.Redirect(rw, r, a.endpint.EndSessionEndpoint, http.StatusFound) | 		http.Redirect(rw, r, a.endpint.EndSessionEndpoint, http.StatusFound) | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package application | |||||||
|  |  | ||||||
| type ProxyClaims struct { | type ProxyClaims struct { | ||||||
| 	UserAttributes  map[string]interface{} `json:"user_attributes"` | 	UserAttributes  map[string]interface{} `json:"user_attributes"` | ||||||
|  | 	BackendOverride string                 `json:"backend_override"` | ||||||
| } | } | ||||||
|  |  | ||||||
| type Claims struct { | type Claims struct { | ||||||
| @ -9,7 +10,7 @@ type Claims struct { | |||||||
| 	Exp               int          `json:"exp"` | 	Exp               int          `json:"exp"` | ||||||
| 	Email             string       `json:"email"` | 	Email             string       `json:"email"` | ||||||
| 	Verified          bool         `json:"email_verified"` | 	Verified          bool         `json:"email_verified"` | ||||||
| 	Proxy             ProxyClaims `json:"ak_proxy"` | 	Proxy             *ProxyClaims `json:"ak_proxy"` | ||||||
| 	Name              string       `json:"name"` | 	Name              string       `json:"name"` | ||||||
| 	PreferredUsername string       `json:"preferred_username"` | 	PreferredUsername string       `json:"preferred_username"` | ||||||
| 	Groups            []string     `json:"groups"` | 	Groups            []string     `json:"groups"` | ||||||
|  | |||||||
| @ -13,15 +13,18 @@ import ( | |||||||
| type OIDCEndpoint struct { | type OIDCEndpoint struct { | ||||||
| 	oauth2.Endpoint | 	oauth2.Endpoint | ||||||
| 	EndSessionEndpoint string | 	EndSessionEndpoint string | ||||||
|  | 	JwksUri            string | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoint { | func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoint { | ||||||
| 	authUrl := p.OidcConfiguration.AuthorizationEndpoint | 	authUrl := p.OidcConfiguration.AuthorizationEndpoint | ||||||
| 	endUrl := p.OidcConfiguration.EndSessionEndpoint | 	endUrl := p.OidcConfiguration.EndSessionEndpoint | ||||||
|  | 	jwksUrl := p.OidcConfiguration.JwksUri | ||||||
| 	if browserHost, found := os.LookupEnv("AUTHENTIK_HOST_BROWSER"); found && browserHost != "" { | 	if browserHost, found := os.LookupEnv("AUTHENTIK_HOST_BROWSER"); found && browserHost != "" { | ||||||
| 		host := os.Getenv("AUTHENTIK_HOST") | 		host := os.Getenv("AUTHENTIK_HOST") | ||||||
| 		authUrl = strings.ReplaceAll(authUrl, host, browserHost) | 		authUrl = strings.ReplaceAll(authUrl, host, browserHost) | ||||||
| 		endUrl = strings.ReplaceAll(endUrl, host, browserHost) | 		endUrl = strings.ReplaceAll(endUrl, host, browserHost) | ||||||
|  | 		jwksUrl = strings.ReplaceAll(jwksUrl, host, browserHost) | ||||||
| 	} | 	} | ||||||
| 	ep := OIDCEndpoint{ | 	ep := OIDCEndpoint{ | ||||||
| 		Endpoint: oauth2.Endpoint{ | 		Endpoint: oauth2.Endpoint{ | ||||||
| @ -30,6 +33,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoin | |||||||
| 			AuthStyle: oauth2.AuthStyleInParams, | 			AuthStyle: oauth2.AuthStyleInParams, | ||||||
| 		}, | 		}, | ||||||
| 		EndSessionEndpoint: endUrl, | 		EndSessionEndpoint: endUrl, | ||||||
|  | 		JwksUri:            jwksUrl, | ||||||
| 	} | 	} | ||||||
| 	authU, err := url.Parse(authUrl) | 	authU, err := url.Parse(authUrl) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -39,6 +43,10 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoin | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return ep | 		return ep | ||||||
| 	} | 	} | ||||||
|  | 	jwksU, err := url.Parse(jwksUrl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return ep | ||||||
|  | 	} | ||||||
| 	if authU.Host != "localhost:8000" { | 	if authU.Host != "localhost:8000" { | ||||||
| 		return ep | 		return ep | ||||||
| 	} | 	} | ||||||
| @ -54,7 +62,10 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoin | |||||||
| 	authU.Scheme = aku.Scheme | 	authU.Scheme = aku.Scheme | ||||||
| 	endU.Host = aku.Host | 	endU.Host = aku.Host | ||||||
| 	endU.Scheme = aku.Scheme | 	endU.Scheme = aku.Scheme | ||||||
|  | 	jwksU.Host = aku.Host | ||||||
|  | 	jwksU.Scheme = aku.Scheme | ||||||
| 	ep.AuthURL = authU.String() | 	ep.AuthURL = authU.String() | ||||||
| 	ep.EndSessionEndpoint = endU.String() | 	ep.EndSessionEndpoint = endU.String() | ||||||
|  | 	ep.JwksUri = jwksU.String() | ||||||
| 	return ep | 	return ep | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,9 @@ | |||||||
| package application | package application | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| @ -14,14 +16,6 @@ import ( | |||||||
| func (a *Application) addHeaders(headers http.Header, c *Claims) { | func (a *Application) addHeaders(headers http.Header, c *Claims) { | ||||||
| 	// https://goauthentik.io/docs/providers/proxy/proxy | 	// https://goauthentik.io/docs/providers/proxy/proxy | ||||||
|  |  | ||||||
| 	// Legacy headers, remove after 2022.1 |  | ||||||
| 	headers.Set("X-Auth-Username", c.PreferredUsername) |  | ||||||
| 	headers.Set("X-Auth-Groups", strings.Join(c.Groups, "|")) |  | ||||||
| 	headers.Set("X-Forwarded-Email", c.Email) |  | ||||||
| 	headers.Set("X-Forwarded-Preferred-Username", c.PreferredUsername) |  | ||||||
| 	headers.Set("X-Forwarded-User", c.Sub) |  | ||||||
|  |  | ||||||
| 	// New headers, unique prefix |  | ||||||
| 	headers.Set("X-authentik-username", c.PreferredUsername) | 	headers.Set("X-authentik-username", c.PreferredUsername) | ||||||
| 	headers.Set("X-authentik-groups", strings.Join(c.Groups, "|")) | 	headers.Set("X-authentik-groups", strings.Join(c.Groups, "|")) | ||||||
| 	headers.Set("X-authentik-email", c.Email) | 	headers.Set("X-authentik-email", c.Email) | ||||||
| @ -30,7 +24,7 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) { | |||||||
| 	headers.Set("X-authentik-jwt", c.RawToken) | 	headers.Set("X-authentik-jwt", c.RawToken) | ||||||
|  |  | ||||||
| 	// System headers | 	// System headers | ||||||
| 	headers.Set("X-authentik-meta-jwks", a.proxyConfig.OidcConfiguration.JwksUri) | 	headers.Set("X-authentik-meta-jwks", a.endpint.JwksUri) | ||||||
| 	headers.Set("X-authentik-meta-outpost", a.outpostName) | 	headers.Set("X-authentik-meta-outpost", a.outpostName) | ||||||
| 	headers.Set("X-authentik-meta-provider", a.proxyConfig.Name) | 	headers.Set("X-authentik-meta-provider", a.proxyConfig.Name) | ||||||
| 	headers.Set("X-authentik-meta-app", a.proxyConfig.AssignedApplicationSlug) | 	headers.Set("X-authentik-meta-app", a.proxyConfig.AssignedApplicationSlug) | ||||||
| @ -65,7 +59,8 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) getTraefikForwardUrl(r *http.Request) *url.URL { | // getTraefikForwardUrl See https://doc.traefik.io/traefik/middlewares/forwardauth/ | ||||||
|  | func (a *Application) getTraefikForwardUrl(r *http.Request) (*url.URL, error) { | ||||||
| 	u, err := url.Parse(fmt.Sprintf( | 	u, err := url.Parse(fmt.Sprintf( | ||||||
| 		"%s://%s%s", | 		"%s://%s%s", | ||||||
| 		r.Header.Get("X-Forwarded-Proto"), | 		r.Header.Get("X-Forwarded-Proto"), | ||||||
| @ -73,33 +68,63 @@ func (a *Application) getTraefikForwardUrl(r *http.Request) *url.URL { | |||||||
| 		r.Header.Get("X-Forwarded-Uri"), | 		r.Header.Get("X-Forwarded-Uri"), | ||||||
| 	)) | 	)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		a.log.WithError(err).Warning("Failed to parse URL from traefik") | 		return nil, err | ||||||
| 		return r.URL |  | ||||||
| 	} | 	} | ||||||
| 	a.log.WithField("url", u.String()).Trace("traefik forwarded url") | 	a.log.WithField("url", u.String()).Trace("traefik forwarded url") | ||||||
| 	return u | 	return u, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) IsAllowlisted(r *http.Request) bool { | // getNginxForwardUrl See https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/template/nginx.tmpl | ||||||
| 	url := r.URL | func (a *Application) getNginxForwardUrl(r *http.Request) (*url.URL, error) { | ||||||
| 	// In Forward auth mode, we can't directly match against the requested URL | 	ou := r.Header.Get("X-Original-URI") | ||||||
| 	// Since that would be /akprox/auth/... | 	if ou != "" { | ||||||
| 	if a.Mode() == api.PROXYMODE_FORWARD_SINGLE || a.Mode() == api.PROXYMODE_FORWARD_DOMAIN { | 		// Turn this full URL into a relative URL | ||||||
| 		// For traefik, we can get the Upstream URL from headers | 		u := &url.URL{ | ||||||
| 		// For nginx we can attempt to as well, but it's not guaranteed to work. | 			Host:   "", | ||||||
| 		if strings.HasPrefix(r.URL.Path, "/akprox/auth") { | 			Scheme: "", | ||||||
| 			url = a.getTraefikForwardUrl(r) | 			Path:   ou, | ||||||
| 		} | 		} | ||||||
|  | 		a.log.WithField("url", u.String()).Info("building forward URL from X-Original-URI") | ||||||
|  | 		return u, nil | ||||||
| 	} | 	} | ||||||
| 	for _, u := range a.UnauthenticatedRegex { | 	h := r.Header.Get("X-Original-URL") | ||||||
|  | 	if len(h) < 1 { | ||||||
|  | 		return nil, errors.New("no forward URL found") | ||||||
|  | 	} | ||||||
|  | 	u, err := url.Parse(h) | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.log.WithError(err).Warning("failed to parse URL from nginx") | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	a.log.WithField("url", u.String()).Trace("nginx forwarded url") | ||||||
|  | 	return u, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Application) ReportMisconfiguration(r *http.Request, msg string, fields map[string]interface{}) { | ||||||
|  | 	fields["message"] = msg | ||||||
|  | 	a.log.WithFields(fields).Error("Reporting configuration error") | ||||||
|  | 	req := api.EventRequest{ | ||||||
|  | 		Action:   api.EVENTACTIONS_CONFIGURATION_ERROR, | ||||||
|  | 		App:      "authentik.providers.proxy", // must match python apps.py name | ||||||
|  | 		ClientIp: *api.NewNullableString(api.PtrString(r.RemoteAddr)), | ||||||
|  | 		Context:  &fields, | ||||||
|  | 	} | ||||||
|  | 	_, _, err := a.ak.Client.EventsApi.EventsEventsCreate(context.Background()).EventRequest(req).Execute() | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.log.WithError(err).Warning("failed to report configuration error") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (a *Application) IsAllowlisted(u *url.URL) bool { | ||||||
|  | 	for _, ur := range a.UnauthenticatedRegex { | ||||||
| 		var testString string | 		var testString string | ||||||
| 		if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE { | 		if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE { | ||||||
| 			testString = url.Path | 			testString = u.Path | ||||||
| 		} else { | 		} else { | ||||||
| 			testString = url.String() | 			testString = u.String() | ||||||
| 		} | 		} | ||||||
| 		a.log.WithField("regex", u.String()).WithField("url", testString).Trace("Matching URL against allow list") | 		a.log.WithField("regex", u.String()).WithField("url", testString).Trace("Matching URL against allow list") | ||||||
| 		if u.MatchString(testString) { | 		if ur.MatchString(testString) { | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"goauthentik.io/api" | 	"goauthentik.io/api" | ||||||
| 	"goauthentik.io/internal/outpost/proxyv2/constants" | 	"goauthentik.io/internal/outpost/proxyv2/constants" | ||||||
| @ -24,39 +25,52 @@ func (a *Application) configureForward() error { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) { | func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) { | ||||||
|  | 	a.log.WithField("header", r.Header).Trace("tracing headers for debug") | ||||||
|  | 	// First check if we've got everything we need | ||||||
|  | 	fwd, err := a.getTraefikForwardUrl(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.ReportMisconfiguration(r, fmt.Sprintf("Outpost %s (Provider %s) failed to detect a forward URL from Traefik", a.outpostName, a.proxyConfig.Name), map[string]interface{}{ | ||||||
|  | 			"provider": a.proxyConfig.Name, | ||||||
|  | 			"outpost":  a.outpostName, | ||||||
|  | 			"url":      r.URL.String(), | ||||||
|  | 			"headers":  cleanseHeaders(r.Header), | ||||||
|  | 		}) | ||||||
|  | 		http.Error(rw, "configuration error", http.StatusInternalServerError) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	claims, err := a.getClaims(r) | 	claims, err := a.getClaims(r) | ||||||
| 	if claims != nil && err == nil { | 	if claims != nil && err == nil { | ||||||
| 		a.addHeaders(rw.Header(), claims) | 		a.addHeaders(rw.Header(), claims) | ||||||
| 		rw.Header().Set("User-Agent", r.Header.Get("User-Agent")) | 		rw.Header().Set("User-Agent", r.Header.Get("User-Agent")) | ||||||
| 		a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth") | 		a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth") | ||||||
| 		return | 		return | ||||||
| 	} else if claims == nil && a.IsAllowlisted(r) { | 	} else if claims == nil && a.IsAllowlisted(fwd) { | ||||||
| 		a.log.Trace("path can be accessed without authentication") | 		a.log.Trace("path can be accessed without authentication") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 	if strings.HasPrefix(r.Header.Get("X-Forwarded-Uri"), "/akprox") { | ||||||
|  | 		a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 	host := "" | 	host := "" | ||||||
| 	s, _ := a.sessions.Get(r, constants.SeesionName) | 	s, _ := a.sessions.Get(r, constants.SeesionName) | ||||||
| 	// Optional suffix, which is appended to the URL | 	// Optional suffix, which is appended to the URL | ||||||
| 	if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_SINGLE { | 	if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_SINGLE { | ||||||
| 		host = web.GetHost(r) | 		host = web.GetHost(r) | ||||||
| 	} else if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_DOMAIN { | 	} else if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_DOMAIN { | ||||||
| 		eh, _ := url.Parse(a.proxyConfig.ExternalHost) | 		eh, err := url.Parse(a.proxyConfig.ExternalHost) | ||||||
|  | 		if err != nil { | ||||||
|  | 			a.log.WithField("host", a.proxyConfig.ExternalHost).WithError(err).Warning("invalid external_host") | ||||||
|  | 		} else { | ||||||
| 			host = eh.Host | 			host = eh.Host | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
| 	// set the redirect flag to the current URL we have, since we redirect | 	// set the redirect flag to the current URL we have, since we redirect | ||||||
| 	// to a (possibly) different domain, but we want to be redirected back | 	// to a (possibly) different domain, but we want to be redirected back | ||||||
| 	// to the application | 	// to the application | ||||||
| 	// see https://doc.traefik.io/traefik/middlewares/forwardauth/ |  | ||||||
| 	// X-Forwarded-Uri is only the path, so we need to build the entire URL | 	// X-Forwarded-Uri is only the path, so we need to build the entire URL | ||||||
| 	s.Values[constants.SessionRedirect] = a.getTraefikForwardUrl(r).String() | 	s.Values[constants.SessionRedirect] = fwd.String() | ||||||
| 	if r.Header.Get("X-Forwarded-Uri") == "/akprox/start" { |  | ||||||
| 		a.log.Info("Detected potential redirect loop") |  | ||||||
| 		if val, ok := s.Values[constants.SessionLoopDetection]; !ok { |  | ||||||
| 			s.Values[constants.SessionLoopDetection] = 1 |  | ||||||
| 		} else { |  | ||||||
| 			s.Values[constants.SessionLoopDetection] = val.(int) + 1 |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	err = s.Save(r, rw) | 	err = s.Save(r, rw) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		a.log.WithError(err).Warning("failed to save session before redirect") | 		a.log.WithError(err).Warning("failed to save session before redirect") | ||||||
| @ -72,6 +86,19 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque | |||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) { | func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) { | ||||||
|  | 	a.log.WithField("header", r.Header).Trace("tracing headers for debug") | ||||||
|  | 	fwd, err := a.getNginxForwardUrl(r) | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.ReportMisconfiguration(r, fmt.Sprintf("Outpost %s (Provider %s) failed to detect a forward URL from nginx", a.outpostName, a.proxyConfig.Name), map[string]interface{}{ | ||||||
|  | 			"provider": a.proxyConfig.Name, | ||||||
|  | 			"outpost":  a.outpostName, | ||||||
|  | 			"url":      r.URL.String(), | ||||||
|  | 			"headers":  cleanseHeaders(r.Header), | ||||||
|  | 		}) | ||||||
|  | 		http.Error(rw, "configuration error", http.StatusInternalServerError) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	claims, err := a.getClaims(r) | 	claims, err := a.getClaims(r) | ||||||
| 	if claims != nil && err == nil { | 	if claims != nil && err == nil { | ||||||
| 		a.addHeaders(rw.Header(), claims) | 		a.addHeaders(rw.Header(), claims) | ||||||
| @ -79,9 +106,23 @@ func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request | |||||||
| 		rw.WriteHeader(200) | 		rw.WriteHeader(200) | ||||||
| 		a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth") | 		a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth") | ||||||
| 		return | 		return | ||||||
| 	} else if claims == nil && a.IsAllowlisted(r) { | 	} else if claims == nil && a.IsAllowlisted(fwd) { | ||||||
| 		a.log.Trace("path can be accessed without authentication") | 		a.log.Trace("path can be accessed without authentication") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(r, constants.SeesionName) | ||||||
|  | 	s.Values[constants.SessionRedirect] = fwd.String() | ||||||
|  | 	err = s.Save(r, rw) | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.log.WithError(err).Warning("failed to save session before redirect") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if fwd.String() != r.URL.String() { | ||||||
|  | 		if strings.HasPrefix(fwd.Path, "/akprox") { | ||||||
|  | 			a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access") | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 	http.Error(rw, "unauthorized request", http.StatusUnauthorized) | 	http.Error(rw, "unauthorized request", http.StatusUnauthorized) | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										134
									
								
								internal/outpost/proxyv2/application/mode_forward_nginx_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								internal/outpost/proxyv2/application/mode_forward_nginx_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,134 @@ | |||||||
|  | package application | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"goauthentik.io/api" | ||||||
|  | 	"goauthentik.io/internal/outpost/proxyv2/constants" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestForwardHandleNginx_Single_Blank(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleNginx(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusInternalServerError, rr.Code) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleNginx_Single_Skip(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | ||||||
|  | 	req.Header.Set("X-Original-URL", "http://test.goauthentik.io/skip") | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleNginx(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusOK, rr.Code) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleNginx_Single_Headers(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | ||||||
|  | 	req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app") | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleNginx(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, rr.Code, http.StatusUnauthorized) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleNginx_Single_URI(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "https://foo.bar/akprox/auth/nginx", nil) | ||||||
|  | 	req.Header.Set("X-Original-URI", "/app") | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleNginx(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, rr.Code, http.StatusUnauthorized) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	assert.Equal(t, "/app", s.Values[constants.SessionRedirect]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleNginx_Single_Claims(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | ||||||
|  | 	req.Header.Set("X-Original-URI", "/") | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleNginx(rr, req) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	s.Values[constants.SessionClaims] = Claims{ | ||||||
|  | 		Sub: "foo", | ||||||
|  | 		Proxy: &ProxyClaims{ | ||||||
|  | 			UserAttributes: map[string]interface{}{ | ||||||
|  | 				"username": "foo", | ||||||
|  | 				"password": "bar", | ||||||
|  | 				"additionalHeaders": map[string]interface{}{ | ||||||
|  | 					"foo": "bar", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	err := a.sessions.Save(req, rr, s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rr = httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleNginx(rr, req) | ||||||
|  |  | ||||||
|  | 	h := rr.Result().Header | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, []string{"Basic Zm9vOmJhcg=="}, h["Authorization"]) | ||||||
|  | 	assert.Equal(t, []string{"bar"}, h["Foo"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["User-Agent"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Email"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Groups"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Jwt"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Meta-App"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Meta-Jwks"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Meta-Outpost"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Name"]) | ||||||
|  | 	assert.Equal(t, []string{"foo"}, h["X-Authentik-Uid"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Username"]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleNginx_Domain_Blank(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
|  | 	a.proxyConfig.CookieDomain = api.PtrString("foo") | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleNginx(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusInternalServerError, rr.Code) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleNginx_Domain_Header(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
|  | 	a.proxyConfig.CookieDomain = api.PtrString("foo") | ||||||
|  | 	a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io" | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | ||||||
|  | 	req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app") | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleNginx(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusUnauthorized, rr.Code) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) | ||||||
|  | } | ||||||
| @ -0,0 +1,132 @@ | |||||||
|  | package application | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"goauthentik.io/api" | ||||||
|  | 	"goauthentik.io/internal/outpost/proxyv2/constants" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestForwardHandleTraefik_Single_Blank(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleTraefik(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusInternalServerError, rr.Code) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleTraefik_Single_Skip(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | ||||||
|  | 	req.Header.Set("X-Forwarded-Proto", "http") | ||||||
|  | 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | ||||||
|  | 	req.Header.Set("X-Forwarded-Uri", "/skip") | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleTraefik(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusOK, rr.Code) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleTraefik_Single_Headers(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | ||||||
|  | 	req.Header.Set("X-Forwarded-Proto", "http") | ||||||
|  | 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | ||||||
|  | 	req.Header.Set("X-Forwarded-Uri", "/app") | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleTraefik(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, rr.Code, http.StatusTemporaryRedirect) | ||||||
|  | 	loc, _ := rr.Result().Location() | ||||||
|  | 	assert.Equal(t, loc.String(), "http://test.goauthentik.io/akprox/start") | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleTraefik_Single_Claims(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | ||||||
|  | 	req.Header.Set("X-Forwarded-Proto", "http") | ||||||
|  | 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | ||||||
|  | 	req.Header.Set("X-Forwarded-Uri", "/app") | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleTraefik(rr, req) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	s.Values[constants.SessionClaims] = Claims{ | ||||||
|  | 		Sub: "foo", | ||||||
|  | 		Proxy: &ProxyClaims{ | ||||||
|  | 			UserAttributes: map[string]interface{}{ | ||||||
|  | 				"username": "foo", | ||||||
|  | 				"password": "bar", | ||||||
|  | 				"additionalHeaders": map[string]interface{}{ | ||||||
|  | 					"foo": "bar", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	err := a.sessions.Save(req, rr, s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rr = httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleTraefik(rr, req) | ||||||
|  |  | ||||||
|  | 	h := rr.Result().Header | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, []string{"Basic Zm9vOmJhcg=="}, h["Authorization"]) | ||||||
|  | 	assert.Equal(t, []string{"bar"}, h["Foo"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["User-Agent"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Email"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Groups"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Jwt"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Meta-App"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Meta-Jwks"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Meta-Outpost"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Name"]) | ||||||
|  | 	assert.Equal(t, []string{"foo"}, h["X-Authentik-Uid"]) | ||||||
|  | 	assert.Equal(t, []string{""}, h["X-Authentik-Username"]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleTraefik_Domain_Blank(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
|  | 	a.proxyConfig.CookieDomain = api.PtrString("foo") | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleTraefik(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusInternalServerError, rr.Code) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestForwardHandleTraefik_Domain_Header(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
|  | 	a.proxyConfig.CookieDomain = api.PtrString("foo") | ||||||
|  | 	a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io" | ||||||
|  | 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | ||||||
|  | 	req.Header.Set("X-Forwarded-Proto", "http") | ||||||
|  | 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | ||||||
|  | 	req.Header.Set("X-Forwarded-Uri", "/app") | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.forwardHandleTraefik(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusTemporaryRedirect, rr.Code) | ||||||
|  | 	loc, _ := rr.Result().Location() | ||||||
|  | 	assert.Equal(t, "http://auth.test.goauthentik.io/akprox/start", loc.String()) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) | ||||||
|  | } | ||||||
| @ -35,7 +35,7 @@ func (a *Application) configureProxy() error { | |||||||
| 	rp.ModifyResponse = a.proxyModifyResponse | 	rp.ModifyResponse = a.proxyModifyResponse | ||||||
| 	a.mux.PathPrefix("/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | 	a.mux.PathPrefix("/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||||
| 		claims, err := a.getClaims(r) | 		claims, err := a.getClaims(r) | ||||||
| 		if claims == nil && a.IsAllowlisted(r) { | 		if claims == nil && a.IsAllowlisted(r.URL) { | ||||||
| 			a.log.Trace("path can be accessed without authentication") | 			a.log.Trace("path can be accessed without authentication") | ||||||
| 		} else if claims == nil && err != nil { | 		} else if claims == nil && err != nil { | ||||||
| 			a.redirectToStart(rw, r) | 			a.redirectToStart(rw, r) | ||||||
| @ -60,7 +60,7 @@ func (a *Application) configureProxy() error { | |||||||
| 		} | 		} | ||||||
| 		metrics.UpstreamTiming.With(prometheus.Labels{ | 		metrics.UpstreamTiming.With(prometheus.Labels{ | ||||||
| 			"outpost_name":  a.outpostName, | 			"outpost_name":  a.outpostName, | ||||||
| 			"upstream_host": u.String(), | 			"upstream_host": r.URL.Host, | ||||||
| 			"scheme":        r.URL.Scheme, | 			"scheme":        r.URL.Scheme, | ||||||
| 			"method":        r.Method, | 			"method":        r.Method, | ||||||
| 			"path":          r.URL.Path, | 			"path":          r.URL.Path, | ||||||
| @ -71,13 +71,27 @@ func (a *Application) configureProxy() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) proxyModifyRequest(u *url.URL) func(req *http.Request) { | func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) { | ||||||
| 	return func(req *http.Request) { | 	return func(r *http.Request) { | ||||||
| 		req.URL.Scheme = u.Scheme | 		claims, _ := a.getClaims(r) | ||||||
| 		req.URL.Host = u.Host | 		r.URL.Scheme = ou.Scheme | ||||||
|  | 		r.URL.Host = ou.Host | ||||||
|  | 		r.Host = ou.Host | ||||||
|  | 		if claims != nil && claims.Proxy != nil && claims.Proxy.BackendOverride != "" { | ||||||
|  | 			u, err := url.Parse(claims.Proxy.BackendOverride) | ||||||
|  | 			if err != nil { | ||||||
|  | 				a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override") | ||||||
|  | 			} else { | ||||||
|  | 				r.URL.Scheme = u.Scheme | ||||||
|  | 				r.URL.Host = u.Host | ||||||
|  | 				r.Host = u.Host | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) proxyModifyResponse(res *http.Response) error { | func (a *Application) proxyModifyResponse(res *http.Response) error { | ||||||
|  | 	res.Header.Set("X-Powered-By", "authentik_proxy2") | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										81
									
								
								internal/outpost/proxyv2/application/mode_proxy_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								internal/outpost/proxyv2/application/mode_proxy_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | package application | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"net/url" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"goauthentik.io/internal/outpost/proxyv2/constants" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestProxy_ModifyRequest(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "http://frontend/foo", nil) | ||||||
|  | 	u, err := url.Parse("http://backend:8012") | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	a.proxyModifyRequest(u)(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, "/foo", req.URL.Path) | ||||||
|  | 	assert.Equal(t, "backend:8012", req.URL.Host) | ||||||
|  | 	assert.Equal(t, "backend:8012", req.Host) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestProxy_ModifyRequest_Claims(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "http://frontend/foo", nil) | ||||||
|  | 	u, err := url.Parse("http://backend:8012") | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	s.Values[constants.SessionClaims] = Claims{ | ||||||
|  | 		Sub: "foo", | ||||||
|  | 		Proxy: &ProxyClaims{ | ||||||
|  | 			BackendOverride: "http://other-backend:8123", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	err = a.sessions.Save(req, rr, s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	a.proxyModifyRequest(u)(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, "/foo", req.URL.Path) | ||||||
|  | 	assert.Equal(t, "other-backend:8123", req.URL.Host) | ||||||
|  | 	assert.Equal(t, "other-backend:8123", req.Host) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestProxy_ModifyRequest_Claims_Invalid(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "http://frontend/foo", nil) | ||||||
|  | 	u, err := url.Parse("http://backend:8012") | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	s.Values[constants.SessionClaims] = Claims{ | ||||||
|  | 		Sub: "foo", | ||||||
|  | 		Proxy: &ProxyClaims{ | ||||||
|  | 			BackendOverride: ":qewr", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	err = a.sessions.Save(req, rr, s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	a.proxyModifyRequest(u)(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, "/foo", req.URL.Path) | ||||||
|  | 	assert.Equal(t, "backend:8012", req.URL.Host) | ||||||
|  | 	assert.Equal(t, "backend:8012", req.Host) | ||||||
|  | } | ||||||
| @ -25,13 +25,6 @@ func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) { | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		a.log.WithError(err).Warning("failed to save session") | 		a.log.WithError(err).Warning("failed to save session") | ||||||
| 	} | 	} | ||||||
| 	if loop, ok := s.Values[constants.SessionLoopDetection]; ok { |  | ||||||
| 		if loop.(int) > 10 { |  | ||||||
| 			rw.WriteHeader(http.StatusBadRequest) |  | ||||||
| 			a.ErrorPage(rw, r, "Detected redirect loop, make sure /akprox is accessible without authentication.") |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(newState), http.StatusFound) | 	http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(newState), http.StatusFound) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store { | |||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			panic(err) | 			panic(err) | ||||||
| 		} | 		} | ||||||
| 		rs.SetMaxLength(math.MaxInt64) | 		rs.SetMaxLength(math.MaxInt) | ||||||
| 		if p.TokenValidity.IsSet() { | 		if p.TokenValidity.IsSet() { | ||||||
| 			t := p.TokenValidity.Get() | 			t := p.TokenValidity.Get() | ||||||
| 			// Add one to the validity to ensure we don't have a session with indefinite length | 			// Add one to the validity to ensure we don't have a session with indefinite length | ||||||
| @ -39,7 +39,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store { | |||||||
| 		// when using OpenID Connect , since this can contain a large amount of extra information in the id_token | 		// when using OpenID Connect , since this can contain a large amount of extra information in the id_token | ||||||
|  |  | ||||||
| 		// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk | 		// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk | ||||||
| 		cs.MaxLength(math.MaxInt64) | 		cs.MaxLength(math.MaxInt) | ||||||
| 		if p.TokenValidity.IsSet() { | 		if p.TokenValidity.IsSet() { | ||||||
| 			t := p.TokenValidity.Get() | 			t := p.TokenValidity.Get() | ||||||
| 			// Add one to the validity to ensure we don't have a session with indefinite length | 			// Add one to the validity to ensure we don't have a session with indefinite length | ||||||
|  | |||||||
							
								
								
									
										40
									
								
								internal/outpost/proxyv2/application/test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								internal/outpost/proxyv2/application/test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | package application | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/quasoft/memstore" | ||||||
|  | 	"goauthentik.io/api" | ||||||
|  | 	"goauthentik.io/internal/outpost/ak" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func newTestApplication() *Application { | ||||||
|  | 	a, _ := NewApplication( | ||||||
|  | 		api.ProxyOutpostConfig{ | ||||||
|  | 			Name:                       ak.TestSecret(), | ||||||
|  | 			ClientId:                   api.PtrString(ak.TestSecret()), | ||||||
|  | 			ClientSecret:               api.PtrString(ak.TestSecret()), | ||||||
|  | 			CookieSecret:               api.PtrString(ak.TestSecret()), | ||||||
|  | 			CookieDomain:               api.PtrString(""), | ||||||
|  | 			Mode:                       api.PROXYMODE_FORWARD_SINGLE.Ptr(), | ||||||
|  | 			SkipPathRegex:              api.PtrString("/skip.*"), | ||||||
|  | 			BasicAuthEnabled:           api.PtrBool(true), | ||||||
|  | 			BasicAuthUserAttribute:     api.PtrString("username"), | ||||||
|  | 			BasicAuthPasswordAttribute: api.PtrString("password"), | ||||||
|  | 		}, | ||||||
|  | 		http.DefaultClient, | ||||||
|  | 		nil, | ||||||
|  | 		ak.MockAK( | ||||||
|  | 			api.Outpost{ | ||||||
|  | 				Config: map[string]interface{}{ | ||||||
|  | 					"authentik_host": ak.TestSecret(), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			ak.MockConfig(), | ||||||
|  | 		), | ||||||
|  | 	) | ||||||
|  | 	a.sessions = memstore.NewMemStore( | ||||||
|  | 		[]byte(ak.TestSecret()), | ||||||
|  | 	) | ||||||
|  | 	return a | ||||||
|  | } | ||||||
| @ -6,7 +6,9 @@ import ( | |||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"goauthentik.io/api" | ||||||
| 	"goauthentik.io/internal/outpost/proxyv2/constants" | 	"goauthentik.io/internal/outpost/proxyv2/constants" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @ -20,6 +22,26 @@ func urlJoin(originalUrl string, newPath string) string { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) { | func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) { | ||||||
|  | 	s, err := a.sessions.Get(r, constants.SeesionName) | ||||||
|  | 	if err == nil { | ||||||
|  | 		a.log.WithError(err).Warning("failed to decode session") | ||||||
|  | 	} | ||||||
|  | 	redirectUrl := urlJoin(a.proxyConfig.ExternalHost, r.URL.Path) | ||||||
|  | 	if a.Mode() == api.PROXYMODE_FORWARD_DOMAIN { | ||||||
|  | 		dom := strings.TrimPrefix(*a.proxyConfig.CookieDomain, ".") | ||||||
|  | 		// In forward_domain we only check that the current URL's host | ||||||
|  | 		// ends with the cookie domain (remove the leading period if set) | ||||||
|  | 		if !strings.HasSuffix(r.URL.Hostname(), dom) { | ||||||
|  | 			a.log.WithField("url", r.URL.String()).WithField("cd", dom).Warning("Invalid redirect found") | ||||||
|  | 			redirectUrl = a.proxyConfig.ExternalHost | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	s.Values[constants.SessionRedirect] = redirectUrl | ||||||
|  | 	err = s.Save(r, rw) | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.log.WithError(err).Warning("failed to save session before redirect") | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	authUrl := urlJoin(a.proxyConfig.ExternalHost, "/akprox/start") | 	authUrl := urlJoin(a.proxyConfig.ExternalHost, "/akprox/start") | ||||||
| 	http.Redirect(rw, r, authUrl, http.StatusFound) | 	http.Redirect(rw, r, authUrl, http.StatusFound) | ||||||
| } | } | ||||||
| @ -65,3 +87,13 @@ func contains(s []string, e string) bool { | |||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func cleanseHeaders(headers http.Header) map[string]string { | ||||||
|  | 	h := make(map[string]string) | ||||||
|  | 	for hk, hv := range headers { | ||||||
|  | 		if len(hv) > 0 { | ||||||
|  | 			h[hk] = hv[0] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return h | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										81
									
								
								internal/outpost/proxyv2/application/utils_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								internal/outpost/proxyv2/application/utils_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | package application | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"goauthentik.io/api" | ||||||
|  | 	"goauthentik.io/internal/outpost/proxyv2/constants" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestRedirectToStart_Proxy(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	a.proxyConfig.Mode = api.PROXYMODE_PROXY.Ptr() | ||||||
|  | 	a.proxyConfig.ExternalHost = "https://test.goauthentik.io" | ||||||
|  | 	req, _ := http.NewRequest("GET", "/foo/bar/baz", nil) | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.redirectToStart(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusFound, rr.Code) | ||||||
|  | 	loc, _ := rr.Result().Location() | ||||||
|  | 	assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String()) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestRedirectToStart_Forward(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_SINGLE.Ptr() | ||||||
|  | 	a.proxyConfig.ExternalHost = "https://test.goauthentik.io" | ||||||
|  | 	req, _ := http.NewRequest("GET", "/foo/bar/baz", nil) | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.redirectToStart(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusFound, rr.Code) | ||||||
|  | 	loc, _ := rr.Result().Location() | ||||||
|  | 	assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String()) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestRedirectToStart_Forward_Domain_Invalid(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	a.proxyConfig.CookieDomain = api.PtrString("foo") | ||||||
|  | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
|  | 	a.proxyConfig.ExternalHost = "https://test.goauthentik.io" | ||||||
|  | 	req, _ := http.NewRequest("GET", "/foo/bar/baz", nil) | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.redirectToStart(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusFound, rr.Code) | ||||||
|  | 	loc, _ := rr.Result().Location() | ||||||
|  | 	assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String()) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect]) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestRedirectToStart_Forward_Domain(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	a.proxyConfig.CookieDomain = api.PtrString("goauthentik.io") | ||||||
|  | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
|  | 	a.proxyConfig.ExternalHost = "https://test.goauthentik.io" | ||||||
|  | 	req, _ := http.NewRequest("GET", "/foo/bar/baz", nil) | ||||||
|  |  | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  | 	a.redirectToStart(rr, req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, http.StatusFound, rr.Code) | ||||||
|  | 	loc, _ := rr.Result().Location() | ||||||
|  | 	assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String()) | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect]) | ||||||
|  | } | ||||||
| @ -6,4 +6,3 @@ const SessionOAuthState = "oauth_state" | |||||||
| const SessionClaims = "claims" | const SessionClaims = "claims" | ||||||
|  |  | ||||||
| const SessionRedirect = "redirect" | const SessionRedirect = "redirect" | ||||||
| const SessionLoopDetection = "loop_detection" |  | ||||||
|  | |||||||
| @ -8,6 +8,8 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/prometheus/client_golang/prometheus" | 	"github.com/prometheus/client_golang/prometheus" | ||||||
|  | 	"goauthentik.io/api" | ||||||
|  | 	"goauthentik.io/internal/outpost/proxyv2/application" | ||||||
| 	"goauthentik.io/internal/outpost/proxyv2/metrics" | 	"goauthentik.io/internal/outpost/proxyv2/metrics" | ||||||
| 	"goauthentik.io/internal/utils/web" | 	"goauthentik.io/internal/utils/web" | ||||||
| 	staticWeb "goauthentik.io/web" | 	staticWeb "goauthentik.io/web" | ||||||
| @ -43,6 +45,50 @@ func (ps *ProxyServer) HandleStatic(rw http.ResponseWriter, r *http.Request) { | |||||||
| 	}).Observe(float64(after)) | 	}).Observe(float64(after)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (ps *ProxyServer) lookupApp(r *http.Request) (*application.Application, string) { | ||||||
|  | 	host := web.GetHost(r) | ||||||
|  | 	// Try to find application by directly looking up host first (proxy, forward_auth_single) | ||||||
|  | 	a, ok := ps.apps[host] | ||||||
|  | 	if ok { | ||||||
|  | 		ps.log.WithField("host", host).WithField("app", a.ProxyConfig().Name).Debug("Found app based direct host match") | ||||||
|  | 		return a, host | ||||||
|  | 	} | ||||||
|  | 	// For forward_auth_domain, we don't have a direct app to domain relationship | ||||||
|  | 	// Check through all apps, and check how much of their cookie domain matches the host | ||||||
|  | 	// Return the application that has the longest match | ||||||
|  | 	var longestMatch *application.Application | ||||||
|  | 	longestMatchLength := 0 | ||||||
|  | 	for _, app := range ps.apps { | ||||||
|  | 		if app.Mode() != api.PROXYMODE_FORWARD_DOMAIN { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		// Check if the cookie domain has a leading period for a wildcard | ||||||
|  | 		// This will decrease the weight of a wildcard domain, but a request to example.com | ||||||
|  | 		// with the cookie domain set to example.com will still be routed correctly. | ||||||
|  | 		cd := strings.TrimPrefix(*app.ProxyConfig().CookieDomain, ".") | ||||||
|  | 		if !strings.HasSuffix(host, cd) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if len(cd) < longestMatchLength { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		longestMatch = app | ||||||
|  | 		longestMatchLength = len(cd) | ||||||
|  | 		// Also for forward_auth_domain, we need to respond on the external domain | ||||||
|  | 		if app.ProxyConfig().ExternalHost == host { | ||||||
|  | 			ps.log.WithField("host", host).WithField("app", app.ProxyConfig().Name).Debug("Found app based on external_host") | ||||||
|  | 			return app, host | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	// Check if our longes match is 0, in which case we didn't match, so we | ||||||
|  | 	// manually return no app | ||||||
|  | 	if longestMatchLength == 0 { | ||||||
|  | 		return nil, host | ||||||
|  | 	} | ||||||
|  | 	ps.log.WithField("host", host).WithField("app", longestMatch.ProxyConfig().Name).Debug("Found app based on cookie domain") | ||||||
|  | 	return longestMatch, host | ||||||
|  | } | ||||||
|  |  | ||||||
| func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) { | func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) { | ||||||
| 	if strings.HasPrefix(r.URL.Path, "/akprox/static") { | 	if strings.HasPrefix(r.URL.Path, "/akprox/static") { | ||||||
| 		ps.HandleStatic(rw, r) | 		ps.HandleStatic(rw, r) | ||||||
| @ -52,9 +98,8 @@ func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) { | |||||||
| 		ps.HandlePing(rw, r) | 		ps.HandlePing(rw, r) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	host := web.GetHost(r) | 	a, host := ps.lookupApp(r) | ||||||
| 	a, ok := ps.apps[host] | 	if a == nil { | ||||||
| 	if !ok { |  | ||||||
| 		// If we only have one handler, host name switching doesn't matter | 		// If we only have one handler, host name switching doesn't matter | ||||||
| 		if len(ps.apps) == 1 { | 		if len(ps.apps) == 1 { | ||||||
| 			ps.log.WithField("host", host).Trace("passing to single app mux") | 			ps.log.WithField("host", host).Trace("passing to single app mux") | ||||||
|  | |||||||
| @ -46,7 +46,7 @@ func NewProxyServer(ac *ak.APIController, portOffset int) *ProxyServer { | |||||||
| 	rootMux.Use(func(h http.Handler) http.Handler { | 	rootMux.Use(func(h http.Handler) http.Handler { | ||||||
| 		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | 		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||||
| 			h.ServeHTTP(rw, r) | 			h.ServeHTTP(rw, r) | ||||||
| 			rw.Header().Set("Server", "authentik_proxy2") | 			rw.Header().Set("X-Powered-By", "authentik_proxy2") | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| @ -70,11 +70,12 @@ func NewProxyServer(ac *ak.APIController, portOffset int) *ProxyServer { | |||||||
| 	return s | 	return s | ||||||
| } | } | ||||||
|  |  | ||||||
| func (ps *ProxyServer) HandleHost(host string, rw http.ResponseWriter, r *http.Request) bool { | func (ps *ProxyServer) HandleHost(rw http.ResponseWriter, r *http.Request) bool { | ||||||
| 	if app, ok := ps.apps[host]; ok { | 	a, host := ps.lookupApp(r) | ||||||
| 		if app.Mode() == api.PROXYMODE_PROXY { | 	if a != nil { | ||||||
|  | 		if a.Mode() == api.PROXYMODE_PROXY { | ||||||
| 			ps.log.WithField("host", host).Trace("routing to proxy outpost") | 			ps.log.WithField("host", host).Trace("routing to proxy outpost") | ||||||
| 			app.ServeHTTP(rw, r) | 			a.ServeHTTP(rw, r) | ||||||
| 			return true | 			return true | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -90,7 +91,7 @@ func (ps *ProxyServer) TimerFlowCacheExpiry() {} | |||||||
| func (ps *ProxyServer) GetCertificate(serverName string) *tls.Certificate { | func (ps *ProxyServer) GetCertificate(serverName string) *tls.Certificate { | ||||||
| 	app, ok := ps.apps[serverName] | 	app, ok := ps.apps[serverName] | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		ps.log.WithField("server-name", serverName).Debug("app does not exist") | 		ps.log.WithField("server-name", serverName).Debug("failed to get certificate for ServerName") | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| 	if app.Cert == nil { | 	if app.Cert == nil { | ||||||
| @ -101,7 +102,11 @@ func (ps *ProxyServer) GetCertificate(serverName string) *tls.Certificate { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (ps *ProxyServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) { | func (ps *ProxyServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) { | ||||||
| 	appCert := ps.GetCertificate(info.ServerName) | 	sn := info.ServerName | ||||||
|  | 	if sn == "" { | ||||||
|  | 		return &ps.defaultCert, nil | ||||||
|  | 	} | ||||||
|  | 	appCert := ps.GetCertificate(sn) | ||||||
| 	if appCert == nil { | 	if appCert == nil { | ||||||
| 		return &ps.defaultCert, nil | 		return &ps.defaultCert, nil | ||||||
| 	} | 	} | ||||||
| @ -150,17 +155,14 @@ func (ps *ProxyServer) Start() error { | |||||||
| 	wg.Add(3) | 	wg.Add(3) | ||||||
| 	go func() { | 	go func() { | ||||||
| 		defer wg.Done() | 		defer wg.Done() | ||||||
| 		ps.log.Debug("Starting HTTP Server...") |  | ||||||
| 		ps.ServeHTTP() | 		ps.ServeHTTP() | ||||||
| 	}() | 	}() | ||||||
| 	go func() { | 	go func() { | ||||||
| 		defer wg.Done() | 		defer wg.Done() | ||||||
| 		ps.log.Debug("Starting HTTPs Server...") |  | ||||||
| 		ps.ServeHTTPS() | 		ps.ServeHTTPS() | ||||||
| 	}() | 	}() | ||||||
| 	go func() { | 	go func() { | ||||||
| 		defer wg.Done() | 		defer wg.Done() | ||||||
| 		ps.log.Debug("Starting Metrics Server...") |  | ||||||
| 		metrics.RunServer() | 		metrics.RunServer() | ||||||
| 	}() | 	}() | ||||||
| 	return nil | 	return nil | ||||||
|  | |||||||
| @ -101,12 +101,12 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |||||||
| 	h.afterHandler(h.logger.WithFields(log.Fields{ | 	h.afterHandler(h.logger.WithFields(log.Fields{ | ||||||
| 		"remote":     req.RemoteAddr, | 		"remote":     req.RemoteAddr, | ||||||
| 		"host":       GetHost(req), | 		"host":       GetHost(req), | ||||||
| 		"request_protocol":  req.Proto, |  | ||||||
| 		"runtime":    fmt.Sprintf("%0.3f", duration), | 		"runtime":    fmt.Sprintf("%0.3f", duration), | ||||||
| 		"method":     req.Method, | 		"method":     req.Method, | ||||||
|  | 		"scheme":     req.URL.Scheme, | ||||||
| 		"size":       responseLogger.Size(), | 		"size":       responseLogger.Size(), | ||||||
| 		"status":     responseLogger.Status(), | 		"status":     responseLogger.Status(), | ||||||
| 		"upstream":   responseLogger.upstream, | 		"upstream":   responseLogger.upstream, | ||||||
| 		"request_useragent": req.UserAgent(), | 		"user_agent": req.UserAgent(), | ||||||
| 	}), req).Info(url.RequestURI()) | 	}), req).Info(url.RequestURI()) | ||||||
| } | } | ||||||
|  | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	