Compare commits

..

3 Commits

Author SHA1 Message Date
18778ce0d9 release: 2021.4.6 2021-05-12 14:13:16 +02:00
14973fb595 ci: run apt update before installing dependencies
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-12 13:44:15 +02:00
9171bd6d6f stages/invitation: fix wrong serializer used for user model
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-05-12 13:36:19 +02:00
301 changed files with 12280 additions and 32613 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2021.5.1-rc2 current_version = 2021.4.6
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
@ -19,14 +19,20 @@ values =
[bumpversion:file:website/docs/installation/docker-compose.md] [bumpversion:file:website/docs/installation/docker-compose.md]
[bumpversion:file:website/docs/installation/kubernetes.md]
[bumpversion:file:docker-compose.yml] [bumpversion:file:docker-compose.yml]
[bumpversion:file:helm/values.yaml]
[bumpversion:file:helm/README.md]
[bumpversion:file:helm/Chart.yaml]
[bumpversion:file:.github/workflows/release.yml] [bumpversion:file:.github/workflows/release.yml]
[bumpversion:file:authentik/__init__.py] [bumpversion:file:authentik/__init__.py]
[bumpversion:file:internal/constants/constants.go]
[bumpversion:file:outpost/pkg/version.go] [bumpversion:file:outpost/pkg/version.go]
[bumpversion:file:web/src/constants.ts] [bumpversion:file:web/src/constants.ts]

View File

@ -10,28 +10,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker Login Registry - name: Docker Login Registry
uses: docker/login-action@v1 env:
with: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: prepare ts api client
run: |
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
- name: Building Docker Image - name: Building Docker Image
uses: docker/build-push-action@v2 run: docker build
with: --no-cache
push: true -t beryju/authentik:2021.4.6
tags: | -t beryju/authentik:latest
beryju/authentik:2021.5.1-rc2, -f Dockerfile .
beryju/authentik:latest, - name: Push Docker Container to Registry (versioned)
ghcr.io/goauthentik/server:2021.5.1-rc2, run: docker push beryju/authentik:2021.4.6
ghcr.io/goauthentik/server:latest - name: Push Docker Container to Registry (latest)
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8 run: docker push beryju/authentik:latest
build-proxy: build-proxy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -44,67 +37,53 @@ jobs:
cd outpost cd outpost
go get -u github.com/go-swagger/go-swagger/cmd/swagger go get -u github.com/go-swagger/go-swagger/cmd/swagger
swagger generate client -f ../swagger.yaml -A authentik -t pkg/ swagger generate client -f ../swagger.yaml -A authentik -t pkg/
go build -v ./cmd/proxy/server.go go build -v .
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker Login Registry - name: Docker Login Registry
uses: docker/login-action@v1 env:
with: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image - name: Building Docker Image
uses: docker/build-push-action@v2 run: |
with: cd outpost/
push: true docker build \
tags: | --no-cache \
beryju/authentik-proxy:2021.5.1-rc2, -t beryju/authentik-proxy:2021.4.6 \
beryju/authentik-proxy:latest, -t beryju/authentik-proxy:latest \
ghcr.io/goauthentik/proxy:2021.5.1-rc2, -f proxy.Dockerfile .
ghcr.io/goauthentik/proxy:latest - name: Push Docker Container to Registry (versioned)
context: outpost/ run: docker push beryju/authentik-proxy:2021.4.6
file: outpost/proxy.Dockerfile - name: Push Docker Container to Registry (latest)
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8 run: docker push beryju/authentik-proxy:latest
build-ldap: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-go@v2 - name: prepare ts api client
with:
go-version: "^1.15"
- name: prepare go api client
run: | run: |
cd outpost docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
go get -u github.com/go-swagger/go-swagger/cmd/swagger
swagger generate client -f ../swagger.yaml -A authentik -t pkg/
go build -v ./cmd/ldap/server.go
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker Login Registry - name: Docker Login Registry
uses: docker/login-action@v1 env:
with: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
username: ${{ secrets.DOCKER_USERNAME }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image - name: Building Docker Image
uses: docker/build-push-action@v2 run: |
with: cd web/
push: true docker build \
tags: | --no-cache \
beryju/authentik-ldap:2021.5.1-rc2, -t beryju/authentik-static:2021.4.6 \
beryju/authentik-ldap:latest, -t beryju/authentik-static:latest \
ghcr.io/goauthentik/ldap:2021.5.1-rc2, -f Dockerfile .
ghcr.io/goauthentik/ldap:latest - name: Push Docker Container to Registry (versioned)
context: outpost/ run: docker push beryju/authentik-static:2021.4.6
file: outpost/ldap.Dockerfile - name: Push Docker Container to Registry (latest)
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8 run: docker push beryju/authentik-static:latest
test-release: test-release:
needs: needs:
- build-server - build-server
- build-static
- build-proxy - build-proxy
- build-ldap
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
@ -124,12 +103,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Create a Sentry.io release - name: Create a Sentry.io release
uses: getsentry/action-release@v1 uses: tclindner/sentry-releases-action@v1.2.0
env: env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: beryjuorg SENTRY_ORG: beryjuorg
SENTRY_PROJECT: authentik SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org SENTRY_URL: https://sentry.beryju.org
with: with:
version: authentik@2021.5.1-rc2 tagName: 2021.4.6
environment: beryjuorg-prod environment: beryjuorg-prod

View File

@ -11,9 +11,6 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: prepare ts api client
run: |
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
- name: Pre-release test - name: Pre-release test
run: | run: |
sudo apt-get install -y pwgen sudo apt-get install -y pwgen
@ -28,6 +25,15 @@ jobs:
docker-compose up --no-start docker-compose up --no-start
docker-compose start postgresql redis docker-compose start postgresql redis
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik" docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
- name: Install Helm
run: |
apt update && apt install -y curl
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
- name: Helm package
run: |
helm dependency update helm/
helm package helm/
mv authentik-*.tgz authentik-chart.tgz
- name: Extract version number - name: Extract version number
id: get_version id: get_version
uses: actions/github-script@0.2.0 uses: actions/github-script@0.2.0
@ -45,3 +51,13 @@ jobs:
release_name: Release ${{ steps.get_version.outputs.result }} release_name: Release ${{ steps.get_version.outputs.result }}
draft: true draft: true
prerelease: false prerelease: false
- name: Upload packaged Helm Chart
id: upload-release-asset
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./authentik-chart.tgz
asset_name: authentik-chart.tgz
asset_content_type: application/gzip

2
.gitignore vendored
View File

@ -202,5 +202,3 @@ selenium_screenshots/
backups/ backups/
media/ media/
*mmdb *mmdb
.idea/

View File

@ -1,4 +1,3 @@
# Stage 1: Lock python dependencies
FROM python:3.9-slim-buster as locker FROM python:3.9-slim-buster as locker
COPY ./Pipfile /app/ COPY ./Pipfile /app/
@ -10,34 +9,6 @@ RUN pip install pipenv && \
pipenv lock -r > requirements.txt && \ pipenv lock -r > requirements.txt && \
pipenv lock -rd > requirements-dev.txt pipenv lock -rd > requirements-dev.txt
# Stage 2: Build webui
FROM node as npm-builder
COPY ./web /static/
ENV NODE_ENV=production
RUN cd /static && npm i --production=false && npm run build
# Stage 3: Build go proxy
FROM golang:1.16.4 AS builder
WORKDIR /work
COPY --from=npm-builder /static/robots.txt /work/web/robots.txt
COPY --from=npm-builder /static/security.txt /work/web/security.txt
COPY --from=npm-builder /static/dist/ /work/web/dist/
COPY --from=npm-builder /static/authentik/ /work/web/authentik/
# RUN ls /work/web/static/authentik/ && exit 1
COPY ./cmd /work/cmd
COPY ./web/static.go /work/web/static.go
COPY ./internal /work/internal
COPY ./go.mod /work/go.mod
COPY ./go.sum /work/go.sum
RUN go build -o /work/authentik ./cmd/server/main.go
# Stage 4: Run
FROM python:3.9-slim-buster FROM python:3.9-slim-buster
WORKDIR / WORKDIR /
@ -73,7 +44,6 @@ COPY ./pyproject.toml /
COPY ./xml /xml COPY ./xml /xml
COPY ./manage.py / COPY ./manage.py /
COPY ./lifecycle/ /lifecycle COPY ./lifecycle/ /lifecycle
COPY --from=builder /work/authentik /authentik-proxy
USER authentik USER authentik
STOPSIGNAL SIGINT STOPSIGNAL SIGINT

View File

@ -1,4 +1,4 @@
all: lint-fix lint test gen all: lint-fix lint coverage gen
test-integration: test-integration:
k3d cluster create || exit 0 k3d cluster create || exit 0
@ -8,7 +8,7 @@ test-integration:
test-e2e: test-e2e:
coverage run manage.py test --failfast -v 3 tests/e2e coverage run manage.py test --failfast -v 3 tests/e2e
test: coverage:
coverage run manage.py test -v 3 authentik coverage run manage.py test -v 3 authentik
coverage html coverage html
coverage report coverage report
@ -22,7 +22,7 @@ lint:
bandit -r authentik tests lifecycle -x node_modules bandit -r authentik tests lifecycle -x node_modules
pylint authentik tests lifecycle pylint authentik tests lifecycle
gen: gen: coverage
./manage.py generate_swagger -o swagger.yaml -f yaml ./manage.py generate_swagger -o swagger.yaml -f yaml
local-stack: local-stack:
@ -31,5 +31,7 @@ local-stack:
docker-compose up -d docker-compose up -d
docker-compose run --rm server migrate docker-compose run --rm server migrate
run: build-static:
go run -v cmd/server/main.go docker-compose -f scripts/ci.docker-compose.yml up -d
docker build -t beryju/authentik-static -f static.Dockerfile --network=scripts_default .
docker-compose -f scripts/ci.docker-compose.yml down -v

View File

@ -32,7 +32,7 @@ lxml = ">=4.6.3"
packaging = "*" packaging = "*"
psycopg2-binary = "*" psycopg2-binary = "*"
pycryptodome = "*" pycryptodome = "*"
pyjwt = "*" pyjwkest = "*"
pyyaml = "*" pyyaml = "*"
requests-oauthlib = "*" requests-oauthlib = "*"
sentry-sdk = "*" sentry-sdk = "*"
@ -59,4 +59,3 @@ pylint-django = "*"
pytest = "*" pytest = "*"
pytest-django = "*" pytest-django = "*"
selenium = "*" selenium = "*"
requests-mock = "*"

282
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "17be2923cf8d281e430ec1467aea723806ac6f7c58fc6553ede92317e43f4d14" "sha256": "a9d504f00ee8820017f26a4fda2938de456cb72b4bc2f8735fc8c6a6c615d46a"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -88,10 +88,10 @@
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:3901be1cb7c2a780f14668691474d9252c070a756be0a9ead98cfeabfa11aeb8", "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:8ee1e5f5a1afc5b19bdfae4fdf0c35ed324074bdce3500c939842c8f818645d9" "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
], ],
"version": "==21.1.0" "version": "==20.3.0"
}, },
"autobahn": { "autobahn": {
"hashes": [ "hashes": [
@ -116,25 +116,25 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:56f1766f1271b6b4e979c7b56225377f8912050e5935adc5c1c9e3a0338b949e", "sha256:1e55df93aa47a84e2a12a639c7f145e16e6e9ef959542d69d5526d50d2e92692",
"sha256:c61c809d288e88b9a0d926f56f803d0128b498aa9b45a42a6e03cd9a83e5c124" "sha256:eab42daaaf68cdad5b112d31dcb0684162098f6558ba7b64156be44f993525fa"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.17.68" "version": "==1.17.54"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:0f693f5ad6348ec1a62b3a66fee2840d3b722d66b44896022d644275ff8b143d", "sha256:20a864fc6570ba11d52532c72c3ccabab5c71a9b4a9418601a313d56f1d2ce5b",
"sha256:eb3544911cb0316a33b328a27d137130af278a9c0006be0c95e5e402b01d9865" "sha256:37ec76ea2df8609540ba6cb0fe360ae1c589d2e1ee91eb642fd767823f3fcedd"
], ],
"version": "==1.20.68" "version": "==1.20.54"
}, },
"cachetools": { "cachetools": {
"hashes": [ "hashes": [
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", "sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2",
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" "sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9"
], ],
"version": "==4.2.2" "version": "==4.2.1"
}, },
"cbor2": { "cbor2": {
"hashes": [ "hashes": [
@ -312,11 +312,11 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:0a1d195ad65c52bf275b8277b3d49680bd1137a5f55039a806f25f6b9752ce3d", "sha256:0604e84c4fb698a5e53e5857b5aea945b2f19a18f25f10b8748dbdf935788927",
"sha256:18dd3145ddbd04bf189ff79b9954d08fda5171ea7b57bf705789fea766a07d50" "sha256:21f0f9643722675976004eb683c55d33c05486f94506672df3d6a141546f389d"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.2.2" "version": "==3.2"
}, },
"django-dbbackup": { "django-dbbackup": {
"hashes": [ "hashes": [
@ -351,11 +351,11 @@
}, },
"django-otp": { "django-otp": {
"hashes": [ "hashes": [
"sha256:04852c5301befb02d1d8ba4a31d375eb08d7c2cb6fe86b5f840867435ab1309c", "sha256:381a15e65293b8b06d47b7d6b306e0b7af2e104137ac92f6c566d3b9b90b6244",
"sha256:3916fc7652c2f934b1cf3807dd8ed257ce7605c10dfefa27fadda5628d9a9c9e" "sha256:f4ab096b424c33ffe69453620356e1b7517f30dfb9ba13bfeaa1d1f20faddc13"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.0.4" "version": "==1.0.3"
}, },
"django-prometheus": { "django-prometheus": {
"hashes": [ "hashes": [
@ -437,14 +437,13 @@
}, },
"google-auth": { "google-auth": {
"hashes": [ "hashes": [
"sha256:588bdb03a41ecb4978472b847881e5518b5d9ec6153d3d679aa127a55e13b39f", "sha256:010f011c4e27d3d5eb01106fba6aac39d164842dfcd8709955c4638f5b11ccf8",
"sha256:9ad25fba07f46a628ad4d0ca09f38dcb262830df2ac95b217f9b0129c9e42206" "sha256:f30a672a64d91cc2e3137765d088c5deec26416246f7a9e956eaf69a8d7ed49c"
], ],
"version": "==1.30.0" "version": "==1.29.0"
}, },
"gunicorn": { "gunicorn": {
"hashes": [ "hashes": [
"sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
"sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
], ],
"index": "pypi", "index": "pypi",
@ -505,23 +504,20 @@
}, },
"httptools": { "httptools": {
"hashes": [ "hashes": [
"sha256:07659649fe6b3948b6490825f89abe5eb1cec79ebfaaa0b4bf30f3f33f3c2ba8", "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
"sha256:08b79e09114e6ab5c3dbf560bba2cb2257ea38cdaeaf99b7cb80d8f92622fcd9", "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
"sha256:1e35aa179b67086cc600a984924a88589b90793c9c1b260152ca4908786e09df", "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
"sha256:31629e1f1b89959f8c0927bad12184dc07977dcf71e24f4772934aa490aa199b", "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
"sha256:851026bd63ec0af7e7592890d97d15c92b62d9e17094353f19a52c8e2b33710a", "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
"sha256:8fcca4b7efe353b13a24017211334c57d055a6e132c7adffed13a10d28efca57", "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
"sha256:9abd788465aa46a0f288bd3a99e53edd184177d6379e2098fd6097bb359ad9d6", "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
"sha256:aebdf0bd7bf7c90ae6b3be458692bf6e9e5b610b501f9f74c7979015a51db4c4", "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
"sha256:bda99a5723e7eab355ce57435c70853fc137a65aebf2f1cd4d15d96e2956da7b", "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
"sha256:c1c63d860749841024951b0a78e4dec6f543d23751ef061d6ab60064c7b8b524", "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
"sha256:c4111a0a8a00eff1e495d43ea5230aaf64968a48ddba8ea2d5f982efae827404", "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
"sha256:dce59ee45dd6ee6c434346a5ac527c44014326f560866b4b2f414a692ee1aca8", "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
"sha256:f759717ca1b2ef498c67ba4169c2b33eecf943a89f5329abcff8b89d153eb500",
"sha256:fb7199b8fb0c50a22e77260bb59017e0c075fa80cb03bb2c8692de76e7bb7fe7",
"sha256:fbf7ecd31c39728f251b1c095fd27c84e4d21f60a1d079a0333472ff3ae59d34"
], ],
"version": "==0.1.2" "version": "==0.1.1"
}, },
"hyperlink": { "hyperlink": {
"hashes": [ "hashes": [
@ -607,24 +603,18 @@
"sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d", "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d",
"sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3", "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3",
"sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2", "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2",
"sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae",
"sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f", "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f",
"sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927", "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927",
"sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3", "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3",
"sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7", "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7",
"sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59",
"sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f", "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f",
"sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade", "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade",
"sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96",
"sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468", "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468",
"sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b", "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b",
"sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4", "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4",
"sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354",
"sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83", "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83",
"sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04", "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04",
"sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16",
"sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791", "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791",
"sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a",
"sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51", "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51",
"sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1", "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1",
"sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a", "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a",
@ -637,14 +627,10 @@
"sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa", "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa",
"sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106", "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106",
"sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d", "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d",
"sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617",
"sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4", "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4",
"sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92",
"sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0", "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0",
"sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4", "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4",
"sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24",
"sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2", "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2",
"sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e",
"sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0", "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0",
"sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654", "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654",
"sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2", "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2",
@ -919,6 +905,41 @@
"index": "pypi", "index": "pypi",
"version": "==3.10.1" "version": "==3.10.1"
}, },
"pycryptodomex": {
"hashes": [
"sha256:00a584ee52bf5e27d540129ca9bf7c4a7e7447f24ff4a220faa1304ad0c09bcd",
"sha256:04265a7a84ae002001249bd1de2823bcf46832bd4b58f6965567cb8a07cf4f00",
"sha256:0bd35af6a18b724c689e56f2dbbdd8e409288be71952d271ba3d9614b31d188c",
"sha256:20c45a30f3389148f94edb77f3b216c677a277942f62a2b81a1cc0b6b2dde7fc",
"sha256:2959304d1ce31ab303d9fb5db2b294814278b35154d9b30bf7facc52d6088d0a",
"sha256:36dab7f506948056ceba2d57c1ade74e898401960de697cefc02f3519bd26c1b",
"sha256:37ec1b407ec032c7a0c1fdd2da12813f560bad38ae61ad9c7ce3c0573b3e5e30",
"sha256:3b8eb85b3cc7f083d87978c264d10ff9de3b4bfc46f1c6fdc2792e7d7ebc87bb",
"sha256:3dfce70c4e425607ae87b8eae67c9c7dbba59a33b62d70f79417aef0bc5c735b",
"sha256:418f51c61eab52d9920f4ef468d22c89dab1be5ac796f71cf3802f6a6e667df0",
"sha256:4195604f75cdc1db9bccdb9e44d783add3c817319c30aaff011670c9ed167690",
"sha256:4344ab16faf6c2d9df2b6772995623698fb2d5f114dace4ab2ff335550cf71d5",
"sha256:541cd3e3e252fb19a7b48f420b798b53483302b7fe4d9954c947605d0a263d62",
"sha256:564063e3782474c92cbb333effd06e6eb718471783c6e67f28c63f0fc3ac7b23",
"sha256:72f44b5be46faef2a1bf2a85902511b31f4dd7b01ce0c3978e92edb2cc812a82",
"sha256:8a98e02cbf8f624add45deff444539bf26345b479fc04fa0937b23cd84078d91",
"sha256:940db96449d7b2ebb2c7bf190be1514f3d67914bd37e54e8d30a182bd375a1a9",
"sha256:961333e7ee896651f02d4692242aa36b787b8e8e0baa2256717b2b9d55ae0a3c",
"sha256:9f713ffb4e27b5575bd917c70bbc3f7b348241a351015dbbc514c01b7061ff7e",
"sha256:a6584ae58001d17bb4dc0faa8a426919c2c028ef4d90ceb4191802ca6edb8204",
"sha256:c2b680987f418858e89dbb4f09c8c919ece62811780a27051ace72b2f69fb1be",
"sha256:d8fae5ba3d34c868ae43614e0bd6fb61114b2687ac3255798791ce075d95aece",
"sha256:dbd2c361db939a4252589baa94da4404d45e3fc70da1a31e541644cdf354336e",
"sha256:e090a8609e2095aa86978559b140cf8968af99ee54b8791b29ff804838f29f10",
"sha256:e4a1245e7b846e88ba63e7543483bda61b9acbaee61eadbead5a1ce479d94740",
"sha256:ec9901d19cadb80d9235ee41cc58983f18660314a0eb3fc7b11b0522ac3b6c4a",
"sha256:f2abeb4c4ce7584912f4d637b2c57f23720d35dd2892bfeb1b2c84b6fb7a8c88",
"sha256:f3bb267df679f70a9f40f17d62d22fe12e8b75e490f41807e7560de4d3e6bf9f",
"sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34",
"sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38"
],
"version": "==3.10.1"
},
"pyhamcrest": { "pyhamcrest": {
"hashes": [ "hashes": [
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316", "sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
@ -926,13 +947,12 @@
], ],
"version": "==2.0.2" "version": "==2.0.2"
}, },
"pyjwt": { "pyjwkest": {
"hashes": [ "hashes": [
"sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1", "sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222"
"sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.1.0" "version": "==1.4.2"
}, },
"pyopenssl": { "pyopenssl": {
"hashes": [ "hashes": [
@ -963,10 +983,10 @@
}, },
"python-dotenv": { "python-dotenv": {
"hashes": [ "hashes": [
"sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544", "sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a",
"sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f" "sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"
], ],
"version": "==0.17.1" "version": "==0.17.0"
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
@ -1086,18 +1106,18 @@
}, },
"s3transfer": { "s3transfer": {
"hashes": [ "hashes": [
"sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc", "sha256:af1af6384bd7fb8208b06480f9be73d0295d965c4c073a5c95ea5b6661dccc18",
"sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2" "sha256:f3dfd791cad2799403e3c8051810a7ca6ee1d2e630e5d2a8f9649d892bdb3db6"
], ],
"version": "==0.4.2" "version": "==0.4.0"
}, },
"sentry-sdk": { "sentry-sdk": {
"hashes": [ "hashes": [
"sha256:c1227d38dca315ba35182373f129c3e2722e8ed999e52584e6aca7d287870739", "sha256:71de00c9711926816f750bc0f57ef2abbcb1bfbdf5378c601df7ec978f44857a",
"sha256:c7d380a21281e15be3d9f67a3c4fbb4f800c481d88ff8d8931f39486dd7b4ada" "sha256:9221e985f425913204989d0e0e1cbb719e8b7fa10540f1bc509f660c06a34e66"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.1.0" "version": "==1.0.0"
}, },
"service-identity": { "service-identity": {
"hashes": [ "hashes": [
@ -1109,10 +1129,10 @@
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
], ],
"version": "==1.16.0" "version": "==1.15.0"
}, },
"sqlparse": { "sqlparse": {
"hashes": [ "hashes": [
@ -1178,11 +1198,11 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
], ],
"version": "==3.10.0.0" "version": "==3.7.4.3"
}, },
"uritemplate": { "uritemplate": {
"hashes": [ "hashes": [
@ -1259,10 +1279,10 @@
}, },
"websocket-client": { "websocket-client": {
"hashes": [ "hashes": [
"sha256:2e50d26ca593f70aba7b13a489435ef88b8fc3b5c5643c1ce8808ff9b40f0b32", "sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663",
"sha256:d376bd60eace9d437ab6d7ee16f4ab4e821c9dae591e1b783c58ebd8aaf80c5c" "sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"
], ],
"version": "==0.59.0" "version": "==0.58.0"
}, },
"websockets": { "websockets": {
"hashes": [ "hashes": [
@ -1293,20 +1313,22 @@
}, },
"xmlsec": { "xmlsec": {
"hashes": [ "hashes": [
"sha256:17d2e66d4e3e601d210eed936b53c3eb44cddaef62f60b5c6ad5c18e948d926c", "sha256:252f79ed4482d6eefcca62c3bfc99b8d95c07abd846262d854a207ec4d67fac5",
"sha256:2bc1b871b49d6580779805a4a1c2d835e834a2fa614fe40cf71931d11a8279cf", "sha256:31884dc97cc34cf1681a0f239f613969e61f9a01f4c2d2a62e53d68216fe42d6",
"sha256:52eded125c0d1ab72125105ef061370c6b06ab9bd37e29a61bc2f8a61205bae4", "sha256:32a669dfe447bccecdb4ef79221c0452ce6dad919f3a75daf512792141a54dac",
"sha256:72af9a5a747a5fe6e425d2be10daa43d18307dbe03498df3820fc3cd93daa148", "sha256:3d13d7b6cb921dbc4d60d00ad00081a038df73a1e69f5bcc3695deb1bf2093b0",
"sha256:806855d505da24aeb77758a6f373b1473e5ed63bdbe346af90cc6d2b053e4716", "sha256:5e2f263a21fd146859911479ec35e40a57f519e650f56c775f91367d2a1b6e15",
"sha256:8746dd992aaec06ed8ff1615f4a8e2a32258e8af38f9a9f8acf3ee1fb34a5da6", "sha256:61076be98da4c7cf842a78aa3f129a5039f2ba4992e02480eefe78028d317698",
"sha256:9d52b2b15d42292725e4f9d8a5b040e39cba0a9cd58059ac951e7310d6340bb9", "sha256:69d7f965d6b74b3266f7baa99a0377d9c76acbf26c615b4ee8d2cbe17bf85528",
"sha256:b380f3ebc042f71afab057632481d06e06f1ba4f90047d91ca92612a7d3d487b", "sha256:6d8bb24c3a4db398011f394e29b58cd34c9c26d76b772c5d418d8579df127234",
"sha256:be0f475edd8e9c98f57449c97839f6a81946e79e4cccb81e4b5196a2cc40e044", "sha256:6d9d46d1f6b4985023469a1e334cb35c7c8fc6bd9d8b65ca52b923a7a6869c2a",
"sha256:bf3c62d154f2222caf56d897ddfd53fd0aef560d5a2202447d90e015301a0a10", "sha256:8a7ffdc4f7f760253aa4dd8d2037358eb33915ca1dcf1c2422b19fcf0ab68506",
"sha256:fe6a5f05aba3ff47e105a308482b68f8b0fd80656eb1456a9c1e4de47d2c580f" "sha256:927fc5755bb93dc09275bd5d818811e016290c194012d63f8e6f86b7ece3e468",
"sha256:dcaa084c3700f775eba09d81a1432444f82d9ad6270320c56c1a733d71cceb3a",
"sha256:f59698cc0366395ca79b48b080674973541aae290670c57d88f05d939a4c00da"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.3.10" "version": "==1.3.9"
}, },
"yarl": { "yarl": {
"hashes": [ "hashes": [
@ -1417,17 +1439,17 @@
}, },
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e", "sha256:ad63b8552c70939568966811a088ef0bc880f99a24a00834abd0e3681b514f91",
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975" "sha256:bea3f32799fbb8581f58431c12591bc20ce11cbc90ad82e2ea5717d94f2080d5"
], ],
"version": "==2.5.6" "version": "==2.5.3"
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:3901be1cb7c2a780f14668691474d9252c070a756be0a9ead98cfeabfa11aeb8", "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:8ee1e5f5a1afc5b19bdfae4fdf0c35ed324074bdce3500c939842c8f818645d9" "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
], ],
"version": "==21.1.0" "version": "==20.3.0"
}, },
"bandit": { "bandit": {
"hashes": [ "hashes": [
@ -1452,20 +1474,6 @@
"index": "pypi", "index": "pypi",
"version": "==1.0.1" "version": "==1.0.1"
}, },
"certifi": {
"hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.12.5"
},
"chardet": {
"hashes": [
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
],
"version": "==4.0.0"
},
"click": { "click": {
"hashes": [ "hashes": [
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
@ -1548,17 +1556,10 @@
}, },
"gitpython": { "gitpython": {
"hashes": [ "hashes": [
"sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e", "sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b",
"sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867" "sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"
], ],
"version": "==3.1.15" "version": "==3.1.14"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
}, },
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
@ -1632,10 +1633,10 @@
}, },
"pbr": { "pbr": {
"hashes": [ "hashes": [
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd", "sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4" "sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
], ],
"version": "==5.6.0" "version": "==5.5.1"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
@ -1653,19 +1654,19 @@
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
"sha256:586d8fa9b1891f4b725f587ef267abe2a1bad89d6b184520c7f07a253dd6e217", "sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a",
"sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b" "sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.8.2" "version": "==2.7.4"
}, },
"pylint-django": { "pylint-django": {
"hashes": [ "hashes": [
"sha256:aff49d9602a39c027b4ed7521a041438893205918f405800063b7ff692b7371b", "sha256:a5a4515209a6237d1d390a4a307d53f53baaf4f058ecf4bb556c775d208f6b0d",
"sha256:f63f717169b0c2e4e19c28f1c32c28290647330184fcb7427805ae9b6994f3fc" "sha256:dc5ed27bb7662d73444ccd15a0b3964ed6ced6cc2712b85db616102062d2ec35"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.4.4" "version": "==2.4.3"
}, },
"pylint-plugin-utils": { "pylint-plugin-utils": {
"hashes": [ "hashes": [
@ -1683,11 +1684,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", "sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634",
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" "sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.2.4" "version": "==6.2.3"
}, },
"pytest-django": { "pytest-django": {
"hashes": [ "hashes": [
@ -1778,21 +1779,6 @@
], ],
"version": "==2021.4.4" "version": "==2021.4.4"
}, },
"requests": {
"hashes": [
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
],
"version": "==2.25.1"
},
"requests-mock": {
"hashes": [
"sha256:33296f228d8c5df11a7988b741325422480baddfdf5dd9318fd0eb40c3ed8595",
"sha256:5c8ef0254c14a84744be146e9799dc13ebc4f6186058112d9aeed96b131b58e2"
],
"index": "pypi",
"version": "==1.9.2"
},
"selenium": { "selenium": {
"hashes": [ "hashes": [
"sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
@ -1803,10 +1789,10 @@
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
], ],
"version": "==1.16.0" "version": "==1.15.0"
}, },
"smmap": { "smmap": {
"hashes": [ "hashes": [
@ -1866,11 +1852,11 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
], ],
"version": "==3.10.0.0" "version": "==3.7.4.3"
}, },
"urllib3": { "urllib3": {
"extras": [ "extras": [

View File

@ -11,7 +11,6 @@
![Docker pulls](https://img.shields.io/docker/pulls/beryju/authentik.svg?style=flat-square) ![Docker pulls](https://img.shields.io/docker/pulls/beryju/authentik.svg?style=flat-square)
![Latest version](https://img.shields.io/docker/v/beryju/authentik?sort=semver&style=flat-square) ![Latest version](https://img.shields.io/docker/v/beryju/authentik?sort=semver&style=flat-square)
![LGTM Grade](https://img.shields.io/lgtm/grade/python/github/goauthentik/authentik?style=flat-square) ![LGTM Grade](https://img.shields.io/lgtm/grade/python/github/goauthentik/authentik?style=flat-square)
[Transifex](https://www.transifex.com/beryjuorg/authentik/)
## What is authentik? ## What is authentik?

View File

@ -4,8 +4,8 @@
| Version | Supported | | Version | Supported |
| ---------- | ------------------ | | ---------- | ------------------ |
| 2021.3.x | :white_check_mark: |
| 2021.4.x | :white_check_mark: | | 2021.4.x | :white_check_mark: |
| 2021.5.x | :white_check_mark: |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -1,3 +1,3 @@
"""authentik""" """authentik"""
__version__ = "2021.5.1-rc2" __version__ = "2021.4.6"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,5 +1,5 @@
"""API Authentication""" """API Authentication"""
from base64 import b64decode from base64 import b64decode, b64encode
from binascii import Error from binascii import Error
from typing import Any, Optional, Union from typing import Any, Optional, Union
@ -19,6 +19,14 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
auth_credentials = raw_header.decode() auth_credentials = raw_header.decode()
if auth_credentials == "": if auth_credentials == "":
return None return None
# Legacy, accept basic auth thats fully encoded (2021.3 outposts)
if " " not in auth_credentials:
try:
plain = b64decode(auth_credentials.encode()).decode()
auth_type, body = plain.split()
auth_credentials = f"{auth_type} {b64encode(body.encode()).decode()}"
except (UnicodeDecodeError, Error):
raise AuthenticationFailed("Malformed header")
auth_type, auth_credentials = auth_credentials.split() auth_type, auth_credentials = auth_credentials.split()
if auth_type.lower() not in ["basic", "bearer"]: if auth_type.lower() not in ["basic", "bearer"]:
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower()) LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())

View File

@ -3,7 +3,7 @@
{% load static %} {% load static %}
{% block title %} {% block title %}
API Browser - {{ config.authentik.branding.title }} authentik API Browser
{% endblock %} {% endblock %}
{% block head %} {% block head %}

View File

@ -1,16 +0,0 @@
"""Test config API"""
from json import loads
from django.urls import reverse
from rest_framework.test import APITestCase
class TestConfig(APITestCase):
"""Test config API"""
def test_config(self):
"""Test YAML generation"""
response = self.client.get(
reverse("authentik_api:configs-list"),
)
self.assertTrue(loads(response.content.decode()))

View File

@ -1,33 +0,0 @@
"""test decorators api"""
from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase
from authentik.core.models import Application, User
class TestAPIDecorators(APITestCase):
"""test decorators api"""
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create(username="test-user")
def test_obj_perm_denied(self):
"""Test object perm denied"""
self.client.force_login(self.user)
app = Application.objects.create(name="denied", slug="denied")
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 403)
def test_other_perm_denied(self):
"""Test other perm denied"""
self.client.force_login(self.user)
app = Application.objects.create(name="denied", slug="denied")
assign_perm("authentik_core.view_application", self.user, app)
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 403)

View File

@ -47,7 +47,6 @@ from authentik.policies.reputation.api import (
ReputationPolicyViewSet, ReputationPolicyViewSet,
UserReputationViewSet, UserReputationViewSet,
) )
from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet
from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet
from authentik.providers.oauth2.api.scope import ScopeMappingViewSet from authentik.providers.oauth2.api.scope import ScopeMappingViewSet
from authentik.providers.oauth2.api.tokens import ( from authentik.providers.oauth2.api.tokens import (
@ -64,7 +63,6 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet
from authentik.sources.oauth.api.source_connection import ( from authentik.sources.oauth.api.source_connection import (
UserOAuthSourceConnectionViewSet, UserOAuthSourceConnectionViewSet,
) )
from authentik.sources.plex.api import PlexSourceViewSet
from authentik.sources.saml.api import SAMLSourceViewSet from authentik.sources.saml.api import SAMLSourceViewSet
from authentik.stages.authenticator_static.api import ( from authentik.stages.authenticator_static.api import (
AuthenticatorStaticStageViewSet, AuthenticatorStaticStageViewSet,
@ -122,7 +120,6 @@ router.register(
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet "outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
) )
router.register("outposts/proxy", ProxyOutpostConfigViewSet) router.register("outposts/proxy", ProxyOutpostConfigViewSet)
router.register("outposts/ldap", LDAPOutpostConfigViewSet)
router.register("flows/instances", FlowViewSet) router.register("flows/instances", FlowViewSet)
router.register("flows/bindings", FlowStageBindingViewSet) router.register("flows/bindings", FlowStageBindingViewSet)
@ -139,7 +136,6 @@ router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewS
router.register("sources/ldap", LDAPSourceViewSet) router.register("sources/ldap", LDAPSourceViewSet)
router.register("sources/saml", SAMLSourceViewSet) router.register("sources/saml", SAMLSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet)
router.register("sources/plex", PlexSourceViewSet)
router.register("policies/all", PolicyViewSet) router.register("policies/all", PolicyViewSet)
router.register("policies/bindings", PolicyBindingViewSet) router.register("policies/bindings", PolicyBindingViewSet)
@ -153,7 +149,6 @@ router.register("policies/reputation/ips", IPReputationViewSet)
router.register("policies/reputation", ReputationPolicyViewSet) router.register("policies/reputation", ReputationPolicyViewSet)
router.register("providers/all", ProviderViewSet) router.register("providers/all", ProviderViewSet)
router.register("providers/ldap", LDAPProviderViewSet)
router.register("providers/proxy", ProxyProviderViewSet) router.register("providers/proxy", ProxyProviderViewSet)
router.register("providers/oauth2", OAuth2ProviderViewSet) router.register("providers/oauth2", OAuth2ProviderViewSet)
router.register("providers/saml", SAMLProviderViewSet) router.register("providers/saml", SAMLProviderViewSet)

View File

@ -91,23 +91,6 @@ class ApplicationViewSet(ModelViewSet):
applications.append(application) applications.append(application)
return applications return applications
@swagger_auto_schema(
responses={
204: "Access granted",
403: "Access denied",
}
)
@action(detail=True, methods=["GET"])
# pylint: disable=unused-argument
def check_access(self, request: Request, slug: str) -> Response:
"""Check access to a single application by slug"""
application = self.get_object()
engine = PolicyEngine(application, self.request.user, self.request)
engine.build()
if engine.passing:
return Response(status=204)
return Response(status=403)
@swagger_auto_schema( @swagger_auto_schema(
manual_parameters=[ manual_parameters=[
openapi.Parameter( openapi.Parameter(

View File

@ -1,9 +1,7 @@
"""Groups API Viewset""" """Groups API Viewset"""
from django.db.models.query import QuerySet
from rest_framework.fields import JSONField from rest_framework.fields import JSONField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.core.api.utils import is_dict from authentik.core.api.utils import is_dict
from authentik.core.models import Group from authentik.core.models import Group
@ -28,16 +26,3 @@ class GroupViewSet(ModelViewSet):
search_fields = ["name", "is_superuser"] search_fields = ["name", "is_superuser"]
filterset_fields = ["name", "is_superuser"] filterset_fields = ["name", "is_superuser"]
ordering = ["name"] ordering = ["name"]
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends):
if backend == ObjectPermissionsFilter:
continue
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
def filter_queryset(self, queryset):
if self.request.user.has_perm("authentik_core.view_group"):
return self._filter_queryset_for_list(queryset)
return super().filter_queryset(queryset)

View File

@ -45,7 +45,6 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"verbose_name", "verbose_name",
"verbose_name_plural", "verbose_name_plural",
"policy_engine_mode", "policy_engine_mode",
"user_matching_mode",
] ]

View File

@ -1,30 +1,18 @@
"""User API Views""" """User API Views"""
from json import loads
from django.db.models.query import QuerySet
from django.http.response import Http404 from django.http.response import Http404
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.http import urlencode from django.utils.http import urlencode
from django_filters.filters import BooleanFilter, CharFilter
from django_filters.filterset import FilterSet
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
from guardian.utils import get_anonymous_user from guardian.utils import get_anonymous_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, JSONField, SerializerMethodField from rest_framework.fields import CharField, JSONField, SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ( from rest_framework.serializers import BooleanField, ModelSerializer
BooleanField,
ListSerializer,
ModelSerializer,
ValidationError,
)
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.api.decorators import permission_required from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
from authentik.core.middleware import ( from authentik.core.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_ORIGINAL_USER,
@ -41,8 +29,6 @@ class UserSerializer(ModelSerializer):
is_superuser = BooleanField(read_only=True) is_superuser = BooleanField(read_only=True)
avatar = CharField(read_only=True) avatar = CharField(read_only=True)
attributes = JSONField(validators=[is_dict], required=False) attributes = JSONField(validators=[is_dict], required=False)
groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
uid = CharField(read_only=True)
class Meta: class Meta:
@ -54,11 +40,9 @@ class UserSerializer(ModelSerializer):
"is_active", "is_active",
"last_login", "last_login",
"is_superuser", "is_superuser",
"groups",
"email", "email",
"avatar", "avatar",
"attributes", "attributes",
"uid",
] ]
@ -100,44 +84,13 @@ class UserMetricsSerializer(PassiveSerializer):
) )
class UsersFilter(FilterSet):
"""Filter for users"""
attributes = CharFilter(
field_name="attributes",
lookup_expr="",
label="Attributes",
method="filter_attributes",
)
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
# pylint: disable=unused-argument
def filter_attributes(self, queryset, name, value):
"""Filter attributes by query args"""
try:
value = loads(value)
except ValueError:
raise ValidationError(detail="filter: failed to parse JSON")
if not isinstance(value, dict):
raise ValidationError(detail="filter: value must be key:value mapping")
qs = {}
for key, _value in value.items():
qs[f"attributes__{key}"] = _value
return queryset.filter(**qs)
class Meta:
model = User
fields = ["username", "name", "is_active", "is_superuser", "attributes"]
class UserViewSet(ModelViewSet): class UserViewSet(ModelViewSet):
"""User Viewset""" """User Viewset"""
queryset = User.objects.none() queryset = User.objects.none()
serializer_class = UserSerializer serializer_class = UserSerializer
search_fields = ["username", "name", "is_active"] search_fields = ["username", "name", "is_active"]
filterset_class = UsersFilter filterset_fields = ["username", "name", "is_active"]
def get_queryset(self): def get_queryset(self):
return User.objects.all().exclude(pk=get_anonymous_user().pk) return User.objects.all().exclude(pk=get_anonymous_user().pk)
@ -191,16 +144,3 @@ class UserViewSet(ModelViewSet):
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}" reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
) )
return Response({"link": link}) return Response({"link": link})
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends):
if backend == ObjectPermissionsFilter:
continue
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
def filter_queryset(self, queryset):
if self.request.user.has_perm("authentik_core.view_group"):
return self._filter_queryset_for_list(queryset)
return super().filter_queryset(queryset)

View File

@ -20,12 +20,10 @@ def is_dict(value: Any):
class PassiveSerializer(Serializer): class PassiveSerializer(Serializer):
"""Base serializer class which doesn't implement create/update methods""" """Base serializer class which doesn't implement create/update methods"""
def create(self, validated_data: dict) -> Model: # pragma: no cover def create(self, validated_data: dict) -> Model:
return Model() return Model()
def update( def update(self, instance: Model, validated_data: dict) -> Model:
self, instance: Model, validated_data: dict
) -> Model: # pragma: no cover
return Model() return Model()

View File

@ -1,40 +0,0 @@
# Generated by Django 3.2 on 2021-05-03 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0019_source_managed"),
]
operations = [
migrations.AddField(
model_name="source",
name="user_matching_mode",
field=models.TextField(
choices=[
("identifier", "Use the source-specific identifier"),
(
"email_link",
"Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses.",
),
(
"email_deny",
"Use the user's email address, but deny enrollment when the email address already exists.",
),
(
"username_link",
"Link to a user with identical username address. Can have security implications when a username is used with another source.",
),
(
"username_deny",
"Use the user's username, but deny enrollment when the username already exists.",
),
],
default="identifier",
help_text="How the source determines if an existing user should be authenticated or a new user enrolled.",
),
),
]

View File

@ -34,7 +34,6 @@ from authentik.policies.models import PolicyBindingModel
LOGGER = get_logger() LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug" USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account" USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
GRAVATAR_URL = "https://secure.gravatar.com" GRAVATAR_URL = "https://secure.gravatar.com"
DEFAULT_AVATAR = static("dist/assets/images/user_default.png") DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
@ -241,30 +240,6 @@ class Application(PolicyBindingModel):
verbose_name_plural = _("Applications") verbose_name_plural = _("Applications")
class SourceUserMatchingModes(models.TextChoices):
"""Different modes a source can handle new/returning users"""
IDENTIFIER = "identifier", _("Use the source-specific identifier")
EMAIL_LINK = "email_link", _(
(
"Link to a user with identical email address. Can have security implications "
"when a source doesn't validate email addresses."
)
)
EMAIL_DENY = "email_deny", _(
"Use the user's email address, but deny enrollment when the email address already exists."
)
USERNAME_LINK = "username_link", _(
(
"Link to a user with identical username address. Can have security implications "
"when a username is used with another source."
)
)
USERNAME_DENY = "username_deny", _(
"Use the user's username, but deny enrollment when the username already exists."
)
class Source(ManagedModel, SerializerModel, PolicyBindingModel): class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
@ -297,17 +272,6 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
related_name="source_enrollment", related_name="source_enrollment",
) )
user_matching_mode = models.TextField(
choices=SourceUserMatchingModes.choices,
default=SourceUserMatchingModes.IDENTIFIER,
help_text=_(
(
"How the source determines if an existing user should be authenticated or "
"a new user enrolled."
)
),
)
objects = InheritanceManager() objects = InheritanceManager()
@property @property
@ -337,8 +301,6 @@ class UserSourceConnection(CreatedUpdatedModel):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
source = models.ForeignKey(Source, on_delete=models.CASCADE) source = models.ForeignKey(Source, on_delete=models.CASCADE)
objects = InheritanceManager()
class Meta: class Meta:
unique_together = (("user", "source"),) unique_together = (("user", "source"),)

View File

@ -1,279 +0,0 @@
"""Source decision helper"""
from enum import Enum
from typing import Any, Optional, Type
from django.contrib import messages
from django.db.models.query_utils import Q
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
from authentik.core.models import (
Source,
SourceUserMatchingModes,
User,
UserSourceConnection,
)
from authentik.core.sources.stage import (
PLAN_CONTEXT_SOURCES_CONNECTION,
PostUserEnrollmentStage,
)
from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow, Stage, in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SOURCE,
PLAN_CONTEXT_SSO,
FlowPlanner,
)
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.utils import delete_none_keys
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
class Action(Enum):
"""Actions that can be decided based on the request
and source settings"""
LINK = "link"
AUTH = "auth"
ENROLL = "enroll"
DENY = "deny"
class SourceFlowManager:
"""Help sources decide what they should do after authorization. Based on source settings and
previous connections, authenticate the user, enroll a new user, link to an existing user
or deny the request."""
source: Source
request: HttpRequest
identifier: str
connection_type: Type[UserSourceConnection] = UserSourceConnection
def __init__(
self,
source: Source,
request: HttpRequest,
identifier: str,
enroll_info: dict[str, Any],
) -> None:
self.source = source
self.request = request
self.identifier = identifier
self.enroll_info = enroll_info
self._logger = get_logger().bind(source=source, identifier=identifier)
# pylint: disable=too-many-return-statements
def get_action(self, **kwargs) -> tuple[Action, Optional[UserSourceConnection]]:
"""decide which action should be taken"""
new_connection = self.connection_type(
source=self.source, identifier=self.identifier
)
# When request is authenticated, always link
if self.request.user.is_authenticated:
new_connection.user = self.request.user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
existing_connections = self.connection_type.objects.filter(
source=self.source, identifier=self.identifier
)
if existing_connections.exists():
connection = existing_connections.first()
return Action.AUTH, self.update_connection(connection, **kwargs)
# No connection exists, but we match on identifier, so enroll
if self.source.user_matching_mode == SourceUserMatchingModes.IDENTIFIER:
# We don't save the connection here cause it doesn't have a user assigned yet
return Action.ENROLL, self.update_connection(new_connection, **kwargs)
# Check for existing users with matching attributes
query = Q()
# Either query existing user based on email or username
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_LINK,
SourceUserMatchingModes.EMAIL_DENY,
]:
if not self.enroll_info.get("email", None):
self._logger.warning("Refusing to use none email", source=self.source)
return Action.DENY, None
query = Q(email__exact=self.enroll_info.get("email", None))
if self.source.user_matching_mode in [
SourceUserMatchingModes.USERNAME_LINK,
SourceUserMatchingModes.USERNAME_DENY,
]:
if not self.enroll_info.get("username", None):
self._logger.warning(
"Refusing to use none username", source=self.source
)
return Action.DENY, None
query = Q(username__exact=self.enroll_info.get("username", None))
matching_users = User.objects.filter(query)
# No matching users, always enroll
if not matching_users.exists():
return Action.ENROLL, self.update_connection(new_connection, **kwargs)
user = matching_users.first()
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_LINK,
SourceUserMatchingModes.USERNAME_LINK,
]:
new_connection.user = user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_DENY,
SourceUserMatchingModes.USERNAME_DENY,
]:
self._logger.info("denying source because user exists", user=user)
return Action.DENY, None
# Should never get here as default enroll case is returned above.
return Action.DENY, None
def update_connection(
self, connection: UserSourceConnection, **kwargs
) -> UserSourceConnection:
"""Optionally make changes to the connection after it is looked up/created."""
return connection
def get_flow(self, **kwargs) -> HttpResponse:
"""Get the flow response based on user_matching_mode"""
action, connection = self.get_action(**kwargs)
self._logger.debug("get_action() says", action=action, connection=connection)
if connection:
if action == Action.LINK:
self._logger.debug("Linking existing user")
return self.handle_existing_user_link(connection)
if action == Action.AUTH:
self._logger.debug("Handling auth user")
return self.handle_auth_user(connection)
if action == Action.ENROLL:
self._logger.debug("Handling enrollment of new user")
return self.handle_enroll(connection)
# Default case, assume deny
messages.error(
self.request,
_(
(
"Request to authenticate with %(source)s has been denied. Please authenticate "
"with the source you've previously signed up with."
)
% {"source": self.source.name}
),
)
return redirect("/")
# pylint: disable=unused-argument
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
"""Hook to override stages which are appended to the flow"""
if flow.slug == self.source.enrollment_flow.slug:
return [
in_memory_stage(PostUserEnrollmentStage),
]
return []
def _handle_login_flow(self, flow: Flow, **kwargs) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor"""
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-admin"
)
kwargs.update(
{
# Since we authenticate the user by their token, they have no backend set
PLAN_CONTEXT_AUTHENTICATION_BACKEND: "django.contrib.auth.backends.ModelBackend",
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_REDIRECT: final_redirect,
}
)
if not flow:
return HttpResponseBadRequest()
# We run the Flow planner here so we can pass the Pending user in the context
planner = FlowPlanner(flow)
plan = planner.plan(self.request, kwargs)
for stage in self.get_stages_to_append(flow):
plan.append(stage)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=flow.slug,
)
# pylint: disable=unused-argument
def handle_auth_user(
self,
connection: UserSourceConnection,
) -> HttpResponse:
"""Login user and redirect."""
messages.success(
self.request,
_(
"Successfully authenticated with %(source)s!"
% {"source": self.source.name}
),
)
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user}
return self._handle_login_flow(self.source.authentication_flow, **flow_kwargs)
def handle_existing_user_link(
self,
connection: UserSourceConnection,
) -> HttpResponse:
"""Handler when the user was already authenticated and linked an external source
to their account."""
# Connection has already been saved
Event.new(
EventAction.SOURCE_LINKED,
message="Linked Source",
source=self.source,
).from_http(self.request)
messages.success(
self.request,
_("Successfully linked %(source)s!" % {"source": self.source.name}),
)
# When request isn't authenticated we jump straight to auth
if not self.request.user.is_authenticated:
return self.handle_auth_user(connection)
return redirect(
reverse(
"authentik_core:if-admin",
)
+ f"#/user;page-{self.source.slug}"
)
def handle_enroll(
self,
connection: UserSourceConnection,
) -> HttpResponse:
"""User was not authenticated and previous request was not authenticated."""
messages.success(
self.request,
_(
"Successfully authenticated with %(source)s!"
% {"source": self.source.name}
),
)
# We run the Flow planner here so we can pass the Pending user in the context
if not self.source.enrollment_flow:
self._logger.warning("source has no enrollment flow")
return HttpResponseBadRequest()
return self._handle_login_flow(
self.source.enrollment_flow,
**{
PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info),
PLAN_CONTEXT_SOURCES_CONNECTION: connection,
},
)

View File

@ -14,9 +14,9 @@
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
<script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script> <script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script>
<script>window["polymerSkipLoadingFontRoboto"] = true;</script> <script>window["polymerSkipLoadingFontRoboto"] = true;</script>
{% block head %} {% block head %}

View File

@ -3,10 +3,6 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block head_before %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
{% endblock %}
{% block body %} {% block body %}
<div class="pf-c-background-image"> <div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0"> <svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">

View File

@ -1,125 +0,0 @@
"""Test Applications API"""
from django.urls import reverse
from django.utils.encoding import force_str
from rest_framework.test import APITestCase
from authentik.core.models import Application, User
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
class TestApplicationsAPI(APITestCase):
"""Test applications API"""
def setUp(self) -> None:
self.user = User.objects.get(username="akadmin")
self.allowed = Application.objects.create(name="allowed", slug="allowed")
self.denied = Application.objects.create(name="denied", slug="denied")
PolicyBinding.objects.create(
target=self.denied,
policy=DummyPolicy.objects.create(
name="deny", result=False, wait_min=1, wait_max=2
),
order=0,
)
def test_check_access(self):
"""Test check_access operation """
self.client.force_login(self.user)
response = self.client.get(
reverse(
"authentik_api:application-check-access",
kwargs={"slug": self.allowed.slug},
)
)
self.assertEqual(response.status_code, 204)
response = self.client.get(
reverse(
"authentik_api:application-check-access",
kwargs={"slug": self.denied.slug},
)
)
self.assertEqual(response.status_code, 403)
def test_list(self):
"""Test list operation without superuser_full_list"""
self.client.force_login(self.user)
response = self.client.get(reverse("authentik_api:application-list"))
self.assertJSONEqual(
force_str(response.content),
{
"pagination": {
"next": 0,
"previous": 0,
"count": 2,
"current": 1,
"total_pages": 1,
"start_index": 1,
"end_index": 2,
},
"results": [
{
"pk": str(self.allowed.pk),
"name": "allowed",
"slug": "allowed",
"provider": None,
"provider_obj": None,
"launch_url": None,
"meta_launch_url": "",
"meta_icon": None,
"meta_description": "",
"meta_publisher": "",
"policy_engine_mode": "any",
},
],
},
)
def test_list_superuser_full_list(self):
"""Test list operation with superuser_full_list"""
self.client.force_login(self.user)
response = self.client.get(
reverse("authentik_api:application-list") + "?superuser_full_list=true"
)
self.assertJSONEqual(
force_str(response.content),
{
"pagination": {
"next": 0,
"previous": 0,
"count": 2,
"current": 1,
"total_pages": 1,
"start_index": 1,
"end_index": 2,
},
"results": [
{
"pk": str(self.allowed.pk),
"name": "allowed",
"slug": "allowed",
"provider": None,
"provider_obj": None,
"launch_url": None,
"meta_launch_url": "",
"meta_icon": None,
"meta_description": "",
"meta_publisher": "",
"policy_engine_mode": "any",
},
{
"launch_url": None,
"meta_description": "",
"meta_icon": None,
"meta_launch_url": "",
"meta_publisher": "",
"name": "denied",
"pk": str(self.denied.pk),
"policy_engine_mode": "any",
"provider": None,
"provider_obj": None,
"slug": "denied",
},
],
},
)

View File

@ -1,14 +1,11 @@
"""authentik core models tests""" """authentik core models tests"""
from time import sleep from time import sleep
from typing import Callable, Type
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import now from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Provider, Source, Token from authentik.core.models import Token
from authentik.flows.models import Stage
from authentik.lib.utils.reflection import all_subclasses
class TestModels(TestCase): class TestModels(TestCase):
@ -27,40 +24,3 @@ class TestModels(TestCase):
) )
sleep(0.5) sleep(0.5)
self.assertFalse(token.is_expired) self.assertFalse(token.is_expired)
def source_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test source"""
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract:
model_class = test_model.__bases__[0]()
else:
model_class = test_model()
model_class.slug = "test"
self.assertIsNotNone(model_class.component)
_ = model_class.ui_login_button
_ = model_class.ui_user_settings
return tester
def provider_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test provider"""
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract:
model_class = test_model.__bases__[0]()
else:
model_class = test_model()
self.assertIsNotNone(model_class.component)
return tester
for model in all_subclasses(Source):
setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model))
for model in all_subclasses(Provider):
setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model))

View File

@ -2,10 +2,9 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional from typing import Optional
from rest_framework.fields import CharField, DictField from rest_framework.fields import CharField
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import Challenge
@dataclass @dataclass
@ -15,8 +14,8 @@ class UILoginButton:
# Name, ran through i18n # Name, ran through i18n
name: str name: str
# Challenge which is presented to the user when they click the button # URL Which Button points to
challenge: Challenge url: str
# Icon URL, used as-is # Icon URL, used as-is
icon_url: Optional[str] = None icon_url: Optional[str] = None
@ -26,7 +25,7 @@ class UILoginButtonSerializer(PassiveSerializer):
"""Serializer for Login buttons of sources""" """Serializer for Login buttons of sources"""
name = CharField() name = CharField()
challenge = DictField() url = CharField()
icon_url = CharField(required=False, allow_null=True) icon_url = CharField(required=False, allow_null=True)

View File

@ -39,7 +39,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
"""Show if this keypair has a private key configured or not""" """Show if this keypair has a private key configured or not"""
return instance.key_data != "" and instance.key_data is not None return instance.key_data != "" and instance.key_data is not None
def validate_certificate_data(self, value: str) -> str: def validate_certificate_data(self, value):
"""Verify that input is a valid PEM x509 Certificate""" """Verify that input is a valid PEM x509 Certificate"""
try: try:
load_pem_x509_certificate(value.encode("utf-8"), default_backend()) load_pem_x509_certificate(value.encode("utf-8"), default_backend())
@ -47,7 +47,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
raise ValidationError("Unable to load certificate.") raise ValidationError("Unable to load certificate.")
return value return value
def validate_key_data(self, value: str) -> str: def validate_key_data(self, value):
"""Verify that input is a valid PEM RSA Key""" """Verify that input is a valid PEM RSA Key"""
# Since this field is optional, data can be empty. # Since this field is optional, data can be empty.
if value != "": if value != "":
@ -57,10 +57,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
password=None, password=None,
backend=default_backend(), backend=default_backend(),
) )
except (ValueError, TypeError): except ValueError:
raise ValidationError( raise ValidationError("Unable to load private key.")
"Unable to load private key (possibly encrypted?)."
)
return value return value
class Meta: class Meta:

View File

@ -33,7 +33,7 @@ class CertificateBuilder:
def save(self) -> Optional[CertificateKeyPair]: def save(self) -> Optional[CertificateKeyPair]:
"""Save generated certificate as model""" """Save generated certificate as model"""
if not self.__certificate: if not self.__certificate:
raise ValueError("Certificated hasn't been built yet") return None
return CertificateKeyPair.objects.create( return CertificateKeyPair.objects.create(
name=self.common_name, name=self.common_name,
certificate_data=self.certificate, certificate_data=self.certificate,

View File

@ -37,8 +37,6 @@ class TestCrypto(TestCase):
"""Test Builder""" """Test Builder"""
builder = CertificateBuilder() builder = CertificateBuilder()
builder.common_name = "test-cert" builder.common_name = "test-cert"
with self.assertRaises(ValueError):
builder.save()
builder.build( builder.build(
subject_alt_names=[], subject_alt_names=[],
validity_days=3, validity_days=3,

View File

@ -8,10 +8,10 @@ from rest_framework.decorators import action
from rest_framework.fields import CharField, DictField, IntegerField from rest_framework.fields import CharField, DictField, IntegerField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer from authentik.core.api.utils import TypeCreateSerializer
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
@ -38,19 +38,31 @@ class EventSerializer(ModelSerializer):
] ]
class EventTopPerUserParams(PassiveSerializer): class EventTopPerUserParams(Serializer):
"""Query params for top_per_user""" """Query params for top_per_user"""
top_n = IntegerField(default=15) top_n = IntegerField(default=15)
def create(self, request: Request) -> Response:
raise NotImplementedError
class EventTopPerUserSerializer(PassiveSerializer): def update(self, request: Request) -> Response:
raise NotImplementedError
class EventTopPerUserSerializer(Serializer):
"""Response object of Event's top_per_user""" """Response object of Event's top_per_user"""
application = DictField() application = DictField()
counted_events = IntegerField() counted_events = IntegerField()
unique_users = IntegerField() unique_users = IntegerField()
def create(self, request: Request) -> Response:
raise NotImplementedError
def update(self, request: Request) -> Response:
raise NotImplementedError
class EventsFilter(django_filters.FilterSet): class EventsFilter(django_filters.FilterSet):
"""Filter for events""" """Filter for events"""
@ -120,7 +132,7 @@ class EventViewSet(ReadOnlyModelViewSet):
def top_per_user(self, request: Request): def top_per_user(self, request: Request):
"""Get the top_n events grouped by user count""" """Get the top_n events grouped by user count"""
filtered_action = request.query_params.get("action", EventAction.LOGIN) filtered_action = request.query_params.get("action", EventAction.LOGIN)
top_n = int(request.query_params.get("top_n", "15")) top_n = request.query_params.get("top_n", 15)
return Response( return Response(
get_objects_for_user(request.user, "authentik_events.view_event") get_objects_for_user(request.user, "authentik_events.view_event")
.filter(action=filtered_action) .filter(action=filtered_action)

View File

@ -1,5 +1,4 @@
"""Notification API Views""" """Notification API Views"""
from guardian.utils import get_anonymous_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.fields import ReadOnlyField from rest_framework.fields import ReadOnlyField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
@ -49,5 +48,6 @@ class NotificationViewSet(
] ]
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user() if not self.request:
return Notification.objects.filter(user=user.pk) return super().get_queryset()
return Notification.objects.filter(user=self.request.user)

View File

@ -1,6 +1,6 @@
"""Event notification tasks""" """Event notification tasks"""
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from structlog.stdlib import get_logger from structlog import get_logger
from authentik.core.models import User from authentik.core.models import User
from authentik.events.models import ( from authentik.events.models import (
@ -35,10 +35,7 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid) LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid)
return return
event: Event = events.first() event: Event = events.first()
triggers: NotificationRule = NotificationRule.objects.filter(name=trigger_name) trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name)
if not triggers.exists():
return
trigger = triggers.first()
if "policy_uuid" in event.context: if "policy_uuid" in event.context:
policy_uuid = event.context["policy_uuid"] policy_uuid = event.context["policy_uuid"]
@ -61,13 +58,7 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
return return
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger) LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
try: user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
user = (
User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
)
except User.DoesNotExist:
LOGGER.warning("e(trigger): failed to get user", trigger=trigger)
return
policy_engine = PolicyEngine(trigger, user) policy_engine = PolicyEngine(trigger, user)
policy_engine.mode = PolicyEngineMode.MODE_ANY policy_engine.mode = PolicyEngineMode.MODE_ANY
policy_engine.empty_result = False policy_engine.empty_result = False

View File

@ -1,32 +0,0 @@
"""base model tests"""
from typing import Callable, Type
from django.test import TestCase
from authentik.flows.models import Stage
from authentik.flows.stage import StageView
from authentik.lib.utils.reflection import all_subclasses
class TestModels(TestCase):
"""Generic model properties tests"""
def model_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test a form"""
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract:
model_class = test_model.__bases__[0]()
else:
model_class = test_model()
self.assertTrue(issubclass(model_class.type, StageView))
self.assertIsNotNone(test_model.component)
_ = test_model.ui_user_settings
return tester
for model in all_subclasses(Stage):
setattr(TestModels, f"test_model_{model.__name__}", model_tester_factory(model))

View File

@ -13,7 +13,7 @@ from django.db.models.query_utils import Q
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.serializers import BaseSerializer, Serializer from rest_framework.serializers import BaseSerializer, Serializer
from structlog.stdlib import BoundLogger, get_logger from structlog import BoundLogger, get_logger
from authentik.flows.models import Flow, FlowStageBinding, Stage from authentik.flows.models import Flow, FlowStageBinding, Stage
from authentik.flows.transfer.common import ( from authentik.flows.transfer.common import (

View File

@ -86,13 +86,6 @@ class ConfigLoader:
url = urlparse(value) url = urlparse(value)
if url.scheme == "env": if url.scheme == "env":
value = os.getenv(url.netloc, url.query) value = os.getenv(url.netloc, url.query)
if url.scheme == "file":
try:
with open(url.netloc, "r") as _file:
value = _file.read()
except OSError:
self._log("error", f"Failed to read config value from {url.netloc}")
value = url.query
return value return value
def update_from_file(self, path: str): def update_from_file(self, path: str):
@ -170,7 +163,6 @@ class ConfigLoader:
# Walk each component of the path # Walk each component of the path
path_parts = path.split(sep) path_parts = path.split(sep)
for comp in path_parts[:-1]: for comp in path_parts[:-1]:
# pyright: reportGeneralTypeIssues=false
if comp not in root: if comp not in root:
root[comp] = {} root[comp] = {}
root = root.get(comp) root = root.get(comp)

View File

@ -5,10 +5,6 @@ postgresql:
user: authentik user: authentik
password: 'env://POSTGRES_PASSWORD' password: 'env://POSTGRES_PASSWORD'
web:
listen: 0.0.0.0:9000
listen_tls: 0.0.0.0:9443
redis: redis:
host: localhost host: localhost
password: '' password: ''
@ -38,10 +34,7 @@ email:
from: authentik@localhost from: authentik@localhost
outposts: outposts:
# Placeholders: docker_image_base: "beryju/authentik" # this is prepended to -proxy:version
# %(type)s: Outpost type; proxy, ldap, etc
# %(version)s: Current version; 2021.4.1
docker_image_base: "beryju/authentik-%(type)s:%(version)s"
authentik: authentik:
avatars: gravatar # gravatar or none avatars: gravatar # gravatar or none

View File

@ -1,16 +0,0 @@
"""Test Reflection utils"""
from datetime import datetime
from django.test import TestCase
from authentik.lib.utils.reflection import path_to_class
class TestReflectionUtils(TestCase):
"""Test Reflection-utils"""
def test_path_to_class(self):
"""Test path_to_class"""
self.assertIsNone(path_to_class(None))
self.assertEqual(path_to_class("datetime.datetime"), datetime)

View File

@ -3,9 +3,6 @@ from typing import Any, Optional
from django.http import HttpRequest from django.http import HttpRequest
OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP"
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]: def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]:
"""Attempt to get the client's IP by checking common HTTP Headers. """Attempt to get the client's IP by checking common HTTP Headers.
@ -21,27 +18,9 @@ def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]:
return None return None
def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
"""Get the actual remote IP when set by an outpost. Only
allowed when the request is authenticated, by a user with USER_ATTRIBUTE_CAN_OVERRIDE_IP set
to outpost"""
if not hasattr(request, "user"):
return None
if not request.user.is_authenticated:
return None
if OUTPOST_REMOTE_IP_HEADER not in request.META:
return None
if request.user.attributes.get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
return None
return request.META[OUTPOST_REMOTE_IP_HEADER]
def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]: def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]:
"""Attempt to get the client's IP by checking common HTTP Headers. """Attempt to get the client's IP by checking common HTTP Headers.
Returns none if no IP Could be found""" Returns none if no IP Could be found"""
if request: if request:
override = _get_outpost_override_ip(request)
if override:
return override
return _get_client_ip_from_meta(request.META) return _get_client_ip_from_meta(request.META)
return None return None

View File

@ -24,7 +24,6 @@ class OutpostSerializer(ModelSerializer):
fields = [ fields = [
"pk", "pk",
"name", "name",
"type",
"providers", "providers",
"providers_obj", "providers_obj",
"service_connection", "service_connection",

View File

@ -82,8 +82,7 @@ class OutpostConsumer(AuthJsonConsumer):
state.version = msg.args.get("version", None) state.version = msg.args.get("version", None)
elif msg.instruction == WebsocketMessageInstruction.ACK: elif msg.instruction == WebsocketMessageInstruction.ACK:
return return
if state.version: state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5)
state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5)
response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK) response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK)
self.send_json(asdict(response)) self.send_json(asdict(response))

View File

@ -1,12 +1,9 @@
"""Base Controller""" """Base Controller"""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from structlog.testing import capture_logs from structlog.testing import capture_logs
from authentik import __version__
from authentik.lib.config import CONFIG
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.outposts.models import Outpost, OutpostServiceConnection from authentik.outposts.models import Outpost, OutpostServiceConnection
@ -24,7 +21,6 @@ class DeploymentPort:
port: int port: int
name: str name: str
protocol: str protocol: str
inner_port: Optional[int] = None
class BaseController: class BaseController:
@ -59,8 +55,3 @@ class BaseController:
def get_static_deployment(self) -> str: def get_static_deployment(self) -> str:
"""Return a static deployment configuration""" """Return a static deployment configuration"""
raise NotImplementedError raise NotImplementedError
def get_container_image(self) -> str:
"""Get container image to use for this outpost"""
image_name_template: str = CONFIG.y("outposts.docker_image_base")
return image_name_template % {"type": self.outpost.type, "version": __version__}

View File

@ -8,6 +8,7 @@ from docker.models.containers import Container
from yaml import safe_dump from yaml import safe_dump
from authentik import __version__ from authentik import __version__
from authentik.lib.config import CONFIG
from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.controllers.base import BaseController, ControllerException
from authentik.outposts.models import ( from authentik.outposts.models import (
DockerServiceConnection, DockerServiceConnection,
@ -59,14 +60,15 @@ class DockerController(BaseController):
return self.client.containers.get(container_name), False return self.client.containers.get(container_name), False
except NotFound: except NotFound:
self.logger.info("Container does not exist, creating") self.logger.info("Container does not exist, creating")
image_name = self.get_container_image() image_prefix = CONFIG.y("outposts.docker_image_base")
image_name = f"{image_prefix}-{self.outpost.type}:{__version__}"
self.client.images.pull(image_name) self.client.images.pull(image_name)
container_args = { container_args = {
"image": image_name, "image": image_name,
"name": f"authentik-proxy-{self.outpost.uuid.hex}", "name": f"authentik-proxy-{self.outpost.uuid.hex}",
"detach": True, "detach": True,
"ports": { "ports": {
f"{port.port}/{port.protocol.lower()}": port.inner_port or port.port f"{port.port}/{port.protocol.lower()}": port.port
for port in self.deployment_ports for port in self.deployment_ports
}, },
"environment": self._get_env(), "environment": self._get_env(),
@ -141,15 +143,15 @@ class DockerController(BaseController):
def get_static_deployment(self) -> str: def get_static_deployment(self) -> str:
"""Generate docker-compose yaml for proxy, version 3.5""" """Generate docker-compose yaml for proxy, version 3.5"""
ports = [ ports = [
f"{port.port}:{port.inner_port or port.port}/{port.protocol.lower()}" f"{port.port}:{port.port}/{port.protocol.lower()}"
for port in self.deployment_ports for port in self.deployment_ports
] ]
image_name = self.get_container_image() image_prefix = CONFIG.y("outposts.docker_image_base")
compose = { compose = {
"version": "3.5", "version": "3.5",
"services": { "services": {
f"authentik_{self.outpost.type}": { f"authentik_{self.outpost.type}": {
"image": image_name, "image": f"{image_prefix}-{self.outpost.type}:{__version__}",
"ports": ports, "ports": ports,
"environment": { "environment": {
"AUTHENTIK_HOST": self.outpost.config.authentik_host, "AUTHENTIK_HOST": self.outpost.config.authentik_host,

View File

@ -1,7 +1,6 @@
"""Base Kubernetes Reconciler""" """Base Kubernetes Reconciler"""
from typing import TYPE_CHECKING, Generic, TypeVar from typing import TYPE_CHECKING, Generic, TypeVar
from django.utils.text import slugify
from kubernetes.client import V1ObjectMeta from kubernetes.client import V1ObjectMeta
from kubernetes.client.models.v1_deployment import V1Deployment from kubernetes.client.models.v1_deployment import V1Deployment
from kubernetes.client.models.v1_pod import V1Pod from kubernetes.client.models.v1_pod import V1Pod
@ -30,11 +29,6 @@ class NeedsUpdate(ReconcileTrigger):
"""Exception to trigger an update to the Kubernetes Object""" """Exception to trigger an update to the Kubernetes Object"""
class Disabled(SentryIgnoredException):
"""Exception which can be thrown in a reconciler to signal than an
object should not be created."""
class KubernetesObjectReconciler(Generic[T]): class KubernetesObjectReconciler(Generic[T]):
"""Base Kubernetes Reconciler, handles the basic logic.""" """Base Kubernetes Reconciler, handles the basic logic."""
@ -43,24 +37,17 @@ class KubernetesObjectReconciler(Generic[T]):
def __init__(self, controller: "KubernetesController"): def __init__(self, controller: "KubernetesController"):
self.controller = controller self.controller = controller
self.namespace = controller.outpost.config.kubernetes_namespace self.namespace = controller.outpost.config.kubernetes_namespace
self.logger = get_logger().bind(type=self.__class__.__name__) self.logger = get_logger()
@property @property
def name(self) -> str: def name(self) -> str:
"""Get the name of the object this reconciler manages""" """Get the name of the object this reconciler manages"""
return self.controller.outpost.config.object_naming_template % { raise NotImplementedError
"name": slugify(self.controller.outpost.name),
"uuid": self.controller.outpost.uuid.hex,
}
def up(self): def up(self):
"""Create object if it doesn't exist, update if needed or recreate if needed.""" """Create object if it doesn't exist, update if needed or recreate if needed."""
current = None current = None
try: reference = self.get_reference_object()
reference = self.get_reference_object()
except Disabled:
self.logger.debug("Object not required")
return
try: try:
try: try:
current = self.retrieve() current = self.retrieve()
@ -71,6 +58,7 @@ class KubernetesObjectReconciler(Generic[T]):
self.logger.debug("Other unhandled error", exc=exc) self.logger.debug("Other unhandled error", exc=exc)
raise exc raise exc
else: else:
self.logger.debug("Got current, running reconcile")
self.reconcile(current, reference) self.reconcile(current, reference)
except NeedsRecreate: except NeedsRecreate:
self.logger.debug("Recreate requested") self.logger.debug("Recreate requested")
@ -79,22 +67,16 @@ class KubernetesObjectReconciler(Generic[T]):
self.delete(current) self.delete(current)
else: else:
self.logger.debug("No old found, creating") self.logger.debug("No old found, creating")
self.logger.debug("Creating") self.logger.debug("Created")
self.create(reference) self.create(reference)
except NeedsUpdate: except NeedsUpdate:
self.logger.debug("Updating") self.logger.debug("Updating")
self.update(current, reference) self.update(current, reference)
else: else:
self.logger.debug("Object is up-to-date.") self.logger.debug("Nothing to do...")
def down(self): def down(self):
"""Delete object if found""" """Delete object if found"""
# Call self.get_reference_object to check if we even need to do anything
try:
self.get_reference_object()
except Disabled:
self.logger.debug("Object not required")
return
try: try:
current = self.retrieve() current = self.retrieve()
self.delete(current) self.delete(current)
@ -138,7 +120,7 @@ class KubernetesObjectReconciler(Generic[T]):
namespace=self.namespace, namespace=self.namespace,
labels={ labels={
"app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}", "app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
"app.kubernetes.io/instance": slugify(self.controller.outpost.name), "app.kubernetes.io/instance": self.controller.outpost.name,
"app.kubernetes.io/version": __version__, "app.kubernetes.io/version": __version__,
"app.kubernetes.io/managed-by": "goauthentik.io", "app.kubernetes.io/managed-by": "goauthentik.io",
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex, "goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,

View File

@ -16,6 +16,8 @@ from kubernetes.client import (
V1SecretKeySelector, V1SecretKeySelector,
) )
from authentik import __version__
from authentik.lib.config import CONFIG
from authentik.outposts.controllers.base import FIELD_MANAGER from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import ( from authentik.outposts.controllers.k8s.base import (
KubernetesObjectReconciler, KubernetesObjectReconciler,
@ -37,6 +39,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
self.api = AppsV1Api(controller.client) self.api = AppsV1Api(controller.client)
self.outpost = self.controller.outpost self.outpost = self.controller.outpost
@property
def name(self) -> str:
return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
def reconcile(self, current: V1Deployment, reference: V1Deployment): def reconcile(self, current: V1Deployment, reference: V1Deployment):
super().reconcile(current, reference) super().reconcile(current, reference)
if current.spec.replicas != reference.spec.replicas: if current.spec.replicas != reference.spec.replicas:
@ -62,13 +68,14 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
for port in self.controller.deployment_ports: for port in self.controller.deployment_ports:
container_ports.append( container_ports.append(
V1ContainerPort( V1ContainerPort(
container_port=port.inner_port or port.port, container_port=port.port,
name=port.name, name=port.name,
protocol=port.protocol.upper(), protocol=port.protocol.upper(),
) )
) )
meta = self.get_object_meta(name=self.name) meta = self.get_object_meta(name=self.name)
image_name = self.controller.get_container_image() secret_name = f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
image_prefix = CONFIG.y("outposts.docker_image_base")
return V1Deployment( return V1Deployment(
metadata=meta, metadata=meta,
spec=V1DeploymentSpec( spec=V1DeploymentSpec(
@ -80,14 +87,14 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
containers=[ containers=[
V1Container( V1Container(
name=str(self.outpost.type), name=str(self.outpost.type),
image=image_name, image=f"{image_prefix}-{self.outpost.type}:{__version__}",
ports=container_ports, ports=container_ports,
env=[ env=[
V1EnvVar( V1EnvVar(
name="AUTHENTIK_HOST", name="AUTHENTIK_HOST",
value_from=V1EnvVarSource( value_from=V1EnvVarSource(
secret_key_ref=V1SecretKeySelector( secret_key_ref=V1SecretKeySelector(
name=self.name, name=secret_name,
key="authentik_host", key="authentik_host",
) )
), ),
@ -96,7 +103,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
name="AUTHENTIK_TOKEN", name="AUTHENTIK_TOKEN",
value_from=V1EnvVarSource( value_from=V1EnvVarSource(
secret_key_ref=V1SecretKeySelector( secret_key_ref=V1SecretKeySelector(
name=self.name, name=secret_name,
key="token", key="token",
) )
), ),
@ -105,7 +112,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
name="AUTHENTIK_INSECURE", name="AUTHENTIK_INSECURE",
value_from=V1EnvVarSource( value_from=V1EnvVarSource(
secret_key_ref=V1SecretKeySelector( secret_key_ref=V1SecretKeySelector(
name=self.name, name=secret_name,
key="authentik_host_insecure", key="authentik_host_insecure",
) )
), ),

View File

@ -26,6 +26,10 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
super().__init__(controller) super().__init__(controller)
self.api = CoreV1Api(controller.client) self.api = CoreV1Api(controller.client)
@property
def name(self) -> str:
return f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
def reconcile(self, current: V1Secret, reference: V1Secret): def reconcile(self, current: V1Secret, reference: V1Secret):
super().reconcile(current, reference) super().reconcile(current, reference)
for key in reference.data.keys(): for key in reference.data.keys():

View File

@ -21,6 +21,10 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
super().__init__(controller) super().__init__(controller)
self.api = CoreV1Api(controller.client) self.api = CoreV1Api(controller.client)
@property
def name(self) -> str:
return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
def reconcile(self, current: V1Service, reference: V1Service): def reconcile(self, current: V1Service, reference: V1Service):
super().reconcile(current, reference) super().reconcile(current, reference)
if len(current.spec.ports) != len(reference.spec.ports): if len(current.spec.ports) != len(reference.spec.ports):
@ -39,17 +43,13 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
name=port.name, name=port.name,
port=port.port, port=port.port,
protocol=port.protocol.upper(), protocol=port.protocol.upper(),
target_port=port.inner_port or port.port, target_port=port.port,
) )
) )
selector_labels = DeploymentReconciler(self.controller).get_pod_meta() selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
return V1Service( return V1Service(
metadata=meta, metadata=meta,
spec=V1ServiceSpec( spec=V1ServiceSpec(ports=ports, selector=selector_labels, type="ClusterIP"),
ports=ports,
selector=selector_labels,
type=self.controller.outpost.config.kubernetes_service_type,
),
) )
def create(self, reference: V1Service): def create(self, reference: V1Service):

View File

@ -2,13 +2,14 @@
from io import StringIO from io import StringIO
from typing import Type from typing import Type
from kubernetes.client import OpenApiException
from kubernetes.client.api_client import ApiClient from kubernetes.client.api_client import ApiClient
from kubernetes.client.exceptions import ApiException
from structlog.testing import capture_logs from structlog.testing import capture_logs
from urllib3.exceptions import HTTPError
from yaml import dump_all from yaml import dump_all
from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.controllers.base import BaseController, ControllerException
from authentik.outposts.controllers.k8s.base import Disabled, KubernetesObjectReconciler from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
from authentik.outposts.controllers.k8s.secret import SecretReconciler from authentik.outposts.controllers.k8s.secret import SecretReconciler
from authentik.outposts.controllers.k8s.service import ServiceReconciler from authentik.outposts.controllers.k8s.service import ServiceReconciler
@ -42,8 +43,8 @@ class KubernetesController(BaseController):
reconciler = self.reconcilers[reconcile_key](self) reconciler = self.reconcilers[reconcile_key](self)
reconciler.up() reconciler.up()
except ApiException as exc: except (OpenApiException, HTTPError) as exc:
raise ControllerException(str(exc)) from exc raise ControllerException from exc
def up_with_logs(self) -> list[str]: def up_with_logs(self) -> list[str]:
try: try:
@ -54,8 +55,8 @@ class KubernetesController(BaseController):
reconciler.up() reconciler.up()
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs] all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
return all_logs return all_logs
except ApiException as exc: except (OpenApiException, HTTPError) as exc:
raise ControllerException(str(exc)) from exc raise ControllerException from exc
def down(self): def down(self):
try: try:
@ -63,17 +64,14 @@ class KubernetesController(BaseController):
reconciler = self.reconcilers[reconcile_key](self) reconciler = self.reconcilers[reconcile_key](self)
reconciler.down() reconciler.down()
except ApiException as exc: except OpenApiException as exc:
raise ControllerException(str(exc)) from exc raise ControllerException from exc
def get_static_deployment(self) -> str: def get_static_deployment(self) -> str:
documents = [] documents = []
for reconcile_key in self.reconcile_order: for reconcile_key in self.reconcile_order:
reconciler = self.reconcilers[reconcile_key](self) reconciler = self.reconcilers[reconcile_key](self)
try: documents.append(reconciler.get_reference_object().to_dict())
documents.append(reconciler.get_reference_object().to_dict())
except Disabled:
continue
with StringIO() as _str: with StringIO() as _str:
dump_all( dump_all(

View File

@ -1,20 +0,0 @@
# Generated by Django 3.2 on 2021-04-26 09:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_outposts", "0015_auto_20201224_1206"),
]
operations = [
migrations.AlterField(
model_name="outpost",
name="type",
field=models.TextField(
choices=[("proxy", "Proxy"), ("ldap", "Ldap")], default="proxy"
),
),
]

View File

@ -5,7 +5,6 @@ from typing import Iterable, Optional, Union
from uuid import uuid4 from uuid import uuid4
from dacite import from_dict from dacite import from_dict
from django.contrib.auth.models import Permission
from django.core.cache import cache from django.core.cache import cache
from django.db import models, transaction from django.db import models, transaction
from django.db.models.base import Model from django.db.models.base import Model
@ -32,7 +31,6 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.models import InheritanceForeignKey from authentik.lib.models import InheritanceForeignKey
from authentik.lib.sentry import SentryIgnoredException from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.http import USER_ATTRIBUTE_CAN_OVERRIDE_IP
from authentik.outposts.docker_tls import DockerInlineTLS from authentik.outposts.docker_tls import DockerInlineTLS
OUR_VERSION = parse(__version__) OUR_VERSION = parse(__version__)
@ -57,18 +55,16 @@ class OutpostConfig:
"error_reporting.environment", "customer" "error_reporting.environment", "customer"
) )
object_naming_template: str = field(default="ak-outpost-%(name)s")
kubernetes_replicas: int = field(default=1) kubernetes_replicas: int = field(default=1)
kubernetes_namespace: str = field(default="default") kubernetes_namespace: str = field(default="default")
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict) kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls") kubernetes_ingress_secret_name: str = field(default="authentik-outpost")
kubernetes_service_type: str = field(default="ClusterIP")
class OutpostModel(Model): class OutpostModel(Model):
"""Base model for providers that need more objects than just themselves""" """Base model for providers that need more objects than just themselves"""
def get_required_objects(self) -> Iterable[Union[models.Model, str]]: def get_required_objects(self) -> Iterable[models.Model]:
"""Return a list of all required objects""" """Return a list of all required objects"""
return [self] return [self]
@ -81,7 +77,6 @@ class OutpostType(models.TextChoices):
"""Outpost types, currently only the reverse proxy is available""" """Outpost types, currently only the reverse proxy is available"""
PROXY = "proxy" PROXY = "proxy"
LDAP = "ldap"
def default_outpost_config(host: Optional[str] = None): def default_outpost_config(host: Optional[str] = None):
@ -331,7 +326,6 @@ class Outpost(models.Model):
if not users.exists(): if not users.exists():
user: User = User.objects.create(username=self.user_identifier) user: User = User.objects.create(username=self.user_identifier)
user.attributes[USER_ATTRIBUTE_SA] = True user.attributes[USER_ATTRIBUTE_SA] = True
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
user.set_unusable_password() user.set_unusable_password()
user.save() user.save()
else: else:
@ -340,29 +334,9 @@ class Outpost(models.Model):
# the ones the user needs # the ones the user needs
with transaction.atomic(): with transaction.atomic():
UserObjectPermission.objects.filter(user=user).delete() UserObjectPermission.objects.filter(user=user).delete()
user.user_permissions.clear() for model in self.get_required_objects():
for model_or_perm in self.get_required_objects(): code_name = f"{model._meta.app_label}.view_{model._meta.model_name}"
if isinstance(model_or_perm, models.Model): assign_perm(code_name, user, model)
model_or_perm: models.Model
code_name = (
f"{model_or_perm._meta.app_label}."
f"view_{model_or_perm._meta.model_name}"
)
assign_perm(code_name, user, model_or_perm)
else:
app_label, perm = model_or_perm.split(".")
permission = Permission.objects.filter(
codename=perm,
content_type__app_label=app_label,
)
if not permission.exists():
LOGGER.warning("permission doesn't exist", perm=model_or_perm)
continue
user.user_permissions.add(permission.first())
LOGGER.debug(
"Updated service account's permissions",
perms=UserObjectPermission.objects.filter(user=user),
)
return user return user
@property @property
@ -385,9 +359,9 @@ class Outpost(models.Model):
managed=f"goauthentik.io/outpost/{self.token_identifier}", managed=f"goauthentik.io/outpost/{self.token_identifier}",
) )
def get_required_objects(self) -> Iterable[Union[models.Model, str]]: def get_required_objects(self) -> Iterable[models.Model]:
"""Get an iterator of all objects the user needs read access to""" """Get an iterator of all objects the user needs read access to"""
objects: list[Union[models.Model, str]] = [self] objects = [self]
for provider in ( for provider in (
Provider.objects.filter(outpost=self).select_related().select_subclasses() Provider.objects.filter(outpost=self).select_related().select_subclasses()
): ):

View File

@ -1,16 +1,15 @@
"""authentik outpost signals""" """authentik outpost signals"""
from django.conf import settings from django.conf import settings
from django.db.models import Model from django.db.models import Model
from django.db.models.signals import post_save, pre_delete, pre_save from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import Provider from authentik.core.models import Provider
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.lib.utils.reflection import class_to_path from authentik.lib.utils.reflection import class_to_path
from authentik.outposts.controllers.base import ControllerException
from authentik.outposts.models import Outpost, OutpostServiceConnection from authentik.outposts.models import Outpost, OutpostServiceConnection
from authentik.outposts.tasks import outpost_controller_down, outpost_post_save from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete
LOGGER = get_logger() LOGGER = get_logger()
UPDATE_TRIGGERING_MODELS = ( UPDATE_TRIGGERING_MODELS = (
@ -21,27 +20,6 @@ UPDATE_TRIGGERING_MODELS = (
) )
@receiver(pre_save, sender=Outpost)
# pylint: disable=unused-argument
def pre_save_outpost(sender, instance: Outpost, **_):
"""Pre-save checks for an outpost, if the name or config.kubernetes_namespace changes,
we call down and then wait for the up after save"""
old_instances = Outpost.objects.filter(pk=instance.pk)
if not old_instances.exists():
return
old_instance = old_instances.first()
dirty = False
# Name changes the deployment name, need to recreate
dirty += old_instance.name != instance.name
# namespace requires re-create
dirty += (
old_instance.config.kubernetes_namespace != instance.config.kubernetes_namespace
)
if bool(dirty):
LOGGER.info("Outpost needs re-deployment due to changes", instance=instance)
outpost_controller_down_wrapper(old_instance)
@receiver(post_save) @receiver(post_save)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def post_save_update(sender, instance: Model, **_): def post_save_update(sender, instance: Model, **_):
@ -63,23 +41,15 @@ def post_save_update(sender, instance: Model, **_):
def pre_delete_cleanup(sender, instance: Outpost, **_): def pre_delete_cleanup(sender, instance: Outpost, **_):
"""Ensure that Outpost's user is deleted (which will delete the token through cascade)""" """Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
instance.user.delete() instance.user.delete()
outpost_controller_down_wrapper(instance) # To ensure that deployment is cleaned up *consistently* we call the controller, and wait
# for it to finish. We don't want to call it in this thread, as we don't have the Outpost
# Service connection here
def outpost_controller_down_wrapper(instance: Outpost):
"""To ensure that deployment is cleaned up *consistently* we call the controller, and wait
for it to finish. We don't want to call it in this thread, as we don't have the Outpost
Service connection here"""
try: try:
outpost_controller_down.delay(instance.pk.hex).get() outpost_pre_delete.delay(instance.pk.hex).get()
except RuntimeError: # pragma: no cover except RuntimeError:
# In e2e/integration tests, this might run inside a thread/process and # In e2e/integration tests, this might run inside a thread/process and
# trigger the celery `Never call result.get() within a task` detection # trigger the celery `Never call result.get() within a task` detection
if settings.TEST: if settings.TEST:
pass pass
else: else:
raise raise
except ControllerException as exc:
LOGGER.warning(
"failed to cleanup outpost deployment", exc=exc, instance=instance
)

View File

@ -3,7 +3,7 @@ from os import R_OK, access
from os.path import expanduser from os.path import expanduser
from pathlib import Path from pathlib import Path
from socket import gethostname from socket import gethostname
from typing import Any, Optional from typing import Any
from urllib.parse import urlparse from urllib.parse import urlparse
import yaml import yaml
@ -19,7 +19,7 @@ from structlog.stdlib import get_logger
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.lib.utils.reflection import path_to_class from authentik.lib.utils.reflection import path_to_class
from authentik.outposts.controllers.base import BaseController, ControllerException from authentik.outposts.controllers.base import ControllerException
from authentik.outposts.models import ( from authentik.outposts.models import (
DockerServiceConnection, DockerServiceConnection,
KubernetesServiceConnection, KubernetesServiceConnection,
@ -29,8 +29,6 @@ from authentik.outposts.models import (
OutpostState, OutpostState,
OutpostType, OutpostType,
) )
from authentik.providers.ldap.controllers.docker import LDAPDockerController
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
from authentik.providers.proxy.controllers.docker import ProxyDockerController from authentik.providers.proxy.controllers.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
@ -38,24 +36,6 @@ from authentik.root.celery import CELERY_APP
LOGGER = get_logger() LOGGER = get_logger()
def controller_for_outpost(outpost: Outpost) -> Optional[BaseController]:
"""Get a controller for the outpost, when a service connection is defined"""
if not outpost.service_connection:
return None
service_connection = outpost.service_connection
if outpost.type == OutpostType.PROXY:
if isinstance(service_connection, DockerServiceConnection):
return ProxyDockerController(outpost, service_connection)
if isinstance(service_connection, KubernetesServiceConnection):
return ProxyKubernetesController(outpost, service_connection)
if outpost.type == OutpostType.LDAP:
if isinstance(service_connection, DockerServiceConnection):
return LDAPDockerController(outpost, service_connection)
if isinstance(service_connection, KubernetesServiceConnection):
return LDAPKubernetesController(outpost, service_connection)
return None
@CELERY_APP.task() @CELERY_APP.task()
def outpost_controller_all(): def outpost_controller_all():
"""Launch Controller for all Outposts which support it""" """Launch Controller for all Outposts which support it"""
@ -96,10 +76,16 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str):
outpost: Outpost = Outpost.objects.get(pk=outpost_pk) outpost: Outpost = Outpost.objects.get(pk=outpost_pk)
self.set_uid(slugify(outpost.name)) self.set_uid(slugify(outpost.name))
try: try:
controller = controller_for_outpost(outpost) if not outpost.service_connection:
if not controller:
return return
logs = controller.up_with_logs() if outpost.type == OutpostType.PROXY:
service_connection = outpost.service_connection
if isinstance(service_connection, DockerServiceConnection):
logs = ProxyDockerController(outpost, service_connection).up_with_logs()
if isinstance(service_connection, KubernetesServiceConnection):
logs = ProxyKubernetesController(
outpost, service_connection
).up_with_logs()
LOGGER.debug("---------------Outpost Controller logs starting----------------") LOGGER.debug("---------------Outpost Controller logs starting----------------")
for log in logs: for log in logs:
LOGGER.debug(log) LOGGER.debug(log)
@ -111,13 +97,15 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str):
@CELERY_APP.task() @CELERY_APP.task()
def outpost_controller_down(outpost_pk: str): def outpost_pre_delete(outpost_pk: str):
"""Delete outpost objects before deleting the DB Object""" """Delete outpost objects before deleting the DB Object"""
outpost = Outpost.objects.get(pk=outpost_pk) outpost = Outpost.objects.get(pk=outpost_pk)
controller = controller_for_outpost(outpost) if outpost.type == OutpostType.PROXY:
if not controller: service_connection = outpost.service_connection
return if isinstance(service_connection, DockerServiceConnection):
controller.down() ProxyDockerController(outpost, service_connection).down()
if isinstance(service_connection, KubernetesServiceConnection):
ProxyKubernetesController(outpost, service_connection).down()
@CELERY_APP.task(bind=True, base=MonitoredTask) @CELERY_APP.task(bind=True, base=MonitoredTask)

View File

@ -1,84 +0,0 @@
# Generated by Django 3.2 on 2021-05-02 17:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_event_matcher", "0012_auto_20210323_1339"),
]
operations = [
migrations.AlterField(
model_name="eventmatcherpolicy",
name="app",
field=models.TextField(
blank=True,
choices=[
("authentik.admin", "authentik Admin"),
("authentik.api", "authentik API"),
("authentik.events", "authentik Events"),
("authentik.crypto", "authentik Crypto"),
("authentik.flows", "authentik Flows"),
("authentik.outposts", "authentik Outpost"),
("authentik.lib", "authentik lib"),
("authentik.policies", "authentik Policies"),
("authentik.policies.dummy", "authentik Policies.Dummy"),
(
"authentik.policies.event_matcher",
"authentik Policies.Event Matcher",
),
("authentik.policies.expiry", "authentik Policies.Expiry"),
("authentik.policies.expression", "authentik Policies.Expression"),
("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
("authentik.policies.password", "authentik Policies.Password"),
("authentik.policies.reputation", "authentik Policies.Reputation"),
("authentik.providers.proxy", "authentik Providers.Proxy"),
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
("authentik.providers.saml", "authentik Providers.SAML"),
("authentik.recovery", "authentik Recovery"),
("authentik.sources.ldap", "authentik Sources.LDAP"),
("authentik.sources.oauth", "authentik Sources.OAuth"),
("authentik.sources.plex", "authentik Sources.Plex"),
("authentik.sources.saml", "authentik Sources.SAML"),
(
"authentik.stages.authenticator_static",
"authentik Stages.Authenticator.Static",
),
(
"authentik.stages.authenticator_totp",
"authentik Stages.Authenticator.TOTP",
),
(
"authentik.stages.authenticator_validate",
"authentik Stages.Authenticator.Validate",
),
(
"authentik.stages.authenticator_webauthn",
"authentik Stages.Authenticator.WebAuthn",
),
("authentik.stages.captcha", "authentik Stages.Captcha"),
("authentik.stages.consent", "authentik Stages.Consent"),
("authentik.stages.deny", "authentik Stages.Deny"),
("authentik.stages.dummy", "authentik Stages.Dummy"),
("authentik.stages.email", "authentik Stages.Email"),
(
"authentik.stages.identification",
"authentik Stages.Identification",
),
("authentik.stages.invitation", "authentik Stages.User Invitation"),
("authentik.stages.password", "authentik Stages.Password"),
("authentik.stages.prompt", "authentik Stages.Prompt"),
("authentik.stages.user_delete", "authentik Stages.User Delete"),
("authentik.stages.user_login", "authentik Stages.User Login"),
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.core", "authentik Core"),
("authentik.managed", "authentik Managed"),
],
default="",
help_text="Match events created by selected application. When left empty, all applications are matched.",
),
),
]

View File

@ -1,85 +0,0 @@
# Generated by Django 3.2.1 on 2021-05-05 17:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_event_matcher", "0013_alter_eventmatcherpolicy_app"),
]
operations = [
migrations.AlterField(
model_name="eventmatcherpolicy",
name="app",
field=models.TextField(
blank=True,
choices=[
("authentik.admin", "authentik Admin"),
("authentik.api", "authentik API"),
("authentik.events", "authentik Events"),
("authentik.crypto", "authentik Crypto"),
("authentik.flows", "authentik Flows"),
("authentik.outposts", "authentik Outpost"),
("authentik.lib", "authentik lib"),
("authentik.policies", "authentik Policies"),
("authentik.policies.dummy", "authentik Policies.Dummy"),
(
"authentik.policies.event_matcher",
"authentik Policies.Event Matcher",
),
("authentik.policies.expiry", "authentik Policies.Expiry"),
("authentik.policies.expression", "authentik Policies.Expression"),
("authentik.policies.hibp", "authentik Policies.HaveIBeenPwned"),
("authentik.policies.password", "authentik Policies.Password"),
("authentik.policies.reputation", "authentik Policies.Reputation"),
("authentik.providers.proxy", "authentik Providers.Proxy"),
("authentik.providers.ldap", "authentik Providers.LDAP"),
("authentik.providers.oauth2", "authentik Providers.OAuth2"),
("authentik.providers.saml", "authentik Providers.SAML"),
("authentik.recovery", "authentik Recovery"),
("authentik.sources.ldap", "authentik Sources.LDAP"),
("authentik.sources.oauth", "authentik Sources.OAuth"),
("authentik.sources.plex", "authentik Sources.Plex"),
("authentik.sources.saml", "authentik Sources.SAML"),
(
"authentik.stages.authenticator_static",
"authentik Stages.Authenticator.Static",
),
(
"authentik.stages.authenticator_totp",
"authentik Stages.Authenticator.TOTP",
),
(
"authentik.stages.authenticator_validate",
"authentik Stages.Authenticator.Validate",
),
(
"authentik.stages.authenticator_webauthn",
"authentik Stages.Authenticator.WebAuthn",
),
("authentik.stages.captcha", "authentik Stages.Captcha"),
("authentik.stages.consent", "authentik Stages.Consent"),
("authentik.stages.deny", "authentik Stages.Deny"),
("authentik.stages.dummy", "authentik Stages.Dummy"),
("authentik.stages.email", "authentik Stages.Email"),
(
"authentik.stages.identification",
"authentik Stages.Identification",
),
("authentik.stages.invitation", "authentik Stages.User Invitation"),
("authentik.stages.password", "authentik Stages.Password"),
("authentik.stages.prompt", "authentik Stages.Prompt"),
("authentik.stages.user_delete", "authentik Stages.User Delete"),
("authentik.stages.user_login", "authentik Stages.User Login"),
("authentik.stages.user_logout", "authentik Stages.User Logout"),
("authentik.stages.user_write", "authentik Stages.User Write"),
("authentik.core", "authentik Core"),
("authentik.managed", "authentik Managed"),
],
default="",
help_text="Match events created by selected application. When left empty, all applications are matched.",
),
),
]

View File

@ -4,7 +4,7 @@
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% trans 'Permission denied' %} - {{ config.authentik.branding.title }} {% trans 'Permission denied - authentik' %}
{% endblock %} {% endblock %}
{% block card_title %} {% block card_title %}

View File

@ -1,54 +0,0 @@
"""LDAPProvider API Views"""
from rest_framework.fields import CharField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.providers.ldap.models import LDAPProvider
class LDAPProviderSerializer(ProviderSerializer):
"""LDAPProvider Serializer"""
class Meta:
model = LDAPProvider
fields = ProviderSerializer.Meta.fields + [
"base_dn",
"search_group",
]
class LDAPProviderViewSet(ModelViewSet):
"""LDAPProvider Viewset"""
queryset = LDAPProvider.objects.all()
serializer_class = LDAPProviderSerializer
ordering = ["name"]
class LDAPOutpostConfigSerializer(ModelSerializer):
"""LDAPProvider Serializer"""
application_slug = CharField(source="application.slug")
bind_flow_slug = CharField(source="authorization_flow.slug")
class Meta:
model = LDAPProvider
fields = [
"pk",
"name",
"base_dn",
"bind_flow_slug",
"application_slug",
"search_group",
]
class LDAPOutpostConfigViewSet(ReadOnlyModelViewSet):
"""LDAPProvider Viewset"""
queryset = LDAPProvider.objects.filter(application__isnull=False)
serializer_class = LDAPOutpostConfigSerializer
ordering = ["name"]

View File

@ -1,10 +0,0 @@
"""authentik ldap provider app config"""
from django.apps import AppConfig
class AuthentikProviderLDAPConfig(AppConfig):
"""authentik ldap provider app config"""
name = "authentik.providers.ldap"
label = "authentik_providers_ldap"
verbose_name = "authentik Providers.LDAP"

View File

@ -1,14 +0,0 @@
"""LDAP Provider Docker Contoller"""
from authentik.outposts.controllers.base import DeploymentPort
from authentik.outposts.controllers.docker import DockerController
from authentik.outposts.models import DockerServiceConnection, Outpost
class LDAPDockerController(DockerController):
"""LDAP Provider Docker Contoller"""
def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
super().__init__(outpost, connection)
self.deployment_ports = [
DeploymentPort(389, "ldap", "tcp", 3389),
]

View File

@ -1,14 +0,0 @@
"""LDAP Provider Kubernetes Contoller"""
from authentik.outposts.controllers.base import DeploymentPort
from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.outposts.models import KubernetesServiceConnection, Outpost
class LDAPKubernetesController(KubernetesController):
"""LDAP Provider Kubernetes Contoller"""
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
super().__init__(outpost, connection)
self.deployment_ports = [
DeploymentPort(389, "ldap", "tcp", 3389),
]

View File

@ -1,44 +0,0 @@
# Generated by Django 3.2 on 2021-04-26 12:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_core", "0019_source_managed"),
]
operations = [
migrations.CreateModel(
name="LDAPProvider",
fields=[
(
"provider_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.provider",
),
),
(
"base_dn",
models.TextField(
default="DC=ldap,DC=goauthentik,DC=io",
help_text="DN under which objects are accessible.",
),
),
],
options={
"verbose_name": "LDAP Provider",
"verbose_name_plural": "LDAP Providers",
},
bases=("authentik_core.provider", models.Model),
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 3.2 on 2021-04-26 19:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0019_source_managed"),
("authentik_providers_ldap", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="ldapprovider",
name="search_group",
field=models.ForeignKey(
default=None,
help_text="Users in this group can do search queries. If not set, every user can execute search queries.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_core.group",
),
),
]

View File

@ -1,55 +0,0 @@
"""LDAP Provider"""
from typing import Iterable, Optional, Type, Union
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import Group, Provider
from authentik.outposts.models import OutpostModel
class LDAPProvider(OutpostModel, Provider):
"""Allow applications to authenticate against authentik's users using LDAP."""
base_dn = models.TextField(
default="DC=ldap,DC=goauthentik,DC=io",
help_text=_("DN under which objects are accessible."),
)
search_group = models.ForeignKey(
Group,
null=True,
default=None,
on_delete=models.SET_DEFAULT,
help_text=_(
"Users in this group can do search queries. "
"If not set, every user can execute search queries."
),
)
@property
def launch_url(self) -> Optional[str]:
"""LDAP never has a launch URL"""
return None
@property
def component(self) -> str:
return "ak-provider-ldap-form"
@property
def serializer(self) -> Type[Serializer]:
from authentik.providers.ldap.api import LDAPProviderSerializer
return LDAPProviderSerializer
def __str__(self):
return f"LDAP Provider {self.name}"
def get_required_objects(self) -> Iterable[Union[models.Model, str]]:
return [self, "authentik_core.view_user", "authentik_core.view_group"]
class Meta:
verbose_name = _("LDAP Provider")
verbose_name_plural = _("LDAP Providers")

View File

@ -38,7 +38,6 @@ class OAuth2ProviderSerializer(ProviderSerializer):
"client_type", "client_type",
"client_id", "client_id",
"client_secret", "client_secret",
"access_code_validity",
"token_validity", "token_validity",
"include_claims_in_id_token", "include_claims_in_id_token",
"jwt_alg", "jwt_alg",

View File

@ -1,5 +1,4 @@
"""OAuth2Provider API Views""" """OAuth2Provider API Views"""
from guardian.utils import get_anonymous_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.fields import CharField, ListField from rest_framework.fields import CharField, ListField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
@ -39,10 +38,11 @@ class AuthorizationCodeViewSet(
ordering = ["provider", "expires"] ordering = ["provider", "expires"]
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user() if not self.request:
if user.is_superuser:
return super().get_queryset() return super().get_queryset()
return super().get_queryset().filter(user=user.pk) if self.request.user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=self.request.user)
class RefreshTokenViewSet( class RefreshTokenViewSet(
@ -59,7 +59,8 @@ class RefreshTokenViewSet(
ordering = ["provider", "expires"] ordering = ["provider", "expires"]
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user() if not self.request:
if user.is_superuser:
return super().get_queryset() return super().get_queryset()
return super().get_queryset().filter(user=user.pk) if self.request.user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=self.request.user)

View File

@ -1,11 +1,11 @@
"""authentik oauth provider app config""" """authentik auth oauth provider app config"""
from importlib import import_module from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
class AuthentikProviderOAuth2Config(AppConfig): class AuthentikProviderOAuth2Config(AppConfig):
"""authentik oauth provider app config""" """authentik auth oauth provider app config"""
name = "authentik.providers.oauth2" name = "authentik.providers.oauth2"
label = "authentik_providers_oauth2" label = "authentik_providers_oauth2"

View File

@ -1,24 +0,0 @@
# Generated by Django 3.2 on 2021-04-28 18:17
from django.db import migrations, models
import authentik.lib.utils.time
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0011_managed"),
]
operations = [
migrations.AddField(
model_name="oauth2provider",
name="access_code_validity",
field=models.TextField(
default="minutes=1",
help_text="Access codes not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3).",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
]

View File

@ -6,18 +6,18 @@ import time
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime from datetime import datetime
from hashlib import sha256 from hashlib import sha256
from typing import Any, Optional, Type, Union from typing import Any, Optional, Type
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import uuid4 from uuid import uuid4
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from dacite import from_dict from dacite import from_dict
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.http import HttpRequest from django.http import HttpRequest
from django.utils import dateformat, timezone from django.utils import dateformat, timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from jwt import encode from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key
from jwkest.jws import JWS
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
@ -175,16 +175,6 @@ class OAuth2Provider(Provider):
), ),
) )
access_code_validity = models.TextField(
default="minutes=1",
validators=[timedelta_string_validator],
help_text=_(
(
"Access codes not valid on or after current time + this value "
"(Format: hours=1;minutes=2;seconds=3)."
)
),
)
token_validity = models.TextField( token_validity = models.TextField(
default="minutes=10", default="minutes=10",
validators=[timedelta_string_validator], validators=[timedelta_string_validator],
@ -239,7 +229,7 @@ class OAuth2Provider(Provider):
token.access_token = token.create_access_token(user, request) token.access_token = token.create_access_token(user, request)
return token return token
def get_jwt_keys(self) -> Union[RSAPrivateKey, str]: def get_jwt_keys(self) -> list[Key]:
""" """
Takes a provider and returns the set of keys associated with it. Takes a provider and returns the set of keys associated with it.
Returns a list of keys. Returns a list of keys.
@ -256,10 +246,17 @@ class OAuth2Provider(Provider):
self.jwt_alg = JWTAlgorithms.HS256 self.jwt_alg = JWTAlgorithms.HS256
self.save() self.save()
else: else:
return self.rsa_key.private_key # Because the JWT Library uses python cryptodome,
# we can't directly pass the RSAPublicKey
# object, but have to load it ourselves
key = import_rsa_key(self.rsa_key.key_data)
keys = [RSAKey(key=key, kid=self.rsa_key.kid)]
if not keys:
raise Exception("You must add at least one RSA Key.")
return keys
if self.jwt_alg == JWTAlgorithms.HS256: if self.jwt_alg == JWTAlgorithms.HS256:
return self.client_secret return [SYMKey(key=self.client_secret, alg=self.jwt_alg)]
raise Exception("Unsupported key algorithm.") raise Exception("Unsupported key algorithm.")
@ -300,11 +297,11 @@ class OAuth2Provider(Provider):
def encode(self, payload: dict[str, Any]) -> str: def encode(self, payload: dict[str, Any]) -> str:
"""Represent the ID Token as a JSON Web Token (JWT).""" """Represent the ID Token as a JSON Web Token (JWT)."""
key = self.get_jwt_keys() keys = self.get_jwt_keys()
# If the provider does not have an RSA Key assigned, it was switched to Symmetric # If the provider does not have an RSA Key assigned, it was switched to Symmetric
self.refresh_from_db() self.refresh_from_db()
# pyright: reportGeneralTypeIssues=false jws = JWS(payload, alg=self.jwt_alg)
return encode(payload, key, algorithm=self.jwt_alg) return jws.sign_compact(keys)
class Meta: class Meta:

View File

@ -14,7 +14,7 @@
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{% trans 'End session' %} - {{ config.authentik.branding.title }} {% trans 'End session' %}
{% endblock %} {% endblock %}
{% block card_title %} {% block card_title %}

View File

@ -1,5 +1,5 @@
"""Test authorize view""" """Test authorize view"""
from django.test import RequestFactory from django.test import RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
@ -11,21 +11,17 @@ from authentik.providers.oauth2.errors import (
ClientIdError, ClientIdError,
RedirectUriError, RedirectUriError,
) )
from authentik.providers.oauth2.generators import ( from authentik.providers.oauth2.generators import generate_client_id
generate_client_id,
generate_client_secret,
)
from authentik.providers.oauth2.models import ( from authentik.providers.oauth2.models import (
AuthorizationCode, AuthorizationCode,
GrantTypes, GrantTypes,
OAuth2Provider, OAuth2Provider,
RefreshToken, RefreshToken,
) )
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams
class TestAuthorize(OAuthTestCase): class TestViewsAuthorize(TestCase):
"""Test authorize view""" """Test authorize view"""
def setUp(self) -> None: def setUp(self) -> None:
@ -204,7 +200,6 @@ class TestAuthorize(OAuthTestCase):
provider = OAuth2Provider.objects.create( provider = OAuth2Provider.objects.create(
name="test", name="test",
client_id="test", client_id="test",
client_secret=generate_client_secret(),
authorization_flow=flow, authorization_flow=flow,
redirect_uris="http://localhost", redirect_uris="http://localhost",
) )
@ -238,4 +233,3 @@ class TestAuthorize(OAuthTestCase):
), ),
}, },
) )
self.validate_jwt(token, provider)

View File

@ -1,11 +1,11 @@
"""Test token view""" """Test token view"""
from base64 import b64encode from base64 import b64encode
from django.test import RequestFactory from django.test import RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_str from django.utils.encoding import force_str
from authentik.core.models import Application, User from authentik.core.models import User
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.providers.oauth2.constants import ( from authentik.providers.oauth2.constants import (
GRANT_TYPE_AUTHORIZATION_CODE, GRANT_TYPE_AUTHORIZATION_CODE,
@ -20,17 +20,15 @@ from authentik.providers.oauth2.models import (
OAuth2Provider, OAuth2Provider,
RefreshToken, RefreshToken,
) )
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.token import TokenParams from authentik.providers.oauth2.views.token import TokenParams
class TestToken(OAuthTestCase): class TestViewsToken(TestCase):
"""Test token view""" """Test token view"""
def setUp(self) -> None: def setUp(self) -> None:
super().setUp() super().setUp()
self.factory = RequestFactory() self.factory = RequestFactory()
self.app = Application.objects.create(name="test", slug="test")
def test_request_auth_code(self): def test_request_auth_code(self):
"""test request param""" """test request param"""
@ -99,15 +97,12 @@ class TestToken(OAuthTestCase):
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
) )
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
self.app.save()
header = b64encode( header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode() f"{provider.client_id}:{provider.client_secret}".encode()
).decode() ).decode()
user = User.objects.get(username="akadmin") user = User.objects.get(username="akadmin")
code = AuthorizationCode.objects.create( code = AuthorizationCode.objects.create(
code="foobar", provider=provider, user=user, is_open_id=True code="foobar", provider=provider, user=user
) )
response = self.client.post( response = self.client.post(
reverse("authentik_providers_oauth2:token"), reverse("authentik_providers_oauth2:token"),
@ -131,7 +126,6 @@ class TestToken(OAuthTestCase):
), ),
}, },
) )
self.validate_jwt(new_token, provider)
def test_refresh_token_view(self): def test_refresh_token_view(self):
"""test request param""" """test request param"""
@ -142,9 +136,6 @@ class TestToken(OAuthTestCase):
authorization_flow=Flow.objects.first(), authorization_flow=Flow.objects.first(),
redirect_uris="http://local.invalid", redirect_uris="http://local.invalid",
) )
# Needs to be assigned to an application for iss to be set
self.app.provider = provider
self.app.save()
header = b64encode( header = b64encode(
f"{provider.client_id}:{provider.client_secret}".encode() f"{provider.client_id}:{provider.client_secret}".encode()
).decode() ).decode()
@ -183,7 +174,6 @@ class TestToken(OAuthTestCase):
), ),
}, },
) )
self.validate_jwt(new_token, provider)
def test_refresh_token_view_invalid_origin(self): def test_refresh_token_view_invalid_origin(self):
"""test request param""" """test request param"""

View File

@ -1,31 +0,0 @@
"""OAuth test helpers"""
from django.test import TestCase
from jwt import decode
from authentik.providers.oauth2.models import OAuth2Provider, RefreshToken
class OAuthTestCase(TestCase):
"""OAuth test helpers"""
required_jwt_keys = [
"exp",
"iat",
"auth_time",
"acr",
"sub",
"iss",
]
def validate_jwt(self, token: RefreshToken, provider: OAuth2Provider):
"""Validate that all required fields are set"""
jwt = decode(
token.access_token,
provider.client_secret,
algorithms=[provider.jwt_alg],
audience=provider.client_id,
)
id_token = token.id_token.to_dict()
for key in self.required_jwt_keys:
self.assertIsNotNone(jwt[key], f"Key {key} is missing in access_token")
self.assertIsNotNone(id_token[key], f"Key {key} is missing in id_token")

View File

@ -219,7 +219,7 @@ class OAuthAuthorizationParams:
code.code_challenge_method = self.code_challenge_method code.code_challenge_method = self.code_challenge_method
code.expires_at = timezone.now() + timedelta_from_string( code.expires_at = timezone.now() + timedelta_from_string(
self.provider.access_code_validity self.provider.token_validity
) )
code.scope = self.scope code.scope = self.scope
code.nonce = self.nonce code.nonce = self.nonce
@ -291,7 +291,7 @@ class OAuthFulfillmentStage(StageView):
GrantTypes.HYBRID, GrantTypes.HYBRID,
]: ]:
code = self.params.create_code(self.request) code = self.params.create_code(self.request)
code.save(force_insert=True) code.save()
if self.params.grant_type == GrantTypes.AUTHORIZATION_CODE: if self.params.grant_type == GrantTypes.AUTHORIZATION_CODE:
query_params["code"] = code.code query_params["code"] = code.code

View File

@ -104,6 +104,7 @@ class TokenIntrospectionView(View):
token: RefreshToken token: RefreshToken
params: TokenIntrospectionParams params: TokenIntrospectionParams
provider: OAuth2Provider provider: OAuth2Provider
id_token: IDToken
def post(self, request: HttpRequest) -> HttpResponse: def post(self, request: HttpRequest) -> HttpResponse:
"""Introspection handler""" """Introspection handler"""

View File

@ -1,23 +1,14 @@
"""authentik OAuth2 JWKS Views""" """authentik OAuth2 JWKS Views"""
from base64 import urlsafe_b64encode
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from django.http import HttpRequest, HttpResponse, JsonResponse from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.views import View from django.views import View
from jwkest import long_to_base64
from jwkest.jwk import import_rsa_key
from authentik.core.models import Application from authentik.core.models import Application
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
def b64_enc(number: int) -> str:
"""Convert number to base64-encoded octet-value"""
length = ((number).bit_length() + 7) // 8
number_bytes = number.to_bytes(length, "big")
final = urlsafe_b64encode(number_bytes).rstrip(b"=")
return final.decode("ascii")
class JWKSView(View): class JWKSView(View):
"""Show RSA Key data for Provider""" """Show RSA Key data for Provider"""
@ -31,16 +22,15 @@ class JWKSView(View):
response_data = {} response_data = {}
if provider.jwt_alg == JWTAlgorithms.RS256: if provider.jwt_alg == JWTAlgorithms.RS256:
public_key: RSAPublicKey = provider.rsa_key.private_key.public_key() public_key = import_rsa_key(provider.rsa_key.key_data).publickey()
public_numbers = public_key.public_numbers()
response_data["keys"] = [ response_data["keys"] = [
{ {
"kty": "RSA", "kty": "RSA",
"alg": "RS256", "alg": "RS256",
"use": "sig", "use": "sig",
"kid": provider.rsa_key.kid, "kid": provider.rsa_key.kid,
"n": b64_enc(public_numbers.n), "n": long_to_base64(public_key.n),
"e": b64_enc(public_numbers.e), "e": long_to_base64(public_key.e),
} }
] ]

View File

@ -16,7 +16,6 @@ from authentik.providers.oauth2.constants import (
from authentik.providers.oauth2.errors import TokenError, UserAuthError from authentik.providers.oauth2.errors import TokenError, UserAuthError
from authentik.providers.oauth2.models import ( from authentik.providers.oauth2.models import (
AuthorizationCode, AuthorizationCode,
ClientTypes,
OAuth2Provider, OAuth2Provider,
RefreshToken, RefreshToken,
) )
@ -76,7 +75,7 @@ class TokenParams:
LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id) LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id)
raise TokenError("invalid_client") raise TokenError("invalid_client")
if self.provider.client_type == ClientTypes.CONFIDENTIAL: if self.provider.client_type == "confidential":
if self.provider.client_secret != self.client_secret: if self.provider.client_secret != self.client_secret:
LOGGER.warning( LOGGER.warning(
"Invalid client secret: client does not have secret", "Invalid client secret: client does not have secret",

View File

@ -1,8 +1,5 @@
"""ProxyProvider API Views""" """ProxyProvider API Views"""
from typing import Any
from drf_yasg.utils import swagger_serializer_method from drf_yasg.utils import swagger_serializer_method
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
@ -33,17 +30,6 @@ class OpenIDConnectConfigurationSerializer(PassiveSerializer):
class ProxyProviderSerializer(ProviderSerializer): class ProxyProviderSerializer(ProviderSerializer):
"""ProxyProvider Serializer""" """ProxyProvider Serializer"""
def validate(self, attrs) -> dict[Any, str]:
"""Check that internal_host is set when forward_auth_mode is disabled"""
if (
not attrs.get("forward_auth_mode", False)
and attrs.get("internal_host", "") == ""
):
raise ValidationError(
"Internal host cannot be empty when forward auth is disabled."
)
return attrs
def create(self, validated_data): def create(self, validated_data):
instance: ProxyProvider = super().create(validated_data) instance: ProxyProvider = super().create(validated_data)
instance.set_oauth_defaults() instance.set_oauth_defaults()
@ -66,7 +52,6 @@ class ProxyProviderSerializer(ProviderSerializer):
"basic_auth_enabled", "basic_auth_enabled",
"basic_auth_password_attribute", "basic_auth_password_attribute",
"basic_auth_user_attribute", "basic_auth_user_attribute",
"forward_auth_mode",
] ]
@ -101,7 +86,6 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
"basic_auth_enabled", "basic_auth_enabled",
"basic_auth_password_attribute", "basic_auth_password_attribute",
"basic_auth_user_attribute", "basic_auth_user_attribute",
"forward_auth_mode",
] ]
@swagger_serializer_method(serializer_or_field=OpenIDConnectConfigurationSerializer) @swagger_serializer_method(serializer_or_field=OpenIDConnectConfigurationSerializer)

View File

@ -17,7 +17,6 @@ from kubernetes.client.models.networking_v1beta1_ingress_rule import (
from authentik.outposts.controllers.base import FIELD_MANAGER from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import ( from authentik.outposts.controllers.k8s.base import (
Disabled,
KubernetesObjectReconciler, KubernetesObjectReconciler,
NeedsUpdate, NeedsUpdate,
) )
@ -34,6 +33,10 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
super().__init__(controller) super().__init__(controller)
self.api = NetworkingV1beta1Api(controller.client) self.api = NetworkingV1beta1Api(controller.client)
@property
def name(self) -> str:
return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
def _check_annotations(self, reference: NetworkingV1beta1Ingress): def _check_annotations(self, reference: NetworkingV1beta1Ingress):
"""Check that all annotations *we* set are correct""" """Check that all annotations *we* set are correct"""
for key, value in self.get_ingress_annotations().items(): for key, value in self.get_ingress_annotations().items():
@ -51,8 +54,7 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
expected_hosts = [] expected_hosts = []
expected_hosts_tls = [] expected_hosts_tls = []
for proxy_provider in ProxyProvider.objects.filter( for proxy_provider in ProxyProvider.objects.filter(
outpost__in=[self.controller.outpost], outpost__in=[self.controller.outpost]
forward_auth_mode=False,
): ):
proxy_provider: ProxyProvider proxy_provider: ProxyProvider
external_host_name = urlparse(proxy_provider.external_host) external_host_name = urlparse(proxy_provider.external_host)
@ -100,46 +102,27 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
rules = [] rules = []
tls_hosts = [] tls_hosts = []
for proxy_provider in ProxyProvider.objects.filter( for proxy_provider in ProxyProvider.objects.filter(
outpost__in=[self.controller.outpost], outpost__in=[self.controller.outpost]
): ):
proxy_provider: ProxyProvider proxy_provider: ProxyProvider
external_host_name = urlparse(proxy_provider.external_host) external_host_name = urlparse(proxy_provider.external_host)
if external_host_name.scheme == "https": if external_host_name.scheme == "https":
tls_hosts.append(external_host_name.hostname) tls_hosts.append(external_host_name.hostname)
if proxy_provider.forward_auth_mode: rule = NetworkingV1beta1IngressRule(
rule = NetworkingV1beta1IngressRule( host=external_host_name.hostname,
host=external_host_name.hostname, http=NetworkingV1beta1HTTPIngressRuleValue(
http=NetworkingV1beta1HTTPIngressRuleValue( paths=[
paths=[ NetworkingV1beta1HTTPIngressPath(
NetworkingV1beta1HTTPIngressPath( backend=NetworkingV1beta1IngressBackend(
backend=NetworkingV1beta1IngressBackend( service_name=self.name,
service_name=self.name, service_port="http",
service_port="http", ),
), path="/",
path="/akprox", )
) ]
] ),
), )
)
else:
rule = NetworkingV1beta1IngressRule(
host=external_host_name.hostname,
http=NetworkingV1beta1HTTPIngressRuleValue(
paths=[
NetworkingV1beta1HTTPIngressPath(
backend=NetworkingV1beta1IngressBackend(
service_name=self.name,
service_port="http",
),
path="/",
)
]
),
)
rules.append(rule) rules.append(rule)
if not rules:
self.logger.debug("No providers use proxying, no ingress needed")
raise Disabled()
tls_config = None tls_config = None
if tls_hosts: if tls_hosts:
tls_config = NetworkingV1beta1IngressTLS( tls_config = NetworkingV1beta1IngressTLS(

View File

@ -1,162 +0,0 @@
"""Kubernetes Traefik Middleware Reconciler"""
from dataclasses import asdict, dataclass, field
from typing import TYPE_CHECKING
from dacite import from_dict
from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import (
Disabled,
KubernetesObjectReconciler,
NeedsUpdate,
)
from authentik.providers.proxy.models import ProxyProvider
if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController
@dataclass
class TraefikMiddlewareSpecForwardAuth:
"""traefik middleware forwardAuth spec"""
address: str
# pylint: disable=invalid-name
authResponseHeaders: list[str]
# pylint: disable=invalid-name
trustForwardHeader: bool
@dataclass
class TraefikMiddlewareSpec:
"""Traefik middleware spec"""
# pylint: disable=invalid-name
forwardAuth: TraefikMiddlewareSpecForwardAuth
@dataclass
class TraefikMiddlewareMetadata:
"""Traefik Middleware metadata"""
name: str
namespace: str
labels: dict = field(default_factory=dict)
@dataclass
class TraefikMiddleware:
"""Traefik Middleware"""
# pylint: disable=invalid-name
apiVersion: str
kind: str
metadata: TraefikMiddlewareMetadata
spec: TraefikMiddlewareSpec
CRD_NAME = "middlewares.traefik.containo.us"
CRD_GROUP = "traefik.containo.us"
CRD_VERSION = "v1alpha1"
CRD_PLURAL = "middlewares"
class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]):
"""Kubernetes Traefik Middleware Reconciler"""
def __init__(self, controller: "KubernetesController") -> None:
super().__init__(controller)
self.api_ex = ApiextensionsV1Api(controller.client)
self.api = CustomObjectsApi(controller.client)
def _crd_exists(self) -> bool:
"""Check if the traefik middleware exists"""
return bool(
len(
self.api_ex.list_custom_resource_definition(
field_selector=f"metadata.name={CRD_NAME}"
).items
)
)
def reconcile(self, current: TraefikMiddleware, reference: TraefikMiddleware):
super().reconcile(current, reference)
if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
raise NeedsUpdate()
def get_reference_object(self) -> TraefikMiddleware:
"""Get deployment object for outpost"""
if not ProxyProvider.objects.filter(
outpost__in=[self.controller.outpost],
forward_auth_mode=True,
).exists():
self.logger.debug("No providers with forward auth enabled.")
raise Disabled()
if not self._crd_exists():
self.logger.debug("CRD doesn't exist")
raise Disabled()
return TraefikMiddleware(
apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
kind="Middleware",
metadata=TraefikMiddlewareMetadata(
name=self.name,
namespace=self.namespace,
labels=self.get_object_meta().labels,
),
spec=TraefikMiddlewareSpec(
forwardAuth=TraefikMiddlewareSpecForwardAuth(
address=f"http://{self.name}.{self.namespace}:4180/akprox/auth?traefik",
authResponseHeaders=[
"Set-Cookie",
"X-Auth-Username",
"X-Forwarded-Email",
"X-Forwarded-Preferred-Username",
"X-Forwarded-User",
],
trustForwardHeader=True,
)
),
)
def create(self, reference: TraefikMiddleware):
return self.api.create_namespaced_custom_object(
group=CRD_GROUP,
version=CRD_VERSION,
plural=CRD_PLURAL,
namespace=self.namespace,
body=asdict(reference),
field_manager=FIELD_MANAGER,
)
def delete(self, reference: TraefikMiddleware):
return self.api.delete_namespaced_custom_object(
group=CRD_GROUP,
version=CRD_VERSION,
namespace=self.namespace,
plural=CRD_PLURAL,
name=self.name,
)
def retrieve(self) -> TraefikMiddleware:
return from_dict(
TraefikMiddleware,
self.api.get_namespaced_custom_object(
group=CRD_GROUP,
version=CRD_VERSION,
namespace=self.namespace,
plural=CRD_PLURAL,
name=self.name,
),
)
def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
return self.api.patch_namespaced_custom_object(
group=CRD_GROUP,
version=CRD_VERSION,
namespace=self.namespace,
plural=CRD_PLURAL,
name=self.name,
body=asdict(reference),
field_manager=FIELD_MANAGER,
)

View File

@ -3,9 +3,6 @@ from authentik.outposts.controllers.base import DeploymentPort
from authentik.outposts.controllers.kubernetes import KubernetesController from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.outposts.models import KubernetesServiceConnection, Outpost from authentik.outposts.models import KubernetesServiceConnection, Outpost
from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler
from authentik.providers.proxy.controllers.k8s.traefik import (
TraefikMiddlewareReconciler,
)
class ProxyKubernetesController(KubernetesController): class ProxyKubernetesController(KubernetesController):
@ -18,6 +15,4 @@ class ProxyKubernetesController(KubernetesController):
DeploymentPort(4443, "https", "tcp"), DeploymentPort(4443, "https", "tcp"),
] ]
self.reconcilers["ingress"] = IngressReconciler self.reconcilers["ingress"] = IngressReconciler
self.reconcilers["traefik middleware"] = TraefikMiddlewareReconciler
self.reconcile_order.append("ingress") self.reconcile_order.append("ingress")
self.reconcile_order.append("traefik middleware")

View File

@ -1,35 +0,0 @@
# Generated by Django 3.2 on 2021-04-27 18:47
from django.db import migrations, models
import authentik.lib.models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_proxy", "0010_auto_20201214_0942"),
]
operations = [
migrations.AddField(
model_name="proxyprovider",
name="forward_auth_mode",
field=models.BooleanField(
default=False,
help_text="Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with internal_host.",
),
),
migrations.AlterField(
model_name="proxyprovider",
name="internal_host",
field=models.TextField(
blank=True,
validators=[
authentik.lib.models.DomainlessURLValidator(
schemes=("http", "https")
)
],
),
),
]

View File

@ -1,7 +1,7 @@
"""authentik proxy models""" """authentik proxy models"""
import string import string
from random import SystemRandom from random import SystemRandom
from typing import Iterable, Optional, Type, Union from typing import Iterable, Optional, Type
from urllib.parse import urljoin from urllib.parse import urljoin
from django.db import models from django.db import models
@ -42,8 +42,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
Protocols by using a Reverse-Proxy.""" Protocols by using a Reverse-Proxy."""
internal_host = models.TextField( internal_host = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))], validators=[DomainlessURLValidator(schemes=("http", "https"))]
blank=True,
) )
external_host = models.TextField( external_host = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))] validators=[DomainlessURLValidator(schemes=("http", "https"))]
@ -53,13 +52,6 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
help_text=_("Validate SSL Certificates of upstream servers"), help_text=_("Validate SSL Certificates of upstream servers"),
verbose_name=_("Internal host SSL Validation"), verbose_name=_("Internal host SSL Validation"),
) )
forward_auth_mode = models.BooleanField(
default=False,
help_text=_(
"Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with "
"internal_host."
),
)
skip_path_regex = models.TextField( skip_path_regex = models.TextField(
default="", default="",
@ -147,7 +139,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
def __str__(self): def __str__(self):
return f"Proxy Provider {self.name}" return f"Proxy Provider {self.name}"
def get_required_objects(self) -> Iterable[Union[models.Model, str]]: def get_required_objects(self) -> Iterable[models.Model]:
required_models = [self] required_models = [self]
if self.certificate is not None: if self.certificate is not None:
required_models.append(self.certificate) required_models.append(self.certificate)

View File

@ -102,13 +102,11 @@ INSTALLED_APPS = [
"authentik.policies.password", "authentik.policies.password",
"authentik.policies.reputation", "authentik.policies.reputation",
"authentik.providers.proxy", "authentik.providers.proxy",
"authentik.providers.ldap",
"authentik.providers.oauth2", "authentik.providers.oauth2",
"authentik.providers.saml", "authentik.providers.saml",
"authentik.recovery", "authentik.recovery",
"authentik.sources.ldap", "authentik.sources.ldap",
"authentik.sources.oauth", "authentik.sources.oauth",
"authentik.sources.plex",
"authentik.sources.saml", "authentik.sources.saml",
"authentik.stages.authenticator_static", "authentik.stages.authenticator_static",
"authentik.stages.authenticator_totp", "authentik.stages.authenticator_totp",

View File

@ -1,4 +1,6 @@
"""authentik URL Configuration""" """authentik URL Configuration"""
from django.conf import settings
from django.conf.urls.static import static
from django.urls import include, path from django.urls import include, path
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
@ -47,3 +49,11 @@ urlpatterns += [
path("-/health/live/", LiveView.as_view(), name="health-live"), path("-/health/live/", LiveView.as_view(), name="health-live"),
path("-/health/ready/", ReadyView.as_view(), name="health-ready"), path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
] ]
if settings.DEBUG: # pragma: no cover
urlpatterns = (
static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+ urlpatterns
)

View File

@ -19,7 +19,7 @@ class BaseLDAPSynchronizer:
def __init__(self, source: LDAPSource): def __init__(self, source: LDAPSource):
self._source = source self._source = source
self._logger = get_logger().bind(source=source, syncer=self.__class__.__name__) self._logger = get_logger().bind(source=source)
@property @property
def base_dn_users(self) -> str: def base_dn_users(self) -> str:

View File

@ -1,5 +1,4 @@
"""OAuth Source Serializer""" """OAuth Source Serializer"""
from guardian.utils import get_anonymous_user
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
@ -27,7 +26,8 @@ class UserOAuthSourceConnectionViewSet(ModelViewSet):
filterset_fields = ["source__slug"] filterset_fields = ["source__slug"]
def get_queryset(self): def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user() if not self.request:
if user.is_superuser:
return super().get_queryset() return super().get_queryset()
return super().get_queryset().filter(user=user.pk) if self.request.user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=self.request.user)

View File

@ -2,21 +2,11 @@
from importlib import import_module from importlib import import_module
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
LOGGER = get_logger() LOGGER = get_logger()
AUTHENTIK_SOURCES_OAUTH_TYPES = [
"authentik.sources.oauth.types.discord",
"authentik.sources.oauth.types.facebook",
"authentik.sources.oauth.types.github",
"authentik.sources.oauth.types.google",
"authentik.sources.oauth.types.reddit",
"authentik.sources.oauth.types.twitter",
"authentik.sources.oauth.types.azure_ad",
"authentik.sources.oauth.types.oidc",
]
class AuthentikSourceOAuthConfig(AppConfig): class AuthentikSourceOAuthConfig(AppConfig):
"""authentik source.oauth config""" """authentik source.oauth config"""
@ -28,7 +18,7 @@ class AuthentikSourceOAuthConfig(AppConfig):
def ready(self): def ready(self):
"""Load source_types from config file""" """Load source_types from config file"""
for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES: for source_type in settings.AUTHENTIK_SOURCES_OAUTH_TYPES:
try: try:
import_module(source_type) import_module(source_type)
LOGGER.debug("Loaded OAuth Source Type", type=source_type) LOGGER.debug("Loaded OAuth Source Type", type=source_type)

View File

@ -0,0 +1,23 @@
"""authentik oauth_client Authorization backend"""
from typing import Optional
from django.contrib.auth.backends import ModelBackend
from django.http import HttpRequest
from authentik.core.models import User
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
class AuthorizedServiceBackend(ModelBackend):
"Authentication backend for users registered with remote OAuth provider."
def authenticate(
self, request: HttpRequest, source: OAuthSource, identifier: str
) -> Optional[User]:
"Fetch user for a given source by id."
access = UserOAuthSourceConnection.objects.filter(
source=source, identifier=identifier
).select_related("user")
if not access.exists():
return None
return access.first().user

View File

@ -9,7 +9,6 @@ from rest_framework.serializers import Serializer
from authentik.core.models import Source, UserSourceConnection from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton, UserSettingSerializer from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
if TYPE_CHECKING: if TYPE_CHECKING:
from authentik.sources.oauth.types.manager import SourceType from authentik.sources.oauth.types.manager import SourceType
@ -68,14 +67,9 @@ class OAuthSource(Source):
@property @property
def ui_login_button(self) -> UILoginButton: def ui_login_button(self) -> UILoginButton:
return UILoginButton( return UILoginButton(
challenge=RedirectChallenge( url=reverse(
instance={ "authentik_sources_oauth:oauth-client-login",
"type": ChallengeTypes.REDIRECT.value, kwargs={"source_slug": self.slug},
"to": reverse(
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug},
),
}
), ),
icon_url=static(f"authentik/sources/{self.provider_type}.svg"), icon_url=static(f"authentik/sources/{self.provider_type}.svg"),
name=self.name, name=self.name,
@ -169,6 +163,16 @@ class OpenIDOAuthSource(OAuthSource):
verbose_name_plural = _("OpenID OAuth Sources") verbose_name_plural = _("OpenID OAuth Sources")
class PlexOAuthSource(OAuthSource):
"""Login using plex.tv."""
class Meta:
abstract = True
verbose_name = _("Plex OAuth Source")
verbose_name_plural = _("Plex OAuth Sources")
class UserOAuthSourceConnection(UserSourceConnection): class UserOAuthSourceConnection(UserSourceConnection):
"""Authorized remote OAuth provider.""" """Authorized remote OAuth provider."""

View File

@ -0,0 +1,13 @@
"""Oauth2 Client Settings"""
AUTHENTIK_SOURCES_OAUTH_TYPES = [
"authentik.sources.oauth.types.discord",
"authentik.sources.oauth.types.facebook",
"authentik.sources.oauth.types.github",
"authentik.sources.oauth.types.google",
"authentik.sources.oauth.types.reddit",
"authentik.sources.oauth.types.twitter",
"authentik.sources.oauth.types.azure_ad",
"authentik.sources.oauth.types.oidc",
"authentik.sources.oauth.types.plex",
]

View File

@ -1,7 +1,7 @@
"""Discord Type tests""" """Discord Type tests"""
from django.test import TestCase from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.discord import DiscordOAuth2Callback from authentik.sources.oauth.types.discord import DiscordOAuth2Callback
# https://discord.com/developers/docs/resources/user#user-object # https://discord.com/developers/docs/resources/user#user-object
@ -18,7 +18,7 @@ DISCORD_USER = {
} }
class TestTypeDiscord(TestCase): class TestTypeGitHub(TestCase):
"""OAuth Source tests""" """OAuth Source tests"""
def setUp(self): def setUp(self):
@ -32,8 +32,10 @@ class TestTypeDiscord(TestCase):
) )
def test_enroll_context(self): def test_enroll_context(self):
"""Test discord Enrollment context""" """Test GitHub Enrollment context"""
ak_context = DiscordOAuth2Callback().get_user_enroll_context(DISCORD_USER) ak_context = DiscordOAuth2Callback().get_user_enroll_context(
self.source, UserOAuthSourceConnection(), DISCORD_USER
)
self.assertEqual(ak_context["username"], DISCORD_USER["username"]) self.assertEqual(ak_context["username"], DISCORD_USER["username"])
self.assertEqual(ak_context["email"], DISCORD_USER["email"]) self.assertEqual(ak_context["email"], DISCORD_USER["email"])
self.assertEqual(ak_context["name"], DISCORD_USER["username"]) self.assertEqual(ak_context["name"], DISCORD_USER["username"])

View File

@ -1,7 +1,7 @@
"""GitHub Type tests""" """GitHub Type tests"""
from django.test import TestCase from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.github import GitHubOAuth2Callback from authentik.sources.oauth.types.github import GitHubOAuth2Callback
# https://developer.github.com/v3/users/#get-the-authenticated-user # https://developer.github.com/v3/users/#get-the-authenticated-user
@ -63,7 +63,9 @@ class TestTypeGitHub(TestCase):
def test_enroll_context(self): def test_enroll_context(self):
"""Test GitHub Enrollment context""" """Test GitHub Enrollment context"""
ak_context = GitHubOAuth2Callback().get_user_enroll_context(GITHUB_USER) ak_context = GitHubOAuth2Callback().get_user_enroll_context(
self.source, UserOAuthSourceConnection(), GITHUB_USER
)
self.assertEqual(ak_context["username"], GITHUB_USER["login"]) self.assertEqual(ak_context["username"], GITHUB_USER["login"])
self.assertEqual(ak_context["email"], GITHUB_USER["email"]) self.assertEqual(ak_context["email"], GITHUB_USER["email"])
self.assertEqual(ak_context["name"], GITHUB_USER["name"]) self.assertEqual(ak_context["name"], GITHUB_USER["name"])

View File

@ -1,38 +0,0 @@
"""google Type tests"""
from django.test import TestCase
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.google import GoogleOAuth2Callback
# https://developers.google.com/identity/protocols/oauth2/openid-connect?hl=en
GOOGLE_USER = {
"id": "1324813249123401234",
"email": "foo@bar.baz",
"verified_email": True,
"name": "foo bar",
"given_name": "foo",
"family_name": "bar",
"picture": "",
"locale": "en",
}
class TestTypeGoogle(TestCase):
"""OAuth Source tests"""
def setUp(self):
self.source = OAuthSource.objects.create(
name="test",
slug="test",
provider_type="google",
authorization_url="",
profile_url="",
consumer_key="",
)
def test_enroll_context(self):
"""Test Google Enrollment context"""
ak_context = GoogleOAuth2Callback().get_user_enroll_context(GOOGLE_USER)
self.assertEqual(ak_context["username"], GOOGLE_USER["email"])
self.assertEqual(ak_context["email"], GOOGLE_USER["email"])
self.assertEqual(ak_context["name"], GOOGLE_USER["name"])

View File

@ -1,7 +1,7 @@
"""Twitter Type tests""" """Twitter Type tests"""
from django.test import Client, TestCase from django.test import Client, TestCase
from authentik.sources.oauth.models import OAuthSource from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
# https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \ # https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \
@ -104,7 +104,9 @@ class TestTypeGitHub(TestCase):
def test_enroll_context(self): def test_enroll_context(self):
"""Test Twitter Enrollment context""" """Test Twitter Enrollment context"""
ak_context = TwitterOAuthCallback().get_user_enroll_context(TWITTER_USER) ak_context = TwitterOAuthCallback().get_user_enroll_context(
self.source, UserOAuthSourceConnection(), TWITTER_USER
)
self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"]) self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"])
self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None)) self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None))
self.assertEqual(ak_context["name"], TWITTER_USER["name"]) self.assertEqual(ak_context["name"], TWITTER_USER["name"])

View File

@ -2,6 +2,7 @@
from typing import Any, Optional from typing import Any, Optional
from uuid import UUID from uuid import UUID
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
from authentik.sources.oauth.types.manager import MANAGER, SourceType from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback from authentik.sources.oauth.views.callback import OAuthCallback
@ -9,7 +10,7 @@ from authentik.sources.oauth.views.callback import OAuthCallback
class AzureADOAuthCallback(OAuthCallback): class AzureADOAuthCallback(OAuthCallback):
"""AzureAD OAuth2 Callback""" """AzureAD OAuth2 Callback"""
def get_user_id(self, info: dict[str, Any]) -> Optional[str]: def get_user_id(self, source: OAuthSource, info: dict[str, Any]) -> Optional[str]:
try: try:
return str(UUID(info.get("objectId")).int) return str(UUID(info.get("objectId")).int)
except TypeError: except TypeError:
@ -17,6 +18,8 @@ class AzureADOAuthCallback(OAuthCallback):
def get_user_enroll_context( def get_user_enroll_context(
self, self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any], info: dict[str, Any],
) -> dict[str, Any]: ) -> dict[str, Any]:
mail = info.get("mail", None) or info.get("otherMails", [None])[0] mail = info.get("mail", None) or info.get("otherMails", [None])[0]

Some files were not shown because too many files have changed in this diff Show More