Compare commits
	
		
			174 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 619203c177 | |||
| 4ae476e58d | |||
| e444d0d640 | |||
| 3869965b4c | |||
| d4e1b95991 | |||
| 2b730dec54 | |||
| 2aacb311bc | |||
| 40055ef01b | |||
| 75608dce5c | |||
| b0f7083879 | |||
| 62bf79ce32 | |||
| 7a16c9cb14 | |||
| d29d161ac6 | |||
| 653631ac77 | |||
| cde303e780 | |||
| aa359a032c | |||
| 6491065aab | |||
| 79eec5a3a0 | |||
| cd5e091937 | |||
| 7ed8952803 | |||
| c1f302fb7c | |||
| cb31e52d0e | |||
| 782764ac73 | |||
| d0c56325ef | |||
| 73d57d6f82 | |||
| 2716a26887 | |||
| 0452537e8b | |||
| d1a1bfbbc5 | |||
| a69fcbca9a | |||
| 1ac4dacc3b | |||
| bcf7e162a4 | |||
| f44956bd61 | |||
| cb37e5c10e | |||
| 73bb778d62 | |||
| b612a82e16 | |||
| 83991c743e | |||
| 09f43ca43b | |||
| 1c91835a26 | |||
| c032914092 | |||
| 3634bf4629 | |||
| 45f99fbaf0 | |||
| e31a3307b5 | |||
| d28fcca344 | |||
| c296e1214c | |||
| d676cf6e3f | |||
| 39d87841d0 | |||
| fcd879034c | |||
| b285814e24 | |||
| 1a6ea72c09 | |||
| c251b87f8c | |||
| 21a9aa229a | |||
| 5f6565ee27 | |||
| afad55a357 | |||
| f25d76fa43 | |||
| 10b45d954e | |||
| 339eaf37f2 | |||
| f723fdd551 | |||
| 8359f0bfb3 | |||
| ee610a906a | |||
| 828eeb5ebb | |||
| c9c177d8f9 | |||
| c19afa4f16 | |||
| 941bc61b31 | |||
| 282b364606 | |||
| ad4bc4083d | |||
| ebe282eb1a | |||
| 830c26ca25 | |||
| ed3b4a3d4a | |||
| 975c4ddc04 | |||
| 7e2896298a | |||
| cba9cf8361 | |||
| bf12580f64 | |||
| 75ef4ce596 | |||
| c2f3ce11b0 | |||
| 3c256fecc6 | |||
| 0285b84133 | |||
| 99a371a02c | |||
| c7e6eb8896 | |||
| 674bd9e05c | |||
| b79901df87 | |||
| b248f450dd | |||
| 05db9e5c40 | |||
| 234a5e2b66 | |||
| aea1736f70 | |||
| 9f4a4449f5 | |||
| b6b55e2336 | |||
| 8f2805e05b | |||
| 4f3583cd7e | |||
| 617e90dca3 | |||
| f7408626a8 | |||
| 4dcb15af46 | |||
| 89beb7a9f7 | |||
| 28eeb4798e | |||
| 79b92e764e | |||
| 919336a519 | |||
| 27e04589c1 | |||
| ba44fbdac8 | |||
| 0e093a8917 | |||
| d0bfb99859 | |||
| 93bdea3769 | |||
| e681654af7 | |||
| cab7593dca | |||
| cf92f9aefc | |||
| 8d72b3498d | |||
| 42ab858c50 | |||
| a1abae9ab1 | |||
| 8f36b49061 | |||
| 64b4e851ce | |||
| 40a62ac1e5 | |||
| 5df60e4d87 | |||
| 50ebc8522d | |||
| eddca478dc | |||
| 99a7fca08e | |||
| a7e3602908 | |||
| 74169860cf | |||
| 52bb774f73 | |||
| f26fcaf825 | |||
| b8e92e2f11 | |||
| 08adfc94d6 | |||
| 236fafb735 | |||
| 5ad9ddee3c | |||
| 24d220ff49 | |||
| 3364c195b7 | |||
| 50aa87d141 | |||
| 72b375023d | |||
| 77ba186818 | |||
| 2fe6de0505 | |||
| bf9e969b53 | |||
| 184f119b16 | |||
| ebc06f1abe | |||
| 0f8880ab0a | |||
| ee56da5092 | |||
| 2152004502 | |||
| 45d0b80d02 | |||
| 96065eb942 | |||
| ac944fee8b | |||
| 1d0e5fc353 | |||
| 1f97420207 | |||
| ae07f13a87 | |||
| 0aec504170 | |||
| 3b4c9bcc57 | |||
| 5182a6741e | |||
| da7635ae5c | |||
| a92a0fb60a | |||
| cb10c1753b | |||
| ae654bd4c8 | |||
| 28192655ec | |||
| 9582294eb8 | |||
| 0172430d7d | |||
| 1454b65933 | |||
| 432a7792e2 | |||
| 54069618b4 | |||
| 81feb313df | |||
| e6b275add3 | |||
| 27016a5527 | |||
| 4c29d517f0 | |||
| 180d27cc37 | |||
| 5a8b356dc7 | |||
| 3195640776 | |||
| f463296d47 | |||
| adf4b23c01 | |||
| d900a2b6a9 | |||
| 95a2fddfa8 | |||
| 8f7d21b692 | |||
| 3f84abec2f | |||
| b5c857aff4 | |||
| f8dee09107 | |||
| 84a800583c | |||
| 88de94f014 | |||
| 25549ec339 | |||
| fe4923bff6 | |||
| bb1a0b6bd2 | |||
| 879b5ead71 | |||
| 1670ec9167 | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
[bumpversion]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 2021.9.1-rc3
 | 
					current_version = 2021.9.8
 | 
				
			||||||
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>.*)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.github/codespell-words.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.github/codespell-words.txt
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					keypair
 | 
				
			||||||
							
								
								
									
										2
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-web.yml
									
									
									
									
										vendored
									
									
								
							@ -61,7 +61,7 @@ jobs:
 | 
				
			|||||||
          npm install
 | 
					          npm install
 | 
				
			||||||
      - name: Generate API
 | 
					      - name: Generate API
 | 
				
			||||||
        run: make gen-web
 | 
					        run: make gen-web
 | 
				
			||||||
      - name: prettier
 | 
					      - name: lit-analyse
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          cd web
 | 
					          cd web
 | 
				
			||||||
          npm run lit-analyse
 | 
					          npm run lit-analyse
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -33,14 +33,14 @@ jobs:
 | 
				
			|||||||
        with:
 | 
					        with:
 | 
				
			||||||
          push: ${{ github.event_name == 'release' }}
 | 
					          push: ${{ github.event_name == 'release' }}
 | 
				
			||||||
          tags: |
 | 
					          tags: |
 | 
				
			||||||
            beryju/authentik:2021.9.1-rc3,
 | 
					            beryju/authentik:2021.9.8,
 | 
				
			||||||
            beryju/authentik:latest,
 | 
					            beryju/authentik:latest,
 | 
				
			||||||
            ghcr.io/goauthentik/server:2021.9.1-rc3,
 | 
					            ghcr.io/goauthentik/server:2021.9.8,
 | 
				
			||||||
            ghcr.io/goauthentik/server:latest
 | 
					            ghcr.io/goauthentik/server:latest
 | 
				
			||||||
          platforms: linux/amd64,linux/arm64
 | 
					          platforms: linux/amd64,linux/arm64
 | 
				
			||||||
          context: .
 | 
					          context: .
 | 
				
			||||||
      - name: Building Docker Image (stable)
 | 
					      - name: Building Docker Image (stable)
 | 
				
			||||||
        if: ${{ github.event_name == 'release' && !contains('2021.9.1-rc3', 'rc') }}
 | 
					        if: ${{ github.event_name == 'release' && !contains('2021.9.8', 'rc') }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          docker pull beryju/authentik:latest
 | 
					          docker pull beryju/authentik:latest
 | 
				
			||||||
          docker tag beryju/authentik:latest beryju/authentik:stable
 | 
					          docker tag beryju/authentik:latest beryju/authentik:stable
 | 
				
			||||||
@ -75,14 +75,14 @@ jobs:
 | 
				
			|||||||
        with:
 | 
					        with:
 | 
				
			||||||
          push: ${{ github.event_name == 'release' }}
 | 
					          push: ${{ github.event_name == 'release' }}
 | 
				
			||||||
          tags: |
 | 
					          tags: |
 | 
				
			||||||
            beryju/authentik-proxy:2021.9.1-rc3,
 | 
					            beryju/authentik-proxy:2021.9.8,
 | 
				
			||||||
            beryju/authentik-proxy:latest,
 | 
					            beryju/authentik-proxy:latest,
 | 
				
			||||||
            ghcr.io/goauthentik/proxy:2021.9.1-rc3,
 | 
					            ghcr.io/goauthentik/proxy:2021.9.8,
 | 
				
			||||||
            ghcr.io/goauthentik/proxy:latest
 | 
					            ghcr.io/goauthentik/proxy:latest
 | 
				
			||||||
          file: proxy.Dockerfile
 | 
					          file: proxy.Dockerfile
 | 
				
			||||||
          platforms: linux/amd64,linux/arm64
 | 
					          platforms: linux/amd64,linux/arm64
 | 
				
			||||||
      - name: Building Docker Image (stable)
 | 
					      - name: Building Docker Image (stable)
 | 
				
			||||||
        if: ${{ github.event_name == 'release' && !contains('2021.9.1-rc3', 'rc') }}
 | 
					        if: ${{ github.event_name == 'release' && !contains('2021.9.8', 'rc') }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          docker pull beryju/authentik-proxy:latest
 | 
					          docker pull beryju/authentik-proxy:latest
 | 
				
			||||||
          docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
 | 
					          docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
 | 
				
			||||||
@ -117,14 +117,14 @@ jobs:
 | 
				
			|||||||
        with:
 | 
					        with:
 | 
				
			||||||
          push: ${{ github.event_name == 'release' }}
 | 
					          push: ${{ github.event_name == 'release' }}
 | 
				
			||||||
          tags: |
 | 
					          tags: |
 | 
				
			||||||
            beryju/authentik-ldap:2021.9.1-rc3,
 | 
					            beryju/authentik-ldap:2021.9.8,
 | 
				
			||||||
            beryju/authentik-ldap:latest,
 | 
					            beryju/authentik-ldap:latest,
 | 
				
			||||||
            ghcr.io/goauthentik/ldap:2021.9.1-rc3,
 | 
					            ghcr.io/goauthentik/ldap:2021.9.8,
 | 
				
			||||||
            ghcr.io/goauthentik/ldap:latest
 | 
					            ghcr.io/goauthentik/ldap:latest
 | 
				
			||||||
          file: ldap.Dockerfile
 | 
					          file: ldap.Dockerfile
 | 
				
			||||||
          platforms: linux/amd64,linux/arm64
 | 
					          platforms: linux/amd64,linux/arm64
 | 
				
			||||||
      - name: Building Docker Image (stable)
 | 
					      - name: Building Docker Image (stable)
 | 
				
			||||||
        if: ${{ github.event_name == 'release' && !contains('2021.9.1-rc3', 'rc') }}
 | 
					        if: ${{ github.event_name == 'release' && !contains('2021.9.8', 'rc') }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          docker pull beryju/authentik-ldap:latest
 | 
					          docker pull beryju/authentik-ldap:latest
 | 
				
			||||||
          docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
 | 
					          docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
 | 
				
			||||||
@ -175,7 +175,7 @@ jobs:
 | 
				
			|||||||
          SENTRY_PROJECT: authentik
 | 
					          SENTRY_PROJECT: authentik
 | 
				
			||||||
          SENTRY_URL: https://sentry.beryju.org
 | 
					          SENTRY_URL: https://sentry.beryju.org
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          version: authentik@2021.9.1-rc3
 | 
					          version: authentik@2021.9.8
 | 
				
			||||||
          environment: beryjuorg-prod
 | 
					          environment: beryjuorg-prod
 | 
				
			||||||
          sourcemaps: './web/dist'
 | 
					          sourcemaps: './web/dist'
 | 
				
			||||||
          url_prefix: '~/static/dist'
 | 
					          url_prefix: '~/static/dist'
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							@ -27,7 +27,7 @@ jobs:
 | 
				
			|||||||
          docker-compose run -u root server test
 | 
					          docker-compose run -u root server test
 | 
				
			||||||
      - name: Extract version number
 | 
					      - name: Extract version number
 | 
				
			||||||
        id: get_version
 | 
					        id: get_version
 | 
				
			||||||
        uses: actions/github-script@v4.1
 | 
					        uses: actions/github-script@v5
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
					          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
          script: |
 | 
					          script: |
 | 
				
			||||||
 | 
				
			|||||||
@ -117,7 +117,7 @@ This section guides you through submitting a bug report for authentik. Following
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
 | 
					Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This form will have the full stack trace of the error that ocurred and shouldn't contain any sensitive data.
 | 
					This form will have the full stack trace of the error that occurred and shouldn't contain any sensitive data.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Suggesting Enhancements
 | 
					### Suggesting Enhancements
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								Dockerfile
									
									
									
									
									
								
							@ -1,5 +1,5 @@
 | 
				
			|||||||
# Stage 1: Lock python dependencies
 | 
					# Stage 1: Lock python dependencies
 | 
				
			||||||
FROM python:3.9-slim-buster as locker
 | 
					FROM docker.io/python:3.9-slim-buster as locker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY ./Pipfile /app/
 | 
					COPY ./Pipfile /app/
 | 
				
			||||||
COPY ./Pipfile.lock /app/
 | 
					COPY ./Pipfile.lock /app/
 | 
				
			||||||
@ -11,7 +11,7 @@ RUN pip install pipenv && \
 | 
				
			|||||||
    pipenv lock -r --dev-only > requirements-dev.txt
 | 
					    pipenv lock -r --dev-only > requirements-dev.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 2: Build website
 | 
					# Stage 2: Build website
 | 
				
			||||||
FROM node as website-builder
 | 
					FROM docker.io/node as website-builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY ./website /static/
 | 
					COPY ./website /static/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -19,7 +19,7 @@ ENV NODE_ENV=production
 | 
				
			|||||||
RUN cd /static && npm i && npm run build-docs-only
 | 
					RUN cd /static && npm i && npm run build-docs-only
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 3: Build webui
 | 
					# Stage 3: Build webui
 | 
				
			||||||
FROM node as web-builder
 | 
					FROM docker.io/node as web-builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY ./web /static/
 | 
					COPY ./web /static/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -27,7 +27,7 @@ ENV NODE_ENV=production
 | 
				
			|||||||
RUN cd /static && npm i && npm run build
 | 
					RUN cd /static && npm i && npm run build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 4: Build go proxy
 | 
					# Stage 4: Build go proxy
 | 
				
			||||||
FROM golang:1.17.1 AS builder
 | 
					FROM docker.io/golang:1.17.1 AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /work
 | 
					WORKDIR /work
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -47,7 +47,7 @@ COPY ./go.sum /work/go.sum
 | 
				
			|||||||
RUN go build -o /work/authentik ./cmd/server/main.go
 | 
					RUN go build -o /work/authentik ./cmd/server/main.go
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Stage 5: Run
 | 
					# Stage 5: Run
 | 
				
			||||||
FROM python:3.9-slim-buster
 | 
					FROM docker.io/python:3.9-slim-buster
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /
 | 
					WORKDIR /
 | 
				
			||||||
COPY --from=locker /app/requirements.txt /
 | 
					COPY --from=locker /app/requirements.txt /
 | 
				
			||||||
@ -80,8 +80,12 @@ COPY ./lifecycle/ /lifecycle
 | 
				
			|||||||
COPY --from=builder /work/authentik /authentik-proxy
 | 
					COPY --from=builder /work/authentik /authentik-proxy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
USER authentik
 | 
					USER authentik
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENV TMPDIR /dev/shm/
 | 
					ENV TMPDIR /dev/shm/
 | 
				
			||||||
ENV PYTHONUNBUFFERED 1
 | 
					ENV PYTHONUNBUFFERED 1
 | 
				
			||||||
ENV prometheus_multiproc_dir /dev/shm/
 | 
					ENV prometheus_multiproc_dir /dev/shm/
 | 
				
			||||||
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
 | 
					ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "/lifecycle/ak", "healthcheck" ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENTRYPOINT [ "/lifecycle/ak" ]
 | 
					ENTRYPOINT [ "/lifecycle/ak" ]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Makefile
									
									
									
									
									
								
							@ -20,6 +20,7 @@ test:
 | 
				
			|||||||
lint-fix:
 | 
					lint-fix:
 | 
				
			||||||
	isort authentik tests lifecycle
 | 
						isort authentik tests lifecycle
 | 
				
			||||||
	black authentik tests lifecycle
 | 
						black authentik tests lifecycle
 | 
				
			||||||
 | 
						codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w authentik internal cmd web/src website/src
 | 
				
			||||||
 | 
					
 | 
				
			||||||
lint:
 | 
					lint:
 | 
				
			||||||
	pyright authentik tests lifecycle
 | 
						pyright authentik tests lifecycle
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							@ -48,6 +48,7 @@ duo-client = "*"
 | 
				
			|||||||
ua-parser = "*"
 | 
					ua-parser = "*"
 | 
				
			||||||
deepmerge = "*"
 | 
					deepmerge = "*"
 | 
				
			||||||
colorama = "*"
 | 
					colorama = "*"
 | 
				
			||||||
 | 
					codespell = "*"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dev-packages]
 | 
					[dev-packages]
 | 
				
			||||||
bandit = "*"
 | 
					bandit = "*"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										305
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										305
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@ -1,7 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "_meta": {
 | 
					    "_meta": {
 | 
				
			||||||
        "hash": {
 | 
					        "hash": {
 | 
				
			||||||
            "sha256": "19d5324fd1a4af125ed57a683030ca14ee2d3648117748e4b32656875484728e"
 | 
					            "sha256": "babb6061c555f8f239f00210b2a0356763bdaaca2f3d704cf3444891b84db84d"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "pipfile-spec": 6,
 | 
					        "pipfile-spec": 6,
 | 
				
			||||||
        "requires": {},
 | 
					        "requires": {},
 | 
				
			||||||
@ -120,27 +120,27 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "boto3": {
 | 
					        "boto3": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:9b6679e3c54f8c32c09872948450ece87473cbc830cfd6c84dad58eb014329ba",
 | 
					                "sha256:7b45b224442c479de4bc6e6e9cb0557b642fc7a77edc8702e393ccaa2e0aa128",
 | 
				
			||||||
                "sha256:caa96b7c2be2168b6efc25ab1fb61c996174bcfbcab21b5f642608185daa6403"
 | 
					                "sha256:c388da7dc1a596755f39de990a72e05cee558d098e81de63de55bd9598cc5134"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==1.18.43"
 | 
					            "version": "==1.18.48"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "botocore": {
 | 
					        "botocore": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:b74d0a5fe0f7b73fa4b5670eaa9ff456b0bce70966d478dcd631c91458916eb6",
 | 
					                "sha256:17a10dd33334e7e3aaa4e12f66317284f96bb53267e20bc877a187c442681772",
 | 
				
			||||||
                "sha256:de7bf9c9098578d386b785b5b6eab954acccd3f79fe3e2eb971da608c967082b"
 | 
					                "sha256:2089f9fa36a59d8c02435c49d58ccc7b3ceb9c0c054ea4f71631c3c3a1c5245e"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version >= '3.6'",
 | 
					            "markers": "python_version >= '3.6'",
 | 
				
			||||||
            "version": "==1.21.43"
 | 
					            "version": "==1.21.51"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "cachetools": {
 | 
					        "cachetools": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
 | 
					                "sha256:0a3d3556c2c3befdbba2f93b78792c199c66201c999e97947ea0b7437758246b",
 | 
				
			||||||
                "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
 | 
					                "sha256:6a6fa6802188ab7e77bab2db001d676e854499552b0037d999d5b9f211db5250"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version ~= '3.5'",
 | 
					            "markers": "python_version ~= '3.5'",
 | 
				
			||||||
            "version": "==4.2.2"
 | 
					            "version": "==4.2.3"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "cbor2": {
 | 
					        "cbor2": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -252,11 +252,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "charset-normalizer": {
 | 
					        "charset-normalizer": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:7098e7e862f6370a2a8d1a6398cd359815c45d12626267652c3f13dec58e2367",
 | 
					                "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6",
 | 
				
			||||||
                "sha256:fa471a601dfea0f492e4f4fca035cd82155e65dc45c9b83bf4322dfab63755dd"
 | 
					                "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version >= '3'",
 | 
					            "markers": "python_version >= '3'",
 | 
				
			||||||
            "version": "==2.0.5"
 | 
					            "version": "==2.0.6"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "click": {
 | 
					        "click": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -268,9 +268,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "click-didyoumean": {
 | 
					        "click-didyoumean": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"
 | 
					                "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
 | 
				
			||||||
 | 
					                "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==0.0.3"
 | 
					            "markers": "python_version < '4' and python_full_version >= '3.6.2'",
 | 
				
			||||||
 | 
					            "version": "==0.3.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "click-plugins": {
 | 
					        "click-plugins": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -286,6 +288,14 @@
 | 
				
			|||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==0.2.0"
 | 
					            "version": "==0.2.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "codespell": {
 | 
				
			||||||
 | 
					            "hashes": [
 | 
				
			||||||
 | 
					                "sha256:19d3fe5644fef3425777e66f225a8c82d39059dcfe9edb3349a8a2cf48383ee5",
 | 
				
			||||||
 | 
					                "sha256:b864c7d917316316ac24272ee992d7937c3519be4569209c5b60035ac5d569b5"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "index": "pypi",
 | 
				
			||||||
 | 
					            "version": "==2.1.0"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "colorama": {
 | 
					        "colorama": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
 | 
					                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
 | 
				
			||||||
@ -303,25 +313,28 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "cryptography": {
 | 
					        "cryptography": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e",
 | 
					                "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6",
 | 
				
			||||||
                "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b",
 | 
					                "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6",
 | 
				
			||||||
                "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7",
 | 
					                "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c",
 | 
				
			||||||
                "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085",
 | 
					                "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999",
 | 
				
			||||||
                "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc",
 | 
					                "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e",
 | 
				
			||||||
                "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a",
 | 
					                "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992",
 | 
				
			||||||
                "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498",
 | 
					                "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d",
 | 
				
			||||||
                "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9",
 | 
					                "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588",
 | 
				
			||||||
                "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c",
 | 
					                "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa",
 | 
				
			||||||
                "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7",
 | 
					                "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d",
 | 
				
			||||||
                "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb",
 | 
					                "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd",
 | 
				
			||||||
                "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14",
 | 
					                "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d",
 | 
				
			||||||
                "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af",
 | 
					                "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953",
 | 
				
			||||||
                "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e",
 | 
					                "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2",
 | 
				
			||||||
                "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5",
 | 
					                "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8",
 | 
				
			||||||
                "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06",
 | 
					                "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6",
 | 
				
			||||||
                "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"
 | 
					                "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9",
 | 
				
			||||||
 | 
					                "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6",
 | 
				
			||||||
 | 
					                "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad",
 | 
				
			||||||
 | 
					                "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==3.4.8"
 | 
					            "version": "==35.0.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "dacite": {
 | 
					        "dacite": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -369,11 +382,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "django-filter": {
 | 
					        "django-filter": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06",
 | 
					                "sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e",
 | 
				
			||||||
                "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"
 | 
					                "sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==2.4.0"
 | 
					            "version": "==21.1"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "django-guardian": {
 | 
					        "django-guardian": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -449,11 +462,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "drf-spectacular": {
 | 
					        "drf-spectacular": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:47ef6ec8ff48ac8aede6ec12450a55fee381cf84de969ef1724dcde5a93de6b8",
 | 
					                "sha256:65df818226477cdfa629947ea52bc0cc13eb40550b192eeccec64a6b782651fd",
 | 
				
			||||||
                "sha256:d746b936cb4cddec380ea95bf91de6a6721777dfc42e0eea53b83c61a625e94e"
 | 
					                "sha256:f71205da3645d770545abeaf48e8a15afd6ee9a76e57c03df4592e51be1059bf"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==0.18.2"
 | 
					            "version": "==0.19.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "duo-client": {
 | 
					        "duo-client": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -480,19 +493,19 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "geoip2": {
 | 
					        "geoip2": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:906a1dbf15a179a1af3522970e8420ab15bb3e0afc526942cc179e12146d9c1d",
 | 
					                "sha256:f150bed3190d543712a17467208388d31bd8ddb49b2226fba53db8aaedb8ba89",
 | 
				
			||||||
                "sha256:b97b44031fdc463e84eb1316b4f19edd978cb1d78703465fcb1e36dc5a822ba6"
 | 
					                "sha256:f9172cdfb2a5f9225ace5e30dd7426413ad28798a5f474cd1538780686bd6a87"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==4.2.0"
 | 
					            "version": "==4.4.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "google-auth": {
 | 
					        "google-auth": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:7ae5eda089d393ca01658b550df24913cbbbdd34e9e6dedc1cea747485ae0c04",
 | 
					                "sha256:2a92b485afed5292946b324e91fcbe03db277ee4cb64c998c6cfa66d4af01dee",
 | 
				
			||||||
                "sha256:bde03220ed56e4e147dec92339c90ce95159dce657e2cccd0ac1fe82f6a96284"
 | 
					                "sha256:6dc8173abd50f25b6e62fc5b42802c96fc7cd9deb9bfeeb10a79f5606225cdf4"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version >= '3.6'",
 | 
					            "markers": "python_version >= '3.6'",
 | 
				
			||||||
            "version": "==2.1.0"
 | 
					            "version": "==2.2.1"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "gunicorn": {
 | 
					        "gunicorn": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -616,10 +629,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "jsonschema": {
 | 
					        "jsonschema": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163",
 | 
					                "sha256:bc51325b929171791c42ebc1c70b9713eb134d3bb8ebd5474c8b659b15be6d86",
 | 
				
			||||||
                "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"
 | 
					                "sha256:c773028c649441ab980015b5b622f4cd5134cf563daaf0235ca4b73cc3734f20"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==3.2.0"
 | 
					            "version": "==4.0.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "kombu": {
 | 
					        "kombu": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -704,10 +717,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "maxminddb": {
 | 
					        "maxminddb": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
 | 
					                "sha256:e37707ec4fab115804670e0fb7aedb4b57075a8b6f80052bdc648d3c005184e5"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version >= '3.6'",
 | 
					            "markers": "python_version >= '3.6'",
 | 
				
			||||||
            "version": "==2.0.3"
 | 
					            "version": "==2.2.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "msgpack": {
 | 
					        "msgpack": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -898,39 +911,39 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "pycryptodome": {
 | 
					        "pycryptodome": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:09c1555a3fa450e7eaca41ea11cd00afe7c91fef52353488e65663777d8524e0",
 | 
					                "sha256:04e14c732c3693d2830839feed5129286ce47ffa8bfe90e4ae042c773e51c677",
 | 
				
			||||||
                "sha256:12222a5edc9ca4a29de15fbd5339099c4c26c56e13c2ceddf0b920794f26165d",
 | 
					                "sha256:11d3164fb49fdee000fde05baecce103c0c698168ef1a18d9c7429dd66f0f5bb",
 | 
				
			||||||
                "sha256:1723ebee5561628ce96748501cdaa7afaa67329d753933296321f0be55358dce",
 | 
					                "sha256:217dcc0c92503f7dd4b3d3b7d974331a4419f97f555c99a845c3b366fed7056b",
 | 
				
			||||||
                "sha256:1c5e1ca507de2ad93474be5cfe2bfa76b7cf039a1a32fc196f40935944871a06",
 | 
					                "sha256:24c1b7705d19d8ae3e7255431efd2e526006855df62620118dd7b5374c6372f6",
 | 
				
			||||||
                "sha256:2603c98ae04aac675fefcf71a6c87dc4bb74a75e9071ae3923bbc91a59f08d35",
 | 
					                "sha256:309529d2526f3fb47102aeef376b3459110a6af7efb162e860b32e3a17a46f06",
 | 
				
			||||||
                "sha256:2dea65df54349cdfa43d6b2e8edb83f5f8d6861e5cf7b1fbc3e34c5694c85e27",
 | 
					                "sha256:3a153658d97258ca20bf18f7fe31c09cc7c558b6f8974a6ec74e19f6c634bd64",
 | 
				
			||||||
                "sha256:31c1df17b3dc5f39600a4057d7db53ac372f492c955b9b75dd439f5d8b460129",
 | 
					                "sha256:3f9fb499e267039262569d08658132c9cd8b136bf1d8c56b72f70ed05551e526",
 | 
				
			||||||
                "sha256:38661348ecb71476037f1e1f553159b80d256c00f6c0b00502acac891f7116d9",
 | 
					                "sha256:3faa6ebd35c61718f3f8862569c1f38450c24f3ededb213e1a64806f02f584bc",
 | 
				
			||||||
                "sha256:3e2e3a06580c5f190df843cdb90ea28d61099cf4924334d5297a995de68e4673",
 | 
					                "sha256:40083b0d7f277452c7f2dd4841801f058cc12a74c219ee4110d65774c6a58bef",
 | 
				
			||||||
                "sha256:3f840c49d38986f6e17dbc0673d37947c88bc9d2d9dba1c01b979b36f8447db1",
 | 
					                "sha256:49e54f2245befb0193848c8c8031d8d1358ed4af5a1ae8d0a3ba669a5cdd3a72",
 | 
				
			||||||
                "sha256:501ab36aae360e31d0ec370cf5ce8ace6cb4112060d099b993bc02b36ac83fb6",
 | 
					                "sha256:4e8fc4c48365ce8a542fe48bf1360da05bb2851df12f64fc94d751705e7cdbe7",
 | 
				
			||||||
                "sha256:60386d1d4cfaad299803b45a5bc2089696eaf6cdd56f9fc17479a6f89595cfc8",
 | 
					                "sha256:54d4e4d45f349d8c4e2f31c2734637ff62a844af391b833f789da88e43a8f338",
 | 
				
			||||||
                "sha256:6260e24d41149268122dd39d4ebd5941e9d107f49463f7e071fd397e29923b0c",
 | 
					                "sha256:66301e4c42dee43ee2da256625d3fe81ef98cc9924c2bd535008cc3ad8ded77b",
 | 
				
			||||||
                "sha256:6bbf7fee7b7948b29d7e71fcacf48bac0c57fb41332007061a933f2d996f9713",
 | 
					                "sha256:6b45fcace5a5d9c57ba87cf804b161adc62aa826295ce7f7acbcbdc0df74ed37",
 | 
				
			||||||
                "sha256:6d2df5223b12437e644ce0a3be7809471ffa71de44ccd28b02180401982594a6",
 | 
					                "sha256:7efec2418e9746ec48e264eea431f8e422d931f71c57b1c96ee202b117f58fa9",
 | 
				
			||||||
                "sha256:758949ca62690b1540dfb24ad773c6da9cd0e425189e83e39c038bbd52b8e438",
 | 
					                "sha256:851e6d4930b160417235955322db44adbdb19589918670d63f4acd5d92959ac0",
 | 
				
			||||||
                "sha256:77997519d8eb8a4adcd9a47b9cec18f9b323e296986528186c0e9a7a15d6a07e",
 | 
					                "sha256:8e82524e7c354033508891405574d12e612cc4fdd3b55d2c238fc1a3e300b606",
 | 
				
			||||||
                "sha256:7fd519b89585abf57bf47d90166903ec7b43af4fe23c92273ea09e6336af5c07",
 | 
					                "sha256:8ec154ec445412df31acf0096e7f715e30e167c8f2318b8f5b1ab7c28f4c82f7",
 | 
				
			||||||
                "sha256:98213ac2b18dc1969a47bc65a79a8fca02a414249d0c8635abb081c7f38c91b6",
 | 
					                "sha256:91ba4215a1f37d0f371fe43bc88c5ff49c274849f3868321c889313787de7672",
 | 
				
			||||||
                "sha256:99b2f3fc51d308286071d0953f92055504a6ffe829a832a9fc7a04318a7683dd",
 | 
					                "sha256:97e7df67a4da2e3f60612bbfd6c3f243a63a15d8f4797dd275e1d7b44a65cb12",
 | 
				
			||||||
                "sha256:9b6f711b25e01931f1c61ce0115245a23cdc8b80bf8539ac0363bdcf27d649b6",
 | 
					                "sha256:9a2312440057bf29b9582f72f14d79692044e63bfbc4b4bbea8559355f44f3dd",
 | 
				
			||||||
                "sha256:a3105a0eb63eacf98c2ecb0eb4aa03f77f40fbac2bdde22020bb8a536b226bb8",
 | 
					                "sha256:a7471646d8cd1a58bb696d667dcb3853e5c9b341b68dcf3c3cc0893d0f98ca5f",
 | 
				
			||||||
                "sha256:a8eb8b6ea09ec1c2535bf39914377bc8abcab2c7d30fa9225eb4fe412024e427",
 | 
					                "sha256:ac3012c36633564b2b5539bb7c6d9175f31d2ce74844e9abe654c428f02d0fd8",
 | 
				
			||||||
                "sha256:a92d5c414e8ee1249e850789052608f582416e82422502dc0ac8c577808a9067",
 | 
					                "sha256:b1daf251395af7336ddde6a0015ba5e632c18fe646ba930ef87402537358e3b4",
 | 
				
			||||||
                "sha256:d3d6958d53ad307df5e8469cc44474a75393a434addf20ecd451f38a72fe29b8",
 | 
					                "sha256:b217b4525e60e1af552d62bec01b4685095436d4de5ecde0f05d75b2f95ba6d4",
 | 
				
			||||||
                "sha256:e0a4d5933a88a2c98bbe19c0c722f5483dc628d7a38338ac2cb64a7dbd34064b",
 | 
					                "sha256:c61ea053bd5d4c12a063d7e704fbe1c45abb5d2510dab55bd95d166ba661604f",
 | 
				
			||||||
                "sha256:e3bf558c6aeb49afa9f0c06cee7fb5947ee5a1ff3bd794b653d39926b49077fa",
 | 
					                "sha256:c6469d1453f5864e3321a172b0aa671b938d753cbf2376b99fa2ab8841539bb8",
 | 
				
			||||||
                "sha256:e61e363d9a5d7916f3a4ce984a929514c0df3daf3b1b2eb5e6edbb131ee771cf",
 | 
					                "sha256:cefe6b267b8e5c3c72e11adec35a9c7285b62e8ea141b63e87055e9a9e5f2f8c",
 | 
				
			||||||
                "sha256:f977cdf725b20f6b8229b0c87acb98c7717e742ef9f46b113985303ae12a99da",
 | 
					                "sha256:d713dc0910e5ded07852a05e9b75f1dd9d3a31895eebee0668f612779b2a748c",
 | 
				
			||||||
                "sha256:fc7489a50323a0df02378bc2fff86eb69d94cc5639914346c736be981c6a02e7"
 | 
					                "sha256:db15fa07d2a4c00beeb5e9acdfdbc1c79f9ccfbdc1a8f36c82c4aa44951b33c9"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==3.10.1"
 | 
					            "version": "==3.10.4"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "pyjwt": {
 | 
					        "pyjwt": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -942,10 +955,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "pyopenssl": {
 | 
					        "pyopenssl": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51",
 | 
					                "sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3",
 | 
				
			||||||
                "sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"
 | 
					                "sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==20.0.1"
 | 
					            "version": "==21.0.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "pyparsing": {
 | 
					        "pyparsing": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1082,11 +1095,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "sentry-sdk": {
 | 
					        "sentry-sdk": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:ebe99144fa9618d4b0e7617e7929b75acd905d258c3c779edcd34c0adfffe26c",
 | 
					                "sha256:b9844751e40710e84a457c5bc29b21c383ccb2b63d76eeaad72f7f1c808c8828",
 | 
				
			||||||
                "sha256:f33d34c886d0ba24c75ea8885a8b3a172358853c7cbde05979fc99c29ef7bc52"
 | 
					                "sha256:c091cc7115ff25fe3a0e410dbecd7a996f81a3f6137d2272daef32d6c3cfa6dc"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==1.3.1"
 | 
					            "version": "==1.4.3"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "service-identity": {
 | 
					        "service-identity": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1176,11 +1189,11 @@
 | 
				
			|||||||
                "secure"
 | 
					                "secure"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
 | 
					                "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
 | 
				
			||||||
                "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
 | 
					                "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==1.26.6"
 | 
					            "version": "==1.26.7"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "uvicorn": {
 | 
					        "uvicorn": {
 | 
				
			||||||
            "extras": [
 | 
					            "extras": [
 | 
				
			||||||
@ -1457,11 +1470,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "charset-normalizer": {
 | 
					        "charset-normalizer": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:7098e7e862f6370a2a8d1a6398cd359815c45d12626267652c3f13dec58e2367",
 | 
					                "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6",
 | 
				
			||||||
                "sha256:fa471a601dfea0f492e4f4fca035cd82155e65dc45c9b83bf4322dfab63755dd"
 | 
					                "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version >= '3'",
 | 
					            "markers": "python_version >= '3'",
 | 
				
			||||||
            "version": "==2.0.5"
 | 
					            "version": "==2.0.6"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "click": {
 | 
					        "click": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1547,11 +1560,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "gitpython": {
 | 
					        "gitpython": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b",
 | 
					                "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647",
 | 
				
			||||||
                "sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"
 | 
					                "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version >= '3.6'",
 | 
					            "markers": "python_version >= '3.7'",
 | 
				
			||||||
            "version": "==3.1.18"
 | 
					            "version": "==3.1.24"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "idna": {
 | 
					        "idna": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1572,7 +1585,7 @@
 | 
				
			|||||||
                "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899",
 | 
					                "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899",
 | 
				
			||||||
                "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"
 | 
					                "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
 | 
					            "markers": "python_version < '4' and python_full_version >= '3.6.1'",
 | 
				
			||||||
            "version": "==5.9.3"
 | 
					            "version": "==5.9.3"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "lazy-object-proxy": {
 | 
					        "lazy-object-proxy": {
 | 
				
			||||||
@ -1642,11 +1655,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "platformdirs": {
 | 
					        "platformdirs": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f",
 | 
					                "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2",
 | 
				
			||||||
                "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"
 | 
					                "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version >= '3.6'",
 | 
					            "markers": "python_version >= '3.6'",
 | 
				
			||||||
            "version": "==2.3.0"
 | 
					            "version": "==2.4.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "pluggy": {
 | 
					        "pluggy": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1748,49 +1761,49 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "regex": {
 | 
					        "regex": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468",
 | 
					                "sha256:0628ed7d6334e8f896f882a5c1240de8c4d9b0dd7c7fb8e9f4692f5684b7d656",
 | 
				
			||||||
                "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354",
 | 
					                "sha256:09eb62654030f39f3ba46bc6726bea464069c29d00a9709e28c9ee9623a8da4a",
 | 
				
			||||||
                "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308",
 | 
					                "sha256:0bba1f6df4eafe79db2ecf38835c2626dbd47911e0516f6962c806f83e7a99ae",
 | 
				
			||||||
                "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d",
 | 
					                "sha256:10a7a9cbe30bd90b7d9a1b4749ef20e13a3528e4215a2852be35784b6bd070f0",
 | 
				
			||||||
                "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc",
 | 
					                "sha256:17310b181902e0bb42b29c700e2c2346b8d81f26e900b1328f642e225c88bce1",
 | 
				
			||||||
                "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8",
 | 
					                "sha256:1e8d1898d4fb817120a5f684363b30108d7b0b46c7261264b100d14ec90a70e7",
 | 
				
			||||||
                "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797",
 | 
					                "sha256:2054dea683f1bda3a804fcfdb0c1c74821acb968093d0be16233873190d459e3",
 | 
				
			||||||
                "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2",
 | 
					                "sha256:29385c4dbb3f8b3a55ce13de6a97a3d21bd00de66acd7cdfc0b49cb2f08c906c",
 | 
				
			||||||
                "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13",
 | 
					                "sha256:295bc8a13554a25ad31e44c4bedabd3c3e28bba027e4feeb9bb157647a2344a7",
 | 
				
			||||||
                "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d",
 | 
					                "sha256:2cdb3789736f91d0b3333ac54d12a7e4f9efbc98f53cb905d3496259a893a8b3",
 | 
				
			||||||
                "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a",
 | 
					                "sha256:3baf3eaa41044d4ced2463fd5d23bf7bd4b03d68739c6c99a59ce1f95599a673",
 | 
				
			||||||
                "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0",
 | 
					                "sha256:4e61100200fa6ab7c99b61476f9f9653962ae71b931391d0264acfb4d9527d9c",
 | 
				
			||||||
                "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73",
 | 
					                "sha256:6266fde576e12357b25096351aac2b4b880b0066263e7bc7a9a1b4307991bb0e",
 | 
				
			||||||
                "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1",
 | 
					                "sha256:650c4f1fc4273f4e783e1d8e8b51a3e2311c2488ba0fcae6425b1e2c248a189d",
 | 
				
			||||||
                "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed",
 | 
					                "sha256:658e3477676009083422042c4bac2bdad77b696e932a3de001c42cc046f8eda2",
 | 
				
			||||||
                "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a",
 | 
					                "sha256:6adc1bd68f81968c9d249aab8c09cdc2cbe384bf2d2cb7f190f56875000cdc72",
 | 
				
			||||||
                "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b",
 | 
					                "sha256:6c4d83d21d23dd854ffbc8154cf293f4e43ba630aa9bd2539c899343d7f59da3",
 | 
				
			||||||
                "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f",
 | 
					                "sha256:6f74b6d8f59f3cfb8237e25c532b11f794b96f5c89a6f4a25857d85f84fbef11",
 | 
				
			||||||
                "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256",
 | 
					                "sha256:7783d89bd5413d183a38761fbc68279b984b9afcfbb39fa89d91f63763fbfb90",
 | 
				
			||||||
                "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb",
 | 
					                "sha256:7e3536f305f42ad6d31fc86636c54c7dafce8d634e56fef790fbacb59d499dd5",
 | 
				
			||||||
                "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2",
 | 
					                "sha256:821e10b73e0898544807a0692a276e539e5bafe0a055506a6882814b6a02c3ec",
 | 
				
			||||||
                "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983",
 | 
					                "sha256:835962f432bce92dc9bf22903d46c50003c8d11b1dc64084c8fae63bca98564a",
 | 
				
			||||||
                "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb",
 | 
					                "sha256:85c61bee5957e2d7be390392feac7e1d7abd3a49cbaed0c8cee1541b784c8561",
 | 
				
			||||||
                "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645",
 | 
					                "sha256:86f9931eb92e521809d4b64ec8514f18faa8e11e97d6c2d1afa1bcf6c20a8eab",
 | 
				
			||||||
                "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8",
 | 
					                "sha256:8a5c2250c0a74428fd5507ae8853706fdde0f23bfb62ee1ec9418eeacf216078",
 | 
				
			||||||
                "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a",
 | 
					                "sha256:8aec4b4da165c4a64ea80443c16e49e3b15df0f56c124ac5f2f8708a65a0eddc",
 | 
				
			||||||
                "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906",
 | 
					                "sha256:8c268e78d175798cd71d29114b0a1f1391c7d011995267d3b62319ec1a4ecaa1",
 | 
				
			||||||
                "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f",
 | 
					                "sha256:8d80087320632457aefc73f686f66139801959bf5b066b4419b92be85be3543c",
 | 
				
			||||||
                "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c",
 | 
					                "sha256:95e89a8558c8c48626dcffdf9c8abac26b7c251d352688e7ab9baf351e1c7da6",
 | 
				
			||||||
                "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892",
 | 
					                "sha256:9c371dd326289d85906c27ec2bc1dcdedd9d0be12b543d16e37bad35754bde48",
 | 
				
			||||||
                "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0",
 | 
					                "sha256:9c7cb25adba814d5f419733fe565f3289d6fa629ab9e0b78f6dff5fa94ab0456",
 | 
				
			||||||
                "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e",
 | 
					                "sha256:a731552729ee8ae9c546fb1c651c97bf5f759018fdd40d0e9b4d129e1e3a44c8",
 | 
				
			||||||
                "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e",
 | 
					                "sha256:aea4006b73b555fc5bdb650a8b92cf486d678afa168cf9b38402bb60bf0f9c18",
 | 
				
			||||||
                "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed",
 | 
					                "sha256:b0e3f59d3c772f2c3baaef2db425e6fc4149d35a052d874bb95ccfca10a1b9f4",
 | 
				
			||||||
                "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c",
 | 
					                "sha256:b15dc34273aefe522df25096d5d087abc626e388a28a28ac75a4404bb7668736",
 | 
				
			||||||
                "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374",
 | 
					                "sha256:c000635fd78400a558bd7a3c2981bb2a430005ebaa909d31e6e300719739a949",
 | 
				
			||||||
                "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd",
 | 
					                "sha256:c31f35a984caffb75f00a86852951a337540b44e4a22171354fb760cefa09346",
 | 
				
			||||||
                "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791",
 | 
					                "sha256:c50a6379763c733562b1fee877372234d271e5c78cd13ade5f25978aa06744db",
 | 
				
			||||||
                "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a",
 | 
					                "sha256:c94722bf403b8da744b7d0bb87e1f2529383003ceec92e754f768ef9323f69ad",
 | 
				
			||||||
                "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1",
 | 
					                "sha256:dcbbc9cfa147d55a577d285fd479b43103188855074552708df7acc31a476dd9",
 | 
				
			||||||
                "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"
 | 
					                "sha256:fb9f5844db480e2ef9fce3a72e71122dd010ab7b2920f777966ba25f7eb63819"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==2021.8.28"
 | 
					            "version": "==2021.9.24"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "requests": {
 | 
					        "requests": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1861,11 +1874,11 @@
 | 
				
			|||||||
                "secure"
 | 
					                "secure"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
 | 
					                "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
 | 
				
			||||||
                "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
 | 
					                "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==1.26.6"
 | 
					            "version": "==1.26.7"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "wrapt": {
 | 
					        "wrapt": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
 | 
				
			|||||||
@ -1,3 +1,3 @@
 | 
				
			|||||||
"""authentik"""
 | 
					"""authentik"""
 | 
				
			||||||
__version__ = "2021.9.1-rc3"
 | 
					__version__ = "2021.9.8"
 | 
				
			||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
					ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
				
			||||||
 | 
				
			|||||||
@ -84,7 +84,7 @@ class SystemSerializer(PassiveSerializer):
 | 
				
			|||||||
        return now()
 | 
					        return now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_embedded_outpost_host(self, request: Request) -> str:
 | 
					    def get_embedded_outpost_host(self, request: Request) -> str:
 | 
				
			||||||
        """Get the FQDN configured on the embeddded outpost"""
 | 
					        """Get the FQDN configured on the embedded outpost"""
 | 
				
			||||||
        outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
					        outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
				
			||||||
        if not outposts.exists():
 | 
					        if not outposts.exists():
 | 
				
			||||||
            return ""
 | 
					            return ""
 | 
				
			||||||
 | 
				
			|||||||
@ -8,3 +8,8 @@ class AuthentikAdminConfig(AppConfig):
 | 
				
			|||||||
    name = "authentik.admin"
 | 
					    name = "authentik.admin"
 | 
				
			||||||
    label = "authentik_admin"
 | 
					    label = "authentik_admin"
 | 
				
			||||||
    verbose_name = "authentik Admin"
 | 
					    verbose_name = "authentik Admin"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def ready(self):
 | 
				
			||||||
 | 
					        from authentik.admin.tasks import clear_update_notifications
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        clear_update_notifications.delay()
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ from requests import RequestException
 | 
				
			|||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik import ENV_GIT_HASH_KEY, __version__
 | 
					from authentik import ENV_GIT_HASH_KEY, __version__
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction, Notification
 | 
				
			||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
from authentik.lib.utils.http import get_http_session
 | 
					from authentik.lib.utils.http import get_http_session
 | 
				
			||||||
@ -35,6 +35,18 @@ def _set_prom_info():
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@CELERY_APP.task()
 | 
				
			||||||
 | 
					def clear_update_notifications():
 | 
				
			||||||
 | 
					    """Clear update notifications on startup if the notification was for the version
 | 
				
			||||||
 | 
					    we're running now."""
 | 
				
			||||||
 | 
					    for notification in Notification.objects.filter(event__action=EventAction.UPDATE_AVAILABLE):
 | 
				
			||||||
 | 
					        if "new_version" not in notification.event.context:
 | 
				
			||||||
 | 
					            continue
 | 
				
			||||||
 | 
					        notification_version = notification.event.context["new_version"]
 | 
				
			||||||
 | 
					        if notification_version == __version__:
 | 
				
			||||||
 | 
					            notification.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
					@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
				
			||||||
def update_latest_version(self: MonitoredTask):
 | 
					def update_latest_version(self: MonitoredTask):
 | 
				
			||||||
    """Update latest version info"""
 | 
					    """Update latest version info"""
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,6 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
 | 
				
			|||||||
        raise AuthenticationFailed("Malformed header")
 | 
					        raise AuthenticationFailed("Malformed header")
 | 
				
			||||||
    tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
 | 
					    tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
 | 
				
			||||||
    if not tokens.exists():
 | 
					    if not tokens.exists():
 | 
				
			||||||
        LOGGER.info("Authenticating via secret_key")
 | 
					 | 
				
			||||||
        user = token_secret_key(password)
 | 
					        user = token_secret_key(password)
 | 
				
			||||||
        if not user:
 | 
					        if not user:
 | 
				
			||||||
            raise AuthenticationFailed("Token invalid/expired")
 | 
					            raise AuthenticationFailed("Token invalid/expired")
 | 
				
			||||||
@ -58,6 +57,7 @@ def token_secret_key(value: str) -> Optional[User]:
 | 
				
			|||||||
    outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
					    outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
 | 
				
			||||||
    if not outposts:
 | 
					    if not outposts:
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					    LOGGER.info("Authenticating via secret_key")
 | 
				
			||||||
    outpost = outposts.first()
 | 
					    outpost = outposts.first()
 | 
				
			||||||
    return outpost.user
 | 
					    return outpost.user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@ from drf_spectacular.types import OpenApiTypes
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def build_standard_type(obj, **kwargs):
 | 
					def build_standard_type(obj, **kwargs):
 | 
				
			||||||
    """Build a basic type with optional add ons."""
 | 
					    """Build a basic type with optional add owns."""
 | 
				
			||||||
    schema = build_basic_type(obj)
 | 
					    schema = build_basic_type(obj)
 | 
				
			||||||
    schema.update(kwargs)
 | 
					    schema.update(kwargs)
 | 
				
			||||||
    return schema
 | 
					    return schema
 | 
				
			||||||
 | 
				
			|||||||
@ -63,7 +63,7 @@ class ConfigView(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    @extend_schema(responses={200: ConfigSerializer(many=False)})
 | 
					    @extend_schema(responses={200: ConfigSerializer(many=False)})
 | 
				
			||||||
    def get(self, request: Request) -> Response:
 | 
					    def get(self, request: Request) -> Response:
 | 
				
			||||||
        """Retrive public configuration options"""
 | 
					        """Retrieve public configuration options"""
 | 
				
			||||||
        config = ConfigSerializer(
 | 
					        config = ConfigSerializer(
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                "error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
 | 
					                "error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
 | 
				
			||||||
 | 
				
			|||||||
@ -10,10 +10,13 @@ from rest_framework.permissions import AllowAny
 | 
				
			|||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.throttling import AnonRateThrottle
 | 
					from rest_framework.throttling import AnonRateThrottle
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.api.tasks import sentry_proxy
 | 
					from authentik.api.tasks import sentry_proxy
 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PlainTextParser(BaseParser):
 | 
					class PlainTextParser(BaseParser):
 | 
				
			||||||
    """Plain text parser."""
 | 
					    """Plain text parser."""
 | 
				
			||||||
@ -45,6 +48,7 @@ class SentryTunnelView(APIView):
 | 
				
			|||||||
        """Sentry tunnel, to prevent ad blockers from blocking sentry"""
 | 
					        """Sentry tunnel, to prevent ad blockers from blocking sentry"""
 | 
				
			||||||
        # Only allow usage of this endpoint when error reporting is enabled
 | 
					        # Only allow usage of this endpoint when error reporting is enabled
 | 
				
			||||||
        if not CONFIG.y_bool("error_reporting.enabled", False):
 | 
					        if not CONFIG.y_bool("error_reporting.enabled", False):
 | 
				
			||||||
 | 
					            LOGGER.debug("error reporting disabled")
 | 
				
			||||||
            return HttpResponse(status=400)
 | 
					            return HttpResponse(status=400)
 | 
				
			||||||
        # Body is 2 json objects separated by \n
 | 
					        # Body is 2 json objects separated by \n
 | 
				
			||||||
        full_body = request.body
 | 
					        full_body = request.body
 | 
				
			||||||
@ -55,6 +59,7 @@ class SentryTunnelView(APIView):
 | 
				
			|||||||
        # Check that the DSN is what we expect
 | 
					        # Check that the DSN is what we expect
 | 
				
			||||||
        dsn = header.get("dsn", "")
 | 
					        dsn = header.get("dsn", "")
 | 
				
			||||||
        if dsn != settings.SENTRY_DSN:
 | 
					        if dsn != settings.SENTRY_DSN:
 | 
				
			||||||
 | 
					            LOGGER.debug("Invalid dsn", have=dsn, expected=settings.SENTRY_DSN)
 | 
				
			||||||
            return HttpResponse(status=400)
 | 
					            return HttpResponse(status=400)
 | 
				
			||||||
        sentry_proxy.delay(full_body.decode())
 | 
					        sentry_proxy.delay(full_body.decode())
 | 
				
			||||||
        return HttpResponse(status=204)
 | 
					        return HttpResponse(status=204)
 | 
				
			||||||
 | 
				
			|||||||
@ -99,6 +99,7 @@ from authentik.stages.user_write.api import UserWriteStageViewSet
 | 
				
			|||||||
from authentik.tenants.api import TenantViewSet
 | 
					from authentik.tenants.api import TenantViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router = routers.DefaultRouter()
 | 
					router = routers.DefaultRouter()
 | 
				
			||||||
 | 
					router.include_format_suffixes = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
 | 
					router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
 | 
				
			||||||
router.register("admin/apps", AppsViewSet, basename="apps")
 | 
					router.register("admin/apps", AppsViewSet, basename="apps")
 | 
				
			||||||
 | 
				
			|||||||
@ -82,6 +82,7 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        "description",
 | 
					        "description",
 | 
				
			||||||
        "expires",
 | 
					        "expires",
 | 
				
			||||||
        "expiring",
 | 
					        "expiring",
 | 
				
			||||||
 | 
					        "managed",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    ordering = ["identifier", "expires"]
 | 
					    ordering = ["identifier", "expires"]
 | 
				
			||||||
    permission_classes = [OwnerSuperuserPermissions]
 | 
					    permission_classes = [OwnerSuperuserPermissions]
 | 
				
			||||||
 | 
				
			|||||||
@ -8,6 +8,7 @@ from django.db.transaction import atomic
 | 
				
			|||||||
from django.db.utils import IntegrityError
 | 
					from django.db.utils import IntegrityError
 | 
				
			||||||
from django.urls import reverse_lazy
 | 
					from django.urls import reverse_lazy
 | 
				
			||||||
from django.utils.http import urlencode
 | 
					from django.utils.http import urlencode
 | 
				
			||||||
 | 
					from django.utils.text import slugify
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
 | 
					from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
 | 
				
			||||||
@ -89,6 +90,9 @@ class UserSerializer(ModelSerializer):
 | 
				
			|||||||
            "attributes",
 | 
					            "attributes",
 | 
				
			||||||
            "uid",
 | 
					            "uid",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					        extra_kwargs = {
 | 
				
			||||||
 | 
					            "name": {"allow_blank": True},
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserSelfSerializer(ModelSerializer):
 | 
					class UserSelfSerializer(ModelSerializer):
 | 
				
			||||||
@ -97,9 +101,25 @@ class UserSelfSerializer(ModelSerializer):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    is_superuser = BooleanField(read_only=True)
 | 
					    is_superuser = BooleanField(read_only=True)
 | 
				
			||||||
    avatar = CharField(read_only=True)
 | 
					    avatar = CharField(read_only=True)
 | 
				
			||||||
    groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
 | 
					    groups = SerializerMethodField()
 | 
				
			||||||
    uid = CharField(read_only=True)
 | 
					    uid = CharField(read_only=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema_field(
 | 
				
			||||||
 | 
					        ListSerializer(
 | 
				
			||||||
 | 
					            child=inline_serializer(
 | 
				
			||||||
 | 
					                "UserSelfGroups",
 | 
				
			||||||
 | 
					                {"name": CharField(read_only=True), "pk": CharField(read_only=True)},
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def get_groups(self, user: User):
 | 
				
			||||||
 | 
					        """Return only the group names a user is member of"""
 | 
				
			||||||
 | 
					        for group in user.ak_groups.all():
 | 
				
			||||||
 | 
					            yield {
 | 
				
			||||||
 | 
					                "name": group.name,
 | 
				
			||||||
 | 
					                "pk": group.pk,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = User
 | 
					        model = User
 | 
				
			||||||
@ -116,6 +136,7 @@ class UserSelfSerializer(ModelSerializer):
 | 
				
			|||||||
        ]
 | 
					        ]
 | 
				
			||||||
        extra_kwargs = {
 | 
					        extra_kwargs = {
 | 
				
			||||||
            "is_active": {"read_only": True},
 | 
					            "is_active": {"read_only": True},
 | 
				
			||||||
 | 
					            "name": {"allow_blank": True},
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -207,6 +228,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
    """User Viewset"""
 | 
					    """User Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = User.objects.none()
 | 
					    queryset = User.objects.none()
 | 
				
			||||||
 | 
					    ordering = ["username"]
 | 
				
			||||||
    serializer_class = UserSerializer
 | 
					    serializer_class = UserSerializer
 | 
				
			||||||
    search_fields = ["username", "name", "is_active", "email"]
 | 
					    search_fields = ["username", "name", "is_active", "email"]
 | 
				
			||||||
    filterset_class = UsersFilter
 | 
					    filterset_class = UsersFilter
 | 
				
			||||||
@ -273,7 +295,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    group.users.add(user)
 | 
					                    group.users.add(user)
 | 
				
			||||||
                token = Token.objects.create(
 | 
					                token = Token.objects.create(
 | 
				
			||||||
                    identifier=f"service-account-{username}-password",
 | 
					                    identifier=slugify(f"service-account-{username}-password"),
 | 
				
			||||||
                    intent=TokenIntents.INTENT_APP_PASSWORD,
 | 
					                    intent=TokenIntents.INTENT_APP_PASSWORD,
 | 
				
			||||||
                    user=user,
 | 
					                    user=user,
 | 
				
			||||||
                    expires=now() + timedelta(days=360),
 | 
					                    expires=now() + timedelta(days=360),
 | 
				
			||||||
@ -307,7 +329,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        """Allow users to change information on their own profile"""
 | 
					        """Allow users to change information on their own profile"""
 | 
				
			||||||
        data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data)
 | 
					        data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data)
 | 
				
			||||||
        if not data.is_valid():
 | 
					        if not data.is_valid():
 | 
				
			||||||
            return Response(data.errors)
 | 
					            return Response(data.errors, status=400)
 | 
				
			||||||
        new_user = data.save()
 | 
					        new_user = data.save()
 | 
				
			||||||
        # If we're impersonating, we need to update that user object
 | 
					        # If we're impersonating, we need to update that user object
 | 
				
			||||||
        # since it caches the full object
 | 
					        # since it caches the full object
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    (
 | 
					                    (
 | 
				
			||||||
                        "username_link",
 | 
					                        "username_link",
 | 
				
			||||||
                        "Link to a user with identical username address. Can have security implications when a username is used with another source.",
 | 
					                        "Link to a user with identical username. Can have security implications when a username is used with another source.",
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
                    (
 | 
					                    (
 | 
				
			||||||
                        "username_deny",
 | 
					                        "username_deny",
 | 
				
			||||||
 | 
				
			|||||||
@ -283,7 +283,7 @@ class SourceUserMatchingModes(models.TextChoices):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    USERNAME_LINK = "username_link", _(
 | 
					    USERNAME_LINK = "username_link", _(
 | 
				
			||||||
        (
 | 
					        (
 | 
				
			||||||
            "Link to a user with identical username address. Can have security implications "
 | 
					            "Link to a user with identical username. Can have security implications "
 | 
				
			||||||
            "when a username is used with another source."
 | 
					            "when a username is used with another source."
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,10 @@
 | 
				
			|||||||
"""NotificationTransport API Views"""
 | 
					"""NotificationTransport API Views"""
 | 
				
			||||||
 | 
					from typing import Any
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from drf_spectacular.types import OpenApiTypes
 | 
					from drf_spectacular.types import OpenApiTypes
 | 
				
			||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
					from drf_spectacular.utils import OpenApiResponse, extend_schema
 | 
				
			||||||
from rest_framework.decorators import action
 | 
					from rest_framework.decorators import action
 | 
				
			||||||
 | 
					from rest_framework.exceptions import ValidationError
 | 
				
			||||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
 | 
					from rest_framework.fields import CharField, ListField, SerializerMethodField
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
@ -29,6 +32,14 @@ class NotificationTransportSerializer(ModelSerializer):
 | 
				
			|||||||
        """Return selected mode with a UI Label"""
 | 
					        """Return selected mode with a UI Label"""
 | 
				
			||||||
        return TransportMode(instance.mode).label
 | 
					        return TransportMode(instance.mode).label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
 | 
				
			||||||
 | 
					        """Ensure the required fields are set."""
 | 
				
			||||||
 | 
					        mode = attrs.get("mode")
 | 
				
			||||||
 | 
					        if mode in [TransportMode.WEBHOOK, TransportMode.WEBHOOK_SLACK]:
 | 
				
			||||||
 | 
					            if "webhook_url" not in attrs or attrs.get("webhook_url", "") == "":
 | 
				
			||||||
 | 
					                raise ValidationError("Webhook URL may not be empty.")
 | 
				
			||||||
 | 
					        return attrs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        model = NotificationTransport
 | 
					        model = NotificationTransport
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.7 on 2021-10-04 15:31
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import django.core.validators
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_events", "0018_auto_20210911_2217"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name="notificationtransport",
 | 
				
			||||||
 | 
					            name="webhook_url",
 | 
				
			||||||
 | 
					            field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Optional, Type, Union
 | 
				
			|||||||
from uuid import uuid4
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.core.validators import URLValidator
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from django.http.request import QueryDict
 | 
					from django.http.request import QueryDict
 | 
				
			||||||
@ -223,7 +224,7 @@ class NotificationTransport(models.Model):
 | 
				
			|||||||
    name = models.TextField(unique=True)
 | 
					    name = models.TextField(unique=True)
 | 
				
			||||||
    mode = models.TextField(choices=TransportMode.choices)
 | 
					    mode = models.TextField(choices=TransportMode.choices)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    webhook_url = models.TextField(blank=True)
 | 
					    webhook_url = models.TextField(blank=True, validators=[URLValidator()])
 | 
				
			||||||
    webhook_mapping = models.ForeignKey(
 | 
					    webhook_mapping = models.ForeignKey(
 | 
				
			||||||
        "NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
 | 
					        "NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,13 @@ from django.urls import reverse
 | 
				
			|||||||
from rest_framework.test import APITestCase
 | 
					from rest_framework.test import APITestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.events.models import Event, EventAction, Notification, NotificationSeverity
 | 
					from authentik.events.models import (
 | 
				
			||||||
 | 
					    Event,
 | 
				
			||||||
 | 
					    EventAction,
 | 
				
			||||||
 | 
					    Notification,
 | 
				
			||||||
 | 
					    NotificationSeverity,
 | 
				
			||||||
 | 
					    TransportMode,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestEventsAPI(APITestCase):
 | 
					class TestEventsAPI(APITestCase):
 | 
				
			||||||
@ -41,3 +47,23 @@ class TestEventsAPI(APITestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        notification.refresh_from_db()
 | 
					        notification.refresh_from_db()
 | 
				
			||||||
        self.assertTrue(notification.seen)
 | 
					        self.assertTrue(notification.seen)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_transport(self):
 | 
				
			||||||
 | 
					        """Test transport API"""
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_api:notificationtransport-list"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "name": "foo-with",
 | 
				
			||||||
 | 
					                "mode": TransportMode.WEBHOOK,
 | 
				
			||||||
 | 
					                "webhook_url": "http://foo.com",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 201)
 | 
				
			||||||
 | 
					        response = self.client.post(
 | 
				
			||||||
 | 
					            reverse("authentik_api:notificationtransport-list"),
 | 
				
			||||||
 | 
					            data={
 | 
				
			||||||
 | 
					                "name": "foo-without",
 | 
				
			||||||
 | 
					                "mode": TransportMode.WEBHOOK,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
				
			|||||||
@ -77,7 +77,7 @@ def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:
 | 
				
			|||||||
    final_dict = {}
 | 
					    final_dict = {}
 | 
				
			||||||
    for key, value in source.items():
 | 
					    for key, value in source.items():
 | 
				
			||||||
        if is_dataclass(value):
 | 
					        if is_dataclass(value):
 | 
				
			||||||
            # Because asdict calls `copy.deepcopy(obj)` on everything thats not tuple/dict,
 | 
					            # Because asdict calls `copy.deepcopy(obj)` on everything that's not tuple/dict,
 | 
				
			||||||
            # and deepcopy doesn't work with HttpRequests (neither django nor rest_framework).
 | 
					            # and deepcopy doesn't work with HttpRequests (neither django nor rest_framework).
 | 
				
			||||||
            # Currently, the only dataclass that actually holds an http request is a PolicyRequest
 | 
					            # Currently, the only dataclass that actually holds an http request is a PolicyRequest
 | 
				
			||||||
            if isinstance(value, PolicyRequest):
 | 
					            if isinstance(value, PolicyRequest):
 | 
				
			||||||
 | 
				
			|||||||
@ -108,6 +108,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
    queryset = Flow.objects.all()
 | 
					    queryset = Flow.objects.all()
 | 
				
			||||||
    serializer_class = FlowSerializer
 | 
					    serializer_class = FlowSerializer
 | 
				
			||||||
    lookup_field = "slug"
 | 
					    lookup_field = "slug"
 | 
				
			||||||
 | 
					    ordering = ["slug", "name"]
 | 
				
			||||||
    search_fields = ["name", "slug", "designation", "title"]
 | 
					    search_fields = ["name", "slug", "designation", "title"]
 | 
				
			||||||
    filterset_fields = ["flow_uuid", "name", "slug", "designation"]
 | 
					    filterset_fields = ["flow_uuid", "name", "slug", "designation"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -57,11 +57,11 @@ class FlowPlan:
 | 
				
			|||||||
    markers: list[StageMarker] = field(default_factory=list)
 | 
					    markers: list[StageMarker] = field(default_factory=list)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
 | 
					    def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
 | 
				
			||||||
        """Append `stage` to all stages, optionall with stage marker"""
 | 
					        """Append `stage` to all stages, optionally with stage marker"""
 | 
				
			||||||
        return self.append(FlowStageBinding(stage=stage), marker)
 | 
					        return self.append(FlowStageBinding(stage=stage), marker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
 | 
					    def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
 | 
				
			||||||
        """Append `stage` to all stages, optionall with stage marker"""
 | 
					        """Append `stage` to all stages, optionally with stage marker"""
 | 
				
			||||||
        self.bindings.append(binding)
 | 
					        self.bindings.append(binding)
 | 
				
			||||||
        self.markers.append(marker or StageMarker())
 | 
					        self.markers.append(marker or StageMarker())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -438,7 +438,7 @@ class TestFlowExecutor(APITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # third request, this should trigger the re-evaluate
 | 
					        # third request, this should trigger the re-evaluate
 | 
				
			||||||
        # A get request will evaluate the policies and this will return stage 4
 | 
					        # A get request will evaluate the policies and this will return stage 4
 | 
				
			||||||
        # but it won't save it, hence we cant' check the plan
 | 
					        # but it won't save it, hence we can't check the plan
 | 
				
			||||||
        response = self.client.get(exec_url)
 | 
					        response = self.client.get(exec_url)
 | 
				
			||||||
        self.assertEqual(response.status_code, 200)
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
        self.assertJSONEqual(
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,7 @@ from authentik.lib.sentry import SentryIgnoredException
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
 | 
					def get_attrs(obj: SerializerModel) -> dict[str, Any]:
 | 
				
			||||||
    """Get object's attributes via their serializer, and covert it to a normal dict"""
 | 
					    """Get object's attributes via their serializer, and convert it to a normal dict"""
 | 
				
			||||||
    data = dict(obj.serializer(obj).data)
 | 
					    data = dict(obj.serializer(obj).data)
 | 
				
			||||||
    to_remove = (
 | 
					    to_remove = (
 | 
				
			||||||
        "policies",
 | 
					        "policies",
 | 
				
			||||||
 | 
				
			|||||||
@ -14,12 +14,7 @@ from django.utils.decorators import method_decorator
 | 
				
			|||||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
 | 
					from django.views.decorators.clickjacking import xframe_options_sameorigin
 | 
				
			||||||
from django.views.generic import View
 | 
					from django.views.generic import View
 | 
				
			||||||
from drf_spectacular.types import OpenApiTypes
 | 
					from drf_spectacular.types import OpenApiTypes
 | 
				
			||||||
from drf_spectacular.utils import (
 | 
					from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema
 | 
				
			||||||
    OpenApiParameter,
 | 
					 | 
				
			||||||
    OpenApiResponse,
 | 
					 | 
				
			||||||
    PolymorphicProxySerializer,
 | 
					 | 
				
			||||||
    extend_schema,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from rest_framework.permissions import AllowAny
 | 
					from rest_framework.permissions import AllowAny
 | 
				
			||||||
from rest_framework.views import APIView
 | 
					from rest_framework.views import APIView
 | 
				
			||||||
from sentry_sdk import capture_exception
 | 
					from sentry_sdk import capture_exception
 | 
				
			||||||
@ -131,12 +126,12 @@ class FlowExecutorView(APIView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # pylint: disable=unused-argument, too-many-return-statements
 | 
					    # pylint: disable=unused-argument, too-many-return-statements
 | 
				
			||||||
    def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
 | 
					    def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
 | 
				
			||||||
        # Early check if theres an active Plan for the current session
 | 
					        # Early check if there's an active Plan for the current session
 | 
				
			||||||
        if SESSION_KEY_PLAN in self.request.session:
 | 
					        if SESSION_KEY_PLAN in self.request.session:
 | 
				
			||||||
            self.plan = self.request.session[SESSION_KEY_PLAN]
 | 
					            self.plan = self.request.session[SESSION_KEY_PLAN]
 | 
				
			||||||
            if self.plan.flow_pk != self.flow.pk.hex:
 | 
					            if self.plan.flow_pk != self.flow.pk.hex:
 | 
				
			||||||
                self._logger.warning(
 | 
					                self._logger.warning(
 | 
				
			||||||
                    "f(exec): Found existing plan for other flow, deleteing plan",
 | 
					                    "f(exec): Found existing plan for other flow, deleting plan",
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                # Existing plan is deleted from session and instance
 | 
					                # Existing plan is deleted from session and instance
 | 
				
			||||||
                self.plan = None
 | 
					                self.plan = None
 | 
				
			||||||
@ -213,9 +208,6 @@ class FlowExecutorView(APIView):
 | 
				
			|||||||
                serializers=challenge_types(),
 | 
					                serializers=challenge_types(),
 | 
				
			||||||
                resource_type_field_name="component",
 | 
					                resource_type_field_name="component",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            404: OpenApiResponse(
 | 
					 | 
				
			||||||
                description="No Token found"
 | 
					 | 
				
			||||||
            ),  # This error can be raised by the email stage
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        request=OpenApiTypes.NONE,
 | 
					        request=OpenApiTypes.NONE,
 | 
				
			||||||
        parameters=[
 | 
					        parameters=[
 | 
				
			||||||
@ -441,7 +433,7 @@ class ToDefaultFlow(View):
 | 
				
			|||||||
            plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
 | 
					            plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
 | 
				
			||||||
            if plan.flow_pk != flow.pk.hex:
 | 
					            if plan.flow_pk != flow.pk.hex:
 | 
				
			||||||
                LOGGER.warning(
 | 
					                LOGGER.warning(
 | 
				
			||||||
                    "f(def): Found existing plan for other flow, deleteing plan",
 | 
					                    "f(def): Found existing plan for other flow, deleting plan",
 | 
				
			||||||
                    flow_slug=flow.slug,
 | 
					                    flow_slug=flow.slug,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                del self.request.session[SESSION_KEY_PLAN]
 | 
					                del self.request.session[SESSION_KEY_PLAN]
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ class TestConfig(TestCase):
 | 
				
			|||||||
        config = ConfigLoader()
 | 
					        config = ConfigLoader()
 | 
				
			||||||
        environ["foo"] = "bar"
 | 
					        environ["foo"] = "bar"
 | 
				
			||||||
        self.assertEqual(config.parse_uri("env://foo"), "bar")
 | 
					        self.assertEqual(config.parse_uri("env://foo"), "bar")
 | 
				
			||||||
        self.assertEqual(config.parse_uri("env://fo?bar"), "bar")
 | 
					        self.assertEqual(config.parse_uri("env://foo?bar"), "bar")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_uri_file(self):
 | 
					    def test_uri_file(self):
 | 
				
			||||||
        """Test URI parsing (file load)"""
 | 
					        """Test URI parsing (file load)"""
 | 
				
			||||||
 | 
				
			|||||||
@ -27,7 +27,7 @@ class TestHTTP(TestCase):
 | 
				
			|||||||
        token = Token.objects.create(
 | 
					        token = Token.objects.create(
 | 
				
			||||||
            identifier="test", user=self.user, intent=TokenIntents.INTENT_API
 | 
					            identifier="test", user=self.user, intent=TokenIntents.INTENT_API
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # Invalid, non-existant token
 | 
					        # Invalid, non-existent token
 | 
				
			||||||
        request = self.factory.get(
 | 
					        request = self.factory.get(
 | 
				
			||||||
            "/",
 | 
					            "/",
 | 
				
			||||||
            **{
 | 
					            **{
 | 
				
			||||||
@ -36,7 +36,7 @@ class TestHTTP(TestCase):
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(get_client_ip(request), "127.0.0.1")
 | 
					        self.assertEqual(get_client_ip(request), "127.0.0.1")
 | 
				
			||||||
        # Invalid, user doesn't have permisions
 | 
					        # Invalid, user doesn't have permissions
 | 
				
			||||||
        request = self.factory.get(
 | 
					        request = self.factory.get(
 | 
				
			||||||
            "/",
 | 
					            "/",
 | 
				
			||||||
            **{
 | 
					            **{
 | 
				
			||||||
 | 
				
			|||||||
@ -104,7 +104,7 @@ class OutpostConsumer(AuthJsonConsumer):
 | 
				
			|||||||
                expected=self.outpost.config.kubernetes_replicas,
 | 
					                expected=self.outpost.config.kubernetes_replicas,
 | 
				
			||||||
            ).inc()
 | 
					            ).inc()
 | 
				
			||||||
            LOGGER.debug(
 | 
					            LOGGER.debug(
 | 
				
			||||||
                "added outpost instace to cache",
 | 
					                "added outpost instance to cache",
 | 
				
			||||||
                outpost=self.outpost,
 | 
					                outpost=self.outpost,
 | 
				
			||||||
                instance_uuid=self.last_uid,
 | 
					                instance_uuid=self.last_uid,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
				
			|||||||
@ -38,6 +38,7 @@ class DockerController(BaseController):
 | 
				
			|||||||
            "AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
 | 
					            "AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
 | 
				
			||||||
            "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure).lower(),
 | 
					            "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure).lower(),
 | 
				
			||||||
            "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
					            "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
				
			||||||
 | 
					            "AUTHENTIK_HOST_BROWSER": self.outpost.config.authentik_host_browser,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _comp_env(self, container: Container) -> bool:
 | 
					    def _comp_env(self, container: Container) -> bool:
 | 
				
			||||||
@ -75,6 +76,9 @@ class DockerController(BaseController):
 | 
				
			|||||||
        #   {'HostIp': '0.0.0.0', 'HostPort': '389'},
 | 
					        #   {'HostIp': '0.0.0.0', 'HostPort': '389'},
 | 
				
			||||||
        #   {'HostIp': '::', 'HostPort': '389'}
 | 
					        #   {'HostIp': '::', 'HostPort': '389'}
 | 
				
			||||||
        # ]}
 | 
					        # ]}
 | 
				
			||||||
 | 
					        # If no ports are mapped (either mapping disabled, or host network)
 | 
				
			||||||
 | 
					        if not container.ports:
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
        for port in self.deployment_ports:
 | 
					        for port in self.deployment_ports:
 | 
				
			||||||
            key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
 | 
					            key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
 | 
				
			||||||
            if key not in container.ports:
 | 
					            if key not in container.ports:
 | 
				
			||||||
@ -98,15 +102,16 @@ class DockerController(BaseController):
 | 
				
			|||||||
                "image": image_name,
 | 
					                "image": image_name,
 | 
				
			||||||
                "name": container_name,
 | 
					                "name": container_name,
 | 
				
			||||||
                "detach": True,
 | 
					                "detach": True,
 | 
				
			||||||
                "ports": {
 | 
					 | 
				
			||||||
                    f"{port.inner_port or port.port}/{port.protocol.lower()}": port.port
 | 
					 | 
				
			||||||
                    for port in self.deployment_ports
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                "environment": self._get_env(),
 | 
					                "environment": self._get_env(),
 | 
				
			||||||
                "labels": self._get_labels(),
 | 
					                "labels": self._get_labels(),
 | 
				
			||||||
                "restart_policy": {"Name": "unless-stopped"},
 | 
					                "restart_policy": {"Name": "unless-stopped"},
 | 
				
			||||||
                "network": self.outpost.config.docker_network,
 | 
					                "network": self.outpost.config.docker_network,
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            if self.outpost.config.docker_map_ports:
 | 
				
			||||||
 | 
					                container_args["ports"] = {
 | 
				
			||||||
 | 
					                    f"{port.inner_port or port.port}/{port.protocol.lower()}": str(port.port)
 | 
				
			||||||
 | 
					                    for port in self.deployment_ports
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            if settings.TEST:
 | 
					            if settings.TEST:
 | 
				
			||||||
                del container_args["ports"]
 | 
					                del container_args["ports"]
 | 
				
			||||||
                del container_args["network"]
 | 
					                del container_args["network"]
 | 
				
			||||||
@ -164,11 +169,9 @@ class DockerController(BaseController):
 | 
				
			|||||||
                self.down()
 | 
					                self.down()
 | 
				
			||||||
                return self.up(depth + 1)
 | 
					                return self.up(depth + 1)
 | 
				
			||||||
            # Check that container is healthy
 | 
					            # Check that container is healthy
 | 
				
			||||||
            if (
 | 
					            if container.status == "running" and container.attrs.get("State", {}).get(
 | 
				
			||||||
                container.status == "running"
 | 
					                "Health", {}
 | 
				
			||||||
                and container.attrs.get("State", {}).get("Health", {}).get("Status", "")
 | 
					            ).get("Status", "") not in ["healthy", "starting"]:
 | 
				
			||||||
                != "healthy"
 | 
					 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                # At this point we know the config is correct, but the container isn't healthy,
 | 
					                # At this point we know the config is correct, but the container isn't healthy,
 | 
				
			||||||
                # so we just restart it with the same config
 | 
					                # so we just restart it with the same config
 | 
				
			||||||
                if has_been_created:
 | 
					                if has_been_created:
 | 
				
			||||||
@ -217,6 +220,7 @@ class DockerController(BaseController):
 | 
				
			|||||||
                        "AUTHENTIK_HOST": self.outpost.config.authentik_host,
 | 
					                        "AUTHENTIK_HOST": self.outpost.config.authentik_host,
 | 
				
			||||||
                        "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
 | 
					                        "AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
 | 
				
			||||||
                        "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
					                        "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
				
			||||||
 | 
					                        "AUTHENTIK_HOST_BROWSER": self.outpost.config.authentik_host_browser,
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    "labels": self._get_labels(),
 | 
					                    "labels": self._get_labels(),
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
				
			|||||||
@ -10,7 +10,7 @@ from structlog.stdlib import get_logger
 | 
				
			|||||||
from urllib3.exceptions import HTTPError
 | 
					from urllib3.exceptions import HTTPError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik import __version__
 | 
					from authentik import __version__
 | 
				
			||||||
from authentik.lib.sentry import SentryIgnoredException
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
 | 
				
			||||||
from authentik.outposts.managed import MANAGED_OUTPOST
 | 
					from authentik.outposts.managed import MANAGED_OUTPOST
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
@ -20,18 +20,6 @@ if TYPE_CHECKING:
 | 
				
			|||||||
T = TypeVar("T", V1Pod, V1Deployment)
 | 
					T = TypeVar("T", V1Pod, V1Deployment)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReconcileTrigger(SentryIgnoredException):
 | 
					 | 
				
			||||||
    """Base trigger raised by child classes to notify us"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NeedsRecreate(ReconcileTrigger):
 | 
					 | 
				
			||||||
    """Exception to trigger a complete recreate of the Kubernetes Object"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class NeedsUpdate(ReconcileTrigger):
 | 
					 | 
				
			||||||
    """Exception to trigger an update to the Kubernetes Object"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class KubernetesObjectReconciler(Generic[T]):
 | 
					class KubernetesObjectReconciler(Generic[T]):
 | 
				
			||||||
    """Base Kubernetes Reconciler, handles the basic logic."""
 | 
					    """Base Kubernetes Reconciler, handles the basic logic."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -109,7 +97,7 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
        except (OpenApiException, HTTPError) as exc:
 | 
					        except (OpenApiException, HTTPError) as exc:
 | 
				
			||||||
            # pylint: disable=no-member
 | 
					            # pylint: disable=no-member
 | 
				
			||||||
            if isinstance(exc, ApiException) and exc.status == 404:
 | 
					            if isinstance(exc, ApiException) and exc.status == 404:
 | 
				
			||||||
                self.logger.debug("Failed to get current, assuming non-existant")
 | 
					                self.logger.debug("Failed to get current, assuming non-existent")
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
            self.logger.debug("Other unhandled error", exc=exc)
 | 
					            self.logger.debug("Other unhandled error", exc=exc)
 | 
				
			||||||
            raise exc
 | 
					            raise exc
 | 
				
			||||||
@ -129,7 +117,7 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def retrieve(self) -> T:
 | 
					    def retrieve(self) -> T:
 | 
				
			||||||
        """API Wrapper to retrive object"""
 | 
					        """API Wrapper to retrieve object"""
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, reference: T):
 | 
					    def delete(self, reference: T):
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,9 @@ from kubernetes.client import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
 | 
					from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.utils import compare_ports
 | 
				
			||||||
from authentik.outposts.models import Outpost
 | 
					from authentik.outposts.models import Outpost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
@ -35,7 +37,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
        self.outpost = self.controller.outpost
 | 
					        self.outpost = self.controller.outpost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reconcile(self, current: V1Deployment, reference: V1Deployment):
 | 
					    def reconcile(self, current: V1Deployment, reference: V1Deployment):
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					        compare_ports(
 | 
				
			||||||
 | 
					            current.spec.template.spec.containers[0].ports,
 | 
				
			||||||
 | 
					            reference.spec.template.spec.containers[0].ports,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        if current.spec.replicas != reference.spec.replicas:
 | 
					        if current.spec.replicas != reference.spec.replicas:
 | 
				
			||||||
            raise NeedsUpdate()
 | 
					            raise NeedsUpdate()
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
@ -43,6 +48,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
            != reference.spec.template.spec.containers[0].image
 | 
					            != reference.spec.template.spec.containers[0].image
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            raise NeedsUpdate()
 | 
					            raise NeedsUpdate()
 | 
				
			||||||
 | 
					        super().reconcile(current, reference)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_pod_meta(self) -> dict[str, str]:
 | 
					    def get_pod_meta(self) -> dict[str, str]:
 | 
				
			||||||
        """Get common object metadata"""
 | 
					        """Get common object metadata"""
 | 
				
			||||||
@ -89,6 +95,15 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
                                            )
 | 
					                                            )
 | 
				
			||||||
                                        ),
 | 
					                                        ),
 | 
				
			||||||
                                    ),
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    V1EnvVar(
 | 
				
			||||||
 | 
					                                        name="AUTHENTIK_HOST_BROWSER",
 | 
				
			||||||
 | 
					                                        value_from=V1EnvVarSource(
 | 
				
			||||||
 | 
					                                            secret_key_ref=V1SecretKeySelector(
 | 
				
			||||||
 | 
					                                                name=self.name,
 | 
				
			||||||
 | 
					                                                key="authentik_host_browser",
 | 
				
			||||||
 | 
					                                            )
 | 
				
			||||||
 | 
					                                        ),
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
                                    V1EnvVar(
 | 
					                                    V1EnvVar(
 | 
				
			||||||
                                        name="AUTHENTIK_TOKEN",
 | 
					                                        name="AUTHENTIK_TOKEN",
 | 
				
			||||||
                                        value_from=V1EnvVarSource(
 | 
					                                        value_from=V1EnvVarSource(
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,8 @@ from typing import TYPE_CHECKING
 | 
				
			|||||||
from kubernetes.client import CoreV1Api, V1Secret
 | 
					from kubernetes.client import CoreV1Api, V1Secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
 | 
					from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
					    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
				
			||||||
@ -26,7 +27,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
				
			|||||||
    def reconcile(self, current: V1Secret, reference: V1Secret):
 | 
					    def reconcile(self, current: V1Secret, reference: V1Secret):
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					        super().reconcile(current, reference)
 | 
				
			||||||
        for key in reference.data.keys():
 | 
					        for key in reference.data.keys():
 | 
				
			||||||
            if current.data[key] != reference.data[key]:
 | 
					            if key not in current.data or current.data[key] != reference.data[key]:
 | 
				
			||||||
                raise NeedsUpdate()
 | 
					                raise NeedsUpdate()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_reference_object(self) -> V1Secret:
 | 
					    def get_reference_object(self) -> V1Secret:
 | 
				
			||||||
@ -40,6 +41,9 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
				
			|||||||
                    str(self.controller.outpost.config.authentik_host_insecure)
 | 
					                    str(self.controller.outpost.config.authentik_host_insecure)
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                "token": b64string(self.controller.outpost.token.key),
 | 
					                "token": b64string(self.controller.outpost.token.key),
 | 
				
			||||||
 | 
					                "authentik_host_browser": b64string(
 | 
				
			||||||
 | 
					                    self.controller.outpost.config.authentik_host_browser
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,8 +4,9 @@ from typing import TYPE_CHECKING
 | 
				
			|||||||
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
 | 
					from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsRecreate
 | 
					from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 | 
				
			||||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
 | 
					from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.utils import compare_ports
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
					    from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
				
			||||||
@ -19,12 +20,12 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
 | 
				
			|||||||
        self.api = CoreV1Api(controller.client)
 | 
					        self.api = CoreV1Api(controller.client)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reconcile(self, current: V1Service, reference: V1Service):
 | 
					    def reconcile(self, current: V1Service, reference: V1Service):
 | 
				
			||||||
 | 
					        compare_ports(current.spec.ports, reference.spec.ports)
 | 
				
			||||||
 | 
					        # run the base reconcile last, as that will probably raise NeedsUpdate
 | 
				
			||||||
 | 
					        # after an authentik update. However the ports might have also changed during
 | 
				
			||||||
 | 
					        # the update, so this causes the service to be re-created with higher
 | 
				
			||||||
 | 
					        # priority than being updated.
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					        super().reconcile(current, reference)
 | 
				
			||||||
        if len(current.spec.ports) != len(reference.spec.ports):
 | 
					 | 
				
			||||||
            raise NeedsRecreate()
 | 
					 | 
				
			||||||
        for port in reference.spec.ports:
 | 
					 | 
				
			||||||
            if port not in current.spec.ports:
 | 
					 | 
				
			||||||
                raise NeedsRecreate()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_reference_object(self) -> V1Service:
 | 
					    def get_reference_object(self) -> V1Service:
 | 
				
			||||||
        """Get deployment object for outpost"""
 | 
					        """Get deployment object for outpost"""
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								authentik/outposts/controllers/k8s/triggers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								authentik/outposts/controllers/k8s/triggers.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					"""exceptions used by the kubernetes reconciler to trigger updates"""
 | 
				
			||||||
 | 
					from authentik.lib.sentry import SentryIgnoredException
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ReconcileTrigger(SentryIgnoredException):
 | 
				
			||||||
 | 
					    """Base trigger raised by child classes to notify us"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NeedsRecreate(ReconcileTrigger):
 | 
				
			||||||
 | 
					    """Exception to trigger a complete recreate of the Kubernetes Object"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class NeedsUpdate(ReconcileTrigger):
 | 
				
			||||||
 | 
					    """Exception to trigger an update to the Kubernetes Object"""
 | 
				
			||||||
@ -1,8 +1,11 @@
 | 
				
			|||||||
"""k8s utils"""
 | 
					"""k8s utils"""
 | 
				
			||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from kubernetes.client.models.v1_container_port import V1ContainerPort
 | 
				
			||||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
 | 
					from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsRecreate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_namespace() -> str:
 | 
					def get_namespace() -> str:
 | 
				
			||||||
    """Get the namespace if we're running in a pod, otherwise default to default"""
 | 
					    """Get the namespace if we're running in a pod, otherwise default to default"""
 | 
				
			||||||
@ -11,3 +14,12 @@ def get_namespace() -> str:
 | 
				
			|||||||
        with open(path, "r", encoding="utf8") as _namespace_file:
 | 
					        with open(path, "r", encoding="utf8") as _namespace_file:
 | 
				
			||||||
            return _namespace_file.read()
 | 
					            return _namespace_file.read()
 | 
				
			||||||
    return "default"
 | 
					    return "default"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def compare_ports(current: list[V1ContainerPort], reference: list[V1ContainerPort]):
 | 
				
			||||||
 | 
					    """Compare ports of a list"""
 | 
				
			||||||
 | 
					    if len(current) != len(reference):
 | 
				
			||||||
 | 
					        raise NeedsRecreate()
 | 
				
			||||||
 | 
					    for port in reference:
 | 
				
			||||||
 | 
					        if port not in current:
 | 
				
			||||||
 | 
					            raise NeedsRecreate()
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,7 @@ class DockerInlineTLS:
 | 
				
			|||||||
        return str(path)
 | 
					        return str(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def write(self) -> TLSConfig:
 | 
					    def write(self) -> TLSConfig:
 | 
				
			||||||
        """Create TLSConfig with Certificate Keypairs"""
 | 
					        """Create TLSConfig with Certificate Key pairs"""
 | 
				
			||||||
        # So yes, this is quite ugly. But sadly, there is no clean way to pass
 | 
					        # So yes, this is quite ugly. But sadly, there is no clean way to pass
 | 
				
			||||||
        # docker-py (which is using requests (which is using urllib3)) a certificate
 | 
					        # docker-py (which is using requests (which is using urllib3)) a certificate
 | 
				
			||||||
        # for verification or authentication as string.
 | 
					        # for verification or authentication as string.
 | 
				
			||||||
 | 
				
			|||||||
@ -64,6 +64,7 @@ class OutpostConfig:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    authentik_host: str = ""
 | 
					    authentik_host: str = ""
 | 
				
			||||||
    authentik_host_insecure: bool = False
 | 
					    authentik_host_insecure: bool = False
 | 
				
			||||||
 | 
					    authentik_host_browser: str = ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log_level: str = CONFIG.y("log_level")
 | 
					    log_level: str = CONFIG.y("log_level")
 | 
				
			||||||
    error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
 | 
					    error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
 | 
				
			||||||
@ -71,6 +72,7 @@ class OutpostConfig:
 | 
				
			|||||||
    object_naming_template: str = field(default="ak-outpost-%(name)s")
 | 
					    object_naming_template: str = field(default="ak-outpost-%(name)s")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    docker_network: Optional[str] = field(default=None)
 | 
					    docker_network: Optional[str] = field(default=None)
 | 
				
			||||||
 | 
					    docker_map_ports: bool = field(default=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    kubernetes_replicas: int = field(default=1)
 | 
					    kubernetes_replicas: int = field(default=1)
 | 
				
			||||||
    kubernetes_namespace: str = field(default_factory=get_namespace)
 | 
					    kubernetes_namespace: str = field(default_factory=get_namespace)
 | 
				
			||||||
@ -339,19 +341,8 @@ class Outpost(ManagedModel):
 | 
				
			|||||||
        """Username for service user"""
 | 
					        """Username for service user"""
 | 
				
			||||||
        return f"ak-outpost-{self.uuid.hex}"
 | 
					        return f"ak-outpost-{self.uuid.hex}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    def build_user_permissions(self, user: User):
 | 
				
			||||||
    def user(self) -> User:
 | 
					        """Create per-object and global permissions for outpost service-account"""
 | 
				
			||||||
        """Get/create user with access to all required objects"""
 | 
					 | 
				
			||||||
        users = User.objects.filter(username=self.user_identifier)
 | 
					 | 
				
			||||||
        if not users.exists():
 | 
					 | 
				
			||||||
            user: User = User.objects.create(username=self.user_identifier)
 | 
					 | 
				
			||||||
            user.set_unusable_password()
 | 
					 | 
				
			||||||
            user.save()
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            user = users.first()
 | 
					 | 
				
			||||||
        user.attributes[USER_ATTRIBUTE_SA] = True
 | 
					 | 
				
			||||||
        user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
 | 
					 | 
				
			||||||
        user.save()
 | 
					 | 
				
			||||||
        # To ensure the user only has the correct permissions, we delete all of them and re-add
 | 
					        # To ensure the user only has the correct permissions, we delete all of them and re-add
 | 
				
			||||||
        # the ones the user needs
 | 
					        # the ones the user needs
 | 
				
			||||||
        with transaction.atomic():
 | 
					        with transaction.atomic():
 | 
				
			||||||
@ -395,6 +386,23 @@ class Outpost(ManagedModel):
 | 
				
			|||||||
            "Updated service account's permissions",
 | 
					            "Updated service account's permissions",
 | 
				
			||||||
            perms=UserObjectPermission.objects.filter(user=user),
 | 
					            perms=UserObjectPermission.objects.filter(user=user),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def user(self) -> User:
 | 
				
			||||||
 | 
					        """Get/create user with access to all required objects"""
 | 
				
			||||||
 | 
					        users = User.objects.filter(username=self.user_identifier)
 | 
				
			||||||
 | 
					        should_create_user = not users.exists()
 | 
				
			||||||
 | 
					        if should_create_user:
 | 
				
			||||||
 | 
					            user: User = User.objects.create(username=self.user_identifier)
 | 
				
			||||||
 | 
					            user.set_unusable_password()
 | 
				
			||||||
 | 
					            user.save()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            user = users.first()
 | 
				
			||||||
 | 
					        user.attributes[USER_ATTRIBUTE_SA] = True
 | 
				
			||||||
 | 
					        user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
 | 
				
			||||||
 | 
					        user.save()
 | 
				
			||||||
 | 
					        if should_create_user:
 | 
				
			||||||
 | 
					            self.build_user_permissions(user)
 | 
				
			||||||
        return user
 | 
					        return user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
 | 
				
			|||||||
@ -126,6 +126,7 @@ def outpost_token_ensurer(self: MonitoredTask):
 | 
				
			|||||||
    all_outposts = Outpost.objects.all()
 | 
					    all_outposts = Outpost.objects.all()
 | 
				
			||||||
    for outpost in all_outposts:
 | 
					    for outpost in all_outposts:
 | 
				
			||||||
        _ = outpost.token
 | 
					        _ = outpost.token
 | 
				
			||||||
 | 
					        outpost.build_user_permissions(outpost.user)
 | 
				
			||||||
    self.set_status(
 | 
					    self.set_status(
 | 
				
			||||||
        TaskResult(
 | 
					        TaskResult(
 | 
				
			||||||
            TaskResultStatus.SUCCESSFUL,
 | 
					            TaskResultStatus.SUCCESSFUL,
 | 
				
			||||||
@ -181,7 +182,7 @@ def outpost_post_save(model_class: str, model_pk: Any):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def outpost_send_update(model_instace: Model):
 | 
					def outpost_send_update(model_instace: Model):
 | 
				
			||||||
    """Send outpost update to all registered outposts, irregardless to which authentik
 | 
					    """Send outpost update to all registered outposts, regardless to which authentik
 | 
				
			||||||
    instance they are connected"""
 | 
					    instance they are connected"""
 | 
				
			||||||
    channel_layer = get_channel_layer()
 | 
					    channel_layer = get_channel_layer()
 | 
				
			||||||
    if isinstance(model_instace, OutpostModel):
 | 
					    if isinstance(model_instace, OutpostModel):
 | 
				
			||||||
@ -196,7 +197,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
 | 
				
			|||||||
    # Ensure token again, because this function is called when anything related to an
 | 
					    # Ensure token again, because this function is called when anything related to an
 | 
				
			||||||
    # OutpostModel is saved, so we can be sure permissions are right
 | 
					    # OutpostModel is saved, so we can be sure permissions are right
 | 
				
			||||||
    _ = outpost.token
 | 
					    _ = outpost.token
 | 
				
			||||||
    _ = outpost.user
 | 
					    outpost.build_user_permissions(outpost.user)
 | 
				
			||||||
    if not layer:  # pragma: no cover
 | 
					    if not layer:  # pragma: no cover
 | 
				
			||||||
        layer = get_channel_layer()
 | 
					        layer = get_channel_layer()
 | 
				
			||||||
    for state in OutpostState.for_outpost(outpost):
 | 
					    for state in OutpostState.for_outpost(outpost):
 | 
				
			||||||
@ -208,7 +209,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
 | 
				
			|||||||
@CELERY_APP.task()
 | 
					@CELERY_APP.task()
 | 
				
			||||||
def outpost_local_connection():
 | 
					def outpost_local_connection():
 | 
				
			||||||
    """Checks the local environment and create Service connections."""
 | 
					    """Checks the local environment and create Service connections."""
 | 
				
			||||||
    # Explicitly check against token filename, as thats
 | 
					    # Explicitly check against token filename, as that's
 | 
				
			||||||
    # only present when the integration is enabled
 | 
					    # only present when the integration is enabled
 | 
				
			||||||
    if Path(SERVICE_TOKEN_FILENAME).exists():
 | 
					    if Path(SERVICE_TOKEN_FILENAME).exists():
 | 
				
			||||||
        LOGGER.debug("Detected in-cluster Kubernetes Config")
 | 
					        LOGGER.debug("Detected in-cluster Kubernetes Config")
 | 
				
			||||||
 | 
				
			|||||||
@ -87,6 +87,7 @@ class PolicyViewSet(
 | 
				
			|||||||
        "promptstage": ["isnull"],
 | 
					        "promptstage": ["isnull"],
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    search_fields = ["name"]
 | 
					    search_fields = ["name"]
 | 
				
			||||||
 | 
					    ordering = ["name"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):  # pragma: no cover
 | 
					    def get_queryset(self):  # pragma: no cover
 | 
				
			||||||
        return Policy.objects.select_subclasses().prefetch_related("bindings", "promptstage_set")
 | 
					        return Policy.objects.select_subclasses().prefetch_related("bindings", "promptstage_set")
 | 
				
			||||||
 | 
				
			|||||||
@ -3,8 +3,10 @@ from ipaddress import ip_address, ip_network
 | 
				
			|||||||
from typing import TYPE_CHECKING, Optional
 | 
					from typing import TYPE_CHECKING, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
 | 
					from django_otp import devices_for_user
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_SSO
 | 
					from authentik.flows.planner import PLAN_CONTEXT_SSO
 | 
				
			||||||
from authentik.lib.expression.evaluator import BaseEvaluator
 | 
					from authentik.lib.expression.evaluator import BaseEvaluator
 | 
				
			||||||
from authentik.lib.utils.http import get_client_ip
 | 
					from authentik.lib.utils.http import get_client_ip
 | 
				
			||||||
@ -28,6 +30,7 @@ class PolicyEvaluator(BaseEvaluator):
 | 
				
			|||||||
        self._messages = []
 | 
					        self._messages = []
 | 
				
			||||||
        self._context["ak_logger"] = get_logger(policy_name)
 | 
					        self._context["ak_logger"] = get_logger(policy_name)
 | 
				
			||||||
        self._context["ak_message"] = self.expr_func_message
 | 
					        self._context["ak_message"] = self.expr_func_message
 | 
				
			||||||
 | 
					        self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator
 | 
				
			||||||
        self._context["ip_address"] = ip_address
 | 
					        self._context["ip_address"] = ip_address
 | 
				
			||||||
        self._context["ip_network"] = ip_network
 | 
					        self._context["ip_network"] = ip_network
 | 
				
			||||||
        self._filename = policy_name or "PolicyEvaluator"
 | 
					        self._filename = policy_name or "PolicyEvaluator"
 | 
				
			||||||
@ -36,6 +39,19 @@ class PolicyEvaluator(BaseEvaluator):
 | 
				
			|||||||
        """Wrapper to append to messages list, which is returned with PolicyResult"""
 | 
					        """Wrapper to append to messages list, which is returned with PolicyResult"""
 | 
				
			||||||
        self._messages.append(message)
 | 
					        self._messages.append(message)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def expr_func_user_has_authenticator(
 | 
				
			||||||
 | 
					        self, user: User, device_type: Optional[str] = None
 | 
				
			||||||
 | 
					    ) -> bool:
 | 
				
			||||||
 | 
					        """Check if a user has any authenticator devices, optionally matching *device_type*"""
 | 
				
			||||||
 | 
					        user_devices = devices_for_user(user)
 | 
				
			||||||
 | 
					        if device_type:
 | 
				
			||||||
 | 
					            for device in user_devices:
 | 
				
			||||||
 | 
					                device_class = device.__class__.__name__.lower().replace("device", "")
 | 
				
			||||||
 | 
					                if device_class == device_type:
 | 
				
			||||||
 | 
					                    return True
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					        return len(user_devices) > 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_policy_request(self, request: PolicyRequest):
 | 
					    def set_policy_request(self, request: PolicyRequest):
 | 
				
			||||||
        """Update context based on policy request (if http request is given, update that too)"""
 | 
					        """Update context based on policy request (if http request is given, update that too)"""
 | 
				
			||||||
        # update website/docs/expressions/_objects.md
 | 
					        # update website/docs/expressions/_objects.md
 | 
				
			||||||
 | 
				
			|||||||
@ -46,7 +46,7 @@ def cache_key(binding: PolicyBinding, request: PolicyRequest) -> str:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PolicyProcess(PROCESS_CLASS):
 | 
					class PolicyProcess(PROCESS_CLASS):
 | 
				
			||||||
    """Evaluate a single policy within a seprate process"""
 | 
					    """Evaluate a single policy within a separate process"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    connection: Connection
 | 
					    connection: Connection
 | 
				
			||||||
    binding: PolicyBinding
 | 
					    binding: PolicyBinding
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,7 @@ def update_score(request: HttpRequest, username: str, amount: int):
 | 
				
			|||||||
@receiver(user_login_failed)
 | 
					@receiver(user_login_failed)
 | 
				
			||||||
# pylint: disable=unused-argument
 | 
					# pylint: disable=unused-argument
 | 
				
			||||||
def handle_failed_login(sender, request, credentials, **_):
 | 
					def handle_failed_login(sender, request, credentials, **_):
 | 
				
			||||||
    """Lower Score for failed loging attempts"""
 | 
					    """Lower Score for failed login attempts"""
 | 
				
			||||||
    if "username" in credentials:
 | 
					    if "username" in credentials:
 | 
				
			||||||
        update_score(request, credentials.get("username"), -1)
 | 
					        update_score(request, credentials.get("username"), -1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ from authentik.policies.types import PolicyRequest
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def clear_policy_cache():
 | 
					def clear_policy_cache():
 | 
				
			||||||
    """Ensure no policy-related keys are stil cached"""
 | 
					    """Ensure no policy-related keys are still cached"""
 | 
				
			||||||
    keys = cache.keys("policy_*")
 | 
					    keys = cache.keys("policy_*")
 | 
				
			||||||
    cache.delete(keys)
 | 
					    cache.delete(keys)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +1,15 @@
 | 
				
			|||||||
"""LDAP Provider Docker Contoller"""
 | 
					"""LDAP Provider Docker Controller"""
 | 
				
			||||||
from authentik.outposts.controllers.base import DeploymentPort
 | 
					from authentik.outposts.controllers.base import DeploymentPort
 | 
				
			||||||
from authentik.outposts.controllers.docker import DockerController
 | 
					from authentik.outposts.controllers.docker import DockerController
 | 
				
			||||||
from authentik.outposts.models import DockerServiceConnection, Outpost
 | 
					from authentik.outposts.models import DockerServiceConnection, Outpost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LDAPDockerController(DockerController):
 | 
					class LDAPDockerController(DockerController):
 | 
				
			||||||
    """LDAP Provider Docker Contoller"""
 | 
					    """LDAP Provider Docker Controller"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
 | 
					    def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
 | 
				
			||||||
        super().__init__(outpost, connection)
 | 
					        super().__init__(outpost, connection)
 | 
				
			||||||
        self.deployment_ports = [
 | 
					        self.deployment_ports = [
 | 
				
			||||||
            DeploymentPort(389, "ldap", "tcp", 3389),
 | 
					            DeploymentPort(389, "ldap", "tcp", 3389),
 | 
				
			||||||
            DeploymentPort(636, "ldaps", "tcp", 6636),
 | 
					            DeploymentPort(636, "ldaps", "tcp", 6636),
 | 
				
			||||||
            DeploymentPort(9300, "http-metrics", "tcp", 9300),
 | 
					 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,11 @@
 | 
				
			|||||||
"""LDAP Provider Kubernetes Contoller"""
 | 
					"""LDAP Provider Kubernetes Controller"""
 | 
				
			||||||
from authentik.outposts.controllers.base import DeploymentPort
 | 
					from authentik.outposts.controllers.base import DeploymentPort
 | 
				
			||||||
from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
					from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
				
			||||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost
 | 
					from authentik.outposts.models import KubernetesServiceConnection, Outpost
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LDAPKubernetesController(KubernetesController):
 | 
					class LDAPKubernetesController(KubernetesController):
 | 
				
			||||||
    """LDAP Provider Kubernetes Contoller"""
 | 
					    """LDAP Provider Kubernetes Controller"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
 | 
					    def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
 | 
				
			||||||
        super().__init__(outpost, connection)
 | 
					        super().__init__(outpost, connection)
 | 
				
			||||||
 | 
				
			|||||||
@ -153,7 +153,7 @@ def protected_resource_view(scopes: list[str]):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                if not set(scopes).issubset(set(token.scope)):
 | 
					                if not set(scopes).issubset(set(token.scope)):
 | 
				
			||||||
                    LOGGER.warning(
 | 
					                    LOGGER.warning(
 | 
				
			||||||
                        "Scope missmatch.",
 | 
					                        "Scope mismatch.",
 | 
				
			||||||
                        required=set(scopes),
 | 
					                        required=set(scopes),
 | 
				
			||||||
                        token_has=set(token.scope),
 | 
					                        token_has=set(token.scope),
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
				
			|||||||
@ -33,7 +33,7 @@ class UserInfoView(View):
 | 
				
			|||||||
        for scope in ScopeMapping.objects.filter(scope_name__in=scopes).order_by("scope_name"):
 | 
					        for scope in ScopeMapping.objects.filter(scope_name__in=scopes).order_by("scope_name"):
 | 
				
			||||||
            if scope.description != "":
 | 
					            if scope.description != "":
 | 
				
			||||||
                scope_descriptions.append({"id": scope.scope_name, "name": scope.description})
 | 
					                scope_descriptions.append({"id": scope.scope_name, "name": scope.description})
 | 
				
			||||||
        # GitHub Compatibility Scopes are handeled differently, since they required custom paths
 | 
					        # GitHub Compatibility Scopes are handled differently, since they required custom paths
 | 
				
			||||||
        # Hence they don't exist as Scope objects
 | 
					        # Hence they don't exist as Scope objects
 | 
				
			||||||
        github_scope_map = {
 | 
					        github_scope_map = {
 | 
				
			||||||
            SCOPE_GITHUB_USER: ("GitHub Compatibility: Access your User Information"),
 | 
					            SCOPE_GITHUB_USER: ("GitHub Compatibility: Access your User Information"),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
"""ProxyProvider API Views"""
 | 
					"""ProxyProvider API Views"""
 | 
				
			||||||
from typing import Any
 | 
					from typing import Any, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from drf_spectacular.utils import extend_schema_field
 | 
					from drf_spectacular.utils import extend_schema_field
 | 
				
			||||||
from rest_framework.exceptions import ValidationError
 | 
					from rest_framework.exceptions import ValidationError
 | 
				
			||||||
@ -10,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
 | 
				
			|||||||
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
 | 
					from authentik.core.api.utils import PassiveSerializer
 | 
				
			||||||
 | 
					from authentik.lib.utils.time import timedelta_from_string
 | 
				
			||||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
 | 
					from authentik.providers.oauth2.views.provider import ProviderInfoView
 | 
				
			||||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
 | 
					from authentik.providers.proxy.models import ProxyMode, ProxyProvider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -106,6 +107,16 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
 | 
				
			|||||||
    """Proxy provider serializer for outposts"""
 | 
					    """Proxy provider serializer for outposts"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    oidc_configuration = SerializerMethodField()
 | 
					    oidc_configuration = SerializerMethodField()
 | 
				
			||||||
 | 
					    token_validity = SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @extend_schema_field(OpenIDConnectConfigurationSerializer)
 | 
				
			||||||
 | 
					    def get_oidc_configuration(self, obj: ProxyProvider):
 | 
				
			||||||
 | 
					        """Embed OpenID Connect provider information"""
 | 
				
			||||||
 | 
					        return ProviderInfoView(request=self.context["request"]._request).get_info(obj)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_token_validity(self, obj: ProxyProvider) -> Optional[float]:
 | 
				
			||||||
 | 
					        """Get token validity as second count"""
 | 
				
			||||||
 | 
					        return timedelta_from_string(obj.token_validity).total_seconds()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -127,13 +138,9 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
 | 
				
			|||||||
            "basic_auth_user_attribute",
 | 
					            "basic_auth_user_attribute",
 | 
				
			||||||
            "mode",
 | 
					            "mode",
 | 
				
			||||||
            "cookie_domain",
 | 
					            "cookie_domain",
 | 
				
			||||||
 | 
					            "token_validity",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema_field(OpenIDConnectConfigurationSerializer)
 | 
					 | 
				
			||||||
    def get_oidc_configuration(self, obj: ProxyProvider):
 | 
					 | 
				
			||||||
        """Embed OpenID Connect provider information"""
 | 
					 | 
				
			||||||
        return ProviderInfoView(request=self.context["request"]._request).get_info(obj)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProxyOutpostConfigViewSet(ReadOnlyModelViewSet):
 | 
					class ProxyOutpostConfigViewSet(ReadOnlyModelViewSet):
 | 
				
			||||||
    """ProxyProvider Viewset"""
 | 
					    """ProxyProvider Viewset"""
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""Proxy Provider Docker Contoller"""
 | 
					"""Proxy Provider Docker Controller"""
 | 
				
			||||||
from urllib.parse import urlparse
 | 
					from urllib.parse import urlparse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import DeploymentPort
 | 
					from authentik.outposts.controllers.base import DeploymentPort
 | 
				
			||||||
@ -8,13 +8,12 @@ from authentik.providers.proxy.models import ProxyProvider
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProxyDockerController(DockerController):
 | 
					class ProxyDockerController(DockerController):
 | 
				
			||||||
    """Proxy Provider Docker Contoller"""
 | 
					    """Proxy Provider Docker Controller"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
 | 
					    def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
 | 
				
			||||||
        super().__init__(outpost, connection)
 | 
					        super().__init__(outpost, connection)
 | 
				
			||||||
        self.deployment_ports = [
 | 
					        self.deployment_ports = [
 | 
				
			||||||
            DeploymentPort(9000, "http", "tcp"),
 | 
					            DeploymentPort(9000, "http", "tcp"),
 | 
				
			||||||
            DeploymentPort(9300, "http-metrics", "tcp"),
 | 
					 | 
				
			||||||
            DeploymentPort(9443, "https", "tcp"),
 | 
					            DeploymentPort(9443, "https", "tcp"),
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -30,6 +29,11 @@ class ProxyDockerController(DockerController):
 | 
				
			|||||||
        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)})"
 | 
				
			||||||
        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[f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path"] = "/"
 | 
					        labels[
 | 
				
			||||||
 | 
					            f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path"
 | 
				
			||||||
 | 
					        ] = "/akprox/ping"
 | 
				
			||||||
 | 
					        labels[
 | 
				
			||||||
 | 
					            f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.port"
 | 
				
			||||||
 | 
					        ] = "9300"
 | 
				
			||||||
        labels[f"traefik.http.services.{traefik_name}-service.loadbalancer.server.port"] = "9000"
 | 
					        labels[f"traefik.http.services.{traefik_name}-service.loadbalancer.server.port"] = "9000"
 | 
				
			||||||
        return labels
 | 
					        return labels
 | 
				
			||||||
 | 
				
			|||||||
@ -14,11 +14,8 @@ from kubernetes.client import (
 | 
				
			|||||||
from kubernetes.client.models.networking_v1beta1_ingress_rule import NetworkingV1beta1IngressRule
 | 
					from kubernetes.client.models.networking_v1beta1_ingress_rule import NetworkingV1beta1IngressRule
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import (
 | 
					from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 | 
				
			||||||
    KubernetesObjectReconciler,
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
 | 
				
			||||||
    NeedsRecreate,
 | 
					 | 
				
			||||||
    NeedsUpdate,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
 | 
					from authentik.providers.proxy.models import ProxyMode, ProxyProvider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
@ -63,8 +60,15 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
 | 
				
			|||||||
        have_hosts_tls = []
 | 
					        have_hosts_tls = []
 | 
				
			||||||
        if current.spec.tls:
 | 
					        if current.spec.tls:
 | 
				
			||||||
            for tls_config in current.spec.tls:
 | 
					            for tls_config in current.spec.tls:
 | 
				
			||||||
                if tls_config and tls_config.hosts:
 | 
					                if not tls_config:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					                if tls_config.hosts:
 | 
				
			||||||
                    have_hosts_tls += tls_config.hosts
 | 
					                    have_hosts_tls += tls_config.hosts
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    tls_config.secret_name
 | 
				
			||||||
 | 
					                    != self.controller.outpost.config.kubernetes_ingress_secret_name
 | 
				
			||||||
 | 
					                ):
 | 
				
			||||||
 | 
					                    raise NeedsUpdate()
 | 
				
			||||||
        have_hosts_tls.sort()
 | 
					        have_hosts_tls.sort()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if have_hosts != expected_hosts:
 | 
					        if have_hosts != expected_hosts:
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,8 @@ from dacite import from_dict
 | 
				
			|||||||
from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
 | 
					from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
 | 
					from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
 | 
				
			||||||
 | 
					from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
 | 
				
			||||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
 | 
					from authentik.providers.proxy.models import ProxyMode, ProxyProvider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
@ -109,11 +110,18 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
 | 
				
			|||||||
                    address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
 | 
					                    address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
 | 
				
			||||||
                    authResponseHeaders=[
 | 
					                    authResponseHeaders=[
 | 
				
			||||||
                        "Set-Cookie",
 | 
					                        "Set-Cookie",
 | 
				
			||||||
 | 
					                        # Legacy headers, remove after 2022.1
 | 
				
			||||||
                        "X-Auth-Username",
 | 
					                        "X-Auth-Username",
 | 
				
			||||||
                        "X-Auth-Groups",
 | 
					                        "X-Auth-Groups",
 | 
				
			||||||
                        "X-Forwarded-Email",
 | 
					                        "X-Forwarded-Email",
 | 
				
			||||||
                        "X-Forwarded-Preferred-Username",
 | 
					                        "X-Forwarded-Preferred-Username",
 | 
				
			||||||
                        "X-Forwarded-User",
 | 
					                        "X-Forwarded-User",
 | 
				
			||||||
 | 
					                        # New headers, unique prefix
 | 
				
			||||||
 | 
					                        "X-authentik-username",
 | 
				
			||||||
 | 
					                        "X-authentik-groups",
 | 
				
			||||||
 | 
					                        "X-authentik-email",
 | 
				
			||||||
 | 
					                        "X-authentik-name",
 | 
				
			||||||
 | 
					                        "X-authentik-uid",
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                    trustForwardHeader=True,
 | 
					                    trustForwardHeader=True,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""Proxy Provider Kubernetes Contoller"""
 | 
					"""Proxy Provider Kubernetes Controller"""
 | 
				
			||||||
from authentik.outposts.controllers.base import DeploymentPort
 | 
					from authentik.outposts.controllers.base import DeploymentPort
 | 
				
			||||||
from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
					from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
				
			||||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost
 | 
					from authentik.outposts.models import KubernetesServiceConnection, Outpost
 | 
				
			||||||
@ -7,7 +7,7 @@ from authentik.providers.proxy.controllers.k8s.traefik import TraefikMiddlewareR
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProxyKubernetesController(KubernetesController):
 | 
					class ProxyKubernetesController(KubernetesController):
 | 
				
			||||||
    """Proxy Provider Kubernetes Contoller"""
 | 
					    """Proxy Provider Kubernetes Controller"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
 | 
					    def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
 | 
				
			||||||
        super().__init__(outpost, connection)
 | 
					        super().__init__(outpost, connection)
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ from getpass import getuser
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.core.management.base import BaseCommand
 | 
					from django.core.management.base import BaseCommand
 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
 | 
					from django.utils.text import slugify
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
@ -42,7 +43,7 @@ class Command(BaseCommand):
 | 
				
			|||||||
            user=user,
 | 
					            user=user,
 | 
				
			||||||
            intent=TokenIntents.INTENT_RECOVERY,
 | 
					            intent=TokenIntents.INTENT_RECOVERY,
 | 
				
			||||||
            description=f"Recovery Token generated by {getuser()} on {_now}",
 | 
					            description=f"Recovery Token generated by {getuser()} on {_now}",
 | 
				
			||||||
            identifier=f"ak-recovery-{user}-{_now}",
 | 
					            identifier=slugify(f"ak-recovery-{user}-{_now}"),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.stdout.write(
 | 
					        self.stdout.write(
 | 
				
			||||||
            (f"Store this link safely, as it will allow" f" anyone to access authentik as {user}.")
 | 
					            (f"Store this link safely, as it will allow" f" anyone to access authentik as {user}.")
 | 
				
			||||||
 | 
				
			|||||||
@ -59,14 +59,14 @@ class MetricsView(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LiveView(View):
 | 
					class LiveView(View):
 | 
				
			||||||
    """View for liveness probe, always returns Http 201"""
 | 
					    """View for liveness probe, always returns Http 204"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request: HttpRequest) -> HttpResponse:
 | 
					    def dispatch(self, request: HttpRequest) -> HttpResponse:
 | 
				
			||||||
        return HttpResponse(status=201)
 | 
					        return HttpResponse(status=204)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReadyView(View):
 | 
					class ReadyView(View):
 | 
				
			||||||
    """View for readiness probe, always returns Http 201, unless sql or redis is down"""
 | 
					    """View for readiness probe, always returns Http 204, unless sql or redis is down"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request: HttpRequest) -> HttpResponse:
 | 
					    def dispatch(self, request: HttpRequest) -> HttpResponse:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
@ -79,4 +79,4 @@ class ReadyView(View):
 | 
				
			|||||||
            redis_conn.ping()
 | 
					            redis_conn.ping()
 | 
				
			||||||
        except RedisError:
 | 
					        except RedisError:
 | 
				
			||||||
            return HttpResponse(status=503)
 | 
					            return HttpResponse(status=503)
 | 
				
			||||||
        return HttpResponse(status=201)
 | 
					        return HttpResponse(status=204)
 | 
				
			||||||
 | 
				
			|||||||
@ -119,7 +119,7 @@ class LDAPPasswordChanger:
 | 
				
			|||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def ad_password_complexity(self, password: str, user: Optional[User] = None) -> bool:
 | 
					    def ad_password_complexity(self, password: str, user: Optional[User] = None) -> bool:
 | 
				
			||||||
        """Check if password matches Active direcotry password policies
 | 
					        """Check if password matches Active directory password policies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        https://docs.microsoft.com/en-us/windows/security/threat-protection/
 | 
					        https://docs.microsoft.com/en-us/windows/security/threat-protection/
 | 
				
			||||||
            security-policy-settings/password-must-meet-complexity-requirements
 | 
					            security-policy-settings/password-must-meet-complexity-requirements
 | 
				
			||||||
 | 
				
			|||||||
@ -25,13 +25,20 @@ from authentik.stages.prompt.signals import password_validate
 | 
				
			|||||||
# pylint: disable=unused-argument
 | 
					# pylint: disable=unused-argument
 | 
				
			||||||
def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
 | 
					def sync_ldap_source_on_save(sender, instance: LDAPSource, **_):
 | 
				
			||||||
    """Ensure that source is synced on save (if enabled)"""
 | 
					    """Ensure that source is synced on save (if enabled)"""
 | 
				
			||||||
    if instance.enabled:
 | 
					    if not instance.enabled:
 | 
				
			||||||
        for sync_class in [
 | 
					        return
 | 
				
			||||||
            UserLDAPSynchronizer,
 | 
					    # Don't sync sources when they don't have any property mappings. This will only happen if:
 | 
				
			||||||
            GroupLDAPSynchronizer,
 | 
					    # - the user forgets to set them or
 | 
				
			||||||
            MembershipLDAPSynchronizer,
 | 
					    # - the source is newly created, this is the first save event
 | 
				
			||||||
        ]:
 | 
					    #   and the mappings are created with an m2m event
 | 
				
			||||||
            ldap_sync.delay(instance.pk, class_to_path(sync_class))
 | 
					    if not instance.property_mappings.exists() or not instance.property_mappings_group.exists():
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    for sync_class in [
 | 
				
			||||||
 | 
					        UserLDAPSynchronizer,
 | 
				
			||||||
 | 
					        GroupLDAPSynchronizer,
 | 
				
			||||||
 | 
					        MembershipLDAPSynchronizer,
 | 
				
			||||||
 | 
					    ]:
 | 
				
			||||||
 | 
					        ldap_sync.delay(instance.pk, class_to_path(sync_class))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(password_validate)
 | 
					@receiver(password_validate)
 | 
				
			||||||
@ -48,7 +55,7 @@ def ldap_password_validate(sender, password: str, plan_context: dict[str, Any],
 | 
				
			|||||||
            password, plan_context.get(PLAN_CONTEXT_PENDING_USER, None)
 | 
					            password, plan_context.get(PLAN_CONTEXT_PENDING_USER, None)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        if not passing:
 | 
					        if not passing:
 | 
				
			||||||
            raise ValidationError(_("Password does not match Active Direcory Complexity."))
 | 
					            raise ValidationError(_("Password does not match Active Directory Complexity."))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@receiver(password_changed)
 | 
					@receiver(password_changed)
 | 
				
			||||||
 | 
				
			|||||||
@ -39,11 +39,17 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
				
			|||||||
            if not ak_group:
 | 
					            if not ak_group:
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            membership_mapping_attribute = LDAP_DISTINGUISHED_NAME
 | 
				
			||||||
 | 
					            if self._source.group_membership_field == "memberUid":
 | 
				
			||||||
 | 
					                # If memberships are based on the posixGroup's 'memberUid'
 | 
				
			||||||
 | 
					                # attribute we use the RDN instead of the FDN to lookup members.
 | 
				
			||||||
 | 
					                membership_mapping_attribute = LDAP_UNIQUENESS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            users = User.objects.filter(
 | 
					            users = User.objects.filter(
 | 
				
			||||||
                Q(**{f"attributes__{LDAP_DISTINGUISHED_NAME}__in": members})
 | 
					                Q(**{f"attributes__{membership_mapping_attribute}__in": members})
 | 
				
			||||||
                | Q(
 | 
					                | Q(
 | 
				
			||||||
                    **{
 | 
					                    **{
 | 
				
			||||||
                        f"attributes__{LDAP_DISTINGUISHED_NAME}__isnull": True,
 | 
					                        f"attributes__{membership_mapping_attribute}__isnull": True,
 | 
				
			||||||
                        "ak_groups__in": [ak_group],
 | 
					                        "ak_groups__in": [ak_group],
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ from pytz import UTC
 | 
				
			|||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.events.models import Event, EventAction
 | 
				
			||||||
from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
 | 
					from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchronizer
 | 
				
			||||||
 | 
					from authentik.sources.ldap.sync.vendor.ad import UserAccountControl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UserLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
					class UserLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
				
			||||||
@ -75,4 +76,8 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
				
			|||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    ak_user.set_unusable_password()
 | 
					                    ak_user.set_unusable_password()
 | 
				
			||||||
                    ak_user.save()
 | 
					                    ak_user.save()
 | 
				
			||||||
 | 
					                if "userAccountControl" in attributes:
 | 
				
			||||||
 | 
					                    uac = UserAccountControl(attributes.get("userAccountControl"))
 | 
				
			||||||
 | 
					                    ak_user.is_active = UserAccountControl.ACCOUNTDISABLE not in uac
 | 
				
			||||||
 | 
					                    ak_user.save()
 | 
				
			||||||
        return user_count
 | 
					        return user_count
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										0
									
								
								authentik/sources/ldap/sync/vendor/__init__.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/sources/ldap/sync/vendor/__init__.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
								
								
									
										32
									
								
								authentik/sources/ldap/sync/vendor/ad.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								authentik/sources/ldap/sync/vendor/ad.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					"""Active Directory specific"""
 | 
				
			||||||
 | 
					from enum import IntFlag
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UserAccountControl(IntFlag):
 | 
				
			||||||
 | 
					    """UserAccountControl attribute for Active directory users"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity
 | 
				
			||||||
 | 
					    #   /useraccountcontrol-manipulate-account-properties
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SCRIPT = 1
 | 
				
			||||||
 | 
					    ACCOUNTDISABLE = 2
 | 
				
			||||||
 | 
					    HOMEDIR_REQUIRED = 8
 | 
				
			||||||
 | 
					    LOCKOUT = 16
 | 
				
			||||||
 | 
					    PASSWD_NOTREQD = 32
 | 
				
			||||||
 | 
					    PASSWD_CANT_CHANGE = 64
 | 
				
			||||||
 | 
					    ENCRYPTED_TEXT_PWD_ALLOWED = 128
 | 
				
			||||||
 | 
					    TEMP_DUPLICATE_ACCOUNT = 256
 | 
				
			||||||
 | 
					    NORMAL_ACCOUNT = 512
 | 
				
			||||||
 | 
					    INTERDOMAIN_TRUST_ACCOUNT = 2048
 | 
				
			||||||
 | 
					    WORKSTATION_TRUST_ACCOUNT = 4096
 | 
				
			||||||
 | 
					    SERVER_TRUST_ACCOUNT = 8192
 | 
				
			||||||
 | 
					    DONT_EXPIRE_PASSWORD = 65536
 | 
				
			||||||
 | 
					    MNS_LOGON_ACCOUNT = 131072
 | 
				
			||||||
 | 
					    SMARTCARD_REQUIRED = 262144
 | 
				
			||||||
 | 
					    TRUSTED_FOR_DELEGATION = 524288
 | 
				
			||||||
 | 
					    NOT_DELEGATED = 1048576
 | 
				
			||||||
 | 
					    USE_DES_KEY_ONLY = 2097152
 | 
				
			||||||
 | 
					    DONT_REQ_PREAUTH = 4194304
 | 
				
			||||||
 | 
					    PASSWORD_EXPIRED = 8388608
 | 
				
			||||||
 | 
					    TRUSTED_TO_AUTH_FOR_DELEGATION = 16777216
 | 
				
			||||||
 | 
					    PARTIAL_SECRETS_ACCOUNT = 67108864
 | 
				
			||||||
@ -2,6 +2,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from ldap3 import MOCK_SYNC, OFFLINE_AD_2012_R2, Connection, Server
 | 
					from ldap3 import MOCK_SYNC, OFFLINE_AD_2012_R2, Connection, Server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.sources.ldap.sync.vendor.ad import UserAccountControl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def mock_ad_connection(password: str) -> Connection:
 | 
					def mock_ad_connection(password: str) -> Connection:
 | 
				
			||||||
    """Create mock AD connection"""
 | 
					    """Create mock AD connection"""
 | 
				
			||||||
@ -54,6 +56,8 @@ def mock_ad_connection(password: str) -> Connection:
 | 
				
			|||||||
            "objectSid": "user0",
 | 
					            "objectSid": "user0",
 | 
				
			||||||
            "objectClass": "person",
 | 
					            "objectClass": "person",
 | 
				
			||||||
            "distinguishedName": "cn=user0,ou=users,dc=goauthentik,dc=io",
 | 
					            "distinguishedName": "cn=user0,ou=users,dc=goauthentik,dc=io",
 | 
				
			||||||
 | 
					            "userAccountControl": UserAccountControl.ACCOUNTDISABLE
 | 
				
			||||||
 | 
					            + UserAccountControl.NORMAL_ACCOUNT,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    # User without SID
 | 
					    # User without SID
 | 
				
			||||||
 | 
				
			|||||||
@ -77,5 +77,24 @@ def mock_slapd_connection(password: str) -> Connection:
 | 
				
			|||||||
            "objectClass": "person",
 | 
					            "objectClass": "person",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    # Group with posixGroup and memberUid
 | 
				
			||||||
 | 
					    connection.strategy.add_entry(
 | 
				
			||||||
 | 
					        "cn=group-posix,ou=groups,dc=goauthentik,dc=io",
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "cn": "group-posix",
 | 
				
			||||||
 | 
					            "objectClass": "posixGroup",
 | 
				
			||||||
 | 
					            "memberUid": ["user-posix"],
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    # User with posixAccount
 | 
				
			||||||
 | 
					    connection.strategy.add_entry(
 | 
				
			||||||
 | 
					        "cn=user-posix,ou=users,dc=goauthentik,dc=io",
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "userPassword": password,
 | 
				
			||||||
 | 
					            "uid": "user-posix",
 | 
				
			||||||
 | 
					            "cn": "user-posix",
 | 
				
			||||||
 | 
					            "objectClass": "posixAccount",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    connection.bind()
 | 
					    connection.bind()
 | 
				
			||||||
    return connection
 | 
					    return connection
 | 
				
			||||||
 | 
				
			|||||||
@ -72,7 +72,8 @@ class LDAPSyncTests(TestCase):
 | 
				
			|||||||
        with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
 | 
					        with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
 | 
				
			||||||
            user_sync = UserLDAPSynchronizer(self.source)
 | 
					            user_sync = UserLDAPSynchronizer(self.source)
 | 
				
			||||||
            user_sync.sync()
 | 
					            user_sync.sync()
 | 
				
			||||||
            self.assertTrue(User.objects.filter(username="user0_sn").exists())
 | 
					            user = User.objects.filter(username="user0_sn").first()
 | 
				
			||||||
 | 
					            self.assertFalse(user.is_active)
 | 
				
			||||||
            self.assertFalse(User.objects.filter(username="user1_sn").exists())
 | 
					            self.assertFalse(User.objects.filter(username="user1_sn").exists())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_sync_users_openldap(self):
 | 
					    def test_sync_users_openldap(self):
 | 
				
			||||||
@ -136,6 +137,34 @@ class LDAPSyncTests(TestCase):
 | 
				
			|||||||
            group = Group.objects.filter(name="group1")
 | 
					            group = Group.objects.filter(name="group1")
 | 
				
			||||||
            self.assertTrue(group.exists())
 | 
					            self.assertTrue(group.exists())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_sync_groups_openldap_posix_group(self):
 | 
				
			||||||
 | 
					        """Test posix group sync"""
 | 
				
			||||||
 | 
					        self.source.object_uniqueness_field = "cn"
 | 
				
			||||||
 | 
					        self.source.group_membership_field = "memberUid"
 | 
				
			||||||
 | 
					        self.source.user_object_filter = "(objectClass=posixAccount)"
 | 
				
			||||||
 | 
					        self.source.group_object_filter = "(objectClass=posixGroup)"
 | 
				
			||||||
 | 
					        self.source.property_mappings.set(
 | 
				
			||||||
 | 
					            LDAPPropertyMapping.objects.filter(
 | 
				
			||||||
 | 
					                Q(managed__startswith="goauthentik.io/sources/ldap/default")
 | 
				
			||||||
 | 
					                | Q(managed__startswith="goauthentik.io/sources/ldap/openldap")
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.source.property_mappings_group.set(
 | 
				
			||||||
 | 
					            LDAPPropertyMapping.objects.filter(managed="goauthentik.io/sources/ldap/openldap-cn")
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.source.save()
 | 
				
			||||||
 | 
					        connection = PropertyMock(return_value=mock_slapd_connection(LDAP_PASSWORD))
 | 
				
			||||||
 | 
					        with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
 | 
				
			||||||
 | 
					            user_sync = UserLDAPSynchronizer(self.source)
 | 
				
			||||||
 | 
					            user_sync.sync()
 | 
				
			||||||
 | 
					            group_sync = GroupLDAPSynchronizer(self.source)
 | 
				
			||||||
 | 
					            group_sync.sync()
 | 
				
			||||||
 | 
					            membership_sync = MembershipLDAPSynchronizer(self.source)
 | 
				
			||||||
 | 
					            membership_sync.sync()
 | 
				
			||||||
 | 
					            # Test if membership mapping based on memberUid works.
 | 
				
			||||||
 | 
					            posix_group = Group.objects.filter(name="group-posix").first()
 | 
				
			||||||
 | 
					            self.assertTrue(posix_group.users.filter(name="user-posix").exists())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_tasks_ad(self):
 | 
					    def test_tasks_ad(self):
 | 
				
			||||||
        """Test Scheduled tasks"""
 | 
					        """Test Scheduled tasks"""
 | 
				
			||||||
        self.source.property_mappings.set(
 | 
					        self.source.property_mappings.set(
 | 
				
			||||||
 | 
				
			|||||||
@ -46,9 +46,9 @@ class OAuthSourceSerializer(SourceSerializer):
 | 
				
			|||||||
    type = SerializerMethodField()
 | 
					    type = SerializerMethodField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @extend_schema_field(SourceTypeSerializer)
 | 
					    @extend_schema_field(SourceTypeSerializer)
 | 
				
			||||||
    def get_type(self, instace: OAuthSource) -> SourceTypeSerializer:
 | 
					    def get_type(self, instance: OAuthSource) -> SourceTypeSerializer:
 | 
				
			||||||
        """Get source's type configuration"""
 | 
					        """Get source's type configuration"""
 | 
				
			||||||
        return SourceTypeSerializer(instace.type).data
 | 
					        return SourceTypeSerializer(instance.type).data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def validate(self, attrs: dict) -> dict:
 | 
					    def validate(self, attrs: dict) -> dict:
 | 
				
			||||||
        provider_type = MANAGER.find_type(attrs.get("provider_type", ""))
 | 
					        provider_type = MANAGER.find_type(attrs.get("provider_type", ""))
 | 
				
			||||||
 | 
				
			|||||||
@ -14,7 +14,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            model_name="oauthsource",
 | 
					            model_name="oauthsource",
 | 
				
			||||||
            name="access_token_url",
 | 
					            name="access_token_url",
 | 
				
			||||||
            field=models.CharField(
 | 
					            field=models.CharField(
 | 
				
			||||||
                help_text="URL used by authentik to retrive tokens.",
 | 
					                help_text="URL used by authentik to retrieve tokens.",
 | 
				
			||||||
                max_length=255,
 | 
					                max_length=255,
 | 
				
			||||||
                verbose_name="Access Token URL",
 | 
					                verbose_name="Access Token URL",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            name="access_token_url",
 | 
					            name="access_token_url",
 | 
				
			||||||
            field=models.CharField(
 | 
					            field=models.CharField(
 | 
				
			||||||
                blank=True,
 | 
					                blank=True,
 | 
				
			||||||
                help_text="URL used by authentik to retrive tokens.",
 | 
					                help_text="URL used by authentik to retrieve tokens.",
 | 
				
			||||||
                max_length=255,
 | 
					                max_length=255,
 | 
				
			||||||
                verbose_name="Access Token URL",
 | 
					                verbose_name="Access Token URL",
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 | 
				
			|||||||
@ -39,7 +39,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            model_name="oauthsource",
 | 
					            model_name="oauthsource",
 | 
				
			||||||
            name="access_token_url",
 | 
					            name="access_token_url",
 | 
				
			||||||
            field=models.CharField(
 | 
					            field=models.CharField(
 | 
				
			||||||
                help_text="URL used by authentik to retrive tokens.",
 | 
					                help_text="URL used by authentik to retrieve tokens.",
 | 
				
			||||||
                max_length=255,
 | 
					                max_length=255,
 | 
				
			||||||
                null=True,
 | 
					                null=True,
 | 
				
			||||||
                verbose_name="Access Token URL",
 | 
					                verbose_name="Access Token URL",
 | 
				
			||||||
 | 
				
			|||||||
@ -37,7 +37,7 @@ class OAuthSource(Source):
 | 
				
			|||||||
        max_length=255,
 | 
					        max_length=255,
 | 
				
			||||||
        null=True,
 | 
					        null=True,
 | 
				
			||||||
        verbose_name=_("Access Token URL"),
 | 
					        verbose_name=_("Access Token URL"),
 | 
				
			||||||
        help_text=_("URL used by authentik to retrive tokens."),
 | 
					        help_text=_("URL used by authentik to retrieve tokens."),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    profile_url = models.CharField(
 | 
					    profile_url = models.CharField(
 | 
				
			||||||
        max_length=255,
 | 
					        max_length=255,
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,7 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
        migrations.AddField(
 | 
					        migrations.AddField(
 | 
				
			||||||
            model_name="plexsource",
 | 
					            model_name="plexsource",
 | 
				
			||||||
            name="plex_token",
 | 
					            name="plex_token",
 | 
				
			||||||
            field=models.TextField(default="", help_text="Plex token used to check firends"),
 | 
					            field=models.TextField(default="", help_text="Plex token used to check friends"),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.AlterField(
 | 
					        migrations.AlterField(
 | 
				
			||||||
            model_name="plexsource",
 | 
					            model_name="plexsource",
 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,6 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
        migrations.AlterField(
 | 
					        migrations.AlterField(
 | 
				
			||||||
            model_name="plexsource",
 | 
					            model_name="plexsource",
 | 
				
			||||||
            name="plex_token",
 | 
					            name="plex_token",
 | 
				
			||||||
            field=models.TextField(help_text="Plex token used to check firends"),
 | 
					            field=models.TextField(help_text="Plex token used to check friends"),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
				
			|||||||
@ -50,7 +50,7 @@ class PlexSource(Source):
 | 
				
			|||||||
        default=True,
 | 
					        default=True,
 | 
				
			||||||
        help_text=_("Allow friends to authenticate, even if you don't share a server."),
 | 
					        help_text=_("Allow friends to authenticate, even if you don't share a server."),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    plex_token = models.TextField(help_text=_("Plex token used to check firends"))
 | 
					    plex_token = models.TextField(help_text=_("Plex token used to check friends"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def component(self) -> str:
 | 
					    def component(self) -> str:
 | 
				
			||||||
 | 
				
			|||||||
@ -54,7 +54,7 @@ class CaptchaChallengeResponse(ChallengeResponse):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CaptchaStageView(ChallengeStageView):
 | 
					class CaptchaStageView(ChallengeStageView):
 | 
				
			||||||
    """Simple captcha checker, logic is handeled in django-captcha module"""
 | 
					    """Simple captcha checker, logic is handled in django-captcha module"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    response_class = CaptchaChallengeResponse
 | 
					    response_class = CaptchaChallengeResponse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -43,6 +43,7 @@ class EmailStageSerializer(StageSerializer):
 | 
				
			|||||||
            "token_expiry",
 | 
					            "token_expiry",
 | 
				
			||||||
            "subject",
 | 
					            "subject",
 | 
				
			||||||
            "template",
 | 
					            "template",
 | 
				
			||||||
 | 
					            "activate_user_on_success",
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        extra_kwargs = {"password": {"write_only": True}}
 | 
					        extra_kwargs = {"password": {"write_only": True}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -65,6 +66,7 @@ class EmailStageViewSet(UsedByMixin, ModelViewSet):
 | 
				
			|||||||
        "token_expiry",
 | 
					        "token_expiry",
 | 
				
			||||||
        "subject",
 | 
					        "subject",
 | 
				
			||||||
        "template",
 | 
					        "template",
 | 
				
			||||||
 | 
					        "activate_user_on_success",
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    ordering = ["name"]
 | 
					    ordering = ["name"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.2.7 on 2021-10-04 16:38
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ("authentik_stages_email", "0003_auto_20210404_1054"),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name="emailstage",
 | 
				
			||||||
 | 
					            name="activate_user_on_success",
 | 
				
			||||||
 | 
					            field=models.BooleanField(
 | 
				
			||||||
 | 
					                default=False, help_text="Activate users upon completion of stage."
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -71,6 +71,10 @@ class EmailStage(Stage):
 | 
				
			|||||||
    timeout = models.IntegerField(default=10)
 | 
					    timeout = models.IntegerField(default=10)
 | 
				
			||||||
    from_address = models.EmailField(default="system@authentik.local")
 | 
					    from_address = models.EmailField(default="system@authentik.local")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    activate_user_on_success = models.BooleanField(
 | 
				
			||||||
 | 
					        default=False, help_text=_("Activate users upon completion of stage.")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    token_expiry = models.IntegerField(
 | 
					    token_expiry = models.IntegerField(
 | 
				
			||||||
        default=30, help_text=_("Time in minutes the token sent is valid.")
 | 
					        default=30, help_text=_("Time in minutes the token sent is valid.")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
				
			|||||||
@ -3,9 +3,9 @@ from datetime import timedelta
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
from django.shortcuts import get_object_or_404
 | 
					 | 
				
			||||||
from django.urls import reverse
 | 
					from django.urls import reverse
 | 
				
			||||||
from django.utils.http import urlencode
 | 
					from django.utils.http import urlencode
 | 
				
			||||||
 | 
					from django.utils.text import slugify
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from rest_framework.fields import CharField
 | 
					from rest_framework.fields import CharField
 | 
				
			||||||
@ -22,7 +22,7 @@ from authentik.stages.email.tasks import send_mails
 | 
				
			|||||||
from authentik.stages.email.utils import TemplateEmailMessage
 | 
					from authentik.stages.email.utils import TemplateEmailMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
QS_KEY_TOKEN = "token"  # nosec
 | 
					QS_KEY_TOKEN = "etoken"  # nosec
 | 
				
			||||||
PLAN_CONTEXT_EMAIL_SENT = "email_sent"
 | 
					PLAN_CONTEXT_EMAIL_SENT = "email_sent"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -65,7 +65,7 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
        )  # + 1 because django timesince always rounds down
 | 
					        )  # + 1 because django timesince always rounds down
 | 
				
			||||||
        token_filters = {
 | 
					        token_filters = {
 | 
				
			||||||
            "user": pending_user,
 | 
					            "user": pending_user,
 | 
				
			||||||
            "identifier": f"ak-email-stage-{current_stage.name}-{pending_user}",
 | 
					            "identifier": slugify(f"ak-email-stage-{current_stage.name}-{pending_user}"),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        # Don't check for validity here, we only care if the token exists
 | 
					        # Don't check for validity here, we only care if the token exists
 | 
				
			||||||
        tokens = Token.objects.filter(**token_filters)
 | 
					        tokens = Token.objects.filter(**token_filters)
 | 
				
			||||||
@ -99,10 +99,16 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
					    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
				
			||||||
        # Check if the user came back from the email link to verify
 | 
					        # Check if the user came back from the email link to verify
 | 
				
			||||||
        if QS_KEY_TOKEN in request.session.get(SESSION_KEY_GET, {}):
 | 
					        if QS_KEY_TOKEN in request.session.get(SESSION_KEY_GET, {}):
 | 
				
			||||||
            token = get_object_or_404(Token, key=request.session[SESSION_KEY_GET][QS_KEY_TOKEN])
 | 
					            tokens = Token.filter_not_expired(key=request.session[SESSION_KEY_GET][QS_KEY_TOKEN])
 | 
				
			||||||
 | 
					            if not tokens.exists():
 | 
				
			||||||
 | 
					                return self.executor.stage_invalid(_("Invalid token"))
 | 
				
			||||||
 | 
					            token = tokens.first()
 | 
				
			||||||
            self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
 | 
					            self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
 | 
				
			||||||
            token.delete()
 | 
					            token.delete()
 | 
				
			||||||
            messages.success(request, _("Successfully verified Email."))
 | 
					            messages.success(request, _("Successfully verified Email."))
 | 
				
			||||||
 | 
					            if self.executor.current_stage.activate_user_on_success:
 | 
				
			||||||
 | 
					                self.executor.plan.context[PLAN_CONTEXT_PENDING_USER].is_active = True
 | 
				
			||||||
 | 
					                self.executor.plan.context[PLAN_CONTEXT_PENDING_USER].save()
 | 
				
			||||||
            return self.executor.stage_ok()
 | 
					            return self.executor.stage_ok()
 | 
				
			||||||
        if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
 | 
					        if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
 | 
				
			||||||
            LOGGER.debug("No pending user")
 | 
					            LOGGER.debug("No pending user")
 | 
				
			||||||
@ -118,7 +124,7 @@ class EmailStageView(ChallengeStageView):
 | 
				
			|||||||
        challenge = EmailChallenge(
 | 
					        challenge = EmailChallenge(
 | 
				
			||||||
            data={
 | 
					            data={
 | 
				
			||||||
                "type": ChallengeTypes.NATIVE.value,
 | 
					                "type": ChallengeTypes.NATIVE.value,
 | 
				
			||||||
                "title": "Email sent.",
 | 
					                "title": _("Email sent."),
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return challenge
 | 
					        return challenge
 | 
				
			||||||
 | 
				
			|||||||
@ -31,6 +31,7 @@ class TestEmailStage(APITestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        self.stage = EmailStage.objects.create(
 | 
					        self.stage = EmailStage.objects.create(
 | 
				
			||||||
            name="email",
 | 
					            name="email",
 | 
				
			||||||
 | 
					            activate_user_on_success=True,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
 | 
					        self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -84,6 +85,8 @@ class TestEmailStage(APITestCase):
 | 
				
			|||||||
        """Test with token"""
 | 
					        """Test with token"""
 | 
				
			||||||
        # Make sure token exists
 | 
					        # Make sure token exists
 | 
				
			||||||
        self.test_pending_user()
 | 
					        self.test_pending_user()
 | 
				
			||||||
 | 
					        self.user.is_active = False
 | 
				
			||||||
 | 
					        self.user.save()
 | 
				
			||||||
        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 | 
					        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 | 
				
			||||||
        session = self.client.session
 | 
					        session = self.client.session
 | 
				
			||||||
        session[SESSION_KEY_PLAN] = plan
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
@ -125,3 +128,4 @@ class TestEmailStage(APITestCase):
 | 
				
			|||||||
            session = self.client.session
 | 
					            session = self.client.session
 | 
				
			||||||
            plan: FlowPlan = session[SESSION_KEY_PLAN]
 | 
					            plan: FlowPlan = session[SESSION_KEY_PLAN]
 | 
				
			||||||
            self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER], self.user)
 | 
					            self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER], self.user)
 | 
				
			||||||
 | 
					            self.assertTrue(plan.context[PLAN_CONTEXT_PENDING_USER].is_active)
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,8 @@ from authentik.stages.invitation.signals import invitation_used
 | 
				
			|||||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
 | 
					from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
INVITATION_TOKEN_KEY = "token"  # nosec
 | 
					INVITATION_TOKEN_KEY_CONTEXT = "token"  # nosec
 | 
				
			||||||
 | 
					INVITATION_TOKEN_KEY = "itoken"  # nosec
 | 
				
			||||||
INVITATION_IN_EFFECT = "invitation_in_effect"
 | 
					INVITATION_IN_EFFECT = "invitation_in_effect"
 | 
				
			||||||
INVITATION = "invitation"
 | 
					INVITATION = "invitation"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -29,10 +30,14 @@ class InvitationStageView(StageView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_token(self) -> Optional[str]:
 | 
					    def get_token(self) -> Optional[str]:
 | 
				
			||||||
        """Get token from saved get-arguments or prompt_data"""
 | 
					        """Get token from saved get-arguments or prompt_data"""
 | 
				
			||||||
 | 
					        # Check for ?token= and ?itoken=
 | 
				
			||||||
        if INVITATION_TOKEN_KEY in self.request.session.get(SESSION_KEY_GET, {}):
 | 
					        if INVITATION_TOKEN_KEY in self.request.session.get(SESSION_KEY_GET, {}):
 | 
				
			||||||
            return self.request.session[SESSION_KEY_GET][INVITATION_TOKEN_KEY]
 | 
					            return self.request.session[SESSION_KEY_GET][INVITATION_TOKEN_KEY]
 | 
				
			||||||
        if INVITATION_TOKEN_KEY in self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}):
 | 
					        if INVITATION_TOKEN_KEY_CONTEXT in self.request.session.get(SESSION_KEY_GET, {}):
 | 
				
			||||||
            return self.executor.plan.context[PLAN_CONTEXT_PROMPT][INVITATION_TOKEN_KEY]
 | 
					            return self.request.session[SESSION_KEY_GET][INVITATION_TOKEN_KEY_CONTEXT]
 | 
				
			||||||
 | 
					        # Check for {'token': ''} in the context
 | 
				
			||||||
 | 
					        if INVITATION_TOKEN_KEY_CONTEXT in self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {}):
 | 
				
			||||||
 | 
					            return self.executor.plan.context[PLAN_CONTEXT_PROMPT][INVITATION_TOKEN_KEY_CONTEXT]
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, request: HttpRequest) -> HttpResponse:
 | 
					    def get(self, request: HttpRequest) -> HttpResponse:
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,11 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
 | 
				
			|||||||
from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
 | 
					from authentik.flows.tests.test_views import TO_STAGE_RESPONSE_MOCK
 | 
				
			||||||
from authentik.flows.views import SESSION_KEY_PLAN
 | 
					from authentik.flows.views import SESSION_KEY_PLAN
 | 
				
			||||||
from authentik.stages.invitation.models import Invitation, InvitationStage
 | 
					from authentik.stages.invitation.models import Invitation, InvitationStage
 | 
				
			||||||
from authentik.stages.invitation.stage import INVITATION_TOKEN_KEY, PLAN_CONTEXT_PROMPT
 | 
					from authentik.stages.invitation.stage import (
 | 
				
			||||||
 | 
					    INVITATION_TOKEN_KEY,
 | 
				
			||||||
 | 
					    INVITATION_TOKEN_KEY_CONTEXT,
 | 
				
			||||||
 | 
					    PLAN_CONTEXT_PROMPT,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from authentik.stages.password import BACKEND_INBUILT
 | 
					from authentik.stages.password import BACKEND_INBUILT
 | 
				
			||||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
 | 
					from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -131,7 +135,7 @@ class TestUserLoginStage(APITestCase):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 | 
					        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 | 
				
			||||||
        plan.context[PLAN_CONTEXT_PROMPT] = {INVITATION_TOKEN_KEY: invite.pk.hex}
 | 
					        plan.context[PLAN_CONTEXT_PROMPT] = {INVITATION_TOKEN_KEY_CONTEXT: invite.pk.hex}
 | 
				
			||||||
        session = self.client.session
 | 
					        session = self.client.session
 | 
				
			||||||
        session[SESSION_KEY_PLAN] = plan
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
        session.save()
 | 
					        session.save()
 | 
				
			||||||
 | 
				
			|||||||
@ -53,9 +53,11 @@ class PromptChallengeResponse(ChallengeResponse):
 | 
				
			|||||||
    def __init__(self, *args, **kwargs):
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
        stage: PromptStage = kwargs.pop("stage", None)
 | 
					        stage: PromptStage = kwargs.pop("stage", None)
 | 
				
			||||||
        plan: FlowPlan = kwargs.pop("plan", None)
 | 
					        plan: FlowPlan = kwargs.pop("plan", None)
 | 
				
			||||||
 | 
					        request: HttpRequest = kwargs.pop("request", None)
 | 
				
			||||||
        super().__init__(*args, **kwargs)
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
        self.stage = stage
 | 
					        self.stage = stage
 | 
				
			||||||
        self.plan = plan
 | 
					        self.plan = plan
 | 
				
			||||||
 | 
					        self.request = request
 | 
				
			||||||
        if not self.stage:
 | 
					        if not self.stage:
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
        # list() is called so we only load the fields once
 | 
					        # list() is called so we only load the fields once
 | 
				
			||||||
@ -104,8 +106,9 @@ class PromptChallengeResponse(ChallengeResponse):
 | 
				
			|||||||
            self._validate_password_fields(*[field.field_key for field in password_fields])
 | 
					            self._validate_password_fields(*[field.field_key for field in password_fields])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        user = self.plan.context.get(PLAN_CONTEXT_PENDING_USER, get_anonymous_user())
 | 
					        user = self.plan.context.get(PLAN_CONTEXT_PENDING_USER, get_anonymous_user())
 | 
				
			||||||
        engine = ListPolicyEngine(self.stage.validation_policies.all(), user)
 | 
					        engine = ListPolicyEngine(self.stage.validation_policies.all(), user, self.request)
 | 
				
			||||||
        engine.request.context = attrs
 | 
					        engine.request.context[PLAN_CONTEXT_PROMPT] = attrs
 | 
				
			||||||
 | 
					        engine.request.context.update(attrs)
 | 
				
			||||||
        engine.build()
 | 
					        engine.build()
 | 
				
			||||||
        result = engine.result
 | 
					        result = engine.result
 | 
				
			||||||
        if not result.passing:
 | 
					        if not result.passing:
 | 
				
			||||||
@ -173,6 +176,7 @@ class PromptStageView(ChallengeStageView):
 | 
				
			|||||||
        return PromptChallengeResponse(
 | 
					        return PromptChallengeResponse(
 | 
				
			||||||
            instance=None,
 | 
					            instance=None,
 | 
				
			||||||
            data=data,
 | 
					            data=data,
 | 
				
			||||||
 | 
					            request=self.request,
 | 
				
			||||||
            stage=self.executor.current_stage,
 | 
					            stage=self.executor.current_stage,
 | 
				
			||||||
            plan=self.executor.plan,
 | 
					            plan=self.executor.plan,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ from django.http import HttpRequest, HttpResponse
 | 
				
			|||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from structlog.stdlib import get_logger
 | 
					from structlog.stdlib import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
 | 
					from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
 | 
				
			||||||
from authentik.flows.stage import StageView
 | 
					from authentik.flows.stage import StageView
 | 
				
			||||||
from authentik.lib.utils.time import timedelta_from_string
 | 
					from authentik.lib.utils.time import timedelta_from_string
 | 
				
			||||||
@ -32,9 +33,12 @@ class UserLoginStageView(StageView):
 | 
				
			|||||||
        backend = self.executor.plan.context.get(
 | 
					        backend = self.executor.plan.context.get(
 | 
				
			||||||
            PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_INBUILT
 | 
					            PLAN_CONTEXT_AUTHENTICATION_BACKEND, BACKEND_INBUILT
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
 | 
				
			||||||
 | 
					        if not user.is_active:
 | 
				
			||||||
 | 
					            LOGGER.warning("User is not active, login will not work.")
 | 
				
			||||||
        login(
 | 
					        login(
 | 
				
			||||||
            self.request,
 | 
					            self.request,
 | 
				
			||||||
            self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
 | 
					            user,
 | 
				
			||||||
            backend=backend,
 | 
					            backend=backend,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        delta = timedelta_from_string(self.executor.current_stage.session_duration)
 | 
					        delta = timedelta_from_string(self.executor.current_stage.session_duration)
 | 
				
			||||||
@ -45,7 +49,7 @@ class UserLoginStageView(StageView):
 | 
				
			|||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "Logged in",
 | 
					            "Logged in",
 | 
				
			||||||
            backend=backend,
 | 
					            backend=backend,
 | 
				
			||||||
            user=self.executor.plan.context[PLAN_CONTEXT_PENDING_USER],
 | 
					            user=user,
 | 
				
			||||||
            flow_slug=self.executor.flow.slug,
 | 
					            flow_slug=self.executor.flow.slug,
 | 
				
			||||||
            session_duration=self.executor.current_stage.session_duration,
 | 
					            session_duration=self.executor.current_stage.session_duration,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
				
			|||||||
@ -109,3 +109,29 @@ class TestUserLoginStage(APITestCase):
 | 
				
			|||||||
                },
 | 
					                },
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_inactive_account(self):
 | 
				
			||||||
 | 
					        """Test with a valid pending user and backend"""
 | 
				
			||||||
 | 
					        self.user.is_active = False
 | 
				
			||||||
 | 
					        self.user.save()
 | 
				
			||||||
 | 
					        plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
 | 
				
			||||||
 | 
					        plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
 | 
				
			||||||
 | 
					        session = self.client.session
 | 
				
			||||||
 | 
					        session[SESSION_KEY_PLAN] = plan
 | 
				
			||||||
 | 
					        session.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        response = self.client.get(
 | 
				
			||||||
 | 
					            reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					        self.assertJSONEqual(
 | 
				
			||||||
 | 
					            force_str(response.content),
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "component": "xak-flow-redirect",
 | 
				
			||||||
 | 
					                "to": reverse("authentik_core:root-redirect"),
 | 
				
			||||||
 | 
					                "type": ChallengeTypes.REDIRECT.value,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        response = self.client.get(reverse("authentik_api:application-list"))
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 403)
 | 
				
			||||||
 | 
				
			|||||||
@ -45,6 +45,9 @@ func main() {
 | 
				
			|||||||
	defer common.Defer()
 | 
						defer common.Defer()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ac := ak.NewAPIController(*akURLActual, akToken)
 | 
						ac := ak.NewAPIController(*akURLActual, akToken)
 | 
				
			||||||
 | 
						if ac == nil {
 | 
				
			||||||
 | 
							os.Exit(1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ac.Server = ldap.NewServer(ac)
 | 
						ac.Server = ldap.NewServer(ac)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -59,6 +59,9 @@ func main() {
 | 
				
			|||||||
	defer common.Defer()
 | 
						defer common.Defer()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ac := ak.NewAPIController(*akURLActual, akToken)
 | 
						ac := ak.NewAPIController(*akURLActual, akToken)
 | 
				
			||||||
 | 
						if ac == nil {
 | 
				
			||||||
 | 
							os.Exit(1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ac.Server = proxyv2.NewProxyServer(ac, portOffset)
 | 
						ac.Server = proxyv2.NewProxyServer(ac, portOffset)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -54,7 +54,7 @@ func main() {
 | 
				
			|||||||
	u, _ := url.Parse("http://localhost:8000")
 | 
						u, _ := url.Parse("http://localhost:8000")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	g := gounicorn.NewGoUnicorn()
 | 
						g := gounicorn.NewGoUnicorn()
 | 
				
			||||||
	ws := web.NewWebServer()
 | 
						ws := web.NewWebServer(g)
 | 
				
			||||||
	defer g.Kill()
 | 
						defer g.Kill()
 | 
				
			||||||
	defer ws.Shutdown()
 | 
						defer ws.Shutdown()
 | 
				
			||||||
	go web.RunMetricsServer()
 | 
						go web.RunMetricsServer()
 | 
				
			||||||
 | 
				
			|||||||
@ -7,8 +7,6 @@ services:
 | 
				
			|||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - database:/var/lib/postgresql/data
 | 
					      - database:/var/lib/postgresql/data
 | 
				
			||||||
    networks:
 | 
					 | 
				
			||||||
      - internal
 | 
					 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      - POSTGRES_PASSWORD=${PG_PASS:?database password required}
 | 
					      - POSTGRES_PASSWORD=${PG_PASS:?database password required}
 | 
				
			||||||
      - POSTGRES_USER=${PG_USER:-authentik}
 | 
					      - POSTGRES_USER=${PG_USER:-authentik}
 | 
				
			||||||
@ -18,10 +16,8 @@ services:
 | 
				
			|||||||
  redis:
 | 
					  redis:
 | 
				
			||||||
    image: redis:alpine
 | 
					    image: redis:alpine
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    networks:
 | 
					 | 
				
			||||||
      - internal
 | 
					 | 
				
			||||||
  server:
 | 
					  server:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.9.1-rc3}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.9.8}
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    command: server
 | 
					    command: server
 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
@ -36,19 +32,15 @@ services:
 | 
				
			|||||||
      - ./media:/media
 | 
					      - ./media:/media
 | 
				
			||||||
      - ./custom-templates:/templates
 | 
					      - ./custom-templates:/templates
 | 
				
			||||||
      - geoip:/geoip
 | 
					      - geoip:/geoip
 | 
				
			||||||
    networks:
 | 
					 | 
				
			||||||
      - internal
 | 
					 | 
				
			||||||
    env_file:
 | 
					    env_file:
 | 
				
			||||||
      - .env
 | 
					      - .env
 | 
				
			||||||
    ports:
 | 
					    ports:
 | 
				
			||||||
      - "0.0.0.0:9000:9000"
 | 
					      - "0.0.0.0:9000:9000"
 | 
				
			||||||
      - "0.0.0.0:9443:9443"
 | 
					      - "0.0.0.0:9443:9443"
 | 
				
			||||||
  worker:
 | 
					  worker:
 | 
				
			||||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.9.1-rc3}
 | 
					    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.9.8}
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
    command: worker
 | 
					    command: worker
 | 
				
			||||||
    networks:
 | 
					 | 
				
			||||||
      - internal
 | 
					 | 
				
			||||||
    environment:
 | 
					    environment:
 | 
				
			||||||
      AUTHENTIK_REDIS__HOST: redis
 | 
					      AUTHENTIK_REDIS__HOST: redis
 | 
				
			||||||
      AUTHENTIK_POSTGRESQL__HOST: postgresql
 | 
					      AUTHENTIK_POSTGRESQL__HOST: postgresql
 | 
				
			||||||
@ -83,6 +75,3 @@ volumes:
 | 
				
			|||||||
    driver: local
 | 
					    driver: local
 | 
				
			||||||
  geoip:
 | 
					  geoip:
 | 
				
			||||||
    driver: local
 | 
					    driver: local
 | 
				
			||||||
 | 
					 | 
				
			||||||
networks:
 | 
					 | 
				
			||||||
  internal: {}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@ -34,7 +34,7 @@ require (
 | 
				
			|||||||
	github.com/recws-org/recws v1.3.1
 | 
						github.com/recws-org/recws v1.3.1
 | 
				
			||||||
	github.com/sirupsen/logrus v1.8.1
 | 
						github.com/sirupsen/logrus v1.8.1
 | 
				
			||||||
	go.mongodb.org/mongo-driver v1.5.2 // indirect
 | 
						go.mongodb.org/mongo-driver v1.5.2 // indirect
 | 
				
			||||||
	goauthentik.io/api v0.0.0-20210913161416-2242c65afb14
 | 
						goauthentik.io/api v0.202195.4
 | 
				
			||||||
	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
 | 
						golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
 | 
				
			||||||
	golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
 | 
						golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
 | 
				
			||||||
	golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
 | 
						golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							@ -554,8 +554,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.0.0-20210913161416-2242c65afb14 h1:sOyZZNbhj6LquWGcGfw0muSbGJcAqRkcvIaGPJkB9I0=
 | 
					goauthentik.io/api v0.202195.4 h1:UQMeaNW/MZsMUrmaJ3p19gve26RIn+y08m9M2QQBWek=
 | 
				
			||||||
goauthentik.io/api v0.0.0-20210913161416-2242c65afb14/go.mod h1:SPObiI/v8m5cjhj+bGvzb4Nm1w5gmlil5zHQx10sfjE=
 | 
					goauthentik.io/api v0.202195.4/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=
 | 
				
			||||||
 | 
				
			|||||||
@ -17,4 +17,4 @@ func OutpostUserAgent() string {
 | 
				
			|||||||
	return fmt.Sprintf("authentik-outpost@%s (build=%s)", VERSION, BUILD())
 | 
						return fmt.Sprintf("authentik-outpost@%s (build=%s)", VERSION, BUILD())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const VERSION = "2021.9.1-rc3"
 | 
					const VERSION = "2021.9.8"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,13 @@
 | 
				
			|||||||
package gounicorn
 | 
					package gounicorn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"os/exec"
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log "github.com/sirupsen/logrus"
 | 
						log "github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"goauthentik.io/internal/outpost/ak"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type GoUnicorn struct {
 | 
					type GoUnicorn struct {
 | 
				
			||||||
@ -12,6 +15,7 @@ type GoUnicorn struct {
 | 
				
			|||||||
	p       *exec.Cmd
 | 
						p       *exec.Cmd
 | 
				
			||||||
	started bool
 | 
						started bool
 | 
				
			||||||
	killed  bool
 | 
						killed  bool
 | 
				
			||||||
 | 
						alive   bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewGoUnicorn() *GoUnicorn {
 | 
					func NewGoUnicorn() *GoUnicorn {
 | 
				
			||||||
@ -20,6 +24,7 @@ func NewGoUnicorn() *GoUnicorn {
 | 
				
			|||||||
		log:     logger,
 | 
							log:     logger,
 | 
				
			||||||
		started: false,
 | 
							started: false,
 | 
				
			||||||
		killed:  false,
 | 
							killed:  false,
 | 
				
			||||||
 | 
							alive:   false,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	g.initCmd()
 | 
						g.initCmd()
 | 
				
			||||||
	return g
 | 
						return g
 | 
				
			||||||
@ -35,6 +40,10 @@ func (g *GoUnicorn) initCmd() {
 | 
				
			|||||||
	g.p.Stderr = os.Stderr
 | 
						g.p.Stderr = os.Stderr
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (g *GoUnicorn) IsRunning() bool {
 | 
				
			||||||
 | 
						return g.alive
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (g *GoUnicorn) Start() error {
 | 
					func (g *GoUnicorn) Start() error {
 | 
				
			||||||
	if g.killed {
 | 
						if g.killed {
 | 
				
			||||||
		g.log.Debug("Not restarting gunicorn since we're killed")
 | 
							g.log.Debug("Not restarting gunicorn since we're killed")
 | 
				
			||||||
@ -44,9 +53,38 @@ func (g *GoUnicorn) Start() error {
 | 
				
			|||||||
		g.initCmd()
 | 
							g.initCmd()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	g.started = true
 | 
						g.started = true
 | 
				
			||||||
 | 
						go g.healthcheck()
 | 
				
			||||||
	return g.p.Run()
 | 
						return g.p.Run()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (g *GoUnicorn) healthcheck() {
 | 
				
			||||||
 | 
						g.log.Debug("starting healthcheck")
 | 
				
			||||||
 | 
						h := &http.Client{
 | 
				
			||||||
 | 
							Transport: ak.NewUserAgentTransport("goauthentik.io go proxy healthcheck", http.DefaultTransport),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						check := func() bool {
 | 
				
			||||||
 | 
							res, err := h.Get("http://localhost:8000/-/health/live/")
 | 
				
			||||||
 | 
							if err == nil && res.StatusCode == 204 {
 | 
				
			||||||
 | 
								g.alive = true
 | 
				
			||||||
 | 
								return true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Default healthcheck is every 1 second on startup
 | 
				
			||||||
 | 
						// once we've been healthy once, increase to 30 seconds
 | 
				
			||||||
 | 
						for range time.Tick(time.Second) {
 | 
				
			||||||
 | 
							if check() {
 | 
				
			||||||
 | 
								g.log.Info("backend is alive, backing off with healthchecks")
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							g.log.Debug("backend not alive yet")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for range time.Tick(30 * time.Second) {
 | 
				
			||||||
 | 
							check()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (g *GoUnicorn) Kill() {
 | 
					func (g *GoUnicorn) Kill() {
 | 
				
			||||||
	g.killed = true
 | 
						g.killed = true
 | 
				
			||||||
	err := g.p.Process.Kill()
 | 
						err := g.p.Process.Kill()
 | 
				
			||||||
 | 
				
			|||||||
@ -116,10 +116,9 @@ func (a *APIController) OnRefresh() error {
 | 
				
			|||||||
		log.WithError(err).Error("Failed to fetch outpost configuration")
 | 
							log.WithError(err).Error("Failed to fetch outpost configuration")
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	outpost := outposts.Results[0]
 | 
						a.Outpost = outposts.Results[0]
 | 
				
			||||||
	doGlobalSetup(outpost.Config)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.WithField("name", outpost.Name).Debug("Fetched outpost configuration")
 | 
						log.WithField("name", a.Outpost.Name).Debug("Fetched outpost configuration")
 | 
				
			||||||
	return a.Server.Refresh()
 | 
						return a.Server.Refresh()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -128,6 +128,7 @@ func (ac *APIController) startWSHealth() {
 | 
				
			|||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error, reconnecting")
 | 
								ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error, reconnecting")
 | 
				
			||||||
			ac.wsConn.CloseAndReconnect()
 | 
								ac.wsConn.CloseAndReconnect()
 | 
				
			||||||
 | 
								time.Sleep(time.Second * 5)
 | 
				
			||||||
			continue
 | 
								continue
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			ConnectionStatus.With(prometheus.Labels{
 | 
								ConnectionStatus.With(prometheus.Labels{
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user