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]
current_version = 2021.5.1-rc2
current_version = 2021.4.6
tag = True
commit = True
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/kubernetes.md]
[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:authentik/__init__.py]
[bumpversion:file:internal/constants/constants.go]
[bumpversion:file:outpost/pkg/version.go]
[bumpversion:file:web/src/constants.ts]

View File

@ -10,28 +10,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- 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
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.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
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
uses: docker/build-push-action@v2
with:
push: true
tags: |
beryju/authentik:2021.5.1-rc2,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.5.1-rc2,
ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
run: docker build
--no-cache
-t beryju/authentik:2021.4.6
-t beryju/authentik:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik:2021.4.6
- name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik:latest
build-proxy:
runs-on: ubuntu-latest
steps:
@ -44,67 +37,53 @@ jobs:
cd outpost
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/proxy/server.go
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
go build -v .
- name: Docker Login Registry
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
uses: docker/build-push-action@v2
with:
push: true
tags: |
beryju/authentik-proxy:2021.5.1-rc2,
beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.5.1-rc2,
ghcr.io/goauthentik/proxy:latest
context: outpost/
file: outpost/proxy.Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
build-ldap:
run: |
cd outpost/
docker build \
--no-cache \
-t beryju/authentik-proxy:2021.4.6 \
-t beryju/authentik-proxy:latest \
-f proxy.Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik-proxy:2021.4.6
- name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik-proxy:latest
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-go@v2
with:
go-version: "^1.15"
- name: prepare go api client
- name: prepare ts api client
run: |
cd outpost
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
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: Docker Login Registry
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
uses: docker/build-push-action@v2
with:
push: true
tags: |
beryju/authentik-ldap:2021.5.1-rc2,
beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.5.1-rc2,
ghcr.io/goauthentik/ldap:latest
context: outpost/
file: outpost/ldap.Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
run: |
cd web/
docker build \
--no-cache \
-t beryju/authentik-static:2021.4.6 \
-t beryju/authentik-static:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/authentik-static:2021.4.6
- name: Push Docker Container to Registry (latest)
run: docker push beryju/authentik-static:latest
test-release:
needs:
- build-server
- build-static
- build-proxy
- build-ldap
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
@ -124,12 +103,12 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Create a Sentry.io release
uses: getsentry/action-release@v1
uses: tclindner/sentry-releases-action@v1.2.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: beryjuorg
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.5.1-rc2
tagName: 2021.4.6
environment: beryjuorg-prod

View File

@ -11,9 +11,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- 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
run: |
sudo apt-get install -y pwgen
@ -28,6 +25,15 @@ jobs:
docker-compose up --no-start
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"
- 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
id: get_version
uses: actions/github-script@0.2.0
@ -45,3 +51,13 @@ jobs:
release_name: Release ${{ steps.get_version.outputs.result }}
draft: true
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/
media/
*mmdb
.idea/

View File

@ -1,4 +1,3 @@
# Stage 1: Lock python dependencies
FROM python:3.9-slim-buster as locker
COPY ./Pipfile /app/
@ -10,34 +9,6 @@ RUN pip install pipenv && \
pipenv lock -r > requirements.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
WORKDIR /
@ -73,7 +44,6 @@ COPY ./pyproject.toml /
COPY ./xml /xml
COPY ./manage.py /
COPY ./lifecycle/ /lifecycle
COPY --from=builder /work/authentik /authentik-proxy
USER authentik
STOPSIGNAL SIGINT

View File

@ -1,4 +1,4 @@
all: lint-fix lint test gen
all: lint-fix lint coverage gen
test-integration:
k3d cluster create || exit 0
@ -8,7 +8,7 @@ test-integration:
test-e2e:
coverage run manage.py test --failfast -v 3 tests/e2e
test:
coverage:
coverage run manage.py test -v 3 authentik
coverage html
coverage report
@ -22,7 +22,7 @@ lint:
bandit -r authentik tests lifecycle -x node_modules
pylint authentik tests lifecycle
gen:
gen: coverage
./manage.py generate_swagger -o swagger.yaml -f yaml
local-stack:
@ -31,5 +31,7 @@ local-stack:
docker-compose up -d
docker-compose run --rm server migrate
run:
go run -v cmd/server/main.go
build-static:
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 = "*"
psycopg2-binary = "*"
pycryptodome = "*"
pyjwt = "*"
pyjwkest = "*"
pyyaml = "*"
requests-oauthlib = "*"
sentry-sdk = "*"
@ -59,4 +59,3 @@ pylint-django = "*"
pytest = "*"
pytest-django = "*"
selenium = "*"
requests-mock = "*"

282
Pipfile.lock generated
View File

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

View File

@ -11,7 +11,6 @@
![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)
![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?

View File

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

View File

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

View File

@ -1,5 +1,5 @@
"""API Authentication"""
from base64 import b64decode
from base64 import b64decode, b64encode
from binascii import Error
from typing import Any, Optional, Union
@ -19,6 +19,14 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
auth_credentials = raw_header.decode()
if auth_credentials == "":
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()
if auth_type.lower() not in ["basic", "bearer"]:
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())

View File

@ -3,7 +3,7 @@
{% load static %}
{% block title %}
API Browser - {{ config.authentik.branding.title }}
authentik API Browser
{% endblock %}
{% 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,
UserReputationViewSet,
)
from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet
from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet
from authentik.providers.oauth2.api.scope import ScopeMappingViewSet
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 (
UserOAuthSourceConnectionViewSet,
)
from authentik.sources.plex.api import PlexSourceViewSet
from authentik.sources.saml.api import SAMLSourceViewSet
from authentik.stages.authenticator_static.api import (
AuthenticatorStaticStageViewSet,
@ -122,7 +120,6 @@ router.register(
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
)
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
router.register("outposts/ldap", LDAPOutpostConfigViewSet)
router.register("flows/instances", FlowViewSet)
router.register("flows/bindings", FlowStageBindingViewSet)
@ -139,7 +136,6 @@ router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewS
router.register("sources/ldap", LDAPSourceViewSet)
router.register("sources/saml", SAMLSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet)
router.register("sources/plex", PlexSourceViewSet)
router.register("policies/all", PolicyViewSet)
router.register("policies/bindings", PolicyBindingViewSet)
@ -153,7 +149,6 @@ router.register("policies/reputation/ips", IPReputationViewSet)
router.register("policies/reputation", ReputationPolicyViewSet)
router.register("providers/all", ProviderViewSet)
router.register("providers/ldap", LDAPProviderViewSet)
router.register("providers/proxy", ProxyProviderViewSet)
router.register("providers/oauth2", OAuth2ProviderViewSet)
router.register("providers/saml", SAMLProviderViewSet)

View File

@ -91,23 +91,6 @@ class ApplicationViewSet(ModelViewSet):
applications.append(application)
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(
manual_parameters=[
openapi.Parameter(

View File

@ -1,9 +1,7 @@
"""Groups API Viewset"""
from django.db.models.query import QuerySet
from rest_framework.fields import JSONField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.core.api.utils import is_dict
from authentik.core.models import Group
@ -28,16 +26,3 @@ class GroupViewSet(ModelViewSet):
search_fields = ["name", "is_superuser"]
filterset_fields = ["name", "is_superuser"]
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_plural",
"policy_engine_mode",
"user_matching_mode",
]

View File

@ -1,30 +1,18 @@
"""User API Views"""
from json import loads
from django.db.models.query import QuerySet
from django.http.response import Http404
from django.urls import reverse_lazy
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 guardian.utils import get_anonymous_user
from rest_framework.decorators import action
from rest_framework.fields import CharField, JSONField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import (
BooleanField,
ListSerializer,
ModelSerializer,
ValidationError,
)
from rest_framework.serializers import BooleanField, ModelSerializer
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.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.middleware import (
SESSION_IMPERSONATE_ORIGINAL_USER,
@ -41,8 +29,6 @@ class UserSerializer(ModelSerializer):
is_superuser = BooleanField(read_only=True)
avatar = CharField(read_only=True)
attributes = JSONField(validators=[is_dict], required=False)
groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
uid = CharField(read_only=True)
class Meta:
@ -54,11 +40,9 @@ class UserSerializer(ModelSerializer):
"is_active",
"last_login",
"is_superuser",
"groups",
"email",
"avatar",
"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):
"""User Viewset"""
queryset = User.objects.none()
serializer_class = UserSerializer
search_fields = ["username", "name", "is_active"]
filterset_class = UsersFilter
filterset_fields = ["username", "name", "is_active"]
def get_queryset(self):
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}"
)
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):
"""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()
def update(
self, instance: Model, validated_data: dict
) -> Model: # pragma: no cover
def update(self, instance: Model, validated_data: dict) -> 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()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
GRAVATAR_URL = "https://secure.gravatar.com"
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
@ -241,30 +240,6 @@ class Application(PolicyBindingModel):
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):
"""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",
)
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()
@property
@ -337,8 +301,6 @@ class UserSourceConnection(CreatedUpdatedModel):
user = models.ForeignKey(User, on_delete=models.CASCADE)
source = models.ForeignKey(Source, on_delete=models.CASCADE)
objects = InheritanceManager()
class Meta:
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/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/authentik.css' %}?v={{ ak_version }}">
{% block head_before %}
{% 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>window["polymerSkipLoadingFontRoboto"] = true;</script>
{% block head %}

View File

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

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"""
from time import sleep
from typing import Callable, Type
from django.test import TestCase
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Provider, Source, Token
from authentik.flows.models import Stage
from authentik.lib.utils.reflection import all_subclasses
from authentik.core.models import Token
class TestModels(TestCase):
@ -27,40 +24,3 @@ class TestModels(TestCase):
)
sleep(0.5)
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 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.flows.challenge import Challenge
@dataclass
@ -15,8 +14,8 @@ class UILoginButton:
# Name, ran through i18n
name: str
# Challenge which is presented to the user when they click the button
challenge: Challenge
# URL Which Button points to
url: str
# Icon URL, used as-is
icon_url: Optional[str] = None
@ -26,7 +25,7 @@ class UILoginButtonSerializer(PassiveSerializer):
"""Serializer for Login buttons of sources"""
name = CharField()
challenge = DictField()
url = CharField()
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"""
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"""
try:
load_pem_x509_certificate(value.encode("utf-8"), default_backend())
@ -47,7 +47,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
raise ValidationError("Unable to load certificate.")
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"""
# Since this field is optional, data can be empty.
if value != "":
@ -57,10 +57,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
password=None,
backend=default_backend(),
)
except (ValueError, TypeError):
raise ValidationError(
"Unable to load private key (possibly encrypted?)."
)
except ValueError:
raise ValidationError("Unable to load private key.")
return value
class Meta:

View File

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

View File

@ -37,8 +37,6 @@ class TestCrypto(TestCase):
"""Test Builder"""
builder = CertificateBuilder()
builder.common_name = "test-cert"
with self.assertRaises(ValueError):
builder.save()
builder.build(
subject_alt_names=[],
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.request import Request
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 authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
from authentik.core.api.utils import TypeCreateSerializer
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"""
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"""
application = DictField()
counted_events = 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):
"""Filter for events"""
@ -120,7 +132,7 @@ class EventViewSet(ReadOnlyModelViewSet):
def top_per_user(self, request: Request):
"""Get the top_n events grouped by user count"""
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(
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(action=filtered_action)

View File

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

View File

@ -1,6 +1,6 @@
"""Event notification tasks"""
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.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)
return
event: Event = events.first()
triggers: NotificationRule = NotificationRule.objects.filter(name=trigger_name)
if not triggers.exists():
return
trigger = triggers.first()
trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name)
if "policy_uuid" in event.context:
policy_uuid = event.context["policy_uuid"]
@ -61,13 +58,7 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
return
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()
)
except User.DoesNotExist:
LOGGER.warning("e(trigger): failed to get user", trigger=trigger)
return
user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
policy_engine = PolicyEngine(trigger, user)
policy_engine.mode = PolicyEngineMode.MODE_ANY
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 rest_framework.exceptions import ValidationError
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.transfer.common import (

View File

@ -86,13 +86,6 @@ class ConfigLoader:
url = urlparse(value)
if url.scheme == "env":
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
def update_from_file(self, path: str):
@ -170,7 +163,6 @@ class ConfigLoader:
# Walk each component of the path
path_parts = path.split(sep)
for comp in path_parts[:-1]:
# pyright: reportGeneralTypeIssues=false
if comp not in root:
root[comp] = {}
root = root.get(comp)

View File

@ -5,10 +5,6 @@ postgresql:
user: authentik
password: 'env://POSTGRES_PASSWORD'
web:
listen: 0.0.0.0:9000
listen_tls: 0.0.0.0:9443
redis:
host: localhost
password: ''
@ -38,10 +34,7 @@ email:
from: authentik@localhost
outposts:
# Placeholders:
# %(type)s: Outpost type; proxy, ldap, etc
# %(version)s: Current version; 2021.4.1
docker_image_base: "beryju/authentik-%(type)s:%(version)s"
docker_image_base: "beryju/authentik" # this is prepended to -proxy:version
authentik:
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
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]:
"""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
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]:
"""Attempt to get the client's IP by checking common HTTP Headers.
Returns none if no IP Could be found"""
if request:
override = _get_outpost_override_ip(request)
if override:
return override
return _get_client_ip_from_meta(request.META)
return None

View File

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

View File

@ -82,8 +82,7 @@ class OutpostConsumer(AuthJsonConsumer):
state.version = msg.args.get("version", None)
elif msg.instruction == WebsocketMessageInstruction.ACK:
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)
self.send_json(asdict(response))

View File

@ -1,12 +1,9 @@
"""Base Controller"""
from dataclasses import dataclass
from typing import Optional
from structlog.stdlib import get_logger
from structlog.testing import capture_logs
from authentik import __version__
from authentik.lib.config import CONFIG
from authentik.lib.sentry import SentryIgnoredException
from authentik.outposts.models import Outpost, OutpostServiceConnection
@ -24,7 +21,6 @@ class DeploymentPort:
port: int
name: str
protocol: str
inner_port: Optional[int] = None
class BaseController:
@ -59,8 +55,3 @@ class BaseController:
def get_static_deployment(self) -> str:
"""Return a static deployment configuration"""
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 authentik import __version__
from authentik.lib.config import CONFIG
from authentik.outposts.controllers.base import BaseController, ControllerException
from authentik.outposts.models import (
DockerServiceConnection,
@ -59,14 +60,15 @@ class DockerController(BaseController):
return self.client.containers.get(container_name), False
except NotFound:
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)
container_args = {
"image": image_name,
"name": f"authentik-proxy-{self.outpost.uuid.hex}",
"detach": True,
"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
},
"environment": self._get_env(),
@ -141,15 +143,15 @@ class DockerController(BaseController):
def get_static_deployment(self) -> str:
"""Generate docker-compose yaml for proxy, version 3.5"""
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
]
image_name = self.get_container_image()
image_prefix = CONFIG.y("outposts.docker_image_base")
compose = {
"version": "3.5",
"services": {
f"authentik_{self.outpost.type}": {
"image": image_name,
"image": f"{image_prefix}-{self.outpost.type}:{__version__}",
"ports": ports,
"environment": {
"AUTHENTIK_HOST": self.outpost.config.authentik_host,

View File

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

View File

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

View File

@ -26,6 +26,10 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
super().__init__(controller)
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):
super().reconcile(current, reference)
for key in reference.data.keys():

View File

@ -21,6 +21,10 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
super().__init__(controller)
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):
super().reconcile(current, reference)
if len(current.spec.ports) != len(reference.spec.ports):
@ -39,17 +43,13 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
name=port.name,
port=port.port,
protocol=port.protocol.upper(),
target_port=port.inner_port or port.port,
target_port=port.port,
)
)
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
return V1Service(
metadata=meta,
spec=V1ServiceSpec(
ports=ports,
selector=selector_labels,
type=self.controller.outpost.config.kubernetes_service_type,
),
spec=V1ServiceSpec(ports=ports, selector=selector_labels, type="ClusterIP"),
)
def create(self, reference: V1Service):

View File

@ -2,13 +2,14 @@
from io import StringIO
from typing import Type
from kubernetes.client import OpenApiException
from kubernetes.client.api_client import ApiClient
from kubernetes.client.exceptions import ApiException
from structlog.testing import capture_logs
from urllib3.exceptions import HTTPError
from yaml import dump_all
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.secret import SecretReconciler
from authentik.outposts.controllers.k8s.service import ServiceReconciler
@ -42,8 +43,8 @@ class KubernetesController(BaseController):
reconciler = self.reconcilers[reconcile_key](self)
reconciler.up()
except ApiException as exc:
raise ControllerException(str(exc)) from exc
except (OpenApiException, HTTPError) as exc:
raise ControllerException from exc
def up_with_logs(self) -> list[str]:
try:
@ -54,8 +55,8 @@ class KubernetesController(BaseController):
reconciler.up()
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
return all_logs
except ApiException as exc:
raise ControllerException(str(exc)) from exc
except (OpenApiException, HTTPError) as exc:
raise ControllerException from exc
def down(self):
try:
@ -63,17 +64,14 @@ class KubernetesController(BaseController):
reconciler = self.reconcilers[reconcile_key](self)
reconciler.down()
except ApiException as exc:
raise ControllerException(str(exc)) from exc
except OpenApiException as exc:
raise ControllerException from exc
def get_static_deployment(self) -> str:
documents = []
for reconcile_key in self.reconcile_order:
reconciler = self.reconcilers[reconcile_key](self)
try:
documents.append(reconciler.get_reference_object().to_dict())
except Disabled:
continue
documents.append(reconciler.get_reference_object().to_dict())
with StringIO() as _str:
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 dacite import from_dict
from django.contrib.auth.models import Permission
from django.core.cache import cache
from django.db import models, transaction
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.models import InheritanceForeignKey
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.http import USER_ATTRIBUTE_CAN_OVERRIDE_IP
from authentik.outposts.docker_tls import DockerInlineTLS
OUR_VERSION = parse(__version__)
@ -57,18 +55,16 @@ class OutpostConfig:
"error_reporting.environment", "customer"
)
object_naming_template: str = field(default="ak-outpost-%(name)s")
kubernetes_replicas: int = field(default=1)
kubernetes_namespace: str = field(default="default")
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
kubernetes_service_type: str = field(default="ClusterIP")
kubernetes_ingress_secret_name: str = field(default="authentik-outpost")
class OutpostModel(Model):
"""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 [self]
@ -81,7 +77,6 @@ class OutpostType(models.TextChoices):
"""Outpost types, currently only the reverse proxy is available"""
PROXY = "proxy"
LDAP = "ldap"
def default_outpost_config(host: Optional[str] = None):
@ -331,7 +326,6 @@ class Outpost(models.Model):
if not users.exists():
user: User = User.objects.create(username=self.user_identifier)
user.attributes[USER_ATTRIBUTE_SA] = True
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
user.set_unusable_password()
user.save()
else:
@ -340,29 +334,9 @@ class Outpost(models.Model):
# the ones the user needs
with transaction.atomic():
UserObjectPermission.objects.filter(user=user).delete()
user.user_permissions.clear()
for model_or_perm in self.get_required_objects():
if isinstance(model_or_perm, models.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),
)
for model in self.get_required_objects():
code_name = f"{model._meta.app_label}.view_{model._meta.model_name}"
assign_perm(code_name, user, model)
return user
@property
@ -385,9 +359,9 @@ class Outpost(models.Model):
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"""
objects: list[Union[models.Model, str]] = [self]
objects = [self]
for provider in (
Provider.objects.filter(outpost=self).select_related().select_subclasses()
):

View File

@ -1,16 +1,15 @@
"""authentik outpost signals"""
from django.conf import settings
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 structlog.stdlib import get_logger
from authentik.core.models import Provider
from authentik.crypto.models import CertificateKeyPair
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.tasks import outpost_controller_down, outpost_post_save
from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete
LOGGER = get_logger()
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)
# pylint: disable=unused-argument
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, **_):
"""Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
instance.user.delete()
outpost_controller_down_wrapper(instance)
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"""
# 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:
outpost_controller_down.delay(instance.pk.hex).get()
except RuntimeError: # pragma: no cover
outpost_pre_delete.delay(instance.pk.hex).get()
except RuntimeError:
# In e2e/integration tests, this might run inside a thread/process and
# trigger the celery `Never call result.get() within a task` detection
if settings.TEST:
pass
else:
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 pathlib import Path
from socket import gethostname
from typing import Any, Optional
from typing import Any
from urllib.parse import urlparse
import yaml
@ -19,7 +19,7 @@ from structlog.stdlib import get_logger
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
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 (
DockerServiceConnection,
KubernetesServiceConnection,
@ -29,8 +29,6 @@ from authentik.outposts.models import (
OutpostState,
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.kubernetes import ProxyKubernetesController
from authentik.root.celery import CELERY_APP
@ -38,24 +36,6 @@ from authentik.root.celery import CELERY_APP
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()
def outpost_controller_all():
"""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)
self.set_uid(slugify(outpost.name))
try:
controller = controller_for_outpost(outpost)
if not controller:
if not outpost.service_connection:
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----------------")
for log in logs:
LOGGER.debug(log)
@ -111,13 +97,15 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str):
@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"""
outpost = Outpost.objects.get(pk=outpost_pk)
controller = controller_for_outpost(outpost)
if not controller:
return
controller.down()
if outpost.type == OutpostType.PROXY:
service_connection = outpost.service_connection
if isinstance(service_connection, DockerServiceConnection):
ProxyDockerController(outpost, service_connection).down()
if isinstance(service_connection, KubernetesServiceConnection):
ProxyKubernetesController(outpost, service_connection).down()
@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 %}
{% block title %}
{% trans 'Permission denied' %} - {{ config.authentik.branding.title }}
{% trans 'Permission denied - authentik' %}
{% endblock %}
{% 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_id",
"client_secret",
"access_code_validity",
"token_validity",
"include_claims_in_id_token",
"jwt_alg",

View File

@ -1,5 +1,4 @@
"""OAuth2Provider API Views"""
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.fields import CharField, ListField
from rest_framework.serializers import ModelSerializer
@ -39,10 +38,11 @@ class AuthorizationCodeViewSet(
ordering = ["provider", "expires"]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
if not self.request:
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(
@ -59,7 +59,8 @@ class RefreshTokenViewSet(
ordering = ["provider", "expires"]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
if not self.request:
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 django.apps import AppConfig
class AuthentikProviderOAuth2Config(AppConfig):
"""authentik oauth provider app config"""
"""authentik auth oauth provider app config"""
name = "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 datetime import datetime
from hashlib import sha256
from typing import Any, Optional, Type, Union
from typing import Any, Optional, Type
from urllib.parse import urlparse
from uuid import uuid4
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from dacite import from_dict
from django.conf import settings
from django.db import models
from django.http import HttpRequest
from django.utils import dateformat, timezone
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 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(
default="minutes=10",
validators=[timedelta_string_validator],
@ -239,7 +229,7 @@ class OAuth2Provider(Provider):
token.access_token = token.create_access_token(user, request)
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.
Returns a list of keys.
@ -256,10 +246,17 @@ class OAuth2Provider(Provider):
self.jwt_alg = JWTAlgorithms.HS256
self.save()
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:
return self.client_secret
return [SYMKey(key=self.client_secret, alg=self.jwt_alg)]
raise Exception("Unsupported key algorithm.")
@ -300,11 +297,11 @@ class OAuth2Provider(Provider):
def encode(self, payload: dict[str, Any]) -> str:
"""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
self.refresh_from_db()
# pyright: reportGeneralTypeIssues=false
return encode(payload, key, algorithm=self.jwt_alg)
jws = JWS(payload, alg=self.jwt_alg)
return jws.sign_compact(keys)
class Meta:

View File

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

View File

@ -1,5 +1,5 @@
"""Test authorize view"""
from django.test import RequestFactory
from django.test import RequestFactory, TestCase
from django.urls import reverse
from django.utils.encoding import force_str
@ -11,21 +11,17 @@ from authentik.providers.oauth2.errors import (
ClientIdError,
RedirectUriError,
)
from authentik.providers.oauth2.generators import (
generate_client_id,
generate_client_secret,
)
from authentik.providers.oauth2.generators import generate_client_id
from authentik.providers.oauth2.models import (
AuthorizationCode,
GrantTypes,
OAuth2Provider,
RefreshToken,
)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams
class TestAuthorize(OAuthTestCase):
class TestViewsAuthorize(TestCase):
"""Test authorize view"""
def setUp(self) -> None:
@ -204,7 +200,6 @@ class TestAuthorize(OAuthTestCase):
provider = OAuth2Provider.objects.create(
name="test",
client_id="test",
client_secret=generate_client_secret(),
authorization_flow=flow,
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"""
from base64 import b64encode
from django.test import RequestFactory
from django.test import RequestFactory, TestCase
from django.urls import reverse
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.providers.oauth2.constants import (
GRANT_TYPE_AUTHORIZATION_CODE,
@ -20,17 +20,15 @@ from authentik.providers.oauth2.models import (
OAuth2Provider,
RefreshToken,
)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.token import TokenParams
class TestToken(OAuthTestCase):
class TestViewsToken(TestCase):
"""Test token view"""
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
self.app = Application.objects.create(name="test", slug="test")
def test_request_auth_code(self):
"""test request param"""
@ -99,15 +97,12 @@ class TestToken(OAuthTestCase):
authorization_flow=Flow.objects.first(),
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(
f"{provider.client_id}:{provider.client_secret}".encode()
).decode()
user = User.objects.get(username="akadmin")
code = AuthorizationCode.objects.create(
code="foobar", provider=provider, user=user, is_open_id=True
code="foobar", provider=provider, user=user
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
@ -131,7 +126,6 @@ class TestToken(OAuthTestCase):
),
},
)
self.validate_jwt(new_token, provider)
def test_refresh_token_view(self):
"""test request param"""
@ -142,9 +136,6 @@ class TestToken(OAuthTestCase):
authorization_flow=Flow.objects.first(),
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(
f"{provider.client_id}:{provider.client_secret}".encode()
).decode()
@ -183,7 +174,6 @@ class TestToken(OAuthTestCase):
),
},
)
self.validate_jwt(new_token, provider)
def test_refresh_token_view_invalid_origin(self):
"""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.expires_at = timezone.now() + timedelta_from_string(
self.provider.access_code_validity
self.provider.token_validity
)
code.scope = self.scope
code.nonce = self.nonce
@ -291,7 +291,7 @@ class OAuthFulfillmentStage(StageView):
GrantTypes.HYBRID,
]:
code = self.params.create_code(self.request)
code.save(force_insert=True)
code.save()
if self.params.grant_type == GrantTypes.AUTHORIZATION_CODE:
query_params["code"] = code.code

View File

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

View File

@ -1,23 +1,14 @@
"""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.shortcuts import get_object_or_404
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.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):
"""Show RSA Key data for Provider"""
@ -31,16 +22,15 @@ class JWKSView(View):
response_data = {}
if provider.jwt_alg == JWTAlgorithms.RS256:
public_key: RSAPublicKey = provider.rsa_key.private_key.public_key()
public_numbers = public_key.public_numbers()
public_key = import_rsa_key(provider.rsa_key.key_data).publickey()
response_data["keys"] = [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": provider.rsa_key.kid,
"n": b64_enc(public_numbers.n),
"e": b64_enc(public_numbers.e),
"n": long_to_base64(public_key.n),
"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.models import (
AuthorizationCode,
ClientTypes,
OAuth2Provider,
RefreshToken,
)
@ -76,7 +75,7 @@ class TokenParams:
LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id)
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:
LOGGER.warning(
"Invalid client secret: client does not have secret",

View File

@ -1,8 +1,5 @@
"""ProxyProvider API Views"""
from typing import Any
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.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
@ -33,17 +30,6 @@ class OpenIDConnectConfigurationSerializer(PassiveSerializer):
class ProxyProviderSerializer(ProviderSerializer):
"""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):
instance: ProxyProvider = super().create(validated_data)
instance.set_oauth_defaults()
@ -66,7 +52,6 @@ class ProxyProviderSerializer(ProviderSerializer):
"basic_auth_enabled",
"basic_auth_password_attribute",
"basic_auth_user_attribute",
"forward_auth_mode",
]
@ -101,7 +86,6 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
"basic_auth_enabled",
"basic_auth_password_attribute",
"basic_auth_user_attribute",
"forward_auth_mode",
]
@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.k8s.base import (
Disabled,
KubernetesObjectReconciler,
NeedsUpdate,
)
@ -34,6 +33,10 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
super().__init__(controller)
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):
"""Check that all annotations *we* set are correct"""
for key, value in self.get_ingress_annotations().items():
@ -51,8 +54,7 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
expected_hosts = []
expected_hosts_tls = []
for proxy_provider in ProxyProvider.objects.filter(
outpost__in=[self.controller.outpost],
forward_auth_mode=False,
outpost__in=[self.controller.outpost]
):
proxy_provider: ProxyProvider
external_host_name = urlparse(proxy_provider.external_host)
@ -100,46 +102,27 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
rules = []
tls_hosts = []
for proxy_provider in ProxyProvider.objects.filter(
outpost__in=[self.controller.outpost],
outpost__in=[self.controller.outpost]
):
proxy_provider: ProxyProvider
external_host_name = urlparse(proxy_provider.external_host)
if external_host_name.scheme == "https":
tls_hosts.append(external_host_name.hostname)
if proxy_provider.forward_auth_mode:
rule = NetworkingV1beta1IngressRule(
host=external_host_name.hostname,
http=NetworkingV1beta1HTTPIngressRuleValue(
paths=[
NetworkingV1beta1HTTPIngressPath(
backend=NetworkingV1beta1IngressBackend(
service_name=self.name,
service_port="http",
),
path="/akprox",
)
]
),
)
else:
rule = NetworkingV1beta1IngressRule(
host=external_host_name.hostname,
http=NetworkingV1beta1HTTPIngressRuleValue(
paths=[
NetworkingV1beta1HTTPIngressPath(
backend=NetworkingV1beta1IngressBackend(
service_name=self.name,
service_port="http",
),
path="/",
)
]
),
)
rule = NetworkingV1beta1IngressRule(
host=external_host_name.hostname,
http=NetworkingV1beta1HTTPIngressRuleValue(
paths=[
NetworkingV1beta1HTTPIngressPath(
backend=NetworkingV1beta1IngressBackend(
service_name=self.name,
service_port="http",
),
path="/",
)
]
),
)
rules.append(rule)
if not rules:
self.logger.debug("No providers use proxying, no ingress needed")
raise Disabled()
tls_config = None
if tls_hosts:
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.models import KubernetesServiceConnection, Outpost
from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler
from authentik.providers.proxy.controllers.k8s.traefik import (
TraefikMiddlewareReconciler,
)
class ProxyKubernetesController(KubernetesController):
@ -18,6 +15,4 @@ class ProxyKubernetesController(KubernetesController):
DeploymentPort(4443, "https", "tcp"),
]
self.reconcilers["ingress"] = IngressReconciler
self.reconcilers["traefik middleware"] = TraefikMiddlewareReconciler
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"""
import string
from random import SystemRandom
from typing import Iterable, Optional, Type, Union
from typing import Iterable, Optional, Type
from urllib.parse import urljoin
from django.db import models
@ -42,8 +42,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
Protocols by using a Reverse-Proxy."""
internal_host = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))],
blank=True,
validators=[DomainlessURLValidator(schemes=("http", "https"))]
)
external_host = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))]
@ -53,13 +52,6 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
help_text=_("Validate SSL Certificates of upstream servers"),
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(
default="",
@ -147,7 +139,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
def __str__(self):
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]
if self.certificate is not None:
required_models.append(self.certificate)

View File

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

View File

@ -1,4 +1,6 @@
"""authentik URL Configuration"""
from django.conf import settings
from django.conf.urls.static import static
from django.urls import include, path
from structlog.stdlib import get_logger
@ -47,3 +49,11 @@ urlpatterns += [
path("-/health/live/", LiveView.as_view(), name="health-live"),
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):
self._source = source
self._logger = get_logger().bind(source=source, syncer=self.__class__.__name__)
self._logger = get_logger().bind(source=source)
@property
def base_dn_users(self) -> str:

View File

@ -1,5 +1,4 @@
"""OAuth Source Serializer"""
from guardian.utils import get_anonymous_user
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
@ -27,7 +26,8 @@ class UserOAuthSourceConnectionViewSet(ModelViewSet):
filterset_fields = ["source__slug"]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
if not self.request:
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 django.apps import AppConfig
from django.conf import settings
from structlog.stdlib import 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):
"""authentik source.oauth config"""
@ -28,7 +18,7 @@ class AuthentikSourceOAuthConfig(AppConfig):
def ready(self):
"""Load source_types from config file"""
for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES:
for source_type in settings.AUTHENTIK_SOURCES_OAUTH_TYPES:
try:
import_module(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.types import UILoginButton, UserSettingSerializer
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
if TYPE_CHECKING:
from authentik.sources.oauth.types.manager import SourceType
@ -68,14 +67,9 @@ class OAuthSource(Source):
@property
def ui_login_button(self) -> UILoginButton:
return UILoginButton(
challenge=RedirectChallenge(
instance={
"type": ChallengeTypes.REDIRECT.value,
"to": reverse(
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug},
),
}
url=reverse(
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug},
),
icon_url=static(f"authentik/sources/{self.provider_type}.svg"),
name=self.name,
@ -169,6 +163,16 @@ class OpenIDOAuthSource(OAuthSource):
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):
"""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"""
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
# https://discord.com/developers/docs/resources/user#user-object
@ -18,7 +18,7 @@ DISCORD_USER = {
}
class TestTypeDiscord(TestCase):
class TestTypeGitHub(TestCase):
"""OAuth Source tests"""
def setUp(self):
@ -32,8 +32,10 @@ class TestTypeDiscord(TestCase):
)
def test_enroll_context(self):
"""Test discord Enrollment context"""
ak_context = DiscordOAuth2Callback().get_user_enroll_context(DISCORD_USER)
"""Test GitHub Enrollment context"""
ak_context = DiscordOAuth2Callback().get_user_enroll_context(
self.source, UserOAuthSourceConnection(), DISCORD_USER
)
self.assertEqual(ak_context["username"], DISCORD_USER["username"])
self.assertEqual(ak_context["email"], DISCORD_USER["email"])
self.assertEqual(ak_context["name"], DISCORD_USER["username"])

View File

@ -1,7 +1,7 @@
"""GitHub Type tests"""
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
# https://developer.github.com/v3/users/#get-the-authenticated-user
@ -63,7 +63,9 @@ class TestTypeGitHub(TestCase):
def test_enroll_context(self):
"""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["email"], GITHUB_USER["email"])
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"""
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
# 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):
"""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["email"], TWITTER_USER.get("email", None))
self.assertEqual(ak_context["name"], TWITTER_USER["name"])

View File

@ -2,6 +2,7 @@
from typing import Any, Optional
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.views.callback import OAuthCallback
@ -9,7 +10,7 @@ from authentik.sources.oauth.views.callback import OAuthCallback
class AzureADOAuthCallback(OAuthCallback):
"""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:
return str(UUID(info.get("objectId")).int)
except TypeError:
@ -17,6 +18,8 @@ class AzureADOAuthCallback(OAuthCallback):
def get_user_enroll_context(
self,
source: OAuthSource,
access: UserOAuthSourceConnection,
info: dict[str, Any],
) -> dict[str, Any]:
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