Compare commits
	
		
			180 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| cf4b4030aa | |||
| 74dc025869 | |||
| cabdc53553 | |||
| 29e9f399bd | |||
| dad43017a0 | |||
| 7fb939f97b | |||
| 88859b1c26 | |||
| c78236a2a2 | |||
| ba55538a34 | |||
| f742c73e24 | |||
| ca314c262c | |||
| b932b6c963 | |||
| 3c048a1921 | |||
| 8a60a7e26f | |||
| f10b57ba0b | |||
| e53114a645 | |||
| 2e50532518 | |||
| 1936ddfecb | |||
| 4afef46cb8 | |||
| 92b4244e81 | |||
| dfbf7027bc | |||
| eca2ef20d0 | |||
| cac5c7b3ea | |||
| 37ee555c8e | |||
| f910da0f8a | |||
| fc9d270992 | |||
| dcbc3d788a | |||
| 4658018a90 | |||
| 577b7ee515 | |||
| 621773c1ea | |||
| 3da526f20e | |||
| 052e465041 | |||
| c843f18743 | |||
| 80d0b14bb8 | |||
| 68637cf7cf | |||
| 82acba26af | |||
| ff8a812823 | |||
| 7f5fed2aea | |||
| a5c30fd9c7 | |||
| ef23a0da52 | |||
| ba527e7141 | |||
| 8edc254ab5 | |||
| 42627d21b0 | |||
| 2479b157d0 | |||
| 602573f83f | |||
| 20c33fa011 | |||
| 8599d9efe0 | |||
| 8e6fcfe350 | |||
| 558aa45201 | |||
| e9910732bc | |||
| 246dd4b062 | |||
| 4425f8d183 | |||
| c410bb8c36 | |||
| 44f62a4773 | |||
| b6ff04694f | |||
| d4ce0e8e41 | |||
| 362d72da8c | |||
| 88d0f8d8a8 | |||
| 61097b9400 | |||
| 7a73ddfb60 | |||
| d66f13c249 | |||
| 8cc3cb6a42 | |||
| 4c5537ddfe | |||
| a95779157d | |||
| 70256727fd | |||
| ac6afb2b82 | |||
| 2ea7bd86e8 | |||
| 95bce9c9e7 | |||
| 71a22c2a34 | |||
| f3eb85877d | |||
| 273f5211a0 | |||
| db06428ab9 | |||
| 109d8e48d4 | |||
| 2ca115285c | |||
| f5459645a5 | |||
| 14c159500d | |||
| 03da87991f | |||
| e38ee9c580 | |||
| 3bf53b2db1 | |||
| f33190caa5 | |||
| 741822424a | |||
| 0ca6fbb224 | |||
| f72b652b24 | |||
| 0a2c1eb419 | |||
| eb9593a847 | |||
| 7c71c52791 | |||
| 59493c02c4 | |||
| 83089b47d3 | |||
| 103e723d8c | |||
| 7d6e88061f | |||
| f8aab40e3e | |||
| 5123bc1316 | |||
| 30e8408e85 | |||
| bb34474101 | |||
| a105760123 | |||
| f410a77010 | |||
| 6ff8fdcc49 | |||
| 50ca3dc772 | |||
| 2a09fc0ae2 | |||
| fbb6756488 | |||
| f45fb2eac0 | |||
| 7b8cde17e6 | |||
| 186634fc67 | |||
| c84b1b7997 | |||
| 6e83467481 | |||
| 72db17f23b | |||
| ee4e176039 | |||
| e18e681c2b | |||
| 10fe67e08d | |||
| fc1db83be7 | |||
| 3740e65906 | |||
| 30386cd899 | |||
| 64a10e9a46 | |||
| 77d6242cce | |||
| 9a86dcaec3 | |||
| 0b00768b84 | |||
| d162c79373 | |||
| 05db352a0f | |||
| 5bf3d7fe02 | |||
| 1ae1cbebf4 | |||
| 8c16dfc478 | |||
| c6a3286e4c | |||
| 44cfd7e5b0 | |||
| 210d4c5058 | |||
| 6b39d616b1 | |||
| 32ace1bece | |||
| 54f893b84f | |||
| b5685ec072 | |||
| 5854833240 | |||
| 4b2437a6f1 | |||
| 2981ac7b10 | |||
| 59a51c859a | |||
| 47bab6c182 | |||
| 4e6714fffe | |||
| aa6b595545 | |||
| 0131b1f6cc | |||
| 9f53c359dd | |||
| 28e4dba3e8 | |||
| 2afd46e1df | |||
| f5991b19be | |||
| 5cc75cb25c | |||
| 68c1df2d39 | |||
| c83724f45c | |||
| 5f91c150df | |||
| 0bfe999442 | |||
| 58440b16c4 | |||
| 57757a2ff5 | |||
| 2993f506a7 | |||
| e4841d54a1 | |||
| 4f05dcec89 | |||
| ede6bcd31e | |||
| 728c8e994d | |||
| 5290b64415 | |||
| fec6de1ba2 | |||
| 69678dcfa6 | |||
| 4911a243ff | |||
| 70316b37da | |||
| 307cb94e3b | |||
| ace53a8fa5 | |||
| 0544dc3f83 | |||
| 708ff300a3 | |||
| 4e63f0f215 | |||
| 141481df3a | |||
| 29241cc287 | |||
| e81e97d404 | |||
| a5182e5c24 | |||
| cf5ff6e160 | |||
| f2b3a2ec91 | |||
| 69780c67a9 | |||
| ac9cf590bc | |||
| cb6edcb198 | |||
| 8eecc28c3c | |||
| 10b16bc36a | |||
| 2fe88cfea9 | |||
| caab396b56 | |||
| 5f0f4284a2 | |||
| c11be2284d | |||
| aa321196d7 | |||
| ff03db61a8 | |||
| f3b3ce6572 | 
@ -1,5 +1,5 @@
 | 
			
		||||
[bumpversion]
 | 
			
		||||
current_version = 2021.12.1-rc4
 | 
			
		||||
current_version = 2021.12.3
 | 
			
		||||
tag = True
 | 
			
		||||
commit = True
 | 
			
		||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							@ -7,6 +7,7 @@ exemptLabels:
 | 
			
		||||
  - pinned
 | 
			
		||||
  - security
 | 
			
		||||
  - pr_wanted
 | 
			
		||||
  - enhancement/confirmed
 | 
			
		||||
# Comment to post when marking an issue as stale. Set to `false` to disable
 | 
			
		||||
markComment: >
 | 
			
		||||
  This issue has been automatically marked as stale because it has not had
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										66
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										66
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							@ -47,7 +47,7 @@ jobs:
 | 
			
		||||
        env:
 | 
			
		||||
          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
			
		||||
        run: scripts/ci_prepare.sh
 | 
			
		||||
      - name: run pylint
 | 
			
		||||
      - name: run job
 | 
			
		||||
        run: pipenv run make ci-${{ matrix.job }}
 | 
			
		||||
  test-migrations:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
@ -86,7 +86,11 @@ jobs:
 | 
			
		||||
          path: ~/.local/share/virtualenvs
 | 
			
		||||
          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
			
		||||
      - name: checkout stable
 | 
			
		||||
        id: stable
 | 
			
		||||
        run: |
 | 
			
		||||
          # Save current branch
 | 
			
		||||
          current=$(git branch --show)
 | 
			
		||||
          echo ##[set-output name=originalBranch]$current
 | 
			
		||||
          # Copy current, latest config to local
 | 
			
		||||
          cp authentik/lib/default.yml local.env.yml
 | 
			
		||||
          cp -R .github ..
 | 
			
		||||
@ -108,7 +112,7 @@ jobs:
 | 
			
		||||
          set -x
 | 
			
		||||
          git fetch
 | 
			
		||||
          git reset --hard HEAD
 | 
			
		||||
          git checkout $GITHUB_HEAD_REF
 | 
			
		||||
          git checkout ${{ steps.stable.outputs.originalBranch }}
 | 
			
		||||
          pipenv sync --dev
 | 
			
		||||
      - name: prepare
 | 
			
		||||
        env:
 | 
			
		||||
@ -176,7 +180,7 @@ jobs:
 | 
			
		||||
          testspace [integration]unittest.xml --link=codecov
 | 
			
		||||
      - if: ${{ always() }}
 | 
			
		||||
        uses: codecov/codecov-action@v2
 | 
			
		||||
  test-e2e:
 | 
			
		||||
  test-e2e-provider:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
@ -215,12 +219,59 @@ jobs:
 | 
			
		||||
          npm run build
 | 
			
		||||
      - name: run e2e
 | 
			
		||||
        run: |
 | 
			
		||||
          pipenv run make test-e2e
 | 
			
		||||
          pipenv run make test-e2e-provider
 | 
			
		||||
          pipenv run coverage xml
 | 
			
		||||
      - name: run testspace
 | 
			
		||||
        if: ${{ always() }}
 | 
			
		||||
        run: |
 | 
			
		||||
          testspace [e2e]unittest.xml --link=codecov
 | 
			
		||||
          testspace [e2e-provider]unittest.xml --link=codecov
 | 
			
		||||
      - if: ${{ always() }}
 | 
			
		||||
        uses: codecov/codecov-action@v2
 | 
			
		||||
  test-e2e-rest:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/setup-python@v2
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.9'
 | 
			
		||||
      - uses: actions/setup-node@v2
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '16'
 | 
			
		||||
          cache: 'npm'
 | 
			
		||||
          cache-dependency-path: web/package-lock.json
 | 
			
		||||
      - uses: testspace-com/setup-testspace@v1
 | 
			
		||||
        with:
 | 
			
		||||
          domain: ${{github.repository_owner}}
 | 
			
		||||
      - id: cache-pipenv
 | 
			
		||||
        uses: actions/cache@v2.1.7
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.local/share/virtualenvs
 | 
			
		||||
          key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
 | 
			
		||||
      - name: prepare
 | 
			
		||||
        env:
 | 
			
		||||
          INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
 | 
			
		||||
        run: |
 | 
			
		||||
          scripts/ci_prepare.sh
 | 
			
		||||
          docker-compose -f tests/e2e/docker-compose.yml up -d
 | 
			
		||||
      - id: cache-web
 | 
			
		||||
        uses: actions/cache@v2.1.7
 | 
			
		||||
        with:
 | 
			
		||||
          path: web/dist
 | 
			
		||||
          key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }}
 | 
			
		||||
      - name: prepare web ui
 | 
			
		||||
        if: steps.cache-web.outputs.cache-hit != 'true'
 | 
			
		||||
        run: |
 | 
			
		||||
          cd web
 | 
			
		||||
          npm i
 | 
			
		||||
          npm run build
 | 
			
		||||
      - name: run e2e
 | 
			
		||||
        run: |
 | 
			
		||||
          pipenv run make test-e2e-rest
 | 
			
		||||
          pipenv run coverage xml
 | 
			
		||||
      - name: run testspace
 | 
			
		||||
        if: ${{ always() }}
 | 
			
		||||
        run: |
 | 
			
		||||
          testspace [e2e-rest]unittest.xml --link=codecov
 | 
			
		||||
      - if: ${{ always() }}
 | 
			
		||||
        uses: codecov/codecov-action@v2
 | 
			
		||||
  ci-core-mark:
 | 
			
		||||
@ -230,7 +281,8 @@ jobs:
 | 
			
		||||
      - test-migrations-from-stable
 | 
			
		||||
      - test-unittest
 | 
			
		||||
      - test-integration
 | 
			
		||||
      - test-e2e
 | 
			
		||||
      - test-e2e-rest
 | 
			
		||||
      - test-e2e-provider
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - run: echo mark
 | 
			
		||||
@ -252,7 +304,7 @@ jobs:
 | 
			
		||||
      - name: prepare variables
 | 
			
		||||
        id: ev
 | 
			
		||||
        env:
 | 
			
		||||
          DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
 | 
			
		||||
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
 | 
			
		||||
        run: |
 | 
			
		||||
          python ./scripts/gh_env.py
 | 
			
		||||
      - name: Login to Container Registry
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										42
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							@ -17,7 +17,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/setup-go@v2
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: '^1.16.3'
 | 
			
		||||
          go-version: "^1.17"
 | 
			
		||||
      - name: Run linter
 | 
			
		||||
        run: |
 | 
			
		||||
          # Create folder structure for go embeds
 | 
			
		||||
@ -58,7 +58,7 @@ jobs:
 | 
			
		||||
      - name: prepare variables
 | 
			
		||||
        id: ev
 | 
			
		||||
        env:
 | 
			
		||||
          DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
 | 
			
		||||
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
 | 
			
		||||
        run: |
 | 
			
		||||
          python ./scripts/gh_env.py
 | 
			
		||||
      - name: Login to Container Registry
 | 
			
		||||
@ -80,3 +80,41 @@ jobs:
 | 
			
		||||
          build-args: |
 | 
			
		||||
            GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
 | 
			
		||||
          platforms: ${{ matrix.arch }}
 | 
			
		||||
  build-outpost-binary:
 | 
			
		||||
    timeout-minutes: 120
 | 
			
		||||
    needs:
 | 
			
		||||
      - ci-outpost-mark
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        type:
 | 
			
		||||
          - proxy
 | 
			
		||||
          - ldap
 | 
			
		||||
        goos: [linux]
 | 
			
		||||
        goarch: [amd64, arm64]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/setup-go@v2
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: "^1.17"
 | 
			
		||||
      - uses: actions/setup-node@v2
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '16'
 | 
			
		||||
          cache: 'npm'
 | 
			
		||||
          cache-dependency-path: web/package-lock.json
 | 
			
		||||
      - name: Build web
 | 
			
		||||
        run: |
 | 
			
		||||
          cd web
 | 
			
		||||
          npm install
 | 
			
		||||
          npm run build-proxy
 | 
			
		||||
      - name: Build outpost
 | 
			
		||||
        run: |
 | 
			
		||||
          set -x
 | 
			
		||||
          export GOOS=${{ matrix.goos }}
 | 
			
		||||
          export GOARCH=${{ matrix.goarch }}
 | 
			
		||||
          go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
 | 
			
		||||
      - uses: actions/upload-artifact@v2
 | 
			
		||||
        with:
 | 
			
		||||
          name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
 | 
			
		||||
          path: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										60
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										60
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							@ -30,14 +30,14 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          push: ${{ github.event_name == 'release' }}
 | 
			
		||||
          tags: |
 | 
			
		||||
            beryju/authentik:2021.12.1-rc4,
 | 
			
		||||
            beryju/authentik:2021.12.3,
 | 
			
		||||
            beryju/authentik:latest,
 | 
			
		||||
            ghcr.io/goauthentik/server:2021.12.1-rc4,
 | 
			
		||||
            ghcr.io/goauthentik/server:2021.12.3,
 | 
			
		||||
            ghcr.io/goauthentik/server:latest
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          context: .
 | 
			
		||||
      - name: Building Docker Image (stable)
 | 
			
		||||
        if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc4', 'rc') }}
 | 
			
		||||
        if: ${{ github.event_name == 'release' && !contains('2021.12.3', 'rc') }}
 | 
			
		||||
        run: |
 | 
			
		||||
          docker pull beryju/authentik:latest
 | 
			
		||||
          docker tag beryju/authentik:latest beryju/authentik:stable
 | 
			
		||||
@ -57,7 +57,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/setup-go@v2
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: "^1.15"
 | 
			
		||||
          go-version: "^1.17"
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v1.2.0
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
@ -78,14 +78,14 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          push: ${{ github.event_name == 'release' }}
 | 
			
		||||
          tags: |
 | 
			
		||||
            beryju/authentik-${{ matrix.type }}:2021.12.1-rc4,
 | 
			
		||||
            beryju/authentik-${{ matrix.type }}:2021.12.3,
 | 
			
		||||
            beryju/authentik-${{ matrix.type }}:latest,
 | 
			
		||||
            ghcr.io/goauthentik/${{ matrix.type }}:2021.12.1-rc4,
 | 
			
		||||
            ghcr.io/goauthentik/${{ matrix.type }}:2021.12.3,
 | 
			
		||||
            ghcr.io/goauthentik/${{ matrix.type }}:latest
 | 
			
		||||
          file: ${{ matrix.type }}.Dockerfile
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
      - name: Building Docker Image (stable)
 | 
			
		||||
        if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc4', 'rc') }}
 | 
			
		||||
        if: ${{ github.event_name == 'release' && !contains('2021.12.3', 'rc') }}
 | 
			
		||||
        run: |
 | 
			
		||||
          docker pull beryju/authentik-${{ matrix.type }}:latest
 | 
			
		||||
          docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
 | 
			
		||||
@ -93,10 +93,50 @@ jobs:
 | 
			
		||||
          docker pull ghcr.io/goauthentik/${{ matrix.type }}:latest
 | 
			
		||||
          docker tag ghcr.io/goauthentik/${{ matrix.type }}:latest ghcr.io/goauthentik/${{ matrix.type }}:stable
 | 
			
		||||
          docker push ghcr.io/goauthentik/${{ matrix.type }}:stable
 | 
			
		||||
  build-outpost-binary:
 | 
			
		||||
    timeout-minutes: 120
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        type:
 | 
			
		||||
          - proxy
 | 
			
		||||
          - ldap
 | 
			
		||||
        goos: [linux, darwin]
 | 
			
		||||
        goarch: [amd64, arm64]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
      - uses: actions/setup-go@v2
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: "^1.17"
 | 
			
		||||
      - uses: actions/setup-node@v2
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '16'
 | 
			
		||||
          cache: 'npm'
 | 
			
		||||
          cache-dependency-path: web/package-lock.json
 | 
			
		||||
      - name: Build web
 | 
			
		||||
        run: |
 | 
			
		||||
          cd web
 | 
			
		||||
          npm install
 | 
			
		||||
          npm run build-proxy
 | 
			
		||||
      - name: Build outpost
 | 
			
		||||
        run: |
 | 
			
		||||
          set -x
 | 
			
		||||
          export GOOS=${{ matrix.goos }}
 | 
			
		||||
          export GOARCH=${{ matrix.goarch }}
 | 
			
		||||
          go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
 | 
			
		||||
      - name: Upload binaries to release
 | 
			
		||||
        uses: svenstaro/upload-release-action@v2
 | 
			
		||||
        with:
 | 
			
		||||
          repo_token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
 | 
			
		||||
          asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
 | 
			
		||||
          tag: ${{ github.ref }}
 | 
			
		||||
  test-release:
 | 
			
		||||
    needs:
 | 
			
		||||
      - build-server
 | 
			
		||||
      - build-outpost
 | 
			
		||||
      - build-outpost-binary
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
@ -110,7 +150,9 @@ jobs:
 | 
			
		||||
          docker-compose run -u root server test
 | 
			
		||||
  sentry-release:
 | 
			
		||||
    needs:
 | 
			
		||||
      - test-release
 | 
			
		||||
      - build-server
 | 
			
		||||
      - build-outpost
 | 
			
		||||
      - build-outpost-binary
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v2
 | 
			
		||||
@ -128,7 +170,7 @@ jobs:
 | 
			
		||||
          SENTRY_PROJECT: authentik
 | 
			
		||||
          SENTRY_URL: https://sentry.beryju.org
 | 
			
		||||
        with:
 | 
			
		||||
          version: authentik@2021.12.1-rc4
 | 
			
		||||
          version: authentik@2021.12.3
 | 
			
		||||
          environment: beryjuorg-prod
 | 
			
		||||
          sourcemaps: './web/dist'
 | 
			
		||||
          url_prefix: '~/static/dist'
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								.python-version
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.python-version
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
3.9.7
 | 
			
		||||
@ -58,8 +58,6 @@ RUN apt-get update && \
 | 
			
		||||
        curl ca-certificates gnupg git runit libpq-dev \
 | 
			
		||||
        postgresql-client build-essential libxmlsec1-dev \
 | 
			
		||||
        pkg-config libmaxminddb0 && \
 | 
			
		||||
    pip install lxml==4.6.4 --no-cache-dir && \
 | 
			
		||||
    export C_INCLUDE_PATH=/usr/local/lib/python3.10/site-packages/lxml/includes && \
 | 
			
		||||
    pip install -r /requirements.txt --no-cache-dir && \
 | 
			
		||||
    apt-get remove --purge -y build-essential git && \
 | 
			
		||||
    apt-get autoremove --purge -y && \
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										9
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										9
									
								
								Makefile
									
									
									
									
									
								
							@ -4,13 +4,16 @@ UID = $(shell id -u)
 | 
			
		||||
GID = $(shell id -g)
 | 
			
		||||
NPM_VERSION = $(shell python -m scripts.npm_version)
 | 
			
		||||
 | 
			
		||||
all: lint-fix lint test gen
 | 
			
		||||
all: lint-fix lint test gen web
 | 
			
		||||
 | 
			
		||||
test-integration:
 | 
			
		||||
	coverage run manage.py test tests/integration
 | 
			
		||||
 | 
			
		||||
test-e2e:
 | 
			
		||||
	coverage run manage.py test tests/e2e
 | 
			
		||||
test-e2e-provider:
 | 
			
		||||
	coverage run manage.py test tests/e2e/test_provider*
 | 
			
		||||
 | 
			
		||||
test-e2e-rest:
 | 
			
		||||
	coverage run manage.py test tests/e2e/test_flows* tests/e2e/test_source*
 | 
			
		||||
 | 
			
		||||
test:
 | 
			
		||||
	coverage run manage.py test authentik
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								Pipfile
									
									
									
									
									
								
							@ -32,15 +32,14 @@ geoip2 = "*"
 | 
			
		||||
gunicorn = "*"
 | 
			
		||||
kubernetes = "==v19.15.0"
 | 
			
		||||
ldap3 = "*"
 | 
			
		||||
# 4.7.0 and later remove `lxml-version.h` which is required by xmlsec
 | 
			
		||||
lxml = "==4.6.5"
 | 
			
		||||
lxml = "*"
 | 
			
		||||
packaging = "*"
 | 
			
		||||
psycopg2-binary = "*"
 | 
			
		||||
pycryptodome = "*"
 | 
			
		||||
pyjwt = "*"
 | 
			
		||||
pyyaml = "*"
 | 
			
		||||
requests-oauthlib = "*"
 | 
			
		||||
sentry-sdk = "*"
 | 
			
		||||
sentry-sdk = { git = 'https://github.com/beryju/sentry-python.git', ref = '379aee28b15d3b87b381317746c4efd24b3d7bc3' }
 | 
			
		||||
service_identity = "*"
 | 
			
		||||
structlog = "*"
 | 
			
		||||
swagger-spec-validator = "*"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										367
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										367
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "_meta": {
 | 
			
		||||
        "hash": {
 | 
			
		||||
            "sha256": "6a89870496296af32dbc2f64b0832d4c20010829ada0b3c4dc27fee56b68fad9"
 | 
			
		||||
            "sha256": "dedb51159ef09fd9b00ab28022706f525c9df057ffd646e2a552784341a10538"
 | 
			
		||||
        },
 | 
			
		||||
        "pipfile-spec": 6,
 | 
			
		||||
        "requires": {},
 | 
			
		||||
@ -109,11 +109,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "amqp": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:4d9cb6b5d69183ba279e97382ff68a071864c25b561d206dab73499d3ed26d1c",
 | 
			
		||||
                "sha256:d757b78fd7d3c6bb60e3ee811b68145583643747ed3ec253329f086aa3a72a5d"
 | 
			
		||||
                "sha256:1e5f707424e544078ca196e72ae6a14887ce74e02bd126be54b7c03c971bef18",
 | 
			
		||||
                "sha256:9cd81f7b023fc04bbb108718fbac674f06901b77bfcdce85b10e2a5d0ee91be5"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==5.0.7"
 | 
			
		||||
            "version": "==5.0.9"
 | 
			
		||||
        },
 | 
			
		||||
        "asgiref": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -169,19 +169,19 @@
 | 
			
		||||
        },
 | 
			
		||||
        "boto3": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:76b3ee0d1dd860c9218bc864cd29f1ee986f6e1e75e8669725dd3c411039379e",
 | 
			
		||||
                "sha256:c39cb6ed376ba1d4689ac8f6759a2b2d8a0b0424dbec0cd3af1558079bcf06e8"
 | 
			
		||||
                "sha256:739705b28e6b2329ea3b481ba801d439c296aaf176f7850729147ba99bbf8a9a",
 | 
			
		||||
                "sha256:8f08e8e94bf107c5e9866684e9aadf8d9f60abed0cfe5c1dba4e7328674a1986"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.20.23"
 | 
			
		||||
            "version": "==1.20.24"
 | 
			
		||||
        },
 | 
			
		||||
        "botocore": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:640b62110aa6d1c25553eceafb5bcd89aedeb84b191598d1f6492ad24374d285",
 | 
			
		||||
                "sha256:7459766c4594f3b8877e8013f93f0dc6c6486acbeb7d9c9ae488396529cc2e84"
 | 
			
		||||
                "sha256:43006b4f52d7bb655319d3da0f615cdbee7762853acc1ebcb1d49f962e6b4806",
 | 
			
		||||
                "sha256:e78d48c50c8c013fb9b362c6202fece2fe868edfd89b51968080180bdff41617"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==1.23.23"
 | 
			
		||||
            "version": "==1.23.24"
 | 
			
		||||
        },
 | 
			
		||||
        "cachetools": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -196,7 +196,7 @@
 | 
			
		||||
                "sha256:1ef33f089e0a494e8d1b487508356f055c865b1955b125c00c991a4358543c80",
 | 
			
		||||
                "sha256:8eca49962b1bfc09c24d442aa55688be88efe5c24aeef89d3be135614b95c678"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.7' and python_version < '4'",
 | 
			
		||||
            "markers": "python_version >= '3.7' and python_full_version < '4.0.0'",
 | 
			
		||||
            "version": "==1.9.0"
 | 
			
		||||
        },
 | 
			
		||||
        "cbor2": {
 | 
			
		||||
@ -325,7 +325,7 @@
 | 
			
		||||
                "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
 | 
			
		||||
                "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version < '4' and python_full_version >= '3.6.2'",
 | 
			
		||||
            "markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'",
 | 
			
		||||
            "version": "==0.3.0"
 | 
			
		||||
        },
 | 
			
		||||
        "click-plugins": {
 | 
			
		||||
@ -367,30 +367,29 @@
 | 
			
		||||
        },
 | 
			
		||||
        "cryptography": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681",
 | 
			
		||||
                "sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed",
 | 
			
		||||
                "sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4",
 | 
			
		||||
                "sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568",
 | 
			
		||||
                "sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e",
 | 
			
		||||
                "sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f",
 | 
			
		||||
                "sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f",
 | 
			
		||||
                "sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712",
 | 
			
		||||
                "sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e",
 | 
			
		||||
                "sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58",
 | 
			
		||||
                "sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44",
 | 
			
		||||
                "sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6",
 | 
			
		||||
                "sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d",
 | 
			
		||||
                "sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636",
 | 
			
		||||
                "sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba",
 | 
			
		||||
                "sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120",
 | 
			
		||||
                "sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3",
 | 
			
		||||
                "sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d",
 | 
			
		||||
                "sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b",
 | 
			
		||||
                "sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81",
 | 
			
		||||
                "sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8"
 | 
			
		||||
                "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3",
 | 
			
		||||
                "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31",
 | 
			
		||||
                "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac",
 | 
			
		||||
                "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf",
 | 
			
		||||
                "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316",
 | 
			
		||||
                "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca",
 | 
			
		||||
                "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638",
 | 
			
		||||
                "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94",
 | 
			
		||||
                "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12",
 | 
			
		||||
                "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173",
 | 
			
		||||
                "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b",
 | 
			
		||||
                "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a",
 | 
			
		||||
                "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f",
 | 
			
		||||
                "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2",
 | 
			
		||||
                "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9",
 | 
			
		||||
                "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46",
 | 
			
		||||
                "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903",
 | 
			
		||||
                "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3",
 | 
			
		||||
                "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1",
 | 
			
		||||
                "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==36.0.0"
 | 
			
		||||
            "version": "==36.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "dacite": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -410,11 +409,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "deepmerge": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:87166dbe9ba1a3348a45c9d4ada6778f518d41afc0b85aa017ea3041facc3f9c",
 | 
			
		||||
                "sha256:f6fd7f1293c535fb599e197e750dbe8674503c5d2a89759b3c72a3c46746d4fd"
 | 
			
		||||
                "sha256:4b44779ed3d2fb791bb181fc2683423496fea428abb7af37feb23286de7f0a1a",
 | 
			
		||||
                "sha256:f851fff957697cb8f4580b465acf5c2d35841695306ff0abb9cb9c273ad76112"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==0.3.0"
 | 
			
		||||
            "version": "==1.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "defusedxml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -470,11 +469,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "django-prometheus": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:c338d6efde1ca336e90c540b5e87afe9287d7bcc82d651a778f302b0be17a933",
 | 
			
		||||
                "sha256:dd3f8da1399140fbef5c00d1526a23d1ade286b144281c325f8e409a781643f2"
 | 
			
		||||
                "sha256:240378a1307c408bd5fc85614a3a57f1ce633d4a222c9e291e2bbf325173b801",
 | 
			
		||||
                "sha256:e6616770d8820b8834762764bf1b76ec08e1b98e72a6f359d488a2e15fe3537c"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==2.1.0"
 | 
			
		||||
            "version": "==2.2.0"
 | 
			
		||||
        },
 | 
			
		||||
        "django-redis": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -494,11 +493,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "djangorestframework": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf",
 | 
			
		||||
                "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"
 | 
			
		||||
                "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee",
 | 
			
		||||
                "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==3.12.4"
 | 
			
		||||
            "version": "==3.13.1"
 | 
			
		||||
        },
 | 
			
		||||
        "djangorestframework-guardian": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -781,11 +780,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "jsonschema": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2a0f162822a64d95287990481b45d82f096e99721c86534f48201b64ebca6e8c",
 | 
			
		||||
                "sha256:390713469ae64b8a58698bb3cbc3859abe6925b565a973f87323ef21b09a27a8"
 | 
			
		||||
                "sha256:0070ca8dd5bf47941d1e9d8bc115a3654b1138cfb8aff44f3e3527276107314f",
 | 
			
		||||
                "sha256:91ffbad994d766041c6003d5f8f475cceb890c30084bd0e64847ccb1c10e48bb"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.7'",
 | 
			
		||||
            "version": "==4.2.1"
 | 
			
		||||
            "version": "==4.3.1"
 | 
			
		||||
        },
 | 
			
		||||
        "kombu": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -816,69 +815,69 @@
 | 
			
		||||
        },
 | 
			
		||||
        "lxml": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59",
 | 
			
		||||
                "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3",
 | 
			
		||||
                "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9",
 | 
			
		||||
                "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f",
 | 
			
		||||
                "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b",
 | 
			
		||||
                "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04",
 | 
			
		||||
                "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0",
 | 
			
		||||
                "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120",
 | 
			
		||||
                "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570",
 | 
			
		||||
                "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b",
 | 
			
		||||
                "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c",
 | 
			
		||||
                "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2",
 | 
			
		||||
                "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9",
 | 
			
		||||
                "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c",
 | 
			
		||||
                "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242",
 | 
			
		||||
                "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41",
 | 
			
		||||
                "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89",
 | 
			
		||||
                "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33",
 | 
			
		||||
                "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d",
 | 
			
		||||
                "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808",
 | 
			
		||||
                "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e",
 | 
			
		||||
                "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c",
 | 
			
		||||
                "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a",
 | 
			
		||||
                "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44",
 | 
			
		||||
                "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5",
 | 
			
		||||
                "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210",
 | 
			
		||||
                "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca",
 | 
			
		||||
                "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8",
 | 
			
		||||
                "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887",
 | 
			
		||||
                "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5",
 | 
			
		||||
                "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71",
 | 
			
		||||
                "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b",
 | 
			
		||||
                "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b",
 | 
			
		||||
                "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996",
 | 
			
		||||
                "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542",
 | 
			
		||||
                "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0",
 | 
			
		||||
                "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace",
 | 
			
		||||
                "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd",
 | 
			
		||||
                "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b",
 | 
			
		||||
                "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c",
 | 
			
		||||
                "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93",
 | 
			
		||||
                "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1",
 | 
			
		||||
                "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2",
 | 
			
		||||
                "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808",
 | 
			
		||||
                "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9",
 | 
			
		||||
                "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090",
 | 
			
		||||
                "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9",
 | 
			
		||||
                "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb",
 | 
			
		||||
                "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144",
 | 
			
		||||
                "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d",
 | 
			
		||||
                "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203",
 | 
			
		||||
                "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a",
 | 
			
		||||
                "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408",
 | 
			
		||||
                "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71",
 | 
			
		||||
                "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952",
 | 
			
		||||
                "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4",
 | 
			
		||||
                "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352",
 | 
			
		||||
                "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8",
 | 
			
		||||
                "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944",
 | 
			
		||||
                "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"
 | 
			
		||||
                "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4",
 | 
			
		||||
                "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f",
 | 
			
		||||
                "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a",
 | 
			
		||||
                "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944",
 | 
			
		||||
                "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1",
 | 
			
		||||
                "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d",
 | 
			
		||||
                "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d",
 | 
			
		||||
                "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e",
 | 
			
		||||
                "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d",
 | 
			
		||||
                "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a",
 | 
			
		||||
                "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675",
 | 
			
		||||
                "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3",
 | 
			
		||||
                "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55",
 | 
			
		||||
                "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60",
 | 
			
		||||
                "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d",
 | 
			
		||||
                "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6",
 | 
			
		||||
                "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e",
 | 
			
		||||
                "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5",
 | 
			
		||||
                "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5",
 | 
			
		||||
                "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42",
 | 
			
		||||
                "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0",
 | 
			
		||||
                "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d",
 | 
			
		||||
                "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489",
 | 
			
		||||
                "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440",
 | 
			
		||||
                "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e",
 | 
			
		||||
                "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6",
 | 
			
		||||
                "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e",
 | 
			
		||||
                "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f",
 | 
			
		||||
                "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d",
 | 
			
		||||
                "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03",
 | 
			
		||||
                "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9",
 | 
			
		||||
                "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9",
 | 
			
		||||
                "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd",
 | 
			
		||||
                "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6",
 | 
			
		||||
                "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4",
 | 
			
		||||
                "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868",
 | 
			
		||||
                "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267",
 | 
			
		||||
                "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2",
 | 
			
		||||
                "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4",
 | 
			
		||||
                "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24",
 | 
			
		||||
                "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2",
 | 
			
		||||
                "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db",
 | 
			
		||||
                "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a",
 | 
			
		||||
                "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8",
 | 
			
		||||
                "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175",
 | 
			
		||||
                "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851",
 | 
			
		||||
                "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b",
 | 
			
		||||
                "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e",
 | 
			
		||||
                "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986",
 | 
			
		||||
                "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f",
 | 
			
		||||
                "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419",
 | 
			
		||||
                "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7",
 | 
			
		||||
                "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7",
 | 
			
		||||
                "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36",
 | 
			
		||||
                "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc",
 | 
			
		||||
                "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b",
 | 
			
		||||
                "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e",
 | 
			
		||||
                "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17",
 | 
			
		||||
                "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3",
 | 
			
		||||
                "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==4.6.5"
 | 
			
		||||
            "version": "==4.7.1"
 | 
			
		||||
        },
 | 
			
		||||
        "maxminddb": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1312,12 +1311,12 @@
 | 
			
		||||
            "version": "==0.5.0"
 | 
			
		||||
        },
 | 
			
		||||
        "sentry-sdk": {
 | 
			
		||||
            "git": "https://github.com/beryju/sentry-python.git",
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:0db297ab32e095705c20f742c3a5dac62fe15c4318681884053d0898e5abb2f6",
 | 
			
		||||
                "sha256:789a11a87ca02491896e121efdd64e8fd93327b69e8f2f7d42f03e2569648e88"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==1.5.0"
 | 
			
		||||
            "ref": "379aee28b15d3b87b381317746c4efd24b3d7bc3"
 | 
			
		||||
        },
 | 
			
		||||
        "service-identity": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1329,11 +1328,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "setuptools": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373",
 | 
			
		||||
                "sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e"
 | 
			
		||||
                "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c",
 | 
			
		||||
                "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==59.6.0"
 | 
			
		||||
            "markers": "python_version >= '3.7'",
 | 
			
		||||
            "version": "==60.0.0"
 | 
			
		||||
        },
 | 
			
		||||
        "six": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1353,11 +1352,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "structlog": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:305a66201f9605a2e8a2595271a446f258175901c09c01e4c2c2a8ac5b68edf1",
 | 
			
		||||
                "sha256:6ed8fadb27cf8362be0e606f5e79ccdd3b1e879aac65f9dc0ac3033fd013a7be"
 | 
			
		||||
                "sha256:68c4c29c003714fe86834f347cb107452847ba52414390a7ee583472bde00fc9",
 | 
			
		||||
                "sha256:fd7922e195262b337da85c2a91c84be94ccab1f8fd1957bd6986f6904e3761c8"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==21.4.0"
 | 
			
		||||
            "version": "==21.5.0"
 | 
			
		||||
        },
 | 
			
		||||
        "swagger-spec-validator": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -1455,9 +1454,7 @@
 | 
			
		||||
            "version": "==4.1.1"
 | 
			
		||||
        },
 | 
			
		||||
        "urllib3": {
 | 
			
		||||
            "extras": [
 | 
			
		||||
                "secure"
 | 
			
		||||
            ],
 | 
			
		||||
            "extras": [],
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
 | 
			
		||||
                "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
 | 
			
		||||
@ -1942,30 +1939,29 @@
 | 
			
		||||
        },
 | 
			
		||||
        "cryptography": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681",
 | 
			
		||||
                "sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed",
 | 
			
		||||
                "sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4",
 | 
			
		||||
                "sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568",
 | 
			
		||||
                "sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e",
 | 
			
		||||
                "sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f",
 | 
			
		||||
                "sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f",
 | 
			
		||||
                "sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712",
 | 
			
		||||
                "sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e",
 | 
			
		||||
                "sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58",
 | 
			
		||||
                "sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44",
 | 
			
		||||
                "sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6",
 | 
			
		||||
                "sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d",
 | 
			
		||||
                "sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636",
 | 
			
		||||
                "sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba",
 | 
			
		||||
                "sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120",
 | 
			
		||||
                "sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3",
 | 
			
		||||
                "sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d",
 | 
			
		||||
                "sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b",
 | 
			
		||||
                "sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81",
 | 
			
		||||
                "sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8"
 | 
			
		||||
                "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3",
 | 
			
		||||
                "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31",
 | 
			
		||||
                "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac",
 | 
			
		||||
                "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf",
 | 
			
		||||
                "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316",
 | 
			
		||||
                "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca",
 | 
			
		||||
                "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638",
 | 
			
		||||
                "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94",
 | 
			
		||||
                "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12",
 | 
			
		||||
                "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173",
 | 
			
		||||
                "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b",
 | 
			
		||||
                "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a",
 | 
			
		||||
                "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f",
 | 
			
		||||
                "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2",
 | 
			
		||||
                "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9",
 | 
			
		||||
                "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46",
 | 
			
		||||
                "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903",
 | 
			
		||||
                "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3",
 | 
			
		||||
                "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1",
 | 
			
		||||
                "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==36.0.0"
 | 
			
		||||
            "version": "==36.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "gitdb": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -2000,11 +1996,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "importlib-metadata": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100",
 | 
			
		||||
                "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"
 | 
			
		||||
                "sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6",
 | 
			
		||||
                "sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"
 | 
			
		||||
            ],
 | 
			
		||||
            "index": "pypi",
 | 
			
		||||
            "version": "==4.8.2"
 | 
			
		||||
            "version": "==4.10.0"
 | 
			
		||||
        },
 | 
			
		||||
        "iniconfig": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -2018,36 +2014,51 @@
 | 
			
		||||
                "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7",
 | 
			
		||||
                "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version < '4' and python_full_version >= '3.6.1'",
 | 
			
		||||
            "markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
 | 
			
		||||
            "version": "==5.10.1"
 | 
			
		||||
        },
 | 
			
		||||
        "lazy-object-proxy": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:17e0967ba374fc24141738c69736da90e94419338fd4c7c7bef01ee26b339653",
 | 
			
		||||
                "sha256:1fee665d2638491f4d6e55bd483e15ef21f6c8c2095f235fef72601021e64f61",
 | 
			
		||||
                "sha256:22ddd618cefe54305df49e4c069fa65715be4ad0e78e8d252a33debf00f6ede2",
 | 
			
		||||
                "sha256:24a5045889cc2729033b3e604d496c2b6f588c754f7a62027ad4437a7ecc4837",
 | 
			
		||||
                "sha256:410283732af311b51b837894fa2f24f2c0039aa7f220135192b38fcc42bd43d3",
 | 
			
		||||
                "sha256:4732c765372bd78a2d6b2150a6e99d00a78ec963375f236979c0626b97ed8e43",
 | 
			
		||||
                "sha256:489000d368377571c6f982fba6497f2aa13c6d1facc40660963da62f5c379726",
 | 
			
		||||
                "sha256:4f60460e9f1eb632584c9685bccea152f4ac2130e299784dbaf9fae9f49891b3",
 | 
			
		||||
                "sha256:5743a5ab42ae40caa8421b320ebf3a998f89c85cdc8376d6b2e00bd12bd1b587",
 | 
			
		||||
                "sha256:85fb7608121fd5621cc4377a8961d0b32ccf84a7285b4f1d21988b2eae2868e8",
 | 
			
		||||
                "sha256:9698110e36e2df951c7c36b6729e96429c9c32b3331989ef19976592c5f3c77a",
 | 
			
		||||
                "sha256:9d397bf41caad3f489e10774667310d73cb9c4258e9aed94b9ec734b34b495fd",
 | 
			
		||||
                "sha256:b579f8acbf2bdd9ea200b1d5dea36abd93cabf56cf626ab9c744a432e15c815f",
 | 
			
		||||
                "sha256:b865b01a2e7f96db0c5d12cfea590f98d8c5ba64ad222300d93ce6ff9138bcad",
 | 
			
		||||
                "sha256:bf34e368e8dd976423396555078def5cfc3039ebc6fc06d1ae2c5a65eebbcde4",
 | 
			
		||||
                "sha256:c6938967f8528b3668622a9ed3b31d145fab161a32f5891ea7b84f6b790be05b",
 | 
			
		||||
                "sha256:d1c2676e3d840852a2de7c7d5d76407c772927addff8d742b9808fe0afccebdf",
 | 
			
		||||
                "sha256:d7124f52f3bd259f510651450e18e0fd081ed82f3c08541dffc7b94b883aa981",
 | 
			
		||||
                "sha256:d900d949b707778696fdf01036f58c9876a0d8bfe116e8d220cfd4b15f14e741",
 | 
			
		||||
                "sha256:ebfd274dcd5133e0afae738e6d9da4323c3eb021b3e13052d8cbd0e457b1256e",
 | 
			
		||||
                "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
 | 
			
		||||
                "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"
 | 
			
		||||
                "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7",
 | 
			
		||||
                "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a",
 | 
			
		||||
                "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c",
 | 
			
		||||
                "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc",
 | 
			
		||||
                "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f",
 | 
			
		||||
                "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09",
 | 
			
		||||
                "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442",
 | 
			
		||||
                "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e",
 | 
			
		||||
                "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029",
 | 
			
		||||
                "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61",
 | 
			
		||||
                "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb",
 | 
			
		||||
                "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0",
 | 
			
		||||
                "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35",
 | 
			
		||||
                "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42",
 | 
			
		||||
                "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1",
 | 
			
		||||
                "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad",
 | 
			
		||||
                "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443",
 | 
			
		||||
                "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd",
 | 
			
		||||
                "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9",
 | 
			
		||||
                "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148",
 | 
			
		||||
                "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38",
 | 
			
		||||
                "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55",
 | 
			
		||||
                "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36",
 | 
			
		||||
                "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a",
 | 
			
		||||
                "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b",
 | 
			
		||||
                "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44",
 | 
			
		||||
                "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6",
 | 
			
		||||
                "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69",
 | 
			
		||||
                "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4",
 | 
			
		||||
                "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84",
 | 
			
		||||
                "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de",
 | 
			
		||||
                "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28",
 | 
			
		||||
                "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c",
 | 
			
		||||
                "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1",
 | 
			
		||||
                "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8",
 | 
			
		||||
                "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b",
 | 
			
		||||
                "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
 | 
			
		||||
            "version": "==1.6.0"
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==1.7.1"
 | 
			
		||||
        },
 | 
			
		||||
        "mccabe": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -2332,11 +2343,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "setuptools": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373",
 | 
			
		||||
                "sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e"
 | 
			
		||||
                "sha256:5ec2bbb534ed160b261acbbdd1b463eb3cf52a8d223d96a8ab9981f63798e85c",
 | 
			
		||||
                "sha256:75fd345a47ce3d79595b27bf57e6f49c2ca7904f3c7ce75f8a87012046c86b0b"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==59.6.0"
 | 
			
		||||
            "markers": "python_version >= '3.7'",
 | 
			
		||||
            "version": "==60.0.0"
 | 
			
		||||
        },
 | 
			
		||||
        "six": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -2387,11 +2398,11 @@
 | 
			
		||||
        },
 | 
			
		||||
        "tomli": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee",
 | 
			
		||||
                "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"
 | 
			
		||||
                "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f",
 | 
			
		||||
                "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"
 | 
			
		||||
            ],
 | 
			
		||||
            "markers": "python_version >= '3.6'",
 | 
			
		||||
            "version": "==1.2.2"
 | 
			
		||||
            "version": "==1.2.3"
 | 
			
		||||
        },
 | 
			
		||||
        "trio": {
 | 
			
		||||
            "hashes": [
 | 
			
		||||
@ -2418,9 +2429,7 @@
 | 
			
		||||
            "version": "==4.0.1"
 | 
			
		||||
        },
 | 
			
		||||
        "urllib3": {
 | 
			
		||||
            "extras": [
 | 
			
		||||
                "secure"
 | 
			
		||||
            ],
 | 
			
		||||
            "extras": [],
 | 
			
		||||
            "hashes": [
 | 
			
		||||
                "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
 | 
			
		||||
                "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.md
									
									
									
									
									
								
							@ -38,3 +38,23 @@ See [Development Documentation](https://goauthentik.io/developer-docs/?utm_sourc
 | 
			
		||||
## Security
 | 
			
		||||
 | 
			
		||||
See [SECURITY.md](SECURITY.md)
 | 
			
		||||
 | 
			
		||||
## Sponsors
 | 
			
		||||
 | 
			
		||||
This project is proudly sponsored by:
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
    <a href="https://www.digitalocean.com/?utm_medium=opensource&utm_source=goauthentik.io">
 | 
			
		||||
        <img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
 | 
			
		||||
    </a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
DigitalOcean provides development and testing resources for authentik.
 | 
			
		||||
 | 
			
		||||
<p>
 | 
			
		||||
    <a href="https://www.netlify.com">
 | 
			
		||||
        <img src="https://www.netlify.com/img/global/badges/netlify-color-accent.svg" alt="Deploys by Netlify" />
 | 
			
		||||
    </a>
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
Netlify hosts the [goauthentik.io](goauthentik.io) site.
 | 
			
		||||
 | 
			
		||||
@ -6,8 +6,8 @@
 | 
			
		||||
 | 
			
		||||
| Version    | Supported          |
 | 
			
		||||
| ---------- | ------------------ |
 | 
			
		||||
| 2021.9.x   | :white_check_mark: |
 | 
			
		||||
| 2021.10.x  | :white_check_mark: |
 | 
			
		||||
| 2021.12.x  | :white_check_mark: |
 | 
			
		||||
 | 
			
		||||
## Reporting a Vulnerability
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,3 @@
 | 
			
		||||
"""authentik"""
 | 
			
		||||
__version__ = "2021.12.1-rc4"
 | 
			
		||||
__version__ = "2021.12.3"
 | 
			
		||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,6 @@
 | 
			
		||||
"""authentik administration metrics"""
 | 
			
		||||
import time
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
 | 
			
		||||
from django.db.models import Count, ExpressionWrapper, F
 | 
			
		||||
from django.db.models.fields import DurationField
 | 
			
		||||
from django.db.models.functions import ExtractHour
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from drf_spectacular.utils import extend_schema, extend_schema_field
 | 
			
		||||
from guardian.shortcuts import get_objects_for_user
 | 
			
		||||
from rest_framework.fields import IntegerField, SerializerMethodField
 | 
			
		||||
from rest_framework.permissions import IsAdminUser
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
@ -15,31 +8,7 @@ from rest_framework.response import Response
 | 
			
		||||
from rest_framework.views import APIView
 | 
			
		||||
 | 
			
		||||
from authentik.core.api.utils import PassiveSerializer
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
 | 
			
		||||
    """Get event count by hour in the last day, fill with zeros"""
 | 
			
		||||
    date_from = now() - timedelta(days=1)
 | 
			
		||||
    result = (
 | 
			
		||||
        Event.objects.filter(created__gte=date_from, **filter_kwargs)
 | 
			
		||||
        .annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
 | 
			
		||||
        .annotate(age_hours=ExtractHour("age"))
 | 
			
		||||
        .values("age_hours")
 | 
			
		||||
        .annotate(count=Count("pk"))
 | 
			
		||||
        .order_by("age_hours")
 | 
			
		||||
    )
 | 
			
		||||
    data = Counter({int(d["age_hours"]): d["count"] for d in result})
 | 
			
		||||
    results = []
 | 
			
		||||
    _now = now()
 | 
			
		||||
    for hour in range(0, -24, -1):
 | 
			
		||||
        results.append(
 | 
			
		||||
            {
 | 
			
		||||
                "x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
 | 
			
		||||
                "y_cord": data[hour * -1],
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    return results
 | 
			
		||||
from authentik.events.models import EventAction
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CoordinateSerializer(PassiveSerializer):
 | 
			
		||||
@ -58,12 +27,22 @@ class LoginMetricsSerializer(PassiveSerializer):
 | 
			
		||||
    @extend_schema_field(CoordinateSerializer(many=True))
 | 
			
		||||
    def get_logins_per_1h(self, _):
 | 
			
		||||
        """Get successful logins per hour for the last 24 hours"""
 | 
			
		||||
        return get_events_per_1h(action=EventAction.LOGIN)
 | 
			
		||||
        user = self.context["user"]
 | 
			
		||||
        return (
 | 
			
		||||
            get_objects_for_user(user, "authentik_events.view_event")
 | 
			
		||||
            .filter(action=EventAction.LOGIN)
 | 
			
		||||
            .get_events_per_hour()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @extend_schema_field(CoordinateSerializer(many=True))
 | 
			
		||||
    def get_logins_failed_per_1h(self, _):
 | 
			
		||||
        """Get failed logins per hour for the last 24 hours"""
 | 
			
		||||
        return get_events_per_1h(action=EventAction.LOGIN_FAILED)
 | 
			
		||||
        user = self.context["user"]
 | 
			
		||||
        return (
 | 
			
		||||
            get_objects_for_user(user, "authentik_events.view_event")
 | 
			
		||||
            .filter(action=EventAction.LOGIN_FAILED)
 | 
			
		||||
            .get_events_per_hour()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AdministrationMetricsViewSet(APIView):
 | 
			
		||||
@ -75,4 +54,5 @@ class AdministrationMetricsViewSet(APIView):
 | 
			
		||||
    def get(self, request: Request) -> Response:
 | 
			
		||||
        """Login Metrics per 1h"""
 | 
			
		||||
        serializer = LoginMetricsSerializer(True)
 | 
			
		||||
        serializer.context["user"] = request.user
 | 
			
		||||
        return Response(serializer.data)
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ from django.http.response import HttpResponseBadRequest
 | 
			
		||||
from django.shortcuts import get_object_or_404
 | 
			
		||||
from drf_spectacular.types import OpenApiTypes
 | 
			
		||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
 | 
			
		||||
from guardian.shortcuts import get_objects_for_user
 | 
			
		||||
from rest_framework.decorators import action
 | 
			
		||||
from rest_framework.fields import ReadOnlyField
 | 
			
		||||
from rest_framework.parsers import MultiPartParser
 | 
			
		||||
@ -15,7 +16,7 @@ from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
 | 
			
		||||
from authentik.admin.api.metrics import CoordinateSerializer
 | 
			
		||||
from authentik.api.decorators import permission_required
 | 
			
		||||
from authentik.core.api.providers import ProviderSerializer
 | 
			
		||||
from authentik.core.api.used_by import UsedByMixin
 | 
			
		||||
@ -239,8 +240,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
        """Metrics for application logins"""
 | 
			
		||||
        app = self.get_object()
 | 
			
		||||
        return Response(
 | 
			
		||||
            get_events_per_1h(
 | 
			
		||||
            get_objects_for_user(request.user, "authentik_events.view_event")
 | 
			
		||||
            .filter(
 | 
			
		||||
                action=EventAction.AUTHORIZE_APPLICATION,
 | 
			
		||||
                context__authorized_application__pk=app.pk.hex,
 | 
			
		||||
            )
 | 
			
		||||
            .get_events_per_hour()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -104,14 +104,14 @@ class SourceViewSet(
 | 
			
		||||
        )
 | 
			
		||||
        matching_sources: list[UserSettingSerializer] = []
 | 
			
		||||
        for source in _all_sources:
 | 
			
		||||
            user_settings = source.ui_user_settings
 | 
			
		||||
            user_settings = source.ui_user_settings()
 | 
			
		||||
            if not user_settings:
 | 
			
		||||
                continue
 | 
			
		||||
            policy_engine = PolicyEngine(source, request.user, request)
 | 
			
		||||
            policy_engine.build()
 | 
			
		||||
            if not policy_engine.passing:
 | 
			
		||||
                continue
 | 
			
		||||
            source_settings = source.ui_user_settings
 | 
			
		||||
            source_settings = source.ui_user_settings()
 | 
			
		||||
            source_settings.initial_data["object_uid"] = source.slug
 | 
			
		||||
            if not source_settings.is_valid():
 | 
			
		||||
                LOGGER.warning(source_settings.errors)
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
 | 
			
		||||
from authentik.admin.api.metrics import CoordinateSerializer
 | 
			
		||||
from authentik.api.decorators import permission_required
 | 
			
		||||
from authentik.core.api.groups import GroupSerializer
 | 
			
		||||
from authentik.core.api.used_by import UsedByMixin
 | 
			
		||||
@ -184,19 +184,31 @@ class UserMetricsSerializer(PassiveSerializer):
 | 
			
		||||
    def get_logins_per_1h(self, _):
 | 
			
		||||
        """Get successful logins per hour for the last 24 hours"""
 | 
			
		||||
        user = self.context["user"]
 | 
			
		||||
        return get_events_per_1h(action=EventAction.LOGIN, user__pk=user.pk)
 | 
			
		||||
        return (
 | 
			
		||||
            get_objects_for_user(user, "authentik_events.view_event")
 | 
			
		||||
            .filter(action=EventAction.LOGIN, user__pk=user.pk)
 | 
			
		||||
            .get_events_per_hour()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @extend_schema_field(CoordinateSerializer(many=True))
 | 
			
		||||
    def get_logins_failed_per_1h(self, _):
 | 
			
		||||
        """Get failed logins per hour for the last 24 hours"""
 | 
			
		||||
        user = self.context["user"]
 | 
			
		||||
        return get_events_per_1h(action=EventAction.LOGIN_FAILED, context__username=user.username)
 | 
			
		||||
        return (
 | 
			
		||||
            get_objects_for_user(user, "authentik_events.view_event")
 | 
			
		||||
            .filter(action=EventAction.LOGIN_FAILED, context__username=user.username)
 | 
			
		||||
            .get_events_per_hour()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @extend_schema_field(CoordinateSerializer(many=True))
 | 
			
		||||
    def get_authorizations_per_1h(self, _):
 | 
			
		||||
        """Get failed logins per hour for the last 24 hours"""
 | 
			
		||||
        user = self.context["user"]
 | 
			
		||||
        return get_events_per_1h(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
 | 
			
		||||
        return (
 | 
			
		||||
            get_objects_for_user(user, "authentik_events.view_event")
 | 
			
		||||
            .filter(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
 | 
			
		||||
            .get_events_per_hour()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UsersFilter(FilterSet):
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ from typing import Callable
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from django.http import HttpRequest, HttpResponse
 | 
			
		||||
from sentry_sdk.api import set_tag
 | 
			
		||||
 | 
			
		||||
SESSION_IMPERSONATE_USER = "authentik_impersonate_user"
 | 
			
		||||
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
 | 
			
		||||
@ -50,6 +51,7 @@ class RequestIDMiddleware:
 | 
			
		||||
                "request_id": request_id,
 | 
			
		||||
                "host": request.get_host(),
 | 
			
		||||
            }
 | 
			
		||||
            set_tag("authentik.request_id", request_id)
 | 
			
		||||
        response = self.get_response(request)
 | 
			
		||||
        response[RESPONSE_HEADER_ID] = request.request_id
 | 
			
		||||
        setattr(response, "ak_context", {})
 | 
			
		||||
 | 
			
		||||
@ -270,15 +270,21 @@ class Application(PolicyBindingModel):
 | 
			
		||||
        """Get launch URL if set, otherwise attempt to get launch URL based on provider."""
 | 
			
		||||
        if self.meta_launch_url:
 | 
			
		||||
            return self.meta_launch_url
 | 
			
		||||
        if self.provider:
 | 
			
		||||
            return self.get_provider().launch_url
 | 
			
		||||
        if provider := self.get_provider():
 | 
			
		||||
            return provider.launch_url
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    def get_provider(self) -> Optional[Provider]:
 | 
			
		||||
        """Get casted provider instance"""
 | 
			
		||||
        if not self.provider:
 | 
			
		||||
            return None
 | 
			
		||||
        # if the Application class has been cache, self.provider is set
 | 
			
		||||
        # but doing a direct query lookup will fail.
 | 
			
		||||
        # In that case, just return None
 | 
			
		||||
        try:
 | 
			
		||||
            return Provider.objects.get_subclass(pk=self.provider.pk)
 | 
			
		||||
        except Provider.DoesNotExist:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return self.name
 | 
			
		||||
@ -359,13 +365,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
 | 
			
		||||
        """Return component used to edit this object"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_login_button(self) -> Optional[UILoginButton]:
 | 
			
		||||
    def ui_login_button(self, request: HttpRequest) -> Optional[UILoginButton]:
 | 
			
		||||
        """If source uses a http-based flow, return UI Information about the login
 | 
			
		||||
        button. If source doesn't use http-based flow, return None."""
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        """Entrypoint to integrate with User settings. Can either return None if no
 | 
			
		||||
        user settings are available, or UserSettingSerializer."""
 | 
			
		||||
@ -452,6 +456,14 @@ class Token(ManagedModel, ExpiringModel):
 | 
			
		||||
        """Handler which is called when this object is expired."""
 | 
			
		||||
        from authentik.events.models import Event, EventAction
 | 
			
		||||
 | 
			
		||||
        if self.intent in [
 | 
			
		||||
            TokenIntents.INTENT_RECOVERY,
 | 
			
		||||
            TokenIntents.INTENT_VERIFICATION,
 | 
			
		||||
            TokenIntents.INTENT_APP_PASSWORD,
 | 
			
		||||
        ]:
 | 
			
		||||
            super().expire_action(*args, **kwargs)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.key = default_token_key()
 | 
			
		||||
        self.expires = default_token_duration()
 | 
			
		||||
        self.save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,7 @@
 | 
			
		||||
        <script src="{% static 'dist/poly.js' %}" type="module"></script>
 | 
			
		||||
        {% block head %}
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
        <meta name="sentry-trace" content="{{ sentry_trace }}" />
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        {% block body %}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
from time import sleep
 | 
			
		||||
from typing import Callable, Type
 | 
			
		||||
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from django.test import RequestFactory, TestCase
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
from guardian.shortcuts import get_anonymous_user
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,9 @@ class TestModels(TestCase):
 | 
			
		||||
def source_tester_factory(test_model: Type[Stage]) -> Callable:
 | 
			
		||||
    """Test source"""
 | 
			
		||||
 | 
			
		||||
    factory = RequestFactory()
 | 
			
		||||
    request = factory.get("/")
 | 
			
		||||
 | 
			
		||||
    def tester(self: TestModels):
 | 
			
		||||
        model_class = None
 | 
			
		||||
        if test_model._meta.abstract:
 | 
			
		||||
@ -38,8 +41,8 @@ def source_tester_factory(test_model: Type[Stage]) -> Callable:
 | 
			
		||||
            model_class = test_model()
 | 
			
		||||
        model_class.slug = "test"
 | 
			
		||||
        self.assertIsNotNone(model_class.component)
 | 
			
		||||
        _ = model_class.ui_login_button
 | 
			
		||||
        _ = model_class.ui_user_settings
 | 
			
		||||
        _ = model_class.ui_login_button(request)
 | 
			
		||||
        _ = model_class.ui_user_settings()
 | 
			
		||||
 | 
			
		||||
    return tester
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@ class TestPropertyMappingAPI(APITestCase):
 | 
			
		||||
        expr = "return True"
 | 
			
		||||
        self.assertEqual(PropertyMappingSerializer().validate_expression(expr), expr)
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            print(PropertyMappingSerializer().validate_expression("/"))
 | 
			
		||||
            PropertyMappingSerializer().validate_expression("/")
 | 
			
		||||
 | 
			
		||||
    def test_types(self):
 | 
			
		||||
        """Test PropertyMappigns's types endpoint"""
 | 
			
		||||
 | 
			
		||||
@ -54,7 +54,9 @@ class TestTokenAPI(APITestCase):
 | 
			
		||||
 | 
			
		||||
    def test_token_expire(self):
 | 
			
		||||
        """Test Token expire task"""
 | 
			
		||||
        token: Token = Token.objects.create(expires=now(), user=get_anonymous_user())
 | 
			
		||||
        token: Token = Token.objects.create(
 | 
			
		||||
            expires=now(), user=get_anonymous_user(), intent=TokenIntents.INTENT_API
 | 
			
		||||
        )
 | 
			
		||||
        key = token.key
 | 
			
		||||
        clean_expired_models.delay().get()
 | 
			
		||||
        token.refresh_from_db()
 | 
			
		||||
 | 
			
		||||
@ -11,10 +11,13 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key
 | 
			
		||||
from cryptography.x509 import Certificate, load_pem_x509_certificate
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.lib.models import CreatedUpdatedModel
 | 
			
		||||
from authentik.managed.models import ManagedModel
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
 | 
			
		||||
    """CertificateKeyPair that can be used for signing or encrypting if `key_data`
 | 
			
		||||
@ -62,7 +65,8 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
 | 
			
		||||
                    password=None,
 | 
			
		||||
                    backend=default_backend(),
 | 
			
		||||
                )
 | 
			
		||||
            except ValueError:
 | 
			
		||||
            except ValueError as exc:
 | 
			
		||||
                LOGGER.warning(exc)
 | 
			
		||||
                return None
 | 
			
		||||
        return self._private_key
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,9 @@
 | 
			
		||||
from glob import glob
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from cryptography.hazmat.backends import default_backend
 | 
			
		||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
 | 
			
		||||
from cryptography.x509.base import load_pem_x509_certificate
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
@ -20,6 +23,22 @@ LOGGER = get_logger()
 | 
			
		||||
MANAGED_DISCOVERED = "goauthentik.io/crypto/discovered/%s"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def ensure_private_key_valid(body: str):
 | 
			
		||||
    """Attempt loading of an RSA Private key without password"""
 | 
			
		||||
    load_pem_private_key(
 | 
			
		||||
        str.encode("\n".join([x.strip() for x in body.split("\n")])),
 | 
			
		||||
        password=None,
 | 
			
		||||
        backend=default_backend(),
 | 
			
		||||
    )
 | 
			
		||||
    return body
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def ensure_certificate_valid(body: str):
 | 
			
		||||
    """Attempt loading of a PEM-encoded certificate"""
 | 
			
		||||
    load_pem_x509_certificate(body.encode("utf-8"), default_backend())
 | 
			
		||||
    return body
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
 | 
			
		||||
@prefill_task
 | 
			
		||||
def certificate_discovery(self: MonitoredTask):
 | 
			
		||||
@ -42,11 +61,11 @@ def certificate_discovery(self: MonitoredTask):
 | 
			
		||||
            with open(path, "r+", encoding="utf-8") as _file:
 | 
			
		||||
                body = _file.read()
 | 
			
		||||
                if "BEGIN RSA PRIVATE KEY" in body:
 | 
			
		||||
                    private_keys[cert_name] = body
 | 
			
		||||
                    private_keys[cert_name] = ensure_private_key_valid(body)
 | 
			
		||||
                else:
 | 
			
		||||
                    certs[cert_name] = body
 | 
			
		||||
        except OSError as exc:
 | 
			
		||||
            LOGGER.warning("Failed to open file", exc=exc, file=path)
 | 
			
		||||
                    certs[cert_name] = ensure_certificate_valid(body)
 | 
			
		||||
        except (OSError, ValueError) as exc:
 | 
			
		||||
            LOGGER.warning("Failed to open file or invalid format", exc=exc, file=path)
 | 
			
		||||
        discovered += 1
 | 
			
		||||
    for name, cert_data in certs.items():
 | 
			
		||||
        cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first()
 | 
			
		||||
@ -60,7 +79,7 @@ def certificate_discovery(self: MonitoredTask):
 | 
			
		||||
            cert.certificate_data = cert_data
 | 
			
		||||
            dirty = True
 | 
			
		||||
        if name in private_keys:
 | 
			
		||||
            if cert.key_data == private_keys[name]:
 | 
			
		||||
            if cert.key_data != private_keys[name]:
 | 
			
		||||
                cert.key_data = private_keys[name]
 | 
			
		||||
                dirty = True
 | 
			
		||||
        if dirty:
 | 
			
		||||
 | 
			
		||||
@ -191,9 +191,12 @@ class TestCrypto(APITestCase):
 | 
			
		||||
            with CONFIG.patch("cert_discovery_dir", temp_dir):
 | 
			
		||||
                # pyright: reportGeneralTypeIssues=false
 | 
			
		||||
                certificate_discovery()  # pylint: disable=no-value-for-parameter
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo").exists()
 | 
			
		||||
        )
 | 
			
		||||
        keypair: CertificateKeyPair = CertificateKeyPair.objects.filter(
 | 
			
		||||
            managed=MANAGED_DISCOVERED % "foo"
 | 
			
		||||
        ).first()
 | 
			
		||||
        self.assertIsNotNone(keypair)
 | 
			
		||||
        self.assertIsNotNone(keypair.certificate)
 | 
			
		||||
        self.assertIsNotNone(keypair.private_key)
 | 
			
		||||
        self.assertTrue(
 | 
			
		||||
            CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
"""Events API Views"""
 | 
			
		||||
from json import loads
 | 
			
		||||
 | 
			
		||||
import django_filters
 | 
			
		||||
from django.db.models.aggregates import Count
 | 
			
		||||
from django.db.models.fields.json import KeyTextTransform
 | 
			
		||||
@ -12,6 +14,7 @@ from rest_framework.response import Response
 | 
			
		||||
from rest_framework.serializers import ModelSerializer
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
 | 
			
		||||
from authentik.admin.api.metrics import CoordinateSerializer
 | 
			
		||||
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
 | 
			
		||||
from authentik.events.models import Event, EventAction
 | 
			
		||||
 | 
			
		||||
@ -110,13 +113,20 @@ class EventViewSet(ModelViewSet):
 | 
			
		||||
    @extend_schema(
 | 
			
		||||
        methods=["GET"],
 | 
			
		||||
        responses={200: EventTopPerUserSerializer(many=True)},
 | 
			
		||||
        filters=[],
 | 
			
		||||
        parameters=[
 | 
			
		||||
            OpenApiParameter(
 | 
			
		||||
                "action",
 | 
			
		||||
                type=OpenApiTypes.STR,
 | 
			
		||||
                location=OpenApiParameter.QUERY,
 | 
			
		||||
                required=False,
 | 
			
		||||
            ),
 | 
			
		||||
            OpenApiParameter(
 | 
			
		||||
                "top_n",
 | 
			
		||||
                type=OpenApiTypes.INT,
 | 
			
		||||
                location=OpenApiParameter.QUERY,
 | 
			
		||||
                required=False,
 | 
			
		||||
            )
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
    @action(detail=False, methods=["GET"], pagination_class=None)
 | 
			
		||||
@ -137,6 +147,40 @@ class EventViewSet(ModelViewSet):
 | 
			
		||||
            .order_by("-counted_events")[:top_n]
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @extend_schema(
 | 
			
		||||
        methods=["GET"],
 | 
			
		||||
        responses={200: CoordinateSerializer(many=True)},
 | 
			
		||||
        filters=[],
 | 
			
		||||
        parameters=[
 | 
			
		||||
            OpenApiParameter(
 | 
			
		||||
                "action",
 | 
			
		||||
                type=OpenApiTypes.STR,
 | 
			
		||||
                location=OpenApiParameter.QUERY,
 | 
			
		||||
                required=False,
 | 
			
		||||
            ),
 | 
			
		||||
            OpenApiParameter(
 | 
			
		||||
                "query",
 | 
			
		||||
                type=OpenApiTypes.STR,
 | 
			
		||||
                location=OpenApiParameter.QUERY,
 | 
			
		||||
                required=False,
 | 
			
		||||
            ),
 | 
			
		||||
        ],
 | 
			
		||||
    )
 | 
			
		||||
    @action(detail=False, methods=["GET"], pagination_class=None)
 | 
			
		||||
    def per_month(self, request: Request):
 | 
			
		||||
        """Get the count of events per month"""
 | 
			
		||||
        filtered_action = request.query_params.get("action", EventAction.LOGIN)
 | 
			
		||||
        try:
 | 
			
		||||
            query = loads(request.query_params.get("query", "{}"))
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            return Response(status=400)
 | 
			
		||||
        return Response(
 | 
			
		||||
            get_objects_for_user(request.user, "authentik_events.view_event")
 | 
			
		||||
            .filter(action=filtered_action)
 | 
			
		||||
            .filter(**query)
 | 
			
		||||
            .get_events_per_day()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @extend_schema(responses={200: TypeCreateSerializer(many=True)})
 | 
			
		||||
    @action(detail=False, pagination_class=None, filter_backends=[])
 | 
			
		||||
    def actions(self, request: Request) -> Response:
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ from typing import Optional, TypedDict
 | 
			
		||||
from geoip2.database import Reader
 | 
			
		||||
from geoip2.errors import GeoIP2Error
 | 
			
		||||
from geoip2.models import City
 | 
			
		||||
from sentry_sdk.hub import Hub
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
@ -62,6 +63,10 @@ class GeoIPReader:
 | 
			
		||||
 | 
			
		||||
    def city(self, ip_address: str) -> Optional[City]:
 | 
			
		||||
        """Wrapper for Reader.city"""
 | 
			
		||||
        with Hub.current.start_span(
 | 
			
		||||
            op="authentik.events.geo.city",
 | 
			
		||||
            description=ip_address,
 | 
			
		||||
        ):
 | 
			
		||||
            if not self.enabled:
 | 
			
		||||
                return None
 | 
			
		||||
            self.__check_expired()
 | 
			
		||||
 | 
			
		||||
@ -314,169 +314,10 @@ class Migration(migrations.Migration):
 | 
			
		||||
            old_name="user_json",
 | 
			
		||||
            new_name="user",
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("sign_up", "Sign Up"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("invitation_created", "Invite Created"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("user_write", "User Write"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("invitation_created", "Invite Created"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="date",
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("user_write", "User Write"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("token_view", "Token View"),
 | 
			
		||||
                    ("invitation_created", "Invite Created"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("user_write", "User Write"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("token_view", "Token View"),
 | 
			
		||||
                    ("invitation_created", "Invite Created"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("policy_execution", "Policy Execution"),
 | 
			
		||||
                    ("policy_exception", "Policy Exception"),
 | 
			
		||||
                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("user_write", "User Write"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("token_view", "Token View"),
 | 
			
		||||
                    ("invitation_created", "Invite Created"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("policy_execution", "Policy Execution"),
 | 
			
		||||
                    ("policy_exception", "Policy Exception"),
 | 
			
		||||
                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("update_available", "Update Available"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("user_write", "User Write"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("token_view", "Token View"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("policy_execution", "Policy Execution"),
 | 
			
		||||
                    ("policy_exception", "Policy Exception"),
 | 
			
		||||
                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
			
		||||
                    ("configuration_error", "Configuration Error"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("update_available", "Update Available"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.CreateModel(
 | 
			
		||||
            name="NotificationTransport",
 | 
			
		||||
            fields=[
 | 
			
		||||
@ -610,68 +451,6 @@ class Migration(migrations.Migration):
 | 
			
		||||
                help_text="Only send notification once, for example when sending a webhook into a chat channel.",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("user_write", "User Write"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("token_view", "Token View"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("policy_execution", "Policy Execution"),
 | 
			
		||||
                    ("policy_exception", "Policy Exception"),
 | 
			
		||||
                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
			
		||||
                    ("system_task_execution", "System Task Execution"),
 | 
			
		||||
                    ("system_task_exception", "System Task Exception"),
 | 
			
		||||
                    ("configuration_error", "Configuration Error"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("update_available", "Update Available"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("user_write", "User Write"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("secret_view", "Secret View"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("policy_execution", "Policy Execution"),
 | 
			
		||||
                    ("policy_exception", "Policy Exception"),
 | 
			
		||||
                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
			
		||||
                    ("system_task_execution", "System Task Execution"),
 | 
			
		||||
                    ("system_task_exception", "System Task Exception"),
 | 
			
		||||
                    ("configuration_error", "Configuration Error"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("update_available", "Update Available"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RunPython(
 | 
			
		||||
            code=token_view_to_secret_view,
 | 
			
		||||
        ),
 | 
			
		||||
@ -688,76 +467,11 @@ class Migration(migrations.Migration):
 | 
			
		||||
        migrations.RunPython(
 | 
			
		||||
            code=update_expires,
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("user_write", "User Write"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("secret_view", "Secret View"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("policy_execution", "Policy Execution"),
 | 
			
		||||
                    ("policy_exception", "Policy Exception"),
 | 
			
		||||
                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
			
		||||
                    ("system_task_execution", "System Task Execution"),
 | 
			
		||||
                    ("system_task_exception", "System Task Exception"),
 | 
			
		||||
                    ("configuration_error", "Configuration Error"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("email_sent", "Email Sent"),
 | 
			
		||||
                    ("update_available", "Update Available"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="tenant",
 | 
			
		||||
            field=models.JSONField(blank=True, default=authentik.events.models.default_tenant),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("login", "Login"),
 | 
			
		||||
                    ("login_failed", "Login Failed"),
 | 
			
		||||
                    ("logout", "Logout"),
 | 
			
		||||
                    ("user_write", "User Write"),
 | 
			
		||||
                    ("suspicious_request", "Suspicious Request"),
 | 
			
		||||
                    ("password_set", "Password Set"),
 | 
			
		||||
                    ("secret_view", "Secret View"),
 | 
			
		||||
                    ("invitation_used", "Invite Used"),
 | 
			
		||||
                    ("authorize_application", "Authorize Application"),
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("policy_execution", "Policy Execution"),
 | 
			
		||||
                    ("policy_exception", "Policy Exception"),
 | 
			
		||||
                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
			
		||||
                    ("system_task_execution", "System Task Execution"),
 | 
			
		||||
                    ("system_task_exception", "System Task Exception"),
 | 
			
		||||
                    ("system_exception", "System Exception"),
 | 
			
		||||
                    ("configuration_error", "Configuration Error"),
 | 
			
		||||
                    ("model_created", "Model Created"),
 | 
			
		||||
                    ("model_updated", "Model Updated"),
 | 
			
		||||
                    ("model_deleted", "Model Deleted"),
 | 
			
		||||
                    ("email_sent", "Email Sent"),
 | 
			
		||||
                    ("update_available", "Update Available"),
 | 
			
		||||
                    ("custom_", "Custom Prefix"),
 | 
			
		||||
                ]
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="event",
 | 
			
		||||
            name="action",
 | 
			
		||||
@ -776,6 +490,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                    ("source_linked", "Source Linked"),
 | 
			
		||||
                    ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                    ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                    ("flow_execution", "Flow Execution"),
 | 
			
		||||
                    ("policy_execution", "Policy Execution"),
 | 
			
		||||
                    ("policy_exception", "Policy Exception"),
 | 
			
		||||
                    ("property_mapping_exception", "Property Mapping Exception"),
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,20 @@
 | 
			
		||||
"""authentik events models"""
 | 
			
		||||
import time
 | 
			
		||||
from collections import Counter
 | 
			
		||||
from datetime import timedelta
 | 
			
		||||
from inspect import getmodule, stack
 | 
			
		||||
from inspect import currentframe
 | 
			
		||||
from smtplib import SMTPException
 | 
			
		||||
from typing import TYPE_CHECKING, Optional, Type, Union
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from django.conf import settings
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Count, ExpressionWrapper, F
 | 
			
		||||
from django.db.models.fields import DurationField
 | 
			
		||||
from django.db.models.functions import ExtractHour
 | 
			
		||||
from django.db.models.functions.datetime import ExtractDay
 | 
			
		||||
from django.db.models.manager import Manager
 | 
			
		||||
from django.db.models.query import QuerySet
 | 
			
		||||
from django.http import HttpRequest
 | 
			
		||||
from django.http.request import QueryDict
 | 
			
		||||
from django.utils.timezone import now
 | 
			
		||||
@ -70,6 +78,7 @@ class EventAction(models.TextChoices):
 | 
			
		||||
    IMPERSONATION_STARTED = "impersonation_started"
 | 
			
		||||
    IMPERSONATION_ENDED = "impersonation_ended"
 | 
			
		||||
 | 
			
		||||
    FLOW_EXECUTION = "flow_execution"
 | 
			
		||||
    POLICY_EXECUTION = "policy_execution"
 | 
			
		||||
    POLICY_EXCEPTION = "policy_exception"
 | 
			
		||||
    PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception"
 | 
			
		||||
@ -90,6 +99,72 @@ class EventAction(models.TextChoices):
 | 
			
		||||
    CUSTOM_PREFIX = "custom_"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EventQuerySet(QuerySet):
 | 
			
		||||
    """Custom events query set with helper functions"""
 | 
			
		||||
 | 
			
		||||
    def get_events_per_hour(self) -> list[dict[str, int]]:
 | 
			
		||||
        """Get event count by hour in the last day, fill with zeros"""
 | 
			
		||||
        date_from = now() - timedelta(days=1)
 | 
			
		||||
        result = (
 | 
			
		||||
            self.filter(created__gte=date_from)
 | 
			
		||||
            .annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
 | 
			
		||||
            .annotate(age_hours=ExtractHour("age"))
 | 
			
		||||
            .values("age_hours")
 | 
			
		||||
            .annotate(count=Count("pk"))
 | 
			
		||||
            .order_by("age_hours")
 | 
			
		||||
        )
 | 
			
		||||
        data = Counter({int(d["age_hours"]): d["count"] for d in result})
 | 
			
		||||
        results = []
 | 
			
		||||
        _now = now()
 | 
			
		||||
        for hour in range(0, -24, -1):
 | 
			
		||||
            results.append(
 | 
			
		||||
                {
 | 
			
		||||
                    "x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
 | 
			
		||||
                    "y_cord": data[hour * -1],
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        return results
 | 
			
		||||
 | 
			
		||||
    def get_events_per_day(self) -> list[dict[str, int]]:
 | 
			
		||||
        """Get event count by hour in the last day, fill with zeros"""
 | 
			
		||||
        date_from = now() - timedelta(weeks=4)
 | 
			
		||||
        result = (
 | 
			
		||||
            self.filter(created__gte=date_from)
 | 
			
		||||
            .annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
 | 
			
		||||
            .annotate(age_days=ExtractDay("age"))
 | 
			
		||||
            .values("age_days")
 | 
			
		||||
            .annotate(count=Count("pk"))
 | 
			
		||||
            .order_by("age_days")
 | 
			
		||||
        )
 | 
			
		||||
        data = Counter({int(d["age_days"]): d["count"] for d in result})
 | 
			
		||||
        results = []
 | 
			
		||||
        _now = now()
 | 
			
		||||
        for day in range(0, -30, -1):
 | 
			
		||||
            results.append(
 | 
			
		||||
                {
 | 
			
		||||
                    "x_cord": time.mktime((_now + timedelta(days=day)).timetuple()) * 1000,
 | 
			
		||||
                    "y_cord": data[day * -1],
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
        return results
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EventManager(Manager):
 | 
			
		||||
    """Custom helper methods for Events"""
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self) -> QuerySet:
 | 
			
		||||
        """use custom queryset"""
 | 
			
		||||
        return EventQuerySet(self.model, using=self._db)
 | 
			
		||||
 | 
			
		||||
    def get_events_per_hour(self) -> list[dict[str, int]]:
 | 
			
		||||
        """Wrap method from queryset"""
 | 
			
		||||
        return self.get_queryset().get_events_per_hour()
 | 
			
		||||
 | 
			
		||||
    def get_events_per_day(self) -> list[dict[str, int]]:
 | 
			
		||||
        """Wrap method from queryset"""
 | 
			
		||||
        return self.get_queryset().get_events_per_day()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Event(ExpiringModel):
 | 
			
		||||
    """An individual Audit/Metrics/Notification/Error Event"""
 | 
			
		||||
 | 
			
		||||
@ -105,6 +180,8 @@ class Event(ExpiringModel):
 | 
			
		||||
    # Shadow the expires attribute from ExpiringModel to override the default duration
 | 
			
		||||
    expires = models.DateTimeField(default=default_event_duration)
 | 
			
		||||
 | 
			
		||||
    objects = EventManager()
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _get_app_from_request(request: HttpRequest) -> str:
 | 
			
		||||
        if not isinstance(request, HttpRequest):
 | 
			
		||||
@ -115,14 +192,15 @@ class Event(ExpiringModel):
 | 
			
		||||
    def new(
 | 
			
		||||
        action: Union[str, EventAction],
 | 
			
		||||
        app: Optional[str] = None,
 | 
			
		||||
        _inspect_offset: int = 1,
 | 
			
		||||
        **kwargs,
 | 
			
		||||
    ) -> "Event":
 | 
			
		||||
        """Create new Event instance from arguments. Instance is NOT saved."""
 | 
			
		||||
        if not isinstance(action, EventAction):
 | 
			
		||||
            action = EventAction.CUSTOM_PREFIX + action
 | 
			
		||||
        if not app:
 | 
			
		||||
            app = getmodule(stack()[_inspect_offset][0]).__name__
 | 
			
		||||
            current = currentframe()
 | 
			
		||||
            parent = current.f_back
 | 
			
		||||
            app = parent.f_globals["__name__"]
 | 
			
		||||
        cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
 | 
			
		||||
        event = Event(action=action, app=app, context=cleaned_kwargs)
 | 
			
		||||
        return event
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@ class TaskResult:
 | 
			
		||||
 | 
			
		||||
    def with_error(self, exc: Exception) -> "TaskResult":
 | 
			
		||||
        """Since errors might not always be pickle-able, set the traceback"""
 | 
			
		||||
        self.messages.extend(exception_to_string(exc).splitlines())
 | 
			
		||||
        self.messages.append(str(exc))
 | 
			
		||||
        return self
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -90,7 +90,7 @@ class StageViewSet(
 | 
			
		||||
            stages += list(configurable_stage.objects.all().order_by("name"))
 | 
			
		||||
        matching_stages: list[dict] = []
 | 
			
		||||
        for stage in stages:
 | 
			
		||||
            user_settings = stage.ui_user_settings
 | 
			
		||||
            user_settings = stage.ui_user_settings()
 | 
			
		||||
            if not user_settings:
 | 
			
		||||
                continue
 | 
			
		||||
            user_settings.initial_data["object_uid"] = str(stage.pk)
 | 
			
		||||
 | 
			
		||||
@ -75,7 +75,6 @@ class Stage(SerializerModel):
 | 
			
		||||
        """Return component used to edit this object"""
 | 
			
		||||
        raise NotImplementedError
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        """Entrypoint to integrate with User settings. Can either return None if no
 | 
			
		||||
        user settings are available, or a challenge."""
 | 
			
		||||
 | 
			
		||||
@ -126,7 +126,9 @@ class FlowPlanner:
 | 
			
		||||
    ) -> FlowPlan:
 | 
			
		||||
        """Check each of the flows' policies, check policies for each stage with PolicyBinding
 | 
			
		||||
        and return ordered list"""
 | 
			
		||||
        with Hub.current.start_span(op="flow.planner.plan", description=self.flow.slug) as span:
 | 
			
		||||
        with Hub.current.start_span(
 | 
			
		||||
            op="authentik.flow.planner.plan", description=self.flow.slug
 | 
			
		||||
        ) as span:
 | 
			
		||||
            span: Span
 | 
			
		||||
            span.set_data("flow", self.flow)
 | 
			
		||||
            span.set_data("request", request)
 | 
			
		||||
@ -181,7 +183,7 @@ class FlowPlanner:
 | 
			
		||||
        """Build flow plan by checking each stage in their respective
 | 
			
		||||
        order and checking the applied policies"""
 | 
			
		||||
        with Hub.current.start_span(
 | 
			
		||||
            op="flow.planner.build_plan",
 | 
			
		||||
            op="authentik.flow.planner.build_plan",
 | 
			
		||||
            description=self.flow.slug,
 | 
			
		||||
        ) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time():
 | 
			
		||||
            span: Span
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ from django.http.response import HttpResponse
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.views.generic.base import View
 | 
			
		||||
from rest_framework.request import Request
 | 
			
		||||
from sentry_sdk.hub import Hub
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import DEFAULT_AVATAR, User
 | 
			
		||||
@ -94,7 +95,15 @@ class ChallengeStageView(StageView):
 | 
			
		||||
                    keep_context=keep_context,
 | 
			
		||||
                )
 | 
			
		||||
                return self.executor.restart_flow(keep_context)
 | 
			
		||||
            with Hub.current.start_span(
 | 
			
		||||
                op="authentik.flow.stage.challenge_invalid",
 | 
			
		||||
                description=self.__class__.__name__,
 | 
			
		||||
            ):
 | 
			
		||||
                return self.challenge_invalid(challenge)
 | 
			
		||||
        with Hub.current.start_span(
 | 
			
		||||
            op="authentik.flow.stage.challenge_valid",
 | 
			
		||||
            description=self.__class__.__name__,
 | 
			
		||||
        ):
 | 
			
		||||
            return self.challenge_valid(challenge)
 | 
			
		||||
 | 
			
		||||
    def format_title(self) -> str:
 | 
			
		||||
@ -104,6 +113,10 @@ class ChallengeStageView(StageView):
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def _get_challenge(self, *args, **kwargs) -> Challenge:
 | 
			
		||||
        with Hub.current.start_span(
 | 
			
		||||
            op="authentik.flow.stage.get_challenge",
 | 
			
		||||
            description=self.__class__.__name__,
 | 
			
		||||
        ):
 | 
			
		||||
            challenge = self.get_challenge(*args, **kwargs)
 | 
			
		||||
        if "flow_info" not in challenge.initial_data:
 | 
			
		||||
            flow_info = ContextualFlowInfo(
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ class TestFlowsAPI(APITestCase):
 | 
			
		||||
 | 
			
		||||
    def test_models(self):
 | 
			
		||||
        """Test that ui_user_settings returns none"""
 | 
			
		||||
        self.assertIsNone(Stage().ui_user_settings)
 | 
			
		||||
        self.assertIsNone(Stage().ui_user_settings())
 | 
			
		||||
 | 
			
		||||
    def test_api_serializer(self):
 | 
			
		||||
        """Test that stage serializer returns the correct type"""
 | 
			
		||||
 | 
			
		||||
@ -23,7 +23,7 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable:
 | 
			
		||||
            model_class = test_model()
 | 
			
		||||
        self.assertTrue(issubclass(model_class.type, StageView))
 | 
			
		||||
        self.assertIsNotNone(test_model.component)
 | 
			
		||||
        _ = model_class.ui_user_settings
 | 
			
		||||
        _ = model_class.ui_user_settings()
 | 
			
		||||
 | 
			
		||||
    return tester
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -160,7 +160,7 @@ class FlowExecutorView(APIView):
 | 
			
		||||
    # pylint: disable=unused-argument, too-many-return-statements
 | 
			
		||||
    def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
 | 
			
		||||
        with Hub.current.start_span(
 | 
			
		||||
            op="flow.executor.dispatch", description=self.flow.slug
 | 
			
		||||
            op="authentik.flow.executor.dispatch", description=self.flow.slug
 | 
			
		||||
        ) as span:
 | 
			
		||||
            span.set_data("authentik Flow", self.flow.slug)
 | 
			
		||||
            get_params = QueryDict(request.GET.get("query", ""))
 | 
			
		||||
@ -275,7 +275,7 @@ class FlowExecutorView(APIView):
 | 
			
		||||
        )
 | 
			
		||||
        try:
 | 
			
		||||
            with Hub.current.start_span(
 | 
			
		||||
                op="flow.executor.stage",
 | 
			
		||||
                op="authentik.flow.executor.stage",
 | 
			
		||||
                description=class_to_path(self.current_stage_view.__class__),
 | 
			
		||||
            ) as span:
 | 
			
		||||
                span.set_data("Method", "GET")
 | 
			
		||||
@ -319,7 +319,7 @@ class FlowExecutorView(APIView):
 | 
			
		||||
        )
 | 
			
		||||
        try:
 | 
			
		||||
            with Hub.current.start_span(
 | 
			
		||||
                op="flow.executor.stage",
 | 
			
		||||
                op="authentik.flow.executor.stage",
 | 
			
		||||
                description=class_to_path(self.current_stage_view.__class__),
 | 
			
		||||
            ) as span:
 | 
			
		||||
                span.set_data("Method", "POST")
 | 
			
		||||
@ -371,6 +371,12 @@ class FlowExecutorView(APIView):
 | 
			
		||||
            NEXT_ARG_NAME, "authentik_core:root-redirect"
 | 
			
		||||
        )
 | 
			
		||||
        self.cancel()
 | 
			
		||||
        Event.new(
 | 
			
		||||
            action=EventAction.FLOW_EXECUTION,
 | 
			
		||||
            flow=self.flow,
 | 
			
		||||
            designation=self.flow.designation,
 | 
			
		||||
            successful=True,
 | 
			
		||||
        ).from_http(self.request)
 | 
			
		||||
        return to_stage_response(self.request, redirect_with_qs(next_param))
 | 
			
		||||
 | 
			
		||||
    def stage_ok(self) -> HttpResponse:
 | 
			
		||||
 | 
			
		||||
@ -106,7 +106,7 @@ class FlowInspectorView(APIView):
 | 
			
		||||
        else:
 | 
			
		||||
            try:
 | 
			
		||||
                current_plan = request.session[SESSION_KEY_HISTORY][-1]
 | 
			
		||||
            except KeyError:
 | 
			
		||||
            except IndexError:
 | 
			
		||||
                return Response(status=400)
 | 
			
		||||
            is_completed = True
 | 
			
		||||
        current_serializer = FlowInspectorPlanSerializer(
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ outposts:
 | 
			
		||||
  # %(type)s: Outpost type; proxy, ldap, etc
 | 
			
		||||
  # %(version)s: Current version; 2021.4.1
 | 
			
		||||
  # %(build_hash)s: Build hash if you're running a beta version
 | 
			
		||||
  container_image_base: goauthentik.io/%(type)s:%(version)s
 | 
			
		||||
  container_image_base: ghcr.io/goauthentik/%(type)s:%(version)s
 | 
			
		||||
 | 
			
		||||
cookie_domain: null
 | 
			
		||||
disable_update_check: false
 | 
			
		||||
 | 
			
		||||
@ -80,8 +80,9 @@ class BaseEvaluator:
 | 
			
		||||
        """Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
 | 
			
		||||
        If any exception is raised during execution, it is raised.
 | 
			
		||||
        The result is returned without any type-checking."""
 | 
			
		||||
        with Hub.current.start_span(op="lib.evaluator.evaluate") as span:
 | 
			
		||||
        with Hub.current.start_span(op="authentik.lib.evaluator.evaluate") as span:
 | 
			
		||||
            span: Span
 | 
			
		||||
            span.description = self._filename
 | 
			
		||||
            span.set_data("expression", expression_source)
 | 
			
		||||
            param_keys = self._context.keys()
 | 
			
		||||
            try:
 | 
			
		||||
 | 
			
		||||
@ -108,6 +108,9 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
 | 
			
		||||
            "multiprocessing",
 | 
			
		||||
            "django_redis",
 | 
			
		||||
            "django.security.DisallowedHost",
 | 
			
		||||
            "django_redis.cache",
 | 
			
		||||
            "celery.backends.redis",
 | 
			
		||||
            "celery.worker",
 | 
			
		||||
        ]:
 | 
			
		||||
            return None
 | 
			
		||||
    LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None))
 | 
			
		||||
 | 
			
		||||
@ -116,6 +116,7 @@ class OutpostFilter(FilterSet):
 | 
			
		||||
            "providers": ["isnull"],
 | 
			
		||||
            "name": ["iexact", "icontains"],
 | 
			
		||||
            "service_connection__name": ["iexact", "icontains"],
 | 
			
		||||
            "managed": ["iexact", "icontains"],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -110,7 +110,7 @@ class DockerController(BaseController):
 | 
			
		||||
        image = self.get_container_image()
 | 
			
		||||
        try:
 | 
			
		||||
            self.client.images.pull(image)
 | 
			
		||||
        except DockerException:
 | 
			
		||||
        except DockerException:  # pragma: no cover
 | 
			
		||||
            image = f"goauthentik.io/{self.outpost.type}:latest"
 | 
			
		||||
            self.client.images.pull(image)
 | 
			
		||||
        return image
 | 
			
		||||
@ -144,7 +144,7 @@ class DockerController(BaseController):
 | 
			
		||||
                True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def _migrate_container_name(self):
 | 
			
		||||
    def _migrate_container_name(self):  # pragma: no cover
 | 
			
		||||
        """Migrate 2021.9 to 2021.10+"""
 | 
			
		||||
        old_name = f"authentik-proxy-{self.outpost.uuid.hex}"
 | 
			
		||||
        try:
 | 
			
		||||
@ -169,7 +169,7 @@ class DockerController(BaseController):
 | 
			
		||||
            # Check if the container is out of date, delete it and retry
 | 
			
		||||
            if len(container.image.tags) > 0:
 | 
			
		||||
                should_image = self.try_pull_image()
 | 
			
		||||
                if should_image not in container.image.tags:
 | 
			
		||||
                if should_image not in container.image.tags:  # pragma: no cover
 | 
			
		||||
                    self.logger.info(
 | 
			
		||||
                        "Container has mismatched image, re-creating...",
 | 
			
		||||
                        has=container.image.tags,
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,11 @@ if TYPE_CHECKING:
 | 
			
		||||
T = TypeVar("T", V1Pod, V1Deployment)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_version() -> str:
 | 
			
		||||
    """Wrapper for __version__ to make testing easier"""
 | 
			
		||||
    return __version__
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KubernetesObjectReconciler(Generic[T]):
 | 
			
		||||
    """Base Kubernetes Reconciler, handles the basic logic."""
 | 
			
		||||
 | 
			
		||||
@ -146,13 +151,13 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
			
		||||
        return V1ObjectMeta(
 | 
			
		||||
            namespace=self.namespace,
 | 
			
		||||
            labels={
 | 
			
		||||
                "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
 | 
			
		||||
                "app.kubernetes.io/instance": slugify(self.controller.outpost.name),
 | 
			
		||||
                "app.kubernetes.io/version": __version__,
 | 
			
		||||
                "app.kubernetes.io/managed-by": "goauthentik.io",
 | 
			
		||||
                "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
 | 
			
		||||
                "goauthentik.io/outpost-type": str(self.controller.outpost.type),
 | 
			
		||||
                "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
 | 
			
		||||
                "app.kubernetes.io/version": get_version(),
 | 
			
		||||
                "goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
 | 
			
		||||
                "goauthentik.io/outpost-type": str(self.controller.outpost.type),
 | 
			
		||||
                "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
 | 
			
		||||
            },
 | 
			
		||||
            **kwargs,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -481,6 +481,8 @@ class OutpostState:
 | 
			
		||||
    def for_outpost(outpost: Outpost) -> list["OutpostState"]:
 | 
			
		||||
        """Get all states for an outpost"""
 | 
			
		||||
        keys = cache.keys(f"{outpost.state_cache_prefix}_*")
 | 
			
		||||
        if not keys:
 | 
			
		||||
            return []
 | 
			
		||||
        states = []
 | 
			
		||||
        for key in keys:
 | 
			
		||||
            instance_uid = key.replace(f"{outpost.state_cache_prefix}_", "")
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										124
									
								
								authentik/outposts/tests/test_controller_docker.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								authentik/outposts/tests/test_controller_docker.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
"""Docker controller tests"""
 | 
			
		||||
from django.test import TestCase
 | 
			
		||||
from docker.models.containers import Container
 | 
			
		||||
 | 
			
		||||
from authentik.managed.manager import ObjectManager
 | 
			
		||||
from authentik.outposts.controllers.base import ControllerException
 | 
			
		||||
from authentik.outposts.controllers.docker import DockerController
 | 
			
		||||
from authentik.outposts.managed import MANAGED_OUTPOST
 | 
			
		||||
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType
 | 
			
		||||
from authentik.providers.proxy.controllers.docker import ProxyDockerController
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DockerControllerTests(TestCase):
 | 
			
		||||
    """Docker controller tests"""
 | 
			
		||||
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.outpost = Outpost.objects.create(
 | 
			
		||||
            name="test",
 | 
			
		||||
            type=OutpostType.PROXY,
 | 
			
		||||
        )
 | 
			
		||||
        self.integration = DockerServiceConnection(name="test")
 | 
			
		||||
        ObjectManager().run()
 | 
			
		||||
 | 
			
		||||
    def test_init_managed(self):
 | 
			
		||||
        """Docker controller shouldn't do anything for managed outpost"""
 | 
			
		||||
        controller = DockerController(
 | 
			
		||||
            Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
 | 
			
		||||
        )
 | 
			
		||||
        self.assertIsNone(controller.up())
 | 
			
		||||
        self.assertIsNone(controller.down())
 | 
			
		||||
 | 
			
		||||
    def test_init_invalid(self):
 | 
			
		||||
        """Ensure init fails with invalid client"""
 | 
			
		||||
        with self.assertRaises(ControllerException):
 | 
			
		||||
            DockerController(self.outpost, self.integration)
 | 
			
		||||
 | 
			
		||||
    def test_env_valid(self):
 | 
			
		||||
        """Test environment check"""
 | 
			
		||||
        controller = DockerController(
 | 
			
		||||
            Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
 | 
			
		||||
        )
 | 
			
		||||
        env = [f"{key}={value}" for key, value in controller._get_env().items()]
 | 
			
		||||
        container = Container(attrs={"Config": {"Env": env}})
 | 
			
		||||
        self.assertFalse(controller._comp_env(container))
 | 
			
		||||
 | 
			
		||||
    def test_env_invalid(self):
 | 
			
		||||
        """Test environment check"""
 | 
			
		||||
        controller = DockerController(
 | 
			
		||||
            Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
 | 
			
		||||
        )
 | 
			
		||||
        container = Container(attrs={"Config": {"Env": []}})
 | 
			
		||||
        self.assertTrue(controller._comp_env(container))
 | 
			
		||||
 | 
			
		||||
    def test_label_valid(self):
 | 
			
		||||
        """Test label check"""
 | 
			
		||||
        controller = DockerController(
 | 
			
		||||
            Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
 | 
			
		||||
        )
 | 
			
		||||
        container = Container(attrs={"Config": {"Labels": controller._get_labels()}})
 | 
			
		||||
        self.assertFalse(controller._comp_labels(container))
 | 
			
		||||
 | 
			
		||||
    def test_label_invalid(self):
 | 
			
		||||
        """Test label check"""
 | 
			
		||||
        controller = DockerController(
 | 
			
		||||
            Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
 | 
			
		||||
        )
 | 
			
		||||
        container = Container(attrs={"Config": {"Labels": {}}})
 | 
			
		||||
        self.assertTrue(controller._comp_labels(container))
 | 
			
		||||
        container = Container(attrs={"Config": {"Labels": {"io.goauthentik.outpost-uuid": "foo"}}})
 | 
			
		||||
        self.assertTrue(controller._comp_labels(container))
 | 
			
		||||
 | 
			
		||||
    def test_port_valid(self):
 | 
			
		||||
        """Test port check"""
 | 
			
		||||
        controller = ProxyDockerController(
 | 
			
		||||
            Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
 | 
			
		||||
        )
 | 
			
		||||
        container = Container(
 | 
			
		||||
            attrs={
 | 
			
		||||
                "NetworkSettings": {
 | 
			
		||||
                    "Ports": {
 | 
			
		||||
                        "9000/tcp": [{"HostIp": "", "HostPort": "9000"}],
 | 
			
		||||
                        "9443/tcp": [{"HostIp": "", "HostPort": "9443"}],
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "State": "",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        with self.settings(TEST=False):
 | 
			
		||||
            self.assertFalse(controller._comp_ports(container))
 | 
			
		||||
            container.attrs["State"] = "running"
 | 
			
		||||
            self.assertFalse(controller._comp_ports(container))
 | 
			
		||||
 | 
			
		||||
    def test_port_invalid(self):
 | 
			
		||||
        """Test port check"""
 | 
			
		||||
        controller = ProxyDockerController(
 | 
			
		||||
            Outpost.objects.filter(managed=MANAGED_OUTPOST).first(), self.integration
 | 
			
		||||
        )
 | 
			
		||||
        container_no_ports = Container(
 | 
			
		||||
            attrs={"NetworkSettings": {"Ports": None}, "State": "running"}
 | 
			
		||||
        )
 | 
			
		||||
        container_missing_port = Container(
 | 
			
		||||
            attrs={
 | 
			
		||||
                "NetworkSettings": {
 | 
			
		||||
                    "Ports": {
 | 
			
		||||
                        "9443/tcp": [{"HostIp": "", "HostPort": "9443"}],
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "State": "running",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        container_mismatched_host = Container(
 | 
			
		||||
            attrs={
 | 
			
		||||
                "NetworkSettings": {
 | 
			
		||||
                    "Ports": {
 | 
			
		||||
                        "9443/tcp": [{"HostIp": "", "HostPort": "123"}],
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "State": "running",
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        with self.settings(TEST=False):
 | 
			
		||||
            self.assertFalse(controller._comp_ports(container_no_ports))
 | 
			
		||||
            self.assertTrue(controller._comp_ports(container_missing_port))
 | 
			
		||||
            self.assertTrue(controller._comp_ports(container_mismatched_host))
 | 
			
		||||
@ -90,7 +90,7 @@ class PolicyEngine:
 | 
			
		||||
    def build(self) -> "PolicyEngine":
 | 
			
		||||
        """Build wrapper which monitors performance"""
 | 
			
		||||
        with Hub.current.start_span(
 | 
			
		||||
            op="policy.engine.build",
 | 
			
		||||
            op="authentik.policy.engine.build",
 | 
			
		||||
            description=self.__pbm,
 | 
			
		||||
        ) as span, HIST_POLICIES_BUILD_TIME.labels(
 | 
			
		||||
            object_name=self.__pbm,
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
                            ("source_linked", "Source Linked"),
 | 
			
		||||
                            ("impersonation_started", "Impersonation Started"),
 | 
			
		||||
                            ("impersonation_ended", "Impersonation Ended"),
 | 
			
		||||
                            ("flow_execution", "Flow Execution"),
 | 
			
		||||
                            ("policy_execution", "Policy Execution"),
 | 
			
		||||
                            ("policy_exception", "Policy Exception"),
 | 
			
		||||
                            ("property_mapping_exception", "Property Mapping Exception"),
 | 
			
		||||
 | 
			
		||||
@ -74,4 +74,4 @@ class TestExpressionPolicyAPI(APITestCase):
 | 
			
		||||
        expr = "return True"
 | 
			
		||||
        self.assertEqual(ExpressionPolicySerializer().validate_expression(expr), expr)
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            print(ExpressionPolicySerializer().validate_expression("/"))
 | 
			
		||||
            ExpressionPolicySerializer().validate_expression("/")
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ class PasswordPolicySerializer(PolicySerializer):
 | 
			
		||||
        model = PasswordPolicy
 | 
			
		||||
        fields = PolicySerializer.Meta.fields + [
 | 
			
		||||
            "password_field",
 | 
			
		||||
            "amount_digits",
 | 
			
		||||
            "amount_uppercase",
 | 
			
		||||
            "amount_lowercase",
 | 
			
		||||
            "amount_symbols",
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
# Generated by Django 4.0 on 2021-12-18 14:54
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("authentik_policies_password", "0002_passwordpolicy_password_field"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="passwordpolicy",
 | 
			
		||||
            name="amount_digits",
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="passwordpolicy",
 | 
			
		||||
            name="amount_lowercase",
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="passwordpolicy",
 | 
			
		||||
            name="amount_symbols",
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="passwordpolicy",
 | 
			
		||||
            name="amount_uppercase",
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name="passwordpolicy",
 | 
			
		||||
            name="length_min",
 | 
			
		||||
            field=models.PositiveIntegerField(default=0),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -13,6 +13,7 @@ from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
RE_LOWER = re.compile("[a-z]")
 | 
			
		||||
RE_UPPER = re.compile("[A-Z]")
 | 
			
		||||
RE_DIGITS = re.compile("[0-9]")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PasswordPolicy(Policy):
 | 
			
		||||
@ -23,10 +24,11 @@ class PasswordPolicy(Policy):
 | 
			
		||||
        help_text=_("Field key to check, field keys defined in Prompt stages are available."),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    amount_uppercase = models.IntegerField(default=0)
 | 
			
		||||
    amount_lowercase = models.IntegerField(default=0)
 | 
			
		||||
    amount_symbols = models.IntegerField(default=0)
 | 
			
		||||
    length_min = models.IntegerField(default=0)
 | 
			
		||||
    amount_digits = models.PositiveIntegerField(default=0)
 | 
			
		||||
    amount_uppercase = models.PositiveIntegerField(default=0)
 | 
			
		||||
    amount_lowercase = models.PositiveIntegerField(default=0)
 | 
			
		||||
    amount_symbols = models.PositiveIntegerField(default=0)
 | 
			
		||||
    length_min = models.PositiveIntegerField(default=0)
 | 
			
		||||
    symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
 | 
			
		||||
    error_message = models.TextField()
 | 
			
		||||
 | 
			
		||||
@ -40,6 +42,7 @@ class PasswordPolicy(Policy):
 | 
			
		||||
    def component(self) -> str:
 | 
			
		||||
        return "ak-policy-password-form"
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=too-many-return-statements
 | 
			
		||||
    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
			
		||||
        if (
 | 
			
		||||
            self.password_field not in request.context
 | 
			
		||||
@ -62,6 +65,9 @@ class PasswordPolicy(Policy):
 | 
			
		||||
            LOGGER.debug("password failed", reason="length")
 | 
			
		||||
            return PolicyResult(False, self.error_message)
 | 
			
		||||
 | 
			
		||||
        if self.amount_digits > 0 and len(RE_DIGITS.findall(password)) < self.amount_digits:
 | 
			
		||||
            LOGGER.debug("password failed", reason="amount_digits")
 | 
			
		||||
            return PolicyResult(False, self.error_message)
 | 
			
		||||
        if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase:
 | 
			
		||||
            LOGGER.debug("password failed", reason="amount_lowercase")
 | 
			
		||||
            return PolicyResult(False, self.error_message)
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ class TestPasswordPolicy(TestCase):
 | 
			
		||||
    def setUp(self) -> None:
 | 
			
		||||
        self.policy = PasswordPolicy.objects.create(
 | 
			
		||||
            name="test_false",
 | 
			
		||||
            amount_digits=1,
 | 
			
		||||
            amount_uppercase=1,
 | 
			
		||||
            amount_lowercase=2,
 | 
			
		||||
            amount_symbols=3,
 | 
			
		||||
@ -38,7 +39,7 @@ class TestPasswordPolicy(TestCase):
 | 
			
		||||
    def test_failed_lowercase(self):
 | 
			
		||||
        """not enough lowercase"""
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe"  # nosec
 | 
			
		||||
        request.context["password"] = "1TTTTTTTTTTTTTTTTTTTTTTe"  # nosec
 | 
			
		||||
        result: PolicyResult = self.policy.passes(request)
 | 
			
		||||
        self.assertFalse(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages, ("test message",))
 | 
			
		||||
@ -46,15 +47,23 @@ class TestPasswordPolicy(TestCase):
 | 
			
		||||
    def test_failed_uppercase(self):
 | 
			
		||||
        """not enough uppercase"""
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        request.context["password"] = "tttttttttttttttttttttttE"  # nosec
 | 
			
		||||
        request.context["password"] = "1tttttttttttttttttttttE"  # nosec
 | 
			
		||||
        result: PolicyResult = self.policy.passes(request)
 | 
			
		||||
        self.assertFalse(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages, ("test message",))
 | 
			
		||||
 | 
			
		||||
    def test_failed_symbols(self):
 | 
			
		||||
        """not enough uppercase"""
 | 
			
		||||
        """not enough symbols"""
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        request.context["password"] = "TETETETETETETETETETETETETe!!!"  # nosec
 | 
			
		||||
        request.context["password"] = "1ETETETETETETETETETETETETe!!!"  # nosec
 | 
			
		||||
        result: PolicyResult = self.policy.passes(request)
 | 
			
		||||
        self.assertFalse(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages, ("test message",))
 | 
			
		||||
 | 
			
		||||
    def test_failed_digits(self):
 | 
			
		||||
        """not enough digits"""
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        request.context["password"] = "TETETETETETETETETETETE1e!!!"  # nosec
 | 
			
		||||
        result: PolicyResult = self.policy.passes(request)
 | 
			
		||||
        self.assertFalse(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages, ("test message",))
 | 
			
		||||
@ -62,7 +71,7 @@ class TestPasswordPolicy(TestCase):
 | 
			
		||||
    def test_true(self):
 | 
			
		||||
        """Positive password case"""
 | 
			
		||||
        request = PolicyRequest(get_anonymous_user())
 | 
			
		||||
        request.context["password"] = generate_key() + "ee!!!"  # nosec
 | 
			
		||||
        request.context["password"] = generate_key() + "1ee!!!"  # nosec
 | 
			
		||||
        result: PolicyResult = self.policy.passes(request)
 | 
			
		||||
        self.assertTrue(result.passing)
 | 
			
		||||
        self.assertEqual(result.messages, tuple())
 | 
			
		||||
 | 
			
		||||
@ -130,7 +130,7 @@ class PolicyProcess(PROCESS_CLASS):
 | 
			
		||||
    def profiling_wrapper(self):
 | 
			
		||||
        """Run with profiling enabled"""
 | 
			
		||||
        with Hub.current.start_span(
 | 
			
		||||
            op="policy.process.execute",
 | 
			
		||||
            op="authentik.policy.process.execute",
 | 
			
		||||
        ) as span, HIST_POLICIES_EXECUTION_TIME.labels(
 | 
			
		||||
            binding_order=self.binding.order,
 | 
			
		||||
            binding_target_type=self.binding.target_type,
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,6 @@ from datetime import datetime
 | 
			
		||||
from hashlib import sha256
 | 
			
		||||
from typing import Any, Optional, Type
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
from uuid import uuid4
 | 
			
		||||
 | 
			
		||||
from dacite import from_dict
 | 
			
		||||
from django.db import models
 | 
			
		||||
@ -225,7 +224,7 @@ class OAuth2Provider(Provider):
 | 
			
		||||
        token = RefreshToken(
 | 
			
		||||
            user=user,
 | 
			
		||||
            provider=self,
 | 
			
		||||
            refresh_token=uuid4().hex,
 | 
			
		||||
            refresh_token=generate_key(),
 | 
			
		||||
            expires=timezone.now() + timedelta_from_string(self.token_validity),
 | 
			
		||||
            scope=scope,
 | 
			
		||||
        )
 | 
			
		||||
@ -434,7 +433,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
 | 
			
		||||
        """Create access token with a similar format as Okta, Keycloak, ADFS"""
 | 
			
		||||
        token = self.create_id_token(user, request).to_dict()
 | 
			
		||||
        token["cid"] = self.provider.client_id
 | 
			
		||||
        token["uid"] = uuid4().hex
 | 
			
		||||
        token["uid"] = generate_key()
 | 
			
		||||
        return self.provider.encode(token)
 | 
			
		||||
 | 
			
		||||
    def create_id_token(self, user: User, request: HttpRequest) -> IDToken:
 | 
			
		||||
 | 
			
		||||
@ -95,6 +95,12 @@ class TokenParams:
 | 
			
		||||
                self.refresh_token = RefreshToken.objects.get(
 | 
			
		||||
                    refresh_token=raw_token, provider=self.provider
 | 
			
		||||
                )
 | 
			
		||||
                if self.refresh_token.is_expired:
 | 
			
		||||
                    LOGGER.warning(
 | 
			
		||||
                        "Refresh token is expired",
 | 
			
		||||
                        token=raw_token,
 | 
			
		||||
                    )
 | 
			
		||||
                    raise TokenError("invalid_grant")
 | 
			
		||||
                # https://tools.ietf.org/html/rfc6749#section-6
 | 
			
		||||
                # Fallback to original token's scopes when none are given
 | 
			
		||||
                if not self.scope:
 | 
			
		||||
@ -138,6 +144,12 @@ class TokenParams:
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self.authorization_code = AuthorizationCode.objects.get(code=raw_code)
 | 
			
		||||
            if self.authorization_code.is_expired:
 | 
			
		||||
                LOGGER.warning(
 | 
			
		||||
                    "Code is expired",
 | 
			
		||||
                    token=raw_code,
 | 
			
		||||
                )
 | 
			
		||||
                raise TokenError("invalid_grant")
 | 
			
		||||
        except AuthorizationCode.DoesNotExist:
 | 
			
		||||
            LOGGER.warning("Code does not exist", code=raw_code)
 | 
			
		||||
            raise TokenError("invalid_grant")
 | 
			
		||||
@ -194,8 +206,10 @@ class TokenView(View):
 | 
			
		||||
            self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
 | 
			
		||||
 | 
			
		||||
            if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
 | 
			
		||||
                LOGGER.info("Converting authorization code to refresh token")
 | 
			
		||||
                return TokenResponse(self.create_code_response())
 | 
			
		||||
            if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
 | 
			
		||||
                LOGGER.info("Refreshing refresh token")
 | 
			
		||||
                return TokenResponse(self.create_refresh_response())
 | 
			
		||||
            raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
 | 
			
		||||
        except TokenError as error:
 | 
			
		||||
 | 
			
		||||
@ -89,6 +89,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
 | 
			
		||||
            # goes to the same pod
 | 
			
		||||
            "nginx.ingress.kubernetes.io/affinity": "cookie",
 | 
			
		||||
            "traefik.ingress.kubernetes.io/affinity": "true",
 | 
			
		||||
            # Buffer sizes for large headers with JWTs
 | 
			
		||||
            "nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
 | 
			
		||||
            "nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -96,6 +96,16 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
 | 
			
		||||
        super().reconcile(current, reference)
 | 
			
		||||
        if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
 | 
			
		||||
            raise NeedsUpdate()
 | 
			
		||||
        if (
 | 
			
		||||
            current.spec.forwardAuth.authResponseHeadersRegex
 | 
			
		||||
            != reference.spec.forwardAuth.authResponseHeadersRegex
 | 
			
		||||
        ):
 | 
			
		||||
            raise NeedsUpdate()
 | 
			
		||||
        # Ensure all of our headers are set, others can be added by the user.
 | 
			
		||||
        if not set(current.spec.forwardAuth.authResponseHeaders).issubset(
 | 
			
		||||
            reference.spec.forwardAuth.authResponseHeaders
 | 
			
		||||
        ):
 | 
			
		||||
            raise NeedsUpdate()
 | 
			
		||||
 | 
			
		||||
    def get_reference_object(self) -> TraefikMiddleware:
 | 
			
		||||
        """Get deployment object for outpost"""
 | 
			
		||||
@ -110,8 +120,27 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
 | 
			
		||||
            spec=TraefikMiddlewareSpec(
 | 
			
		||||
                forwardAuth=TraefikMiddlewareSpecForwardAuth(
 | 
			
		||||
                    address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
 | 
			
		||||
                    authResponseHeaders=[],
 | 
			
		||||
                    authResponseHeadersRegex="^.*$",
 | 
			
		||||
                    authResponseHeaders=[
 | 
			
		||||
                        # Legacy headers, remove after 2022.1
 | 
			
		||||
                        "X-Auth-Username",
 | 
			
		||||
                        "X-Auth-Groups",
 | 
			
		||||
                        "X-Forwarded-Email",
 | 
			
		||||
                        "X-Forwarded-Preferred-Username",
 | 
			
		||||
                        "X-Forwarded-User",
 | 
			
		||||
                        # New headers, unique prefix
 | 
			
		||||
                        "X-authentik-username",
 | 
			
		||||
                        "X-authentik-groups",
 | 
			
		||||
                        "X-authentik-email",
 | 
			
		||||
                        "X-authentik-name",
 | 
			
		||||
                        "X-authentik-uid",
 | 
			
		||||
                        "X-authentik-jwt",
 | 
			
		||||
                        "X-authentik-meta-jwks",
 | 
			
		||||
                        "X-authentik-meta-outpost",
 | 
			
		||||
                        "X-authentik-meta-provider",
 | 
			
		||||
                        "X-authentik-meta-app",
 | 
			
		||||
                        "X-authentik-meta-version",
 | 
			
		||||
                    ],
 | 
			
		||||
                    authResponseHeadersRegex="",
 | 
			
		||||
                    trustForwardHeader=True,
 | 
			
		||||
                )
 | 
			
		||||
            ),
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
            name="audience",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                default="",
 | 
			
		||||
                help_text="Value of the audience restriction field of the asseration.",
 | 
			
		||||
                help_text="Value of the audience restriction field of the assertion.",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,7 @@ class Migration(migrations.Migration):
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                blank=True,
 | 
			
		||||
                default="",
 | 
			
		||||
                help_text="Value of the audience restriction field of the asseration. When left empty, no audience restriction will be added.",
 | 
			
		||||
                help_text="Value of the audience restriction field of the assertion. When left empty, no audience restriction will be added.",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,7 @@ class SAMLProvider(Provider):
 | 
			
		||||
        blank=True,
 | 
			
		||||
        help_text=_(
 | 
			
		||||
            (
 | 
			
		||||
                "Value of the audience restriction field of the asseration. When left empty, "
 | 
			
		||||
                "Value of the audience restriction field of the assertion. When left empty, "
 | 
			
		||||
                "no audience restriction will be added."
 | 
			
		||||
            )
 | 
			
		||||
        ),
 | 
			
		||||
 | 
			
		||||
@ -70,13 +70,14 @@ class AssertionProcessor:
 | 
			
		||||
        """Get AttributeStatement Element with Attributes from Property Mappings."""
 | 
			
		||||
        # https://commons.lbl.gov/display/IDMgmt/Attribute+Definitions
 | 
			
		||||
        attribute_statement = Element(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")
 | 
			
		||||
        user = self.http_request.user
 | 
			
		||||
        for mapping in self.provider.property_mappings.all().select_subclasses():
 | 
			
		||||
            if not isinstance(mapping, SAMLPropertyMapping):
 | 
			
		||||
                continue
 | 
			
		||||
            try:
 | 
			
		||||
                mapping: SAMLPropertyMapping
 | 
			
		||||
                value = mapping.evaluate(
 | 
			
		||||
                    user=self.http_request.user,
 | 
			
		||||
                    user=user,
 | 
			
		||||
                    request=self.http_request,
 | 
			
		||||
                    provider=self.provider,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
@ -28,6 +28,7 @@ from sentry_sdk.integrations.boto3 import Boto3Integration
 | 
			
		||||
from sentry_sdk.integrations.celery import CeleryIntegration
 | 
			
		||||
from sentry_sdk.integrations.django import DjangoIntegration
 | 
			
		||||
from sentry_sdk.integrations.redis import RedisIntegration
 | 
			
		||||
from sentry_sdk.integrations.threading import ThreadingIntegration
 | 
			
		||||
 | 
			
		||||
from authentik import ENV_GIT_HASH_KEY, __version__
 | 
			
		||||
from authentik.core.middleware import structlog_add_request_id
 | 
			
		||||
@ -66,7 +67,7 @@ SECRET_KEY = CONFIG.y("secret_key")
 | 
			
		||||
INTERNAL_IPS = ["127.0.0.1"]
 | 
			
		||||
ALLOWED_HOSTS = ["*"]
 | 
			
		||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
 | 
			
		||||
 | 
			
		||||
SECURE_CROSS_ORIGIN_OPENER_POLICY = None
 | 
			
		||||
LOGIN_URL = "authentik_flows:default-authentication"
 | 
			
		||||
 | 
			
		||||
# Custom user model
 | 
			
		||||
@ -219,15 +220,16 @@ REDIS_CELERY_TLS_REQUIREMENTS = ""
 | 
			
		||||
if CONFIG.y_bool("redis.tls", False):
 | 
			
		||||
    REDIS_PROTOCOL_PREFIX = "rediss://"
 | 
			
		||||
    REDIS_CELERY_TLS_REQUIREMENTS = f"?ssl_cert_reqs={CONFIG.y('redis.tls_reqs')}"
 | 
			
		||||
_redis_url = (
 | 
			
		||||
    f"{REDIS_PROTOCOL_PREFIX}:"
 | 
			
		||||
    f"{quote(CONFIG.y('redis.password'))}@{quote(CONFIG.y('redis.host'))}:"
 | 
			
		||||
    f"{int(CONFIG.y('redis.port'))}"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CACHES = {
 | 
			
		||||
    "default": {
 | 
			
		||||
        "BACKEND": "django_redis.cache.RedisCache",
 | 
			
		||||
        "LOCATION": (
 | 
			
		||||
            f"{REDIS_PROTOCOL_PREFIX}:"
 | 
			
		||||
            f"{quote(CONFIG.y('redis.password'))}@{quote(CONFIG.y('redis.host'))}:"
 | 
			
		||||
            f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.cache_db')}"
 | 
			
		||||
        ),
 | 
			
		||||
        "LOCATION": f"{_redis_url}/{CONFIG.y('redis.cache_db')}",
 | 
			
		||||
        "TIMEOUT": int(CONFIG.y("redis.cache_timeout", 300)),
 | 
			
		||||
        "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
 | 
			
		||||
    }
 | 
			
		||||
@ -286,11 +288,7 @@ CHANNEL_LAYERS = {
 | 
			
		||||
    "default": {
 | 
			
		||||
        "BACKEND": "channels_redis.core.RedisChannelLayer",
 | 
			
		||||
        "CONFIG": {
 | 
			
		||||
            "hosts": [
 | 
			
		||||
                f"{REDIS_PROTOCOL_PREFIX}:"
 | 
			
		||||
                f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
 | 
			
		||||
                f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.ws_db')}"
 | 
			
		||||
            ],
 | 
			
		||||
            "hosts": [f"{_redis_url}/{CONFIG.y('redis.ws_db')}"],
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
@ -344,8 +342,6 @@ TIME_ZONE = "UTC"
 | 
			
		||||
 | 
			
		||||
USE_I18N = True
 | 
			
		||||
 | 
			
		||||
USE_L10N = True
 | 
			
		||||
 | 
			
		||||
USE_TZ = True
 | 
			
		||||
 | 
			
		||||
LOCALE_PATHS = ["./locale"]
 | 
			
		||||
@ -368,16 +364,10 @@ CELERY_BEAT_SCHEDULE = {
 | 
			
		||||
CELERY_TASK_CREATE_MISSING_QUEUES = True
 | 
			
		||||
CELERY_TASK_DEFAULT_QUEUE = "authentik"
 | 
			
		||||
CELERY_BROKER_URL = (
 | 
			
		||||
    f"{REDIS_PROTOCOL_PREFIX}:"
 | 
			
		||||
    f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
 | 
			
		||||
    f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.message_queue_db')}"
 | 
			
		||||
    f"{REDIS_CELERY_TLS_REQUIREMENTS}"
 | 
			
		||||
    f"{_redis_url}/{CONFIG.y('redis.message_queue_db')}{REDIS_CELERY_TLS_REQUIREMENTS}"
 | 
			
		||||
)
 | 
			
		||||
CELERY_RESULT_BACKEND = (
 | 
			
		||||
    f"{REDIS_PROTOCOL_PREFIX}:"
 | 
			
		||||
    f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
 | 
			
		||||
    f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.message_queue_db')}"
 | 
			
		||||
    f"{REDIS_CELERY_TLS_REQUIREMENTS}"
 | 
			
		||||
    f"{_redis_url}/{CONFIG.y('redis.message_queue_db')}{REDIS_CELERY_TLS_REQUIREMENTS}"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# Database backup
 | 
			
		||||
@ -424,6 +414,7 @@ if _ERROR_REPORTING:
 | 
			
		||||
            CeleryIntegration(),
 | 
			
		||||
            RedisIntegration(),
 | 
			
		||||
            Boto3Integration(),
 | 
			
		||||
            ThreadingIntegration(propagate_hub=True),
 | 
			
		||||
        ],
 | 
			
		||||
        before_send=before_send,
 | 
			
		||||
        release=f"authentik@{__version__}",
 | 
			
		||||
@ -470,6 +461,11 @@ TEST = False
 | 
			
		||||
TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
 | 
			
		||||
# We can't check TEST here as its set later by the test runner
 | 
			
		||||
LOG_LEVEL = CONFIG.y("log_level").upper() if "TF_BUILD" not in os.environ else "DEBUG"
 | 
			
		||||
# We could add a custom level to stdlib logging and structlog, but it's not easy or clean
 | 
			
		||||
# https://stackoverflow.com/questions/54505487/custom-log-level-not-working-with-structlog
 | 
			
		||||
# Additionally, the entire code uses debug as highest level so that would have to be re-written too
 | 
			
		||||
if LOG_LEVEL == "TRACE":
 | 
			
		||||
    LOG_LEVEL = "DEBUG"
 | 
			
		||||
 | 
			
		||||
structlog.configure_once(
 | 
			
		||||
    processors=[
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
"""Source API Views"""
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from django.utils.text import slugify
 | 
			
		||||
from django_filters.filters import AllValuesMultipleFilter
 | 
			
		||||
from django_filters.filterset import FilterSet
 | 
			
		||||
from drf_spectacular.types import OpenApiTypes
 | 
			
		||||
@ -110,7 +109,8 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
            GroupLDAPSynchronizer,
 | 
			
		||||
            MembershipLDAPSynchronizer,
 | 
			
		||||
        ]:
 | 
			
		||||
            task = TaskInfo.by_name(f"ldap_sync_{slugify(source.name)}-{sync_class.__name__}")
 | 
			
		||||
            sync_name = sync_class.__name__.replace("LDAPSynchronizer", "").lower()
 | 
			
		||||
            task = TaskInfo.by_name(f"ldap_sync_{source.slug}_{sync_name}")
 | 
			
		||||
            if task:
 | 
			
		||||
                results.append(task)
 | 
			
		||||
        return Response(TaskSerializer(results, many=True).data)
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
			
		||||
            group_dn = self._flatten(self._flatten(group.get("entryDN", group.get("dn"))))
 | 
			
		||||
            if self._source.object_uniqueness_field not in attributes:
 | 
			
		||||
                self.message(
 | 
			
		||||
                    f"Cannot find uniqueness field in attributes: '{group_dn}",
 | 
			
		||||
                    f"Cannot find uniqueness field in attributes: '{group_dn}'",
 | 
			
		||||
                    attributes=attributes.keys(),
 | 
			
		||||
                    dn=group_dn,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
 | 
			
		||||
            user_dn = self._flatten(user.get("entryDN", user.get("dn")))
 | 
			
		||||
            if self._source.object_uniqueness_field not in attributes:
 | 
			
		||||
                self.message(
 | 
			
		||||
                    f"Cannot find uniqueness field in attributes: '{user_dn}",
 | 
			
		||||
                    f"Cannot find uniqueness field in attributes: '{user_dn}'",
 | 
			
		||||
                    attributes=attributes.keys(),
 | 
			
		||||
                    dn=user_dn,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
"""LDAP Sync tasks"""
 | 
			
		||||
from django.utils.text import slugify
 | 
			
		||||
from ldap3.core.exceptions import LDAPException
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
@ -39,7 +38,7 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
 | 
			
		||||
        # to set the state with
 | 
			
		||||
        return
 | 
			
		||||
    sync = path_to_class(sync_class)
 | 
			
		||||
    self.set_uid(f"{slugify(source.name)}_{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
 | 
			
		||||
    self.set_uid(f"{source.slug}_{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
 | 
			
		||||
    try:
 | 
			
		||||
        sync_inst = sync(source)
 | 
			
		||||
        count = sync_inst.sync()
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ class UserOAuthSourceConnectionSerializer(SourceSerializer):
 | 
			
		||||
        model = UserOAuthSourceConnection
 | 
			
		||||
        fields = ["pk", "user", "source", "identifier", "access_token"]
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            "user": {"read_only": True},
 | 
			
		||||
            "access_token": {"write_only": True},
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [
 | 
			
		||||
    "authentik.sources.oauth.types.github",
 | 
			
		||||
    "authentik.sources.oauth.types.google",
 | 
			
		||||
    "authentik.sources.oauth.types.oidc",
 | 
			
		||||
    "authentik.sources.oauth.types.okta",
 | 
			
		||||
    "authentik.sources.oauth.types.reddit",
 | 
			
		||||
    "authentik.sources.oauth.types.twitter",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
@ -2,13 +2,13 @@
 | 
			
		||||
from typing import TYPE_CHECKING, Optional, Type
 | 
			
		||||
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.http.request import HttpRequest
 | 
			
		||||
from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework.serializers import Serializer
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import Source, UserSourceConnection
 | 
			
		||||
from authentik.core.types import UILoginButton, UserSettingSerializer
 | 
			
		||||
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from authentik.sources.oauth.types.manager import SourceType
 | 
			
		||||
@ -64,24 +64,15 @@ class OAuthSource(Source):
 | 
			
		||||
 | 
			
		||||
        return OAuthSourceSerializer
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_login_button(self) -> UILoginButton:
 | 
			
		||||
    def ui_login_button(self, request: HttpRequest) -> UILoginButton:
 | 
			
		||||
        provider_type = self.type
 | 
			
		||||
        provider = provider_type()
 | 
			
		||||
        return UILoginButton(
 | 
			
		||||
            challenge=RedirectChallenge(
 | 
			
		||||
                instance={
 | 
			
		||||
                    "type": ChallengeTypes.REDIRECT.value,
 | 
			
		||||
                    "to": reverse(
 | 
			
		||||
                        "authentik_sources_oauth:oauth-client-login",
 | 
			
		||||
                        kwargs={"source_slug": self.slug},
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            icon_url=provider_type().icon_url(),
 | 
			
		||||
            name=self.name,
 | 
			
		||||
            icon_url=provider.icon_url(),
 | 
			
		||||
            challenge=provider.login_challenge(self, request),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        return UserSettingSerializer(
 | 
			
		||||
            data={
 | 
			
		||||
@ -183,6 +174,16 @@ class AppleOAuthSource(OAuthSource):
 | 
			
		||||
        verbose_name_plural = _("Apple OAuth Sources")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OktaOAuthSource(OAuthSource):
 | 
			
		||||
    """Login using a okta.com."""
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        abstract = True
 | 
			
		||||
        verbose_name = _("Okta OAuth Source")
 | 
			
		||||
        verbose_name_plural = _("Okta OAuth Sources")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserOAuthSourceConnection(UserSourceConnection):
 | 
			
		||||
    """Authorized remote OAuth provider."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,15 @@
 | 
			
		||||
from time import time
 | 
			
		||||
from typing import Any, Optional
 | 
			
		||||
 | 
			
		||||
from django.http.request import HttpRequest
 | 
			
		||||
from django.urls.base import reverse
 | 
			
		||||
from jwt import decode, encode
 | 
			
		||||
from rest_framework.fields import CharField
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
 | 
			
		||||
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
 | 
			
		||||
from authentik.sources.oauth.models import OAuthSource
 | 
			
		||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
 | 
			
		||||
from authentik.sources.oauth.views.callback import OAuthCallback
 | 
			
		||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
			
		||||
@ -13,18 +18,34 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppleLoginChallenge(Challenge):
 | 
			
		||||
    """Special challenge for apple-native authentication flow, which happens on the client."""
 | 
			
		||||
 | 
			
		||||
    client_id = CharField()
 | 
			
		||||
    component = CharField(default="ak-flow-sources-oauth-apple")
 | 
			
		||||
    scope = CharField()
 | 
			
		||||
    redirect_uri = CharField()
 | 
			
		||||
    state = CharField()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppleChallengeResponse(ChallengeResponse):
 | 
			
		||||
    """Pseudo class for plex response"""
 | 
			
		||||
 | 
			
		||||
    component = CharField(default="ak-flow-sources-oauth-apple")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AppleOAuthClient(OAuth2Client):
 | 
			
		||||
    """Apple OAuth2 client"""
 | 
			
		||||
 | 
			
		||||
    def get_client_id(self) -> str:
 | 
			
		||||
        parts = self.source.consumer_key.split(";")
 | 
			
		||||
        parts: list[str] = self.source.consumer_key.split(";")
 | 
			
		||||
        if len(parts) < 3:
 | 
			
		||||
            return self.source.consumer_key
 | 
			
		||||
        return parts[0]
 | 
			
		||||
        return parts[0].strip()
 | 
			
		||||
 | 
			
		||||
    def get_client_secret(self) -> str:
 | 
			
		||||
        now = time()
 | 
			
		||||
        parts = self.source.consumer_key.split(";")
 | 
			
		||||
        parts: list[str] = self.source.consumer_key.split(";")
 | 
			
		||||
        if len(parts) < 3:
 | 
			
		||||
            raise ValueError(
 | 
			
		||||
                (
 | 
			
		||||
@ -34,14 +55,14 @@ class AppleOAuthClient(OAuth2Client):
 | 
			
		||||
            )
 | 
			
		||||
        LOGGER.debug("got values from client_id", team=parts[1], kid=parts[2])
 | 
			
		||||
        payload = {
 | 
			
		||||
            "iss": parts[1],
 | 
			
		||||
            "iss": parts[1].strip(),
 | 
			
		||||
            "iat": now,
 | 
			
		||||
            "exp": now + 86400 * 180,
 | 
			
		||||
            "aud": "https://appleid.apple.com",
 | 
			
		||||
            "sub": parts[0],
 | 
			
		||||
            "sub": parts[0].strip(),
 | 
			
		||||
        }
 | 
			
		||||
        # pyright: reportGeneralTypeIssues=false
 | 
			
		||||
        jwt = encode(payload, self.source.consumer_secret, "ES256", {"kid": parts[2]})
 | 
			
		||||
        jwt = encode(payload, self.source.consumer_secret, "ES256", {"kid": parts[2].strip()})
 | 
			
		||||
        LOGGER.debug("signing payload as secret key", payload=payload, jwt=jwt)
 | 
			
		||||
        return jwt
 | 
			
		||||
 | 
			
		||||
@ -55,7 +76,7 @@ class AppleOAuthRedirect(OAuthRedirect):
 | 
			
		||||
 | 
			
		||||
    client_class = AppleOAuthClient
 | 
			
		||||
 | 
			
		||||
    def get_additional_parameters(self, source):  # pragma: no cover
 | 
			
		||||
    def get_additional_parameters(self, source: OAuthSource):  # pragma: no cover
 | 
			
		||||
        return {
 | 
			
		||||
            "scope": "name email",
 | 
			
		||||
            "response_mode": "form_post",
 | 
			
		||||
@ -74,7 +95,6 @@ class AppleOAuth2Callback(OAuthCallback):
 | 
			
		||||
        self,
 | 
			
		||||
        info: dict[str, Any],
 | 
			
		||||
    ) -> dict[str, Any]:
 | 
			
		||||
        print(info)
 | 
			
		||||
        return {
 | 
			
		||||
            "email": info.get("email"),
 | 
			
		||||
            "name": info.get("name"),
 | 
			
		||||
@ -96,3 +116,24 @@ class AppleType(SourceType):
 | 
			
		||||
 | 
			
		||||
    def icon_url(self) -> str:
 | 
			
		||||
        return "https://appleid.cdn-apple.com/appleid/button/logo"
 | 
			
		||||
 | 
			
		||||
    def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge:
 | 
			
		||||
        """Pre-general all the things required for the JS SDK"""
 | 
			
		||||
        apple_client = AppleOAuthClient(
 | 
			
		||||
            source,
 | 
			
		||||
            request,
 | 
			
		||||
            callback=reverse(
 | 
			
		||||
                "authentik_sources_oauth:oauth-client-callback",
 | 
			
		||||
                kwargs={"source_slug": source.slug},
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        args = apple_client.get_redirect_args()
 | 
			
		||||
        return AppleLoginChallenge(
 | 
			
		||||
            instance={
 | 
			
		||||
                "client_id": apple_client.get_client_id(),
 | 
			
		||||
                "scope": "name email",
 | 
			
		||||
                "redirect_uri": args["redirect_uri"],
 | 
			
		||||
                "state": args["state"],
 | 
			
		||||
                "type": ChallengeTypes.NATIVE.value,
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,13 @@
 | 
			
		||||
from enum import Enum
 | 
			
		||||
from typing import Callable, Optional, Type
 | 
			
		||||
 | 
			
		||||
from django.http.request import HttpRequest
 | 
			
		||||
from django.templatetags.static import static
 | 
			
		||||
from django.urls.base import reverse
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.flows.challenge import Challenge, ChallengeTypes, RedirectChallenge
 | 
			
		||||
from authentik.sources.oauth.models import OAuthSource
 | 
			
		||||
from authentik.sources.oauth.views.callback import OAuthCallback
 | 
			
		||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
			
		||||
 | 
			
		||||
@ -37,6 +41,19 @@ class SourceType:
 | 
			
		||||
        """Get Icon URL for login"""
 | 
			
		||||
        return static(f"authentik/sources/{self.slug}.svg")
 | 
			
		||||
 | 
			
		||||
    # pylint: disable=unused-argument
 | 
			
		||||
    def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge:
 | 
			
		||||
        """Allow types to return custom challenges"""
 | 
			
		||||
        return RedirectChallenge(
 | 
			
		||||
            instance={
 | 
			
		||||
                "type": ChallengeTypes.REDIRECT.value,
 | 
			
		||||
                "to": reverse(
 | 
			
		||||
                    "authentik_sources_oauth:oauth-client-login",
 | 
			
		||||
                    kwargs={"source_slug": source.slug},
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SourceTypeManager:
 | 
			
		||||
    """Manager to hold all Source types."""
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										51
									
								
								authentik/sources/oauth/types/okta.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								authentik/sources/oauth/types/okta.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
"""Okta OAuth Views"""
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from authentik.sources.oauth.models import OAuthSource
 | 
			
		||||
from authentik.sources.oauth.types.azure_ad import AzureADClient
 | 
			
		||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
 | 
			
		||||
from authentik.sources.oauth.views.callback import OAuthCallback
 | 
			
		||||
from authentik.sources.oauth.views.redirect import OAuthRedirect
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OktaOAuthRedirect(OAuthRedirect):
 | 
			
		||||
    """Okta OAuth2 Redirect"""
 | 
			
		||||
 | 
			
		||||
    def get_additional_parameters(self, source: OAuthSource):  # pragma: no cover
 | 
			
		||||
        return {
 | 
			
		||||
            "scope": "openid email profile",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OktaOAuth2Callback(OAuthCallback):
 | 
			
		||||
    """Okta OAuth2 Callback"""
 | 
			
		||||
 | 
			
		||||
    # Okta has the same quirk as azure and throws an error if the access token
 | 
			
		||||
    # is set via query parameter, so we re-use the azure client
 | 
			
		||||
    # see https://github.com/goauthentik/authentik/issues/1910
 | 
			
		||||
    client_class = AzureADClient
 | 
			
		||||
 | 
			
		||||
    def get_user_id(self, info: dict[str, str]) -> str:
 | 
			
		||||
        return info.get("sub", "")
 | 
			
		||||
 | 
			
		||||
    def get_user_enroll_context(
 | 
			
		||||
        self,
 | 
			
		||||
        info: dict[str, Any],
 | 
			
		||||
    ) -> dict[str, Any]:
 | 
			
		||||
        return {
 | 
			
		||||
            "username": info.get("nickname"),
 | 
			
		||||
            "email": info.get("email"),
 | 
			
		||||
            "name": info.get("name"),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@MANAGER.type()
 | 
			
		||||
class OktaType(SourceType):
 | 
			
		||||
    """Okta Type definition"""
 | 
			
		||||
 | 
			
		||||
    callback_view = OktaOAuth2Callback
 | 
			
		||||
    redirect_view = OktaOAuthRedirect
 | 
			
		||||
    name = "Okta"
 | 
			
		||||
    slug = "okta"
 | 
			
		||||
 | 
			
		||||
    urls_customizable = True
 | 
			
		||||
@ -3,6 +3,7 @@ from typing import Optional
 | 
			
		||||
 | 
			
		||||
from django.contrib.postgres.fields import ArrayField
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.http.request import HttpRequest
 | 
			
		||||
from django.templatetags.static import static
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework.fields import CharField
 | 
			
		||||
@ -62,8 +63,7 @@ class PlexSource(Source):
 | 
			
		||||
 | 
			
		||||
        return PlexSourceSerializer
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_login_button(self) -> UILoginButton:
 | 
			
		||||
    def ui_login_button(self, request: HttpRequest) -> UILoginButton:
 | 
			
		||||
        return UILoginButton(
 | 
			
		||||
            challenge=PlexAuthenticationChallenge(
 | 
			
		||||
                {
 | 
			
		||||
@ -77,7 +77,6 @@ class PlexSource(Source):
 | 
			
		||||
            name=self.name,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        return UserSettingSerializer(
 | 
			
		||||
            data={
 | 
			
		||||
 | 
			
		||||
@ -167,8 +167,7 @@ class SAMLSource(Source):
 | 
			
		||||
            reverse(f"authentik_sources_saml:{view}", kwargs={"source_slug": self.slug})
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_login_button(self) -> UILoginButton:
 | 
			
		||||
    def ui_login_button(self, request: HttpRequest) -> UILoginButton:
 | 
			
		||||
        return UILoginButton(
 | 
			
		||||
            challenge=RedirectChallenge(
 | 
			
		||||
                instance={
 | 
			
		||||
 | 
			
		||||
@ -48,7 +48,6 @@ class AuthenticatorDuoStage(ConfigurableStage, Stage):
 | 
			
		||||
    def component(self) -> str:
 | 
			
		||||
        return "ak-stage-authenticator-duo-form"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        return UserSettingSerializer(
 | 
			
		||||
            data={
 | 
			
		||||
 | 
			
		||||
@ -141,7 +141,6 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage):
 | 
			
		||||
    def component(self) -> str:
 | 
			
		||||
        return "ak-stage-authenticator-sms-form"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        return UserSettingSerializer(
 | 
			
		||||
            data={
 | 
			
		||||
 | 
			
		||||
@ -90,6 +90,5 @@ class AuthenticatorSMSStageTests(APITestCase):
 | 
			
		||||
                    "code": int(self.client.session[SESSION_SMS_DEVICE].token),
 | 
			
		||||
                },
 | 
			
		||||
            )
 | 
			
		||||
            print(response.content)
 | 
			
		||||
            self.assertEqual(response.status_code, 200)
 | 
			
		||||
            sms_send_mock.assert_not_called()
 | 
			
		||||
 | 
			
		||||
@ -31,7 +31,6 @@ class AuthenticatorStaticStage(ConfigurableStage, Stage):
 | 
			
		||||
    def component(self) -> str:
 | 
			
		||||
        return "ak-stage-authenticator-static-form"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        return UserSettingSerializer(
 | 
			
		||||
            data={
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,6 @@ class AuthenticatorTOTPStage(ConfigurableStage, Stage):
 | 
			
		||||
    def component(self) -> str:
 | 
			
		||||
        return "ak-stage-authenticator-totp-form"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        return UserSettingSerializer(
 | 
			
		||||
            data={
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer):
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
        model = AuthenticateWebAuthnStage
 | 
			
		||||
        fields = StageSerializer.Meta.fields + ["configure_flow"]
 | 
			
		||||
        fields = StageSerializer.Meta.fields + ["configure_flow", "user_verification"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
# Generated by Django 4.0 on 2021-12-14 09:05
 | 
			
		||||
 | 
			
		||||
from django.db import migrations, models
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ("authentik_stages_authenticator_webauthn", "0004_auto_20210304_1850"),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name="authenticatewebauthnstage",
 | 
			
		||||
            name="user_verification",
 | 
			
		||||
            field=models.TextField(
 | 
			
		||||
                choices=[
 | 
			
		||||
                    ("required", "Required"),
 | 
			
		||||
                    ("preferred", "Preferred"),
 | 
			
		||||
                    ("discouraged", "Discouraged"),
 | 
			
		||||
                ],
 | 
			
		||||
                default="preferred",
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
@ -15,9 +15,30 @@ from authentik.core.types import UserSettingSerializer
 | 
			
		||||
from authentik.flows.models import ConfigurableStage, Stage
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UserVerification(models.TextChoices):
 | 
			
		||||
    """The degree to which the Relying Party wishes to verify a user's identity.
 | 
			
		||||
 | 
			
		||||
    Members:
 | 
			
		||||
        `REQUIRED`: User verification must occur
 | 
			
		||||
        `PREFERRED`: User verification would be great, but if not that's okay too
 | 
			
		||||
        `DISCOURAGED`: User verification should not occur, but it's okay if it does
 | 
			
		||||
 | 
			
		||||
    https://www.w3.org/TR/webauthn-2/#enumdef-userverificationrequirement
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    REQUIRED = "required"
 | 
			
		||||
    PREFERRED = "preferred"
 | 
			
		||||
    DISCOURAGED = "discouraged"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
 | 
			
		||||
    """WebAuthn stage"""
 | 
			
		||||
 | 
			
		||||
    user_verification = models.TextField(
 | 
			
		||||
        choices=UserVerification.choices,
 | 
			
		||||
        default=UserVerification.PREFERRED,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def serializer(self) -> BaseSerializer:
 | 
			
		||||
        from authentik.stages.authenticator_webauthn.api import AuthenticateWebAuthnStageSerializer
 | 
			
		||||
@ -34,7 +55,6 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
 | 
			
		||||
    def component(self) -> str:
 | 
			
		||||
        return "ak-stage-authenticator-webauthn-form"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        return UserSettingSerializer(
 | 
			
		||||
            data={
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,6 @@ from webauthn.helpers.structs import (
 | 
			
		||||
    PublicKeyCredentialCreationOptions,
 | 
			
		||||
    RegistrationCredential,
 | 
			
		||||
    ResidentKeyRequirement,
 | 
			
		||||
    UserVerificationRequirement,
 | 
			
		||||
)
 | 
			
		||||
from webauthn.registration.verify_registration_response import VerifiedRegistration
 | 
			
		||||
 | 
			
		||||
@ -27,7 +26,7 @@ from authentik.flows.challenge import (
 | 
			
		||||
)
 | 
			
		||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
 | 
			
		||||
from authentik.flows.stage import ChallengeStageView
 | 
			
		||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
 | 
			
		||||
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
 | 
			
		||||
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
 | 
			
		||||
 | 
			
		||||
LOGGER = get_logger()
 | 
			
		||||
@ -83,7 +82,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
 | 
			
		||||
    def get_challenge(self, *args, **kwargs) -> Challenge:
 | 
			
		||||
        # clear session variables prior to starting a new registration
 | 
			
		||||
        self.request.session.pop("challenge", None)
 | 
			
		||||
 | 
			
		||||
        stage: AuthenticateWebAuthnStage = self.executor.current_stage
 | 
			
		||||
        user = self.get_pending_user()
 | 
			
		||||
 | 
			
		||||
        registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
 | 
			
		||||
@ -94,10 +93,9 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
 | 
			
		||||
            user_display_name=user.name,
 | 
			
		||||
            authenticator_selection=AuthenticatorSelectionCriteria(
 | 
			
		||||
                resident_key=ResidentKeyRequirement.PREFERRED,
 | 
			
		||||
                user_verification=UserVerificationRequirement.PREFERRED,
 | 
			
		||||
                user_verification=str(stage.user_verification),
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
        registration_options.user.id = user.uid
 | 
			
		||||
 | 
			
		||||
        self.request.session["challenge"] = registration_options.challenge
 | 
			
		||||
        return AuthenticatorWebAuthnChallenge(
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ def get_template_choices():
 | 
			
		||||
 | 
			
		||||
    dirs = [Path(x) for x in settings.TEMPLATES[0]["DIRS"]]
 | 
			
		||||
    for template_dir in dirs:
 | 
			
		||||
        if not template_dir.exists():
 | 
			
		||||
        if not template_dir.exists() or not template_dir.is_dir():
 | 
			
		||||
            continue
 | 
			
		||||
        for template in template_dir.glob("**/*.html"):
 | 
			
		||||
            path = str(template)
 | 
			
		||||
 | 
			
		||||
@ -29,4 +29,4 @@ class TestEmailStageAPI(APITestCase):
 | 
			
		||||
            EmailTemplates.ACCOUNT_CONFIRM,
 | 
			
		||||
        )
 | 
			
		||||
        with self.assertRaises(ValidationError):
 | 
			
		||||
            print(EmailStageSerializer().validate_template("foobar"))
 | 
			
		||||
            EmailStageSerializer().validate_template("foobar")
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,7 @@ from authentik.stages.email.models import get_template_choices
 | 
			
		||||
def get_templates_setting(temp_dir: str) -> dict[str, Any]:
 | 
			
		||||
    """Patch settings TEMPLATE's dir property"""
 | 
			
		||||
    templates_setting = settings.TEMPLATES
 | 
			
		||||
    templates_setting[0]["DIRS"] = [temp_dir]
 | 
			
		||||
    templates_setting[0]["DIRS"] = [temp_dir, "foo"]
 | 
			
		||||
    return templates_setting
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@ from django.utils.translation import gettext as _
 | 
			
		||||
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema_field
 | 
			
		||||
from rest_framework.fields import BooleanField, CharField, DictField, ListField
 | 
			
		||||
from rest_framework.serializers import ValidationError
 | 
			
		||||
from sentry_sdk.hub import Hub
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.core.api.utils import PassiveSerializer
 | 
			
		||||
@ -25,6 +26,7 @@ from authentik.flows.challenge import (
 | 
			
		||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
 | 
			
		||||
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
 | 
			
		||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
 | 
			
		||||
from authentik.sources.oauth.types.apple import AppleLoginChallenge
 | 
			
		||||
from authentik.sources.plex.models import PlexAuthenticationChallenge
 | 
			
		||||
from authentik.stages.identification.models import IdentificationStage
 | 
			
		||||
from authentik.stages.identification.signals import identification_failed
 | 
			
		||||
@ -39,6 +41,7 @@ LOGGER = get_logger()
 | 
			
		||||
        serializers={
 | 
			
		||||
            RedirectChallenge().fields["component"].default: RedirectChallenge,
 | 
			
		||||
            PlexAuthenticationChallenge().fields["component"].default: PlexAuthenticationChallenge,
 | 
			
		||||
            AppleLoginChallenge().fields["component"].default: AppleLoginChallenge,
 | 
			
		||||
        },
 | 
			
		||||
        resource_type_field_name="component",
 | 
			
		||||
    )
 | 
			
		||||
@ -88,8 +91,12 @@ class IdentificationChallengeResponse(ChallengeResponse):
 | 
			
		||||
 | 
			
		||||
        pre_user = self.stage.get_user(uid_field)
 | 
			
		||||
        if not pre_user:
 | 
			
		||||
            with Hub.current.start_span(
 | 
			
		||||
                op="authentik.stages.identification.validate_invalid_wait",
 | 
			
		||||
                description="Sleep random time on invalid user identifier",
 | 
			
		||||
            ):
 | 
			
		||||
                # Sleep a random time (between 90 and 210ms) to "prevent" user enumeration attacks
 | 
			
		||||
            sleep(0.30 * SystemRandom().randint(3, 7))
 | 
			
		||||
                sleep(0.030 * SystemRandom().randint(3, 7))
 | 
			
		||||
            LOGGER.debug("invalid_login", identifier=uid_field)
 | 
			
		||||
            identification_failed.send(sender=self, request=self.stage.request, uid_field=uid_field)
 | 
			
		||||
            # We set the pending_user even on failure so it's part of the context, even
 | 
			
		||||
@ -112,6 +119,10 @@ class IdentificationChallengeResponse(ChallengeResponse):
 | 
			
		||||
        if not password:
 | 
			
		||||
            LOGGER.warning("Password not set for ident+auth attempt")
 | 
			
		||||
        try:
 | 
			
		||||
            with Hub.current.start_span(
 | 
			
		||||
                op="authentik.stages.identification.authenticate",
 | 
			
		||||
                description="User authenticate call (combo stage)",
 | 
			
		||||
            ):
 | 
			
		||||
                user = authenticate(
 | 
			
		||||
                    self.stage.request,
 | 
			
		||||
                    current_stage.password_stage.backends,
 | 
			
		||||
@ -191,7 +202,7 @@ class IdentificationStageView(ChallengeStageView):
 | 
			
		||||
            current_stage.sources.filter(enabled=True).order_by("name").select_subclasses()
 | 
			
		||||
        )
 | 
			
		||||
        for source in sources:
 | 
			
		||||
            ui_login_button = source.ui_login_button
 | 
			
		||||
            ui_login_button = source.ui_login_button(self.request)
 | 
			
		||||
            if ui_login_button:
 | 
			
		||||
                button = asdict(ui_login_button)
 | 
			
		||||
                button["challenge"] = ui_login_button.challenge.data
 | 
			
		||||
 | 
			
		||||
@ -5,8 +5,8 @@ from rest_framework.fields import JSONField
 | 
			
		||||
from rest_framework.serializers import ModelSerializer
 | 
			
		||||
from rest_framework.viewsets import ModelViewSet
 | 
			
		||||
 | 
			
		||||
from authentik.core.api.groups import GroupMemberSerializer
 | 
			
		||||
from authentik.core.api.used_by import UsedByMixin
 | 
			
		||||
from authentik.core.api.users import UserSerializer
 | 
			
		||||
from authentik.core.api.utils import is_dict
 | 
			
		||||
from authentik.flows.api.stages import StageSerializer
 | 
			
		||||
from authentik.stages.invitation.models import Invitation, InvitationStage
 | 
			
		||||
@ -46,7 +46,7 @@ class InvitationStageViewSet(UsedByMixin, ModelViewSet):
 | 
			
		||||
class InvitationSerializer(ModelSerializer):
 | 
			
		||||
    """Invitation Serializer"""
 | 
			
		||||
 | 
			
		||||
    created_by = UserSerializer(read_only=True)
 | 
			
		||||
    created_by = GroupMemberSerializer(read_only=True)
 | 
			
		||||
    fixed_data = JSONField(validators=[is_dict], required=False)
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
 | 
			
		||||
@ -63,7 +63,6 @@ class PasswordStage(ConfigurableStage, Stage):
 | 
			
		||||
    def component(self) -> str:
 | 
			
		||||
        return "ak-stage-password-form"
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ui_user_settings(self) -> Optional[UserSettingSerializer]:
 | 
			
		||||
        if not self.configure_flow:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ from django.urls import reverse
 | 
			
		||||
from django.utils.translation import gettext as _
 | 
			
		||||
from rest_framework.exceptions import ErrorDetail, ValidationError
 | 
			
		||||
from rest_framework.fields import CharField
 | 
			
		||||
from sentry_sdk.hub import Hub
 | 
			
		||||
from structlog.stdlib import get_logger
 | 
			
		||||
 | 
			
		||||
from authentik.core.models import User
 | 
			
		||||
@ -43,6 +44,10 @@ def authenticate(request: HttpRequest, backends: list[str], **credentials: Any)
 | 
			
		||||
            LOGGER.warning("Failed to import backend", path=backend_path)
 | 
			
		||||
            continue
 | 
			
		||||
        LOGGER.debug("Attempting authentication...", backend=backend_path)
 | 
			
		||||
        with Hub.current.start_span(
 | 
			
		||||
            op="authentik.stages.password.authenticate",
 | 
			
		||||
            description=backend_path,
 | 
			
		||||
        ):
 | 
			
		||||
            user = backend.authenticate(request, **credentials)
 | 
			
		||||
        if user is None:
 | 
			
		||||
            LOGGER.debug("Backend returned nothing, continuing", backend=backend_path)
 | 
			
		||||
@ -120,7 +125,13 @@ class PasswordStageView(ChallengeStageView):
 | 
			
		||||
            "username": pending_user.username,
 | 
			
		||||
        }
 | 
			
		||||
        try:
 | 
			
		||||
            user = authenticate(self.request, self.executor.current_stage.backends, **auth_kwargs)
 | 
			
		||||
            with Hub.current.start_span(
 | 
			
		||||
                op="authentik.stages.password.authenticate",
 | 
			
		||||
                description="User authenticate call",
 | 
			
		||||
            ):
 | 
			
		||||
                user = authenticate(
 | 
			
		||||
                    self.request, self.executor.current_stage.backends, **auth_kwargs
 | 
			
		||||
                )
 | 
			
		||||
        except PermissionDenied:
 | 
			
		||||
            del auth_kwargs["password"]
 | 
			
		||||
            # User was found, but permission was denied (i.e. user is not active)
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ from typing import Any
 | 
			
		||||
from django.db.models import F, Q
 | 
			
		||||
from django.db.models import Value as V
 | 
			
		||||
from django.http.request import HttpRequest
 | 
			
		||||
from sentry_sdk.hub import Hub
 | 
			
		||||
 | 
			
		||||
from authentik.lib.config import CONFIG
 | 
			
		||||
from authentik.tenants.models import Tenant
 | 
			
		||||
@ -28,7 +29,12 @@ def get_tenant_for_request(request: HttpRequest) -> Tenant:
 | 
			
		||||
def context_processor(request: HttpRequest) -> dict[str, Any]:
 | 
			
		||||
    """Context Processor that injects tenant object into every template"""
 | 
			
		||||
    tenant = getattr(request, "tenant", DEFAULT_TENANT)
 | 
			
		||||
    trace = ""
 | 
			
		||||
    span = Hub.current.scope.span
 | 
			
		||||
    if span:
 | 
			
		||||
        trace = span.to_traceparent()
 | 
			
		||||
    return {
 | 
			
		||||
        "tenant": tenant,
 | 
			
		||||
        "footer_links": CONFIG.y("footer_links"),
 | 
			
		||||
        "sentry_trace": trace,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,12 @@ Required environment variables:
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	log.SetLevel(log.DebugLevel)
 | 
			
		||||
	log.SetFormatter(&log.JSONFormatter{
 | 
			
		||||
		FieldMap: log.FieldMap{
 | 
			
		||||
			log.FieldKeyMsg:  "event",
 | 
			
		||||
			log.FieldKeyTime: "timestamp",
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	akURL, found := os.LookupEnv("AUTHENTIK_HOST")
 | 
			
		||||
	if !found {
 | 
			
		||||
		fmt.Println("env AUTHENTIK_HOST not set!")
 | 
			
		||||
 | 
			
		||||
@ -26,6 +26,12 @@ Optionally, you can set these:
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	log.SetLevel(log.DebugLevel)
 | 
			
		||||
	log.SetFormatter(&log.JSONFormatter{
 | 
			
		||||
		FieldMap: log.FieldMap{
 | 
			
		||||
			log.FieldKeyMsg:  "event",
 | 
			
		||||
			log.FieldKeyTime: "timestamp",
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	akURL, found := os.LookupEnv("AUTHENTIK_HOST")
 | 
			
		||||
	if !found {
 | 
			
		||||
		fmt.Println("env AUTHENTIK_HOST not set!")
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
@ -20,7 +21,12 @@ var running = true
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	log.SetLevel(log.DebugLevel)
 | 
			
		||||
	log.SetFormatter(&log.JSONFormatter{})
 | 
			
		||||
	log.SetFormatter(&log.JSONFormatter{
 | 
			
		||||
		FieldMap: log.FieldMap{
 | 
			
		||||
			log.FieldKeyMsg:  "event",
 | 
			
		||||
			log.FieldKeyTime: "timestamp",
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	l := log.WithField("logger", "authentik.root")
 | 
			
		||||
	config.DefaultConfig()
 | 
			
		||||
	err := config.LoadConfig("./authentik/lib/default.yml")
 | 
			
		||||
@ -41,9 +47,12 @@ func main() {
 | 
			
		||||
		err := sentry.Init(sentry.ClientOptions{
 | 
			
		||||
			Dsn:              config.G.ErrorReporting.DSN,
 | 
			
		||||
			AttachStacktrace: true,
 | 
			
		||||
			TracesSampleRate: 0.6,
 | 
			
		||||
			TracesSampleRate: config.G.ErrorReporting.SampleRate,
 | 
			
		||||
			Release:          fmt.Sprintf("authentik@%s", constants.VERSION),
 | 
			
		||||
			Environment:      config.G.ErrorReporting.Environment,
 | 
			
		||||
			IgnoreErrors: []string{
 | 
			
		||||
				http.ErrAbortHandler.Error(),
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			l.WithError(err).Warning("failed to init sentry")
 | 
			
		||||
@ -69,9 +78,9 @@ func main() {
 | 
			
		||||
 | 
			
		||||
		<-ex
 | 
			
		||||
		running = false
 | 
			
		||||
		l.WithField("logger", "authentik").Info("shutting down gunicorn")
 | 
			
		||||
		l.Info("shutting down gunicorn")
 | 
			
		||||
		go g.Kill()
 | 
			
		||||
		l.WithField("logger", "authentik").Info("shutting down webserver")
 | 
			
		||||
		l.Info("shutting down webserver")
 | 
			
		||||
		go ws.Shutdown()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -89,8 +98,9 @@ func attemptStartBackend(g *gounicorn.GoUnicorn) {
 | 
			
		||||
func attemptProxyStart(ws *web.WebServer, u *url.URL) {
 | 
			
		||||
	maxTries := 100
 | 
			
		||||
	attempt := 0
 | 
			
		||||
	l := log.WithField("logger", "authentik.server")
 | 
			
		||||
	for {
 | 
			
		||||
		log.WithField("logger", "authentik").Debug("attempting to init outpost")
 | 
			
		||||
		l.Debug("attempting to init outpost")
 | 
			
		||||
		ac := ak.NewAPIController(*u, config.G.SecretKey)
 | 
			
		||||
		if ac == nil {
 | 
			
		||||
			attempt += 1
 | 
			
		||||
@ -103,10 +113,10 @@ func attemptProxyStart(ws *web.WebServer, u *url.URL) {
 | 
			
		||||
		srv := proxyv2.NewProxyServer(ac, 0)
 | 
			
		||||
		ws.ProxyServer = srv
 | 
			
		||||
		ac.Server = srv
 | 
			
		||||
		log.WithField("logger", "authentik").Debug("attempting to start outpost")
 | 
			
		||||
		l.Debug("attempting to start outpost")
 | 
			
		||||
		err := ac.StartBackgorundTasks()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.WithField("logger", "authentik").WithError(err).Warning("outpost failed to start")
 | 
			
		||||
			l.WithError(err).Warning("outpost failed to start")
 | 
			
		||||
			attempt += 1
 | 
			
		||||
			time.Sleep(15 * time.Second)
 | 
			
		||||
			if attempt > maxTries {
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ services:
 | 
			
		||||
    image: redis:alpine
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
  server:
 | 
			
		||||
    image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.12.1-rc4}
 | 
			
		||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.12.3}
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    command: server
 | 
			
		||||
    environment:
 | 
			
		||||
@ -38,7 +38,7 @@ services:
 | 
			
		||||
      - "0.0.0.0:9000:9000"
 | 
			
		||||
      - "0.0.0.0:9443:9443"
 | 
			
		||||
  worker:
 | 
			
		||||
    image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.12.1-rc4}
 | 
			
		||||
    image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.12.3}
 | 
			
		||||
    restart: unless-stopped
 | 
			
		||||
    command: worker
 | 
			
		||||
    environment:
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@ -28,7 +28,7 @@ require (
 | 
			
		||||
	github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
 | 
			
		||||
	github.com/prometheus/client_golang v1.11.0
 | 
			
		||||
	github.com/sirupsen/logrus v1.8.1
 | 
			
		||||
	goauthentik.io/api v0.2021104.11
 | 
			
		||||
	goauthentik.io/api v0.2021122.2
 | 
			
		||||
	golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
 | 
			
		||||
	golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
 | 
			
		||||
	golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							@ -558,8 +558,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.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 | 
			
		||||
goauthentik.io/api v0.2021104.11 h1:LqT0LM0e/RRrxPuo6Xl5uz3PCR5ytuE+YlNlfW9w0yU=
 | 
			
		||||
goauthentik.io/api v0.2021104.11/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
 | 
			
		||||
goauthentik.io/api v0.2021122.2 h1:3kvyBS7F+uxJ38qrUoWB0Rpidmnw/MHei1NNQ34daAU=
 | 
			
		||||
goauthentik.io/api v0.2021122.2/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-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,7 @@ package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	env "github.com/Netflix/go-env"
 | 
			
		||||
	"github.com/imdario/mergo"
 | 
			
		||||
@ -26,6 +27,7 @@ func DefaultConfig() {
 | 
			
		||||
		ErrorReporting: ErrorReportingConfig{
 | 
			
		||||
			Enabled:    false,
 | 
			
		||||
			DSN:        "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
 | 
			
		||||
			SampleRate: 1,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -61,7 +63,7 @@ func FromEnv() error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ConfigureLogger() {
 | 
			
		||||
	switch G.LogLevel {
 | 
			
		||||
	switch strings.ToLower(G.LogLevel) {
 | 
			
		||||
	case "trace":
 | 
			
		||||
		log.SetLevel(log.TraceLevel)
 | 
			
		||||
	case "debug":
 | 
			
		||||
@ -76,14 +78,14 @@ func ConfigureLogger() {
 | 
			
		||||
		log.SetLevel(log.DebugLevel)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if G.Debug {
 | 
			
		||||
		log.SetFormatter(&log.TextFormatter{})
 | 
			
		||||
	} else {
 | 
			
		||||
		log.SetFormatter(&log.JSONFormatter{
 | 
			
		||||
			FieldMap: log.FieldMap{
 | 
			
		||||
	fm := log.FieldMap{
 | 
			
		||||
		log.FieldKeyMsg:  "event",
 | 
			
		||||
		log.FieldKeyTime: "timestamp",
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if G.Debug {
 | 
			
		||||
		log.SetFormatter(&log.TextFormatter{FieldMap: fm})
 | 
			
		||||
	} else {
 | 
			
		||||
		log.SetFormatter(&log.JSONFormatter{FieldMap: fm})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user