Compare commits
3 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
18778ce0d9 | |||
14973fb595 | |||
9171bd6d6f |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2021.5.1-rc2
|
current_version = 2021.4.6
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||||
@ -19,14 +19,20 @@ values =
|
|||||||
|
|
||||||
[bumpversion:file:website/docs/installation/docker-compose.md]
|
[bumpversion:file:website/docs/installation/docker-compose.md]
|
||||||
|
|
||||||
|
[bumpversion:file:website/docs/installation/kubernetes.md]
|
||||||
|
|
||||||
[bumpversion:file:docker-compose.yml]
|
[bumpversion:file:docker-compose.yml]
|
||||||
|
|
||||||
|
[bumpversion:file:helm/values.yaml]
|
||||||
|
|
||||||
|
[bumpversion:file:helm/README.md]
|
||||||
|
|
||||||
|
[bumpversion:file:helm/Chart.yaml]
|
||||||
|
|
||||||
[bumpversion:file:.github/workflows/release.yml]
|
[bumpversion:file:.github/workflows/release.yml]
|
||||||
|
|
||||||
[bumpversion:file:authentik/__init__.py]
|
[bumpversion:file:authentik/__init__.py]
|
||||||
|
|
||||||
[bumpversion:file:internal/constants/constants.go]
|
|
||||||
|
|
||||||
[bumpversion:file:outpost/pkg/version.go]
|
[bumpversion:file:outpost/pkg/version.go]
|
||||||
|
|
||||||
[bumpversion:file:web/src/constants.ts]
|
[bumpversion:file:web/src/constants.ts]
|
||||||
|
121
.github/workflows/release.yml
vendored
121
.github/workflows/release.yml
vendored
@ -10,28 +10,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Docker Login Registry
|
- name: Docker Login Registry
|
||||||
uses: docker/login-action@v1
|
env:
|
||||||
with:
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||||
- name: prepare ts api client
|
|
||||||
run: |
|
|
||||||
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
|
||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
uses: docker/build-push-action@v2
|
run: docker build
|
||||||
with:
|
--no-cache
|
||||||
push: true
|
-t beryju/authentik:2021.4.6
|
||||||
tags: |
|
-t beryju/authentik:latest
|
||||||
beryju/authentik:2021.5.1-rc2,
|
-f Dockerfile .
|
||||||
beryju/authentik:latest,
|
- name: Push Docker Container to Registry (versioned)
|
||||||
ghcr.io/goauthentik/server:2021.5.1-rc2,
|
run: docker push beryju/authentik:2021.4.6
|
||||||
ghcr.io/goauthentik/server:latest
|
- name: Push Docker Container to Registry (latest)
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
run: docker push beryju/authentik:latest
|
||||||
build-proxy:
|
build-proxy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -44,67 +37,53 @@ jobs:
|
|||||||
cd outpost
|
cd outpost
|
||||||
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||||
swagger generate client -f ../swagger.yaml -A authentik -t pkg/
|
swagger generate client -f ../swagger.yaml -A authentik -t pkg/
|
||||||
go build -v ./cmd/proxy/server.go
|
go build -v .
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Docker Login Registry
|
- name: Docker Login Registry
|
||||||
uses: docker/login-action@v1
|
env:
|
||||||
with:
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
uses: docker/build-push-action@v2
|
run: |
|
||||||
with:
|
cd outpost/
|
||||||
push: true
|
docker build \
|
||||||
tags: |
|
--no-cache \
|
||||||
beryju/authentik-proxy:2021.5.1-rc2,
|
-t beryju/authentik-proxy:2021.4.6 \
|
||||||
beryju/authentik-proxy:latest,
|
-t beryju/authentik-proxy:latest \
|
||||||
ghcr.io/goauthentik/proxy:2021.5.1-rc2,
|
-f proxy.Dockerfile .
|
||||||
ghcr.io/goauthentik/proxy:latest
|
- name: Push Docker Container to Registry (versioned)
|
||||||
context: outpost/
|
run: docker push beryju/authentik-proxy:2021.4.6
|
||||||
file: outpost/proxy.Dockerfile
|
- name: Push Docker Container to Registry (latest)
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
run: docker push beryju/authentik-proxy:latest
|
||||||
build-ldap:
|
build-static:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- uses: actions/setup-go@v2
|
- name: prepare ts api client
|
||||||
with:
|
|
||||||
go-version: "^1.15"
|
|
||||||
- name: prepare go api client
|
|
||||||
run: |
|
run: |
|
||||||
cd outpost
|
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
||||||
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
|
||||||
swagger generate client -f ../swagger.yaml -A authentik -t pkg/
|
|
||||||
go build -v ./cmd/ldap/server.go
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Docker Login Registry
|
- name: Docker Login Registry
|
||||||
uses: docker/login-action@v1
|
env:
|
||||||
with:
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
uses: docker/build-push-action@v2
|
run: |
|
||||||
with:
|
cd web/
|
||||||
push: true
|
docker build \
|
||||||
tags: |
|
--no-cache \
|
||||||
beryju/authentik-ldap:2021.5.1-rc2,
|
-t beryju/authentik-static:2021.4.6 \
|
||||||
beryju/authentik-ldap:latest,
|
-t beryju/authentik-static:latest \
|
||||||
ghcr.io/goauthentik/ldap:2021.5.1-rc2,
|
-f Dockerfile .
|
||||||
ghcr.io/goauthentik/ldap:latest
|
- name: Push Docker Container to Registry (versioned)
|
||||||
context: outpost/
|
run: docker push beryju/authentik-static:2021.4.6
|
||||||
file: outpost/ldap.Dockerfile
|
- name: Push Docker Container to Registry (latest)
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
run: docker push beryju/authentik-static:latest
|
||||||
test-release:
|
test-release:
|
||||||
needs:
|
needs:
|
||||||
- build-server
|
- build-server
|
||||||
|
- build-static
|
||||||
- build-proxy
|
- build-proxy
|
||||||
- build-ldap
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
@ -124,12 +103,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
- name: Create a Sentry.io release
|
- name: Create a Sentry.io release
|
||||||
uses: getsentry/action-release@v1
|
uses: tclindner/sentry-releases-action@v1.2.0
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: beryjuorg
|
SENTRY_ORG: beryjuorg
|
||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
version: authentik@2021.5.1-rc2
|
tagName: 2021.4.6
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
|
22
.github/workflows/tag.yml
vendored
22
.github/workflows/tag.yml
vendored
@ -11,9 +11,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: actions/checkout@master
|
||||||
- name: prepare ts api client
|
|
||||||
run: |
|
|
||||||
docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/api --additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
|
||||||
- name: Pre-release test
|
- name: Pre-release test
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y pwgen
|
sudo apt-get install -y pwgen
|
||||||
@ -28,6 +25,15 @@ jobs:
|
|||||||
docker-compose up --no-start
|
docker-compose up --no-start
|
||||||
docker-compose start postgresql redis
|
docker-compose start postgresql redis
|
||||||
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
|
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
|
||||||
|
- name: Install Helm
|
||||||
|
run: |
|
||||||
|
apt update && apt install -y curl
|
||||||
|
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
|
||||||
|
- name: Helm package
|
||||||
|
run: |
|
||||||
|
helm dependency update helm/
|
||||||
|
helm package helm/
|
||||||
|
mv authentik-*.tgz authentik-chart.tgz
|
||||||
- name: Extract version number
|
- name: Extract version number
|
||||||
id: get_version
|
id: get_version
|
||||||
uses: actions/github-script@0.2.0
|
uses: actions/github-script@0.2.0
|
||||||
@ -45,3 +51,13 @@ jobs:
|
|||||||
release_name: Release ${{ steps.get_version.outputs.result }}
|
release_name: Release ${{ steps.get_version.outputs.result }}
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
- name: Upload packaged Helm Chart
|
||||||
|
id: upload-release-asset
|
||||||
|
uses: actions/upload-release-asset@v1.0.1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./authentik-chart.tgz
|
||||||
|
asset_name: authentik-chart.tgz
|
||||||
|
asset_content_type: application/gzip
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -202,5 +202,3 @@ selenium_screenshots/
|
|||||||
backups/
|
backups/
|
||||||
media/
|
media/
|
||||||
*mmdb
|
*mmdb
|
||||||
|
|
||||||
.idea/
|
|
||||||
|
30
Dockerfile
30
Dockerfile
@ -1,4 +1,3 @@
|
|||||||
# Stage 1: Lock python dependencies
|
|
||||||
FROM python:3.9-slim-buster as locker
|
FROM python:3.9-slim-buster as locker
|
||||||
|
|
||||||
COPY ./Pipfile /app/
|
COPY ./Pipfile /app/
|
||||||
@ -10,34 +9,6 @@ RUN pip install pipenv && \
|
|||||||
pipenv lock -r > requirements.txt && \
|
pipenv lock -r > requirements.txt && \
|
||||||
pipenv lock -rd > requirements-dev.txt
|
pipenv lock -rd > requirements-dev.txt
|
||||||
|
|
||||||
# Stage 2: Build webui
|
|
||||||
FROM node as npm-builder
|
|
||||||
|
|
||||||
COPY ./web /static/
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
RUN cd /static && npm i --production=false && npm run build
|
|
||||||
|
|
||||||
# Stage 3: Build go proxy
|
|
||||||
FROM golang:1.16.4 AS builder
|
|
||||||
|
|
||||||
WORKDIR /work
|
|
||||||
|
|
||||||
COPY --from=npm-builder /static/robots.txt /work/web/robots.txt
|
|
||||||
COPY --from=npm-builder /static/security.txt /work/web/security.txt
|
|
||||||
COPY --from=npm-builder /static/dist/ /work/web/dist/
|
|
||||||
COPY --from=npm-builder /static/authentik/ /work/web/authentik/
|
|
||||||
|
|
||||||
# RUN ls /work/web/static/authentik/ && exit 1
|
|
||||||
COPY ./cmd /work/cmd
|
|
||||||
COPY ./web/static.go /work/web/static.go
|
|
||||||
COPY ./internal /work/internal
|
|
||||||
COPY ./go.mod /work/go.mod
|
|
||||||
COPY ./go.sum /work/go.sum
|
|
||||||
|
|
||||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
|
||||||
|
|
||||||
# Stage 4: Run
|
|
||||||
FROM python:3.9-slim-buster
|
FROM python:3.9-slim-buster
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
@ -73,7 +44,6 @@ COPY ./pyproject.toml /
|
|||||||
COPY ./xml /xml
|
COPY ./xml /xml
|
||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
COPY --from=builder /work/authentik /authentik-proxy
|
|
||||||
|
|
||||||
USER authentik
|
USER authentik
|
||||||
STOPSIGNAL SIGINT
|
STOPSIGNAL SIGINT
|
||||||
|
12
Makefile
12
Makefile
@ -1,4 +1,4 @@
|
|||||||
all: lint-fix lint test gen
|
all: lint-fix lint coverage gen
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
k3d cluster create || exit 0
|
k3d cluster create || exit 0
|
||||||
@ -8,7 +8,7 @@ test-integration:
|
|||||||
test-e2e:
|
test-e2e:
|
||||||
coverage run manage.py test --failfast -v 3 tests/e2e
|
coverage run manage.py test --failfast -v 3 tests/e2e
|
||||||
|
|
||||||
test:
|
coverage:
|
||||||
coverage run manage.py test -v 3 authentik
|
coverage run manage.py test -v 3 authentik
|
||||||
coverage html
|
coverage html
|
||||||
coverage report
|
coverage report
|
||||||
@ -22,7 +22,7 @@ lint:
|
|||||||
bandit -r authentik tests lifecycle -x node_modules
|
bandit -r authentik tests lifecycle -x node_modules
|
||||||
pylint authentik tests lifecycle
|
pylint authentik tests lifecycle
|
||||||
|
|
||||||
gen:
|
gen: coverage
|
||||||
./manage.py generate_swagger -o swagger.yaml -f yaml
|
./manage.py generate_swagger -o swagger.yaml -f yaml
|
||||||
|
|
||||||
local-stack:
|
local-stack:
|
||||||
@ -31,5 +31,7 @@ local-stack:
|
|||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
docker-compose run --rm server migrate
|
docker-compose run --rm server migrate
|
||||||
|
|
||||||
run:
|
build-static:
|
||||||
go run -v cmd/server/main.go
|
docker-compose -f scripts/ci.docker-compose.yml up -d
|
||||||
|
docker build -t beryju/authentik-static -f static.Dockerfile --network=scripts_default .
|
||||||
|
docker-compose -f scripts/ci.docker-compose.yml down -v
|
||||||
|
3
Pipfile
3
Pipfile
@ -32,7 +32,7 @@ lxml = ">=4.6.3"
|
|||||||
packaging = "*"
|
packaging = "*"
|
||||||
psycopg2-binary = "*"
|
psycopg2-binary = "*"
|
||||||
pycryptodome = "*"
|
pycryptodome = "*"
|
||||||
pyjwt = "*"
|
pyjwkest = "*"
|
||||||
pyyaml = "*"
|
pyyaml = "*"
|
||||||
requests-oauthlib = "*"
|
requests-oauthlib = "*"
|
||||||
sentry-sdk = "*"
|
sentry-sdk = "*"
|
||||||
@ -59,4 +59,3 @@ pylint-django = "*"
|
|||||||
pytest = "*"
|
pytest = "*"
|
||||||
pytest-django = "*"
|
pytest-django = "*"
|
||||||
selenium = "*"
|
selenium = "*"
|
||||||
requests-mock = "*"
|
|
||||||
|
282
Pipfile.lock
generated
282
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "17be2923cf8d281e430ec1467aea723806ac6f7c58fc6553ede92317e43f4d14"
|
"sha256": "a9d504f00ee8820017f26a4fda2938de456cb72b4bc2f8735fc8c6a6c615d46a"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -88,10 +88,10 @@
|
|||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3901be1cb7c2a780f14668691474d9252c070a756be0a9ead98cfeabfa11aeb8",
|
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||||
"sha256:8ee1e5f5a1afc5b19bdfae4fdf0c35ed324074bdce3500c939842c8f818645d9"
|
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||||
],
|
],
|
||||||
"version": "==21.1.0"
|
"version": "==20.3.0"
|
||||||
},
|
},
|
||||||
"autobahn": {
|
"autobahn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -116,25 +116,25 @@
|
|||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:56f1766f1271b6b4e979c7b56225377f8912050e5935adc5c1c9e3a0338b949e",
|
"sha256:1e55df93aa47a84e2a12a639c7f145e16e6e9ef959542d69d5526d50d2e92692",
|
||||||
"sha256:c61c809d288e88b9a0d926f56f803d0128b498aa9b45a42a6e03cd9a83e5c124"
|
"sha256:eab42daaaf68cdad5b112d31dcb0684162098f6558ba7b64156be44f993525fa"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.17.68"
|
"version": "==1.17.54"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0f693f5ad6348ec1a62b3a66fee2840d3b722d66b44896022d644275ff8b143d",
|
"sha256:20a864fc6570ba11d52532c72c3ccabab5c71a9b4a9418601a313d56f1d2ce5b",
|
||||||
"sha256:eb3544911cb0316a33b328a27d137130af278a9c0006be0c95e5e402b01d9865"
|
"sha256:37ec76ea2df8609540ba6cb0fe360ae1c589d2e1ee91eb642fd767823f3fcedd"
|
||||||
],
|
],
|
||||||
"version": "==1.20.68"
|
"version": "==1.20.54"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
|
"sha256:1d9d5f567be80f7c07d765e21b814326d78c61eb0c3a637dffc0e5d1796cb2e2",
|
||||||
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
|
"sha256:f469e29e7aa4cff64d8de4aad95ce76de8ea1125a16c68e0d93f65c3c3dc92e9"
|
||||||
],
|
],
|
||||||
"version": "==4.2.2"
|
"version": "==4.2.1"
|
||||||
},
|
},
|
||||||
"cbor2": {
|
"cbor2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -312,11 +312,11 @@
|
|||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0a1d195ad65c52bf275b8277b3d49680bd1137a5f55039a806f25f6b9752ce3d",
|
"sha256:0604e84c4fb698a5e53e5857b5aea945b2f19a18f25f10b8748dbdf935788927",
|
||||||
"sha256:18dd3145ddbd04bf189ff79b9954d08fda5171ea7b57bf705789fea766a07d50"
|
"sha256:21f0f9643722675976004eb683c55d33c05486f94506672df3d6a141546f389d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.2.2"
|
"version": "==3.2"
|
||||||
},
|
},
|
||||||
"django-dbbackup": {
|
"django-dbbackup": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -351,11 +351,11 @@
|
|||||||
},
|
},
|
||||||
"django-otp": {
|
"django-otp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:04852c5301befb02d1d8ba4a31d375eb08d7c2cb6fe86b5f840867435ab1309c",
|
"sha256:381a15e65293b8b06d47b7d6b306e0b7af2e104137ac92f6c566d3b9b90b6244",
|
||||||
"sha256:3916fc7652c2f934b1cf3807dd8ed257ce7605c10dfefa27fadda5628d9a9c9e"
|
"sha256:f4ab096b424c33ffe69453620356e1b7517f30dfb9ba13bfeaa1d1f20faddc13"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.0.4"
|
"version": "==1.0.3"
|
||||||
},
|
},
|
||||||
"django-prometheus": {
|
"django-prometheus": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -437,14 +437,13 @@
|
|||||||
},
|
},
|
||||||
"google-auth": {
|
"google-auth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:588bdb03a41ecb4978472b847881e5518b5d9ec6153d3d679aa127a55e13b39f",
|
"sha256:010f011c4e27d3d5eb01106fba6aac39d164842dfcd8709955c4638f5b11ccf8",
|
||||||
"sha256:9ad25fba07f46a628ad4d0ca09f38dcb262830df2ac95b217f9b0129c9e42206"
|
"sha256:f30a672a64d91cc2e3137765d088c5deec26416246f7a9e956eaf69a8d7ed49c"
|
||||||
],
|
],
|
||||||
"version": "==1.30.0"
|
"version": "==1.29.0"
|
||||||
},
|
},
|
||||||
"gunicorn": {
|
"gunicorn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
|
|
||||||
"sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
|
"sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
@ -505,23 +504,20 @@
|
|||||||
},
|
},
|
||||||
"httptools": {
|
"httptools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:07659649fe6b3948b6490825f89abe5eb1cec79ebfaaa0b4bf30f3f33f3c2ba8",
|
"sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
|
||||||
"sha256:08b79e09114e6ab5c3dbf560bba2cb2257ea38cdaeaf99b7cb80d8f92622fcd9",
|
"sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
|
||||||
"sha256:1e35aa179b67086cc600a984924a88589b90793c9c1b260152ca4908786e09df",
|
"sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
|
||||||
"sha256:31629e1f1b89959f8c0927bad12184dc07977dcf71e24f4772934aa490aa199b",
|
"sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
|
||||||
"sha256:851026bd63ec0af7e7592890d97d15c92b62d9e17094353f19a52c8e2b33710a",
|
"sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
|
||||||
"sha256:8fcca4b7efe353b13a24017211334c57d055a6e132c7adffed13a10d28efca57",
|
"sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
|
||||||
"sha256:9abd788465aa46a0f288bd3a99e53edd184177d6379e2098fd6097bb359ad9d6",
|
"sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
|
||||||
"sha256:aebdf0bd7bf7c90ae6b3be458692bf6e9e5b610b501f9f74c7979015a51db4c4",
|
"sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
|
||||||
"sha256:bda99a5723e7eab355ce57435c70853fc137a65aebf2f1cd4d15d96e2956da7b",
|
"sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
|
||||||
"sha256:c1c63d860749841024951b0a78e4dec6f543d23751ef061d6ab60064c7b8b524",
|
"sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
|
||||||
"sha256:c4111a0a8a00eff1e495d43ea5230aaf64968a48ddba8ea2d5f982efae827404",
|
"sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
|
||||||
"sha256:dce59ee45dd6ee6c434346a5ac527c44014326f560866b4b2f414a692ee1aca8",
|
"sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
|
||||||
"sha256:f759717ca1b2ef498c67ba4169c2b33eecf943a89f5329abcff8b89d153eb500",
|
|
||||||
"sha256:fb7199b8fb0c50a22e77260bb59017e0c075fa80cb03bb2c8692de76e7bb7fe7",
|
|
||||||
"sha256:fbf7ecd31c39728f251b1c095fd27c84e4d21f60a1d079a0333472ff3ae59d34"
|
|
||||||
],
|
],
|
||||||
"version": "==0.1.2"
|
"version": "==0.1.1"
|
||||||
},
|
},
|
||||||
"hyperlink": {
|
"hyperlink": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -607,24 +603,18 @@
|
|||||||
"sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d",
|
"sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d",
|
||||||
"sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3",
|
"sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3",
|
||||||
"sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2",
|
"sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2",
|
||||||
"sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae",
|
|
||||||
"sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f",
|
"sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f",
|
||||||
"sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927",
|
"sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927",
|
||||||
"sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3",
|
"sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3",
|
||||||
"sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7",
|
"sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7",
|
||||||
"sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59",
|
|
||||||
"sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f",
|
"sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f",
|
||||||
"sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade",
|
"sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade",
|
||||||
"sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96",
|
|
||||||
"sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468",
|
"sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468",
|
||||||
"sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b",
|
"sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b",
|
||||||
"sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4",
|
"sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4",
|
||||||
"sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354",
|
|
||||||
"sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83",
|
"sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83",
|
||||||
"sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04",
|
"sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04",
|
||||||
"sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16",
|
|
||||||
"sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791",
|
"sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791",
|
||||||
"sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a",
|
|
||||||
"sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51",
|
"sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51",
|
||||||
"sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1",
|
"sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1",
|
||||||
"sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a",
|
"sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a",
|
||||||
@ -637,14 +627,10 @@
|
|||||||
"sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa",
|
"sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa",
|
||||||
"sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106",
|
"sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106",
|
||||||
"sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d",
|
"sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d",
|
||||||
"sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617",
|
|
||||||
"sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4",
|
"sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4",
|
||||||
"sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92",
|
|
||||||
"sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0",
|
"sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0",
|
||||||
"sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4",
|
"sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4",
|
||||||
"sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24",
|
|
||||||
"sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2",
|
"sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2",
|
||||||
"sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e",
|
|
||||||
"sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0",
|
"sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0",
|
||||||
"sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654",
|
"sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654",
|
||||||
"sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2",
|
"sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2",
|
||||||
@ -919,6 +905,41 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.10.1"
|
"version": "==3.10.1"
|
||||||
},
|
},
|
||||||
|
"pycryptodomex": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00a584ee52bf5e27d540129ca9bf7c4a7e7447f24ff4a220faa1304ad0c09bcd",
|
||||||
|
"sha256:04265a7a84ae002001249bd1de2823bcf46832bd4b58f6965567cb8a07cf4f00",
|
||||||
|
"sha256:0bd35af6a18b724c689e56f2dbbdd8e409288be71952d271ba3d9614b31d188c",
|
||||||
|
"sha256:20c45a30f3389148f94edb77f3b216c677a277942f62a2b81a1cc0b6b2dde7fc",
|
||||||
|
"sha256:2959304d1ce31ab303d9fb5db2b294814278b35154d9b30bf7facc52d6088d0a",
|
||||||
|
"sha256:36dab7f506948056ceba2d57c1ade74e898401960de697cefc02f3519bd26c1b",
|
||||||
|
"sha256:37ec1b407ec032c7a0c1fdd2da12813f560bad38ae61ad9c7ce3c0573b3e5e30",
|
||||||
|
"sha256:3b8eb85b3cc7f083d87978c264d10ff9de3b4bfc46f1c6fdc2792e7d7ebc87bb",
|
||||||
|
"sha256:3dfce70c4e425607ae87b8eae67c9c7dbba59a33b62d70f79417aef0bc5c735b",
|
||||||
|
"sha256:418f51c61eab52d9920f4ef468d22c89dab1be5ac796f71cf3802f6a6e667df0",
|
||||||
|
"sha256:4195604f75cdc1db9bccdb9e44d783add3c817319c30aaff011670c9ed167690",
|
||||||
|
"sha256:4344ab16faf6c2d9df2b6772995623698fb2d5f114dace4ab2ff335550cf71d5",
|
||||||
|
"sha256:541cd3e3e252fb19a7b48f420b798b53483302b7fe4d9954c947605d0a263d62",
|
||||||
|
"sha256:564063e3782474c92cbb333effd06e6eb718471783c6e67f28c63f0fc3ac7b23",
|
||||||
|
"sha256:72f44b5be46faef2a1bf2a85902511b31f4dd7b01ce0c3978e92edb2cc812a82",
|
||||||
|
"sha256:8a98e02cbf8f624add45deff444539bf26345b479fc04fa0937b23cd84078d91",
|
||||||
|
"sha256:940db96449d7b2ebb2c7bf190be1514f3d67914bd37e54e8d30a182bd375a1a9",
|
||||||
|
"sha256:961333e7ee896651f02d4692242aa36b787b8e8e0baa2256717b2b9d55ae0a3c",
|
||||||
|
"sha256:9f713ffb4e27b5575bd917c70bbc3f7b348241a351015dbbc514c01b7061ff7e",
|
||||||
|
"sha256:a6584ae58001d17bb4dc0faa8a426919c2c028ef4d90ceb4191802ca6edb8204",
|
||||||
|
"sha256:c2b680987f418858e89dbb4f09c8c919ece62811780a27051ace72b2f69fb1be",
|
||||||
|
"sha256:d8fae5ba3d34c868ae43614e0bd6fb61114b2687ac3255798791ce075d95aece",
|
||||||
|
"sha256:dbd2c361db939a4252589baa94da4404d45e3fc70da1a31e541644cdf354336e",
|
||||||
|
"sha256:e090a8609e2095aa86978559b140cf8968af99ee54b8791b29ff804838f29f10",
|
||||||
|
"sha256:e4a1245e7b846e88ba63e7543483bda61b9acbaee61eadbead5a1ce479d94740",
|
||||||
|
"sha256:ec9901d19cadb80d9235ee41cc58983f18660314a0eb3fc7b11b0522ac3b6c4a",
|
||||||
|
"sha256:f2abeb4c4ce7584912f4d637b2c57f23720d35dd2892bfeb1b2c84b6fb7a8c88",
|
||||||
|
"sha256:f3bb267df679f70a9f40f17d62d22fe12e8b75e490f41807e7560de4d3e6bf9f",
|
||||||
|
"sha256:f933ecf4cb736c7af60a6a533db2bf569717f2318b265f92907acff1db43bc34",
|
||||||
|
"sha256:fc9c55dc1ed57db76595f2d19a479fc1c3a1be2c9da8de798a93d286c5f65f38"
|
||||||
|
],
|
||||||
|
"version": "==3.10.1"
|
||||||
|
},
|
||||||
"pyhamcrest": {
|
"pyhamcrest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
||||||
@ -926,13 +947,12 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.0.2"
|
"version": "==2.0.2"
|
||||||
},
|
},
|
||||||
"pyjwt": {
|
"pyjwkest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1",
|
"sha256:5560fd5ba08655f29ff6ad1df1e15dc05abc9d976fcbcec8d2b5167f49b70222"
|
||||||
"sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.1.0"
|
"version": "==1.4.2"
|
||||||
},
|
},
|
||||||
"pyopenssl": {
|
"pyopenssl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -963,10 +983,10 @@
|
|||||||
},
|
},
|
||||||
"python-dotenv": {
|
"python-dotenv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544",
|
"sha256:471b782da0af10da1a80341e8438fca5fadeba2881c54360d5fd8d03d03a4f4a",
|
||||||
"sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"
|
"sha256:49782a97c9d641e8a09ae1d9af0856cc587c8d2474919342d5104d85be9890b2"
|
||||||
],
|
],
|
||||||
"version": "==0.17.1"
|
"version": "==0.17.0"
|
||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1086,18 +1106,18 @@
|
|||||||
},
|
},
|
||||||
"s3transfer": {
|
"s3transfer": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc",
|
"sha256:af1af6384bd7fb8208b06480f9be73d0295d965c4c073a5c95ea5b6661dccc18",
|
||||||
"sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2"
|
"sha256:f3dfd791cad2799403e3c8051810a7ca6ee1d2e630e5d2a8f9649d892bdb3db6"
|
||||||
],
|
],
|
||||||
"version": "==0.4.2"
|
"version": "==0.4.0"
|
||||||
},
|
},
|
||||||
"sentry-sdk": {
|
"sentry-sdk": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c1227d38dca315ba35182373f129c3e2722e8ed999e52584e6aca7d287870739",
|
"sha256:71de00c9711926816f750bc0f57ef2abbcb1bfbdf5378c601df7ec978f44857a",
|
||||||
"sha256:c7d380a21281e15be3d9f67a3c4fbb4f800c481d88ff8d8931f39486dd7b4ada"
|
"sha256:9221e985f425913204989d0e0e1cbb719e8b7fa10540f1bc509f660c06a34e66"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.1.0"
|
"version": "==1.0.0"
|
||||||
},
|
},
|
||||||
"service-identity": {
|
"service-identity": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1109,10 +1129,10 @@
|
|||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||||
],
|
],
|
||||||
"version": "==1.16.0"
|
"version": "==1.15.0"
|
||||||
},
|
},
|
||||||
"sqlparse": {
|
"sqlparse": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1178,11 +1198,11 @@
|
|||||||
},
|
},
|
||||||
"typing-extensions": {
|
"typing-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
|
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
|
||||||
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
|
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
|
||||||
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
|
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
|
||||||
],
|
],
|
||||||
"version": "==3.10.0.0"
|
"version": "==3.7.4.3"
|
||||||
},
|
},
|
||||||
"uritemplate": {
|
"uritemplate": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1259,10 +1279,10 @@
|
|||||||
},
|
},
|
||||||
"websocket-client": {
|
"websocket-client": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2e50d26ca593f70aba7b13a489435ef88b8fc3b5c5643c1ce8808ff9b40f0b32",
|
"sha256:44b5df8f08c74c3d82d28100fdc81f4536809ce98a17f0757557813275fbb663",
|
||||||
"sha256:d376bd60eace9d437ab6d7ee16f4ab4e821c9dae591e1b783c58ebd8aaf80c5c"
|
"sha256:63509b41d158ae5b7f67eb4ad20fecbb4eee99434e73e140354dc3ff8e09716f"
|
||||||
],
|
],
|
||||||
"version": "==0.59.0"
|
"version": "==0.58.0"
|
||||||
},
|
},
|
||||||
"websockets": {
|
"websockets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1293,20 +1313,22 @@
|
|||||||
},
|
},
|
||||||
"xmlsec": {
|
"xmlsec": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:17d2e66d4e3e601d210eed936b53c3eb44cddaef62f60b5c6ad5c18e948d926c",
|
"sha256:252f79ed4482d6eefcca62c3bfc99b8d95c07abd846262d854a207ec4d67fac5",
|
||||||
"sha256:2bc1b871b49d6580779805a4a1c2d835e834a2fa614fe40cf71931d11a8279cf",
|
"sha256:31884dc97cc34cf1681a0f239f613969e61f9a01f4c2d2a62e53d68216fe42d6",
|
||||||
"sha256:52eded125c0d1ab72125105ef061370c6b06ab9bd37e29a61bc2f8a61205bae4",
|
"sha256:32a669dfe447bccecdb4ef79221c0452ce6dad919f3a75daf512792141a54dac",
|
||||||
"sha256:72af9a5a747a5fe6e425d2be10daa43d18307dbe03498df3820fc3cd93daa148",
|
"sha256:3d13d7b6cb921dbc4d60d00ad00081a038df73a1e69f5bcc3695deb1bf2093b0",
|
||||||
"sha256:806855d505da24aeb77758a6f373b1473e5ed63bdbe346af90cc6d2b053e4716",
|
"sha256:5e2f263a21fd146859911479ec35e40a57f519e650f56c775f91367d2a1b6e15",
|
||||||
"sha256:8746dd992aaec06ed8ff1615f4a8e2a32258e8af38f9a9f8acf3ee1fb34a5da6",
|
"sha256:61076be98da4c7cf842a78aa3f129a5039f2ba4992e02480eefe78028d317698",
|
||||||
"sha256:9d52b2b15d42292725e4f9d8a5b040e39cba0a9cd58059ac951e7310d6340bb9",
|
"sha256:69d7f965d6b74b3266f7baa99a0377d9c76acbf26c615b4ee8d2cbe17bf85528",
|
||||||
"sha256:b380f3ebc042f71afab057632481d06e06f1ba4f90047d91ca92612a7d3d487b",
|
"sha256:6d8bb24c3a4db398011f394e29b58cd34c9c26d76b772c5d418d8579df127234",
|
||||||
"sha256:be0f475edd8e9c98f57449c97839f6a81946e79e4cccb81e4b5196a2cc40e044",
|
"sha256:6d9d46d1f6b4985023469a1e334cb35c7c8fc6bd9d8b65ca52b923a7a6869c2a",
|
||||||
"sha256:bf3c62d154f2222caf56d897ddfd53fd0aef560d5a2202447d90e015301a0a10",
|
"sha256:8a7ffdc4f7f760253aa4dd8d2037358eb33915ca1dcf1c2422b19fcf0ab68506",
|
||||||
"sha256:fe6a5f05aba3ff47e105a308482b68f8b0fd80656eb1456a9c1e4de47d2c580f"
|
"sha256:927fc5755bb93dc09275bd5d818811e016290c194012d63f8e6f86b7ece3e468",
|
||||||
|
"sha256:dcaa084c3700f775eba09d81a1432444f82d9ad6270320c56c1a733d71cceb3a",
|
||||||
|
"sha256:f59698cc0366395ca79b48b080674973541aae290670c57d88f05d939a4c00da"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.10"
|
"version": "==1.3.9"
|
||||||
},
|
},
|
||||||
"yarl": {
|
"yarl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1417,17 +1439,17 @@
|
|||||||
},
|
},
|
||||||
"astroid": {
|
"astroid": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
|
"sha256:ad63b8552c70939568966811a088ef0bc880f99a24a00834abd0e3681b514f91",
|
||||||
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
|
"sha256:bea3f32799fbb8581f58431c12591bc20ce11cbc90ad82e2ea5717d94f2080d5"
|
||||||
],
|
],
|
||||||
"version": "==2.5.6"
|
"version": "==2.5.3"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3901be1cb7c2a780f14668691474d9252c070a756be0a9ead98cfeabfa11aeb8",
|
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||||
"sha256:8ee1e5f5a1afc5b19bdfae4fdf0c35ed324074bdce3500c939842c8f818645d9"
|
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||||
],
|
],
|
||||||
"version": "==21.1.0"
|
"version": "==20.3.0"
|
||||||
},
|
},
|
||||||
"bandit": {
|
"bandit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1452,20 +1474,6 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.0.1"
|
"version": "==1.0.1"
|
||||||
},
|
},
|
||||||
"certifi": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
|
||||||
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
|
||||||
],
|
|
||||||
"version": "==2020.12.5"
|
|
||||||
},
|
|
||||||
"chardet": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
|
||||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
|
||||||
],
|
|
||||||
"version": "==4.0.0"
|
|
||||||
},
|
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||||
@ -1548,17 +1556,10 @@
|
|||||||
},
|
},
|
||||||
"gitpython": {
|
"gitpython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:05af150f47a5cca3f4b0af289b73aef8cf3c4fe2385015b06220cbcdee48bb6e",
|
"sha256:3283ae2fba31c913d857e12e5ba5f9a7772bbc064ae2bb09efafa71b0dd4939b",
|
||||||
"sha256:a77824e516d3298b04fb36ec7845e92747df8fcfee9cacc32dd6239f9652f867"
|
"sha256:be27633e7509e58391f10207cd32b2a6cf5b908f92d9cd30da2e514e1137af61"
|
||||||
],
|
],
|
||||||
"version": "==3.1.15"
|
"version": "==3.1.14"
|
||||||
},
|
|
||||||
"idna": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
|
||||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
|
||||||
],
|
|
||||||
"version": "==2.10"
|
|
||||||
},
|
},
|
||||||
"iniconfig": {
|
"iniconfig": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1632,10 +1633,10 @@
|
|||||||
},
|
},
|
||||||
"pbr": {
|
"pbr": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd",
|
"sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
|
||||||
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"
|
"sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
|
||||||
],
|
],
|
||||||
"version": "==5.6.0"
|
"version": "==5.5.1"
|
||||||
},
|
},
|
||||||
"pluggy": {
|
"pluggy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1653,19 +1654,19 @@
|
|||||||
},
|
},
|
||||||
"pylint": {
|
"pylint": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:586d8fa9b1891f4b725f587ef267abe2a1bad89d6b184520c7f07a253dd6e217",
|
"sha256:209d712ec870a0182df034ae19f347e725c1e615b2269519ab58a35b3fcbbe7a",
|
||||||
"sha256:f7e2072654a6b6afdf5e2fb38147d3e2d2d43c89f648637baab63e026481279b"
|
"sha256:bd38914c7731cdc518634a8d3c5585951302b6e2b6de60fbb3f7a0220e21eeee"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.8.2"
|
"version": "==2.7.4"
|
||||||
},
|
},
|
||||||
"pylint-django": {
|
"pylint-django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:aff49d9602a39c027b4ed7521a041438893205918f405800063b7ff692b7371b",
|
"sha256:a5a4515209a6237d1d390a4a307d53f53baaf4f058ecf4bb556c775d208f6b0d",
|
||||||
"sha256:f63f717169b0c2e4e19c28f1c32c28290647330184fcb7427805ae9b6994f3fc"
|
"sha256:dc5ed27bb7662d73444ccd15a0b3964ed6ced6cc2712b85db616102062d2ec35"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.4.4"
|
"version": "==2.4.3"
|
||||||
},
|
},
|
||||||
"pylint-plugin-utils": {
|
"pylint-plugin-utils": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1683,11 +1684,11 @@
|
|||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
|
"sha256:671238a46e4df0f3498d1c3270e5deb9b32d25134c99b7d75370a68cfbe9b634",
|
||||||
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
|
"sha256:6ad9c7bdf517a808242b998ac20063c41532a570d088d77eec1ee12b0b5574bc"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==6.2.4"
|
"version": "==6.2.3"
|
||||||
},
|
},
|
||||||
"pytest-django": {
|
"pytest-django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1778,21 +1779,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==2021.4.4"
|
"version": "==2021.4.4"
|
||||||
},
|
},
|
||||||
"requests": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
|
||||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
|
||||||
],
|
|
||||||
"version": "==2.25.1"
|
|
||||||
},
|
|
||||||
"requests-mock": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:33296f228d8c5df11a7988b741325422480baddfdf5dd9318fd0eb40c3ed8595",
|
|
||||||
"sha256:5c8ef0254c14a84744be146e9799dc13ebc4f6186058112d9aeed96b131b58e2"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.9.2"
|
|
||||||
},
|
|
||||||
"selenium": {
|
"selenium": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
|
"sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c",
|
||||||
@ -1803,10 +1789,10 @@
|
|||||||
},
|
},
|
||||||
"six": {
|
"six": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||||
],
|
],
|
||||||
"version": "==1.16.0"
|
"version": "==1.15.0"
|
||||||
},
|
},
|
||||||
"smmap": {
|
"smmap": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1866,11 +1852,11 @@
|
|||||||
},
|
},
|
||||||
"typing-extensions": {
|
"typing-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
|
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
|
||||||
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
|
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
|
||||||
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
|
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
|
||||||
],
|
],
|
||||||
"version": "==3.10.0.0"
|
"version": "==3.7.4.3"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"extras": [
|
"extras": [
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||

|

|
||||||

|

|
||||||

|

|
||||||
[Transifex](https://www.transifex.com/beryjuorg/authentik/)
|
|
||||||
|
|
||||||
## What is authentik?
|
## What is authentik?
|
||||||
|
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ---------- | ------------------ |
|
| ---------- | ------------------ |
|
||||||
|
| 2021.3.x | :white_check_mark: |
|
||||||
| 2021.4.x | :white_check_mark: |
|
| 2021.4.x | :white_check_mark: |
|
||||||
| 2021.5.x | :white_check_mark: |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "2021.5.1-rc2"
|
__version__ = "2021.4.6"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""API Authentication"""
|
"""API Authentication"""
|
||||||
from base64 import b64decode
|
from base64 import b64decode, b64encode
|
||||||
from binascii import Error
|
from binascii import Error
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
@ -19,6 +19,14 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
|
|||||||
auth_credentials = raw_header.decode()
|
auth_credentials = raw_header.decode()
|
||||||
if auth_credentials == "":
|
if auth_credentials == "":
|
||||||
return None
|
return None
|
||||||
|
# Legacy, accept basic auth thats fully encoded (2021.3 outposts)
|
||||||
|
if " " not in auth_credentials:
|
||||||
|
try:
|
||||||
|
plain = b64decode(auth_credentials.encode()).decode()
|
||||||
|
auth_type, body = plain.split()
|
||||||
|
auth_credentials = f"{auth_type} {b64encode(body.encode()).decode()}"
|
||||||
|
except (UnicodeDecodeError, Error):
|
||||||
|
raise AuthenticationFailed("Malformed header")
|
||||||
auth_type, auth_credentials = auth_credentials.split()
|
auth_type, auth_credentials = auth_credentials.split()
|
||||||
if auth_type.lower() not in ["basic", "bearer"]:
|
if auth_type.lower() not in ["basic", "bearer"]:
|
||||||
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
|
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
API Browser - {{ config.authentik.branding.title }}
|
authentik API Browser
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
@ -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()))
|
|
@ -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)
|
|
@ -47,7 +47,6 @@ from authentik.policies.reputation.api import (
|
|||||||
ReputationPolicyViewSet,
|
ReputationPolicyViewSet,
|
||||||
UserReputationViewSet,
|
UserReputationViewSet,
|
||||||
)
|
)
|
||||||
from authentik.providers.ldap.api import LDAPOutpostConfigViewSet, LDAPProviderViewSet
|
|
||||||
from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet
|
from authentik.providers.oauth2.api.provider import OAuth2ProviderViewSet
|
||||||
from authentik.providers.oauth2.api.scope import ScopeMappingViewSet
|
from authentik.providers.oauth2.api.scope import ScopeMappingViewSet
|
||||||
from authentik.providers.oauth2.api.tokens import (
|
from authentik.providers.oauth2.api.tokens import (
|
||||||
@ -64,7 +63,6 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
|||||||
from authentik.sources.oauth.api.source_connection import (
|
from authentik.sources.oauth.api.source_connection import (
|
||||||
UserOAuthSourceConnectionViewSet,
|
UserOAuthSourceConnectionViewSet,
|
||||||
)
|
)
|
||||||
from authentik.sources.plex.api import PlexSourceViewSet
|
|
||||||
from authentik.sources.saml.api import SAMLSourceViewSet
|
from authentik.sources.saml.api import SAMLSourceViewSet
|
||||||
from authentik.stages.authenticator_static.api import (
|
from authentik.stages.authenticator_static.api import (
|
||||||
AuthenticatorStaticStageViewSet,
|
AuthenticatorStaticStageViewSet,
|
||||||
@ -122,7 +120,6 @@ router.register(
|
|||||||
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
|
"outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet
|
||||||
)
|
)
|
||||||
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
|
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
|
||||||
router.register("outposts/ldap", LDAPOutpostConfigViewSet)
|
|
||||||
|
|
||||||
router.register("flows/instances", FlowViewSet)
|
router.register("flows/instances", FlowViewSet)
|
||||||
router.register("flows/bindings", FlowStageBindingViewSet)
|
router.register("flows/bindings", FlowStageBindingViewSet)
|
||||||
@ -139,7 +136,6 @@ router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewS
|
|||||||
router.register("sources/ldap", LDAPSourceViewSet)
|
router.register("sources/ldap", LDAPSourceViewSet)
|
||||||
router.register("sources/saml", SAMLSourceViewSet)
|
router.register("sources/saml", SAMLSourceViewSet)
|
||||||
router.register("sources/oauth", OAuthSourceViewSet)
|
router.register("sources/oauth", OAuthSourceViewSet)
|
||||||
router.register("sources/plex", PlexSourceViewSet)
|
|
||||||
|
|
||||||
router.register("policies/all", PolicyViewSet)
|
router.register("policies/all", PolicyViewSet)
|
||||||
router.register("policies/bindings", PolicyBindingViewSet)
|
router.register("policies/bindings", PolicyBindingViewSet)
|
||||||
@ -153,7 +149,6 @@ router.register("policies/reputation/ips", IPReputationViewSet)
|
|||||||
router.register("policies/reputation", ReputationPolicyViewSet)
|
router.register("policies/reputation", ReputationPolicyViewSet)
|
||||||
|
|
||||||
router.register("providers/all", ProviderViewSet)
|
router.register("providers/all", ProviderViewSet)
|
||||||
router.register("providers/ldap", LDAPProviderViewSet)
|
|
||||||
router.register("providers/proxy", ProxyProviderViewSet)
|
router.register("providers/proxy", ProxyProviderViewSet)
|
||||||
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
||||||
router.register("providers/saml", SAMLProviderViewSet)
|
router.register("providers/saml", SAMLProviderViewSet)
|
||||||
|
@ -91,23 +91,6 @@ class ApplicationViewSet(ModelViewSet):
|
|||||||
applications.append(application)
|
applications.append(application)
|
||||||
return applications
|
return applications
|
||||||
|
|
||||||
@swagger_auto_schema(
|
|
||||||
responses={
|
|
||||||
204: "Access granted",
|
|
||||||
403: "Access denied",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@action(detail=True, methods=["GET"])
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def check_access(self, request: Request, slug: str) -> Response:
|
|
||||||
"""Check access to a single application by slug"""
|
|
||||||
application = self.get_object()
|
|
||||||
engine = PolicyEngine(application, self.request.user, self.request)
|
|
||||||
engine.build()
|
|
||||||
if engine.passing:
|
|
||||||
return Response(status=204)
|
|
||||||
return Response(status=403)
|
|
||||||
|
|
||||||
@swagger_auto_schema(
|
@swagger_auto_schema(
|
||||||
manual_parameters=[
|
manual_parameters=[
|
||||||
openapi.Parameter(
|
openapi.Parameter(
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
"""Groups API Viewset"""
|
"""Groups API Viewset"""
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
from rest_framework.fields import JSONField
|
from rest_framework.fields import JSONField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
|
||||||
|
|
||||||
from authentik.core.api.utils import is_dict
|
from authentik.core.api.utils import is_dict
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
@ -28,16 +26,3 @@ class GroupViewSet(ModelViewSet):
|
|||||||
search_fields = ["name", "is_superuser"]
|
search_fields = ["name", "is_superuser"]
|
||||||
filterset_fields = ["name", "is_superuser"]
|
filterset_fields = ["name", "is_superuser"]
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
|
||||||
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
|
||||||
for backend in list(self.filter_backends):
|
|
||||||
if backend == ObjectPermissionsFilter:
|
|
||||||
continue
|
|
||||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
|
||||||
if self.request.user.has_perm("authentik_core.view_group"):
|
|
||||||
return self._filter_queryset_for_list(queryset)
|
|
||||||
return super().filter_queryset(queryset)
|
|
||||||
|
@ -45,7 +45,6 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
"verbose_name",
|
"verbose_name",
|
||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
"policy_engine_mode",
|
"policy_engine_mode",
|
||||||
"user_matching_mode",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,30 +1,18 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django_filters.filters import BooleanFilter, CharFilter
|
|
||||||
from django_filters.filterset import FilterSet
|
|
||||||
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
|
from drf_yasg.utils import swagger_auto_schema, swagger_serializer_method
|
||||||
from guardian.utils import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import BooleanField, ModelSerializer
|
||||||
BooleanField,
|
|
||||||
ListSerializer,
|
|
||||||
ModelSerializer,
|
|
||||||
ValidationError,
|
|
||||||
)
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
|
||||||
|
|
||||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.groups import GroupSerializer
|
|
||||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
||||||
from authentik.core.middleware import (
|
from authentik.core.middleware import (
|
||||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||||
@ -41,8 +29,6 @@ class UserSerializer(ModelSerializer):
|
|||||||
is_superuser = BooleanField(read_only=True)
|
is_superuser = BooleanField(read_only=True)
|
||||||
avatar = CharField(read_only=True)
|
avatar = CharField(read_only=True)
|
||||||
attributes = JSONField(validators=[is_dict], required=False)
|
attributes = JSONField(validators=[is_dict], required=False)
|
||||||
groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
|
|
||||||
uid = CharField(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -54,11 +40,9 @@ class UserSerializer(ModelSerializer):
|
|||||||
"is_active",
|
"is_active",
|
||||||
"last_login",
|
"last_login",
|
||||||
"is_superuser",
|
"is_superuser",
|
||||||
"groups",
|
|
||||||
"email",
|
"email",
|
||||||
"avatar",
|
"avatar",
|
||||||
"attributes",
|
"attributes",
|
||||||
"uid",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -100,44 +84,13 @@ class UserMetricsSerializer(PassiveSerializer):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UsersFilter(FilterSet):
|
|
||||||
"""Filter for users"""
|
|
||||||
|
|
||||||
attributes = CharFilter(
|
|
||||||
field_name="attributes",
|
|
||||||
lookup_expr="",
|
|
||||||
label="Attributes",
|
|
||||||
method="filter_attributes",
|
|
||||||
)
|
|
||||||
|
|
||||||
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def filter_attributes(self, queryset, name, value):
|
|
||||||
"""Filter attributes by query args"""
|
|
||||||
try:
|
|
||||||
value = loads(value)
|
|
||||||
except ValueError:
|
|
||||||
raise ValidationError(detail="filter: failed to parse JSON")
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
raise ValidationError(detail="filter: value must be key:value mapping")
|
|
||||||
qs = {}
|
|
||||||
for key, _value in value.items():
|
|
||||||
qs[f"attributes__{key}"] = _value
|
|
||||||
return queryset.filter(**qs)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = User
|
|
||||||
fields = ["username", "name", "is_active", "is_superuser", "attributes"]
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(ModelViewSet):
|
||||||
"""User Viewset"""
|
"""User Viewset"""
|
||||||
|
|
||||||
queryset = User.objects.none()
|
queryset = User.objects.none()
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
search_fields = ["username", "name", "is_active"]
|
search_fields = ["username", "name", "is_active"]
|
||||||
filterset_class = UsersFilter
|
filterset_fields = ["username", "name", "is_active"]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
return User.objects.all().exclude(pk=get_anonymous_user().pk)
|
||||||
@ -191,16 +144,3 @@ class UserViewSet(ModelViewSet):
|
|||||||
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
|
reverse_lazy("authentik_flows:default-recovery") + f"?{querystring}"
|
||||||
)
|
)
|
||||||
return Response({"link": link})
|
return Response({"link": link})
|
||||||
|
|
||||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
|
||||||
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
|
||||||
for backend in list(self.filter_backends):
|
|
||||||
if backend == ObjectPermissionsFilter:
|
|
||||||
continue
|
|
||||||
queryset = backend().filter_queryset(self.request, queryset, self)
|
|
||||||
return queryset
|
|
||||||
|
|
||||||
def filter_queryset(self, queryset):
|
|
||||||
if self.request.user.has_perm("authentik_core.view_group"):
|
|
||||||
return self._filter_queryset_for_list(queryset)
|
|
||||||
return super().filter_queryset(queryset)
|
|
||||||
|
@ -20,12 +20,10 @@ def is_dict(value: Any):
|
|||||||
class PassiveSerializer(Serializer):
|
class PassiveSerializer(Serializer):
|
||||||
"""Base serializer class which doesn't implement create/update methods"""
|
"""Base serializer class which doesn't implement create/update methods"""
|
||||||
|
|
||||||
def create(self, validated_data: dict) -> Model: # pragma: no cover
|
def create(self, validated_data: dict) -> Model:
|
||||||
return Model()
|
return Model()
|
||||||
|
|
||||||
def update(
|
def update(self, instance: Model, validated_data: dict) -> Model:
|
||||||
self, instance: Model, validated_data: dict
|
|
||||||
) -> Model: # pragma: no cover
|
|
||||||
return Model()
|
return Model()
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -34,7 +34,6 @@ from authentik.policies.models import PolicyBindingModel
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||||
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
||||||
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
|
||||||
|
|
||||||
GRAVATAR_URL = "https://secure.gravatar.com"
|
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||||
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
||||||
@ -241,30 +240,6 @@ class Application(PolicyBindingModel):
|
|||||||
verbose_name_plural = _("Applications")
|
verbose_name_plural = _("Applications")
|
||||||
|
|
||||||
|
|
||||||
class SourceUserMatchingModes(models.TextChoices):
|
|
||||||
"""Different modes a source can handle new/returning users"""
|
|
||||||
|
|
||||||
IDENTIFIER = "identifier", _("Use the source-specific identifier")
|
|
||||||
EMAIL_LINK = "email_link", _(
|
|
||||||
(
|
|
||||||
"Link to a user with identical email address. Can have security implications "
|
|
||||||
"when a source doesn't validate email addresses."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
EMAIL_DENY = "email_deny", _(
|
|
||||||
"Use the user's email address, but deny enrollment when the email address already exists."
|
|
||||||
)
|
|
||||||
USERNAME_LINK = "username_link", _(
|
|
||||||
(
|
|
||||||
"Link to a user with identical username address. Can have security implications "
|
|
||||||
"when a username is used with another source."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
USERNAME_DENY = "username_deny", _(
|
|
||||||
"Use the user's username, but deny enrollment when the username already exists."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||||
|
|
||||||
@ -297,17 +272,6 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
related_name="source_enrollment",
|
related_name="source_enrollment",
|
||||||
)
|
)
|
||||||
|
|
||||||
user_matching_mode = models.TextField(
|
|
||||||
choices=SourceUserMatchingModes.choices,
|
|
||||||
default=SourceUserMatchingModes.IDENTIFIER,
|
|
||||||
help_text=_(
|
|
||||||
(
|
|
||||||
"How the source determines if an existing user should be authenticated or "
|
|
||||||
"a new user enrolled."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -337,8 +301,6 @@ class UserSourceConnection(CreatedUpdatedModel):
|
|||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||||
|
|
||||||
objects = InheritanceManager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
unique_together = (("user", "source"),)
|
unique_together = (("user", "source"),)
|
||||||
|
@ -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,
|
|
||||||
},
|
|
||||||
)
|
|
@ -14,9 +14,9 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
|
||||||
<script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script>
|
<script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script>
|
||||||
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
@ -3,10 +3,6 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="pf-c-background-image">
|
<div class="pf-c-background-image">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
||||||
|
@ -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",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
@ -1,14 +1,11 @@
|
|||||||
"""authentik core models tests"""
|
"""authentik core models tests"""
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Callable, Type
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
from authentik.core.models import Provider, Source, Token
|
from authentik.core.models import Token
|
||||||
from authentik.flows.models import Stage
|
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
|
||||||
|
|
||||||
|
|
||||||
class TestModels(TestCase):
|
class TestModels(TestCase):
|
||||||
@ -27,40 +24,3 @@ class TestModels(TestCase):
|
|||||||
)
|
)
|
||||||
sleep(0.5)
|
sleep(0.5)
|
||||||
self.assertFalse(token.is_expired)
|
self.assertFalse(token.is_expired)
|
||||||
|
|
||||||
|
|
||||||
def source_tester_factory(test_model: Type[Stage]) -> Callable:
|
|
||||||
"""Test source"""
|
|
||||||
|
|
||||||
def tester(self: TestModels):
|
|
||||||
model_class = None
|
|
||||||
if test_model._meta.abstract:
|
|
||||||
model_class = test_model.__bases__[0]()
|
|
||||||
else:
|
|
||||||
model_class = test_model()
|
|
||||||
model_class.slug = "test"
|
|
||||||
self.assertIsNotNone(model_class.component)
|
|
||||||
_ = model_class.ui_login_button
|
|
||||||
_ = model_class.ui_user_settings
|
|
||||||
|
|
||||||
return tester
|
|
||||||
|
|
||||||
|
|
||||||
def provider_tester_factory(test_model: Type[Stage]) -> Callable:
|
|
||||||
"""Test provider"""
|
|
||||||
|
|
||||||
def tester(self: TestModels):
|
|
||||||
model_class = None
|
|
||||||
if test_model._meta.abstract:
|
|
||||||
model_class = test_model.__bases__[0]()
|
|
||||||
else:
|
|
||||||
model_class = test_model()
|
|
||||||
self.assertIsNotNone(model_class.component)
|
|
||||||
|
|
||||||
return tester
|
|
||||||
|
|
||||||
|
|
||||||
for model in all_subclasses(Source):
|
|
||||||
setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model))
|
|
||||||
for model in all_subclasses(Provider):
|
|
||||||
setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model))
|
|
||||||
|
@ -2,10 +2,9 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from rest_framework.fields import CharField, DictField
|
from rest_framework.fields import CharField
|
||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.flows.challenge import Challenge
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -15,8 +14,8 @@ class UILoginButton:
|
|||||||
# Name, ran through i18n
|
# Name, ran through i18n
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
# Challenge which is presented to the user when they click the button
|
# URL Which Button points to
|
||||||
challenge: Challenge
|
url: str
|
||||||
|
|
||||||
# Icon URL, used as-is
|
# Icon URL, used as-is
|
||||||
icon_url: Optional[str] = None
|
icon_url: Optional[str] = None
|
||||||
@ -26,7 +25,7 @@ class UILoginButtonSerializer(PassiveSerializer):
|
|||||||
"""Serializer for Login buttons of sources"""
|
"""Serializer for Login buttons of sources"""
|
||||||
|
|
||||||
name = CharField()
|
name = CharField()
|
||||||
challenge = DictField()
|
url = CharField()
|
||||||
icon_url = CharField(required=False, allow_null=True)
|
icon_url = CharField(required=False, allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||||||
"""Show if this keypair has a private key configured or not"""
|
"""Show if this keypair has a private key configured or not"""
|
||||||
return instance.key_data != "" and instance.key_data is not None
|
return instance.key_data != "" and instance.key_data is not None
|
||||||
|
|
||||||
def validate_certificate_data(self, value: str) -> str:
|
def validate_certificate_data(self, value):
|
||||||
"""Verify that input is a valid PEM x509 Certificate"""
|
"""Verify that input is a valid PEM x509 Certificate"""
|
||||||
try:
|
try:
|
||||||
load_pem_x509_certificate(value.encode("utf-8"), default_backend())
|
load_pem_x509_certificate(value.encode("utf-8"), default_backend())
|
||||||
@ -47,7 +47,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||||||
raise ValidationError("Unable to load certificate.")
|
raise ValidationError("Unable to load certificate.")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate_key_data(self, value: str) -> str:
|
def validate_key_data(self, value):
|
||||||
"""Verify that input is a valid PEM RSA Key"""
|
"""Verify that input is a valid PEM RSA Key"""
|
||||||
# Since this field is optional, data can be empty.
|
# Since this field is optional, data can be empty.
|
||||||
if value != "":
|
if value != "":
|
||||||
@ -57,10 +57,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||||||
password=None,
|
password=None,
|
||||||
backend=default_backend(),
|
backend=default_backend(),
|
||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except ValueError:
|
||||||
raise ValidationError(
|
raise ValidationError("Unable to load private key.")
|
||||||
"Unable to load private key (possibly encrypted?)."
|
|
||||||
)
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -33,7 +33,7 @@ class CertificateBuilder:
|
|||||||
def save(self) -> Optional[CertificateKeyPair]:
|
def save(self) -> Optional[CertificateKeyPair]:
|
||||||
"""Save generated certificate as model"""
|
"""Save generated certificate as model"""
|
||||||
if not self.__certificate:
|
if not self.__certificate:
|
||||||
raise ValueError("Certificated hasn't been built yet")
|
return None
|
||||||
return CertificateKeyPair.objects.create(
|
return CertificateKeyPair.objects.create(
|
||||||
name=self.common_name,
|
name=self.common_name,
|
||||||
certificate_data=self.certificate,
|
certificate_data=self.certificate,
|
||||||
|
@ -37,8 +37,6 @@ class TestCrypto(TestCase):
|
|||||||
"""Test Builder"""
|
"""Test Builder"""
|
||||||
builder = CertificateBuilder()
|
builder = CertificateBuilder()
|
||||||
builder.common_name = "test-cert"
|
builder.common_name = "test-cert"
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
builder.save()
|
|
||||||
builder.build(
|
builder.build(
|
||||||
subject_alt_names=[],
|
subject_alt_names=[],
|
||||||
validity_days=3,
|
validity_days=3,
|
||||||
|
@ -8,10 +8,10 @@ from rest_framework.decorators import action
|
|||||||
from rest_framework.fields import CharField, DictField, IntegerField
|
from rest_framework.fields import CharField, DictField, IntegerField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer, Serializer
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
|
from authentik.core.api.utils import TypeCreateSerializer
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
|
||||||
@ -38,19 +38,31 @@ class EventSerializer(ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class EventTopPerUserParams(PassiveSerializer):
|
class EventTopPerUserParams(Serializer):
|
||||||
"""Query params for top_per_user"""
|
"""Query params for top_per_user"""
|
||||||
|
|
||||||
top_n = IntegerField(default=15)
|
top_n = IntegerField(default=15)
|
||||||
|
|
||||||
|
def create(self, request: Request) -> Response:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
class EventTopPerUserSerializer(PassiveSerializer):
|
def update(self, request: Request) -> Response:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class EventTopPerUserSerializer(Serializer):
|
||||||
"""Response object of Event's top_per_user"""
|
"""Response object of Event's top_per_user"""
|
||||||
|
|
||||||
application = DictField()
|
application = DictField()
|
||||||
counted_events = IntegerField()
|
counted_events = IntegerField()
|
||||||
unique_users = IntegerField()
|
unique_users = IntegerField()
|
||||||
|
|
||||||
|
def create(self, request: Request) -> Response:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def update(self, request: Request) -> Response:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class EventsFilter(django_filters.FilterSet):
|
class EventsFilter(django_filters.FilterSet):
|
||||||
"""Filter for events"""
|
"""Filter for events"""
|
||||||
@ -120,7 +132,7 @@ class EventViewSet(ReadOnlyModelViewSet):
|
|||||||
def top_per_user(self, request: Request):
|
def top_per_user(self, request: Request):
|
||||||
"""Get the top_n events grouped by user count"""
|
"""Get the top_n events grouped by user count"""
|
||||||
filtered_action = request.query_params.get("action", EventAction.LOGIN)
|
filtered_action = request.query_params.get("action", EventAction.LOGIN)
|
||||||
top_n = int(request.query_params.get("top_n", "15"))
|
top_n = request.query_params.get("top_n", 15)
|
||||||
return Response(
|
return Response(
|
||||||
get_objects_for_user(request.user, "authentik_events.view_event")
|
get_objects_for_user(request.user, "authentik_events.view_event")
|
||||||
.filter(action=filtered_action)
|
.filter(action=filtered_action)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""Notification API Views"""
|
"""Notification API Views"""
|
||||||
from guardian.utils import get_anonymous_user
|
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.fields import ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
@ -49,5 +48,6 @@ class NotificationViewSet(
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
if not self.request:
|
||||||
return Notification.objects.filter(user=user.pk)
|
return super().get_queryset()
|
||||||
|
return Notification.objects.filter(user=self.request.user)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
"""Event notification tasks"""
|
"""Event notification tasks"""
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from structlog.stdlib import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import (
|
from authentik.events.models import (
|
||||||
@ -35,10 +35,7 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
|
|||||||
LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid)
|
LOGGER.warning("event doesn't exist yet or anymore", event_uuid=event_uuid)
|
||||||
return
|
return
|
||||||
event: Event = events.first()
|
event: Event = events.first()
|
||||||
triggers: NotificationRule = NotificationRule.objects.filter(name=trigger_name)
|
trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name)
|
||||||
if not triggers.exists():
|
|
||||||
return
|
|
||||||
trigger = triggers.first()
|
|
||||||
|
|
||||||
if "policy_uuid" in event.context:
|
if "policy_uuid" in event.context:
|
||||||
policy_uuid = event.context["policy_uuid"]
|
policy_uuid = event.context["policy_uuid"]
|
||||||
@ -61,13 +58,7 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
|
|||||||
return
|
return
|
||||||
|
|
||||||
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
|
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
|
||||||
try:
|
user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
|
||||||
user = (
|
|
||||||
User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
|
|
||||||
)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
LOGGER.warning("e(trigger): failed to get user", trigger=trigger)
|
|
||||||
return
|
|
||||||
policy_engine = PolicyEngine(trigger, user)
|
policy_engine = PolicyEngine(trigger, user)
|
||||||
policy_engine.mode = PolicyEngineMode.MODE_ANY
|
policy_engine.mode = PolicyEngineMode.MODE_ANY
|
||||||
policy_engine.empty_result = False
|
policy_engine.empty_result = False
|
||||||
|
@ -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))
|
|
@ -13,7 +13,7 @@ from django.db.models.query_utils import Q
|
|||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.serializers import BaseSerializer, Serializer
|
from rest_framework.serializers import BaseSerializer, Serializer
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog import BoundLogger, get_logger
|
||||||
|
|
||||||
from authentik.flows.models import Flow, FlowStageBinding, Stage
|
from authentik.flows.models import Flow, FlowStageBinding, Stage
|
||||||
from authentik.flows.transfer.common import (
|
from authentik.flows.transfer.common import (
|
||||||
|
@ -86,13 +86,6 @@ class ConfigLoader:
|
|||||||
url = urlparse(value)
|
url = urlparse(value)
|
||||||
if url.scheme == "env":
|
if url.scheme == "env":
|
||||||
value = os.getenv(url.netloc, url.query)
|
value = os.getenv(url.netloc, url.query)
|
||||||
if url.scheme == "file":
|
|
||||||
try:
|
|
||||||
with open(url.netloc, "r") as _file:
|
|
||||||
value = _file.read()
|
|
||||||
except OSError:
|
|
||||||
self._log("error", f"Failed to read config value from {url.netloc}")
|
|
||||||
value = url.query
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def update_from_file(self, path: str):
|
def update_from_file(self, path: str):
|
||||||
@ -170,7 +163,6 @@ class ConfigLoader:
|
|||||||
# Walk each component of the path
|
# Walk each component of the path
|
||||||
path_parts = path.split(sep)
|
path_parts = path.split(sep)
|
||||||
for comp in path_parts[:-1]:
|
for comp in path_parts[:-1]:
|
||||||
# pyright: reportGeneralTypeIssues=false
|
|
||||||
if comp not in root:
|
if comp not in root:
|
||||||
root[comp] = {}
|
root[comp] = {}
|
||||||
root = root.get(comp)
|
root = root.get(comp)
|
||||||
|
@ -5,10 +5,6 @@ postgresql:
|
|||||||
user: authentik
|
user: authentik
|
||||||
password: 'env://POSTGRES_PASSWORD'
|
password: 'env://POSTGRES_PASSWORD'
|
||||||
|
|
||||||
web:
|
|
||||||
listen: 0.0.0.0:9000
|
|
||||||
listen_tls: 0.0.0.0:9443
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
password: ''
|
password: ''
|
||||||
@ -38,10 +34,7 @@ email:
|
|||||||
from: authentik@localhost
|
from: authentik@localhost
|
||||||
|
|
||||||
outposts:
|
outposts:
|
||||||
# Placeholders:
|
docker_image_base: "beryju/authentik" # this is prepended to -proxy:version
|
||||||
# %(type)s: Outpost type; proxy, ldap, etc
|
|
||||||
# %(version)s: Current version; 2021.4.1
|
|
||||||
docker_image_base: "beryju/authentik-%(type)s:%(version)s"
|
|
||||||
|
|
||||||
authentik:
|
authentik:
|
||||||
avatars: gravatar # gravatar or none
|
avatars: gravatar # gravatar or none
|
||||||
|
@ -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)
|
|
@ -3,9 +3,6 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP"
|
|
||||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
|
||||||
|
|
||||||
|
|
||||||
def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]:
|
def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]:
|
||||||
"""Attempt to get the client's IP by checking common HTTP Headers.
|
"""Attempt to get the client's IP by checking common HTTP Headers.
|
||||||
@ -21,27 +18,9 @@ def _get_client_ip_from_meta(meta: dict[str, Any]) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
|
|
||||||
"""Get the actual remote IP when set by an outpost. Only
|
|
||||||
allowed when the request is authenticated, by a user with USER_ATTRIBUTE_CAN_OVERRIDE_IP set
|
|
||||||
to outpost"""
|
|
||||||
if not hasattr(request, "user"):
|
|
||||||
return None
|
|
||||||
if not request.user.is_authenticated:
|
|
||||||
return None
|
|
||||||
if OUTPOST_REMOTE_IP_HEADER not in request.META:
|
|
||||||
return None
|
|
||||||
if request.user.attributes.get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
|
||||||
return None
|
|
||||||
return request.META[OUTPOST_REMOTE_IP_HEADER]
|
|
||||||
|
|
||||||
|
|
||||||
def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]:
|
def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]:
|
||||||
"""Attempt to get the client's IP by checking common HTTP Headers.
|
"""Attempt to get the client's IP by checking common HTTP Headers.
|
||||||
Returns none if no IP Could be found"""
|
Returns none if no IP Could be found"""
|
||||||
if request:
|
if request:
|
||||||
override = _get_outpost_override_ip(request)
|
|
||||||
if override:
|
|
||||||
return override
|
|
||||||
return _get_client_ip_from_meta(request.META)
|
return _get_client_ip_from_meta(request.META)
|
||||||
return None
|
return None
|
||||||
|
@ -24,7 +24,6 @@ class OutpostSerializer(ModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
"type",
|
|
||||||
"providers",
|
"providers",
|
||||||
"providers_obj",
|
"providers_obj",
|
||||||
"service_connection",
|
"service_connection",
|
||||||
|
@ -82,8 +82,7 @@ class OutpostConsumer(AuthJsonConsumer):
|
|||||||
state.version = msg.args.get("version", None)
|
state.version = msg.args.get("version", None)
|
||||||
elif msg.instruction == WebsocketMessageInstruction.ACK:
|
elif msg.instruction == WebsocketMessageInstruction.ACK:
|
||||||
return
|
return
|
||||||
if state.version:
|
state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5)
|
||||||
state.save(timeout=OUTPOST_HELLO_INTERVAL * 1.5)
|
|
||||||
|
|
||||||
response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK)
|
response = WebsocketMessage(instruction=WebsocketMessageInstruction.ACK)
|
||||||
self.send_json(asdict(response))
|
self.send_json(asdict(response))
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
"""Base Controller"""
|
"""Base Controller"""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
from structlog.testing import capture_logs
|
from structlog.testing import capture_logs
|
||||||
|
|
||||||
from authentik import __version__
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
||||||
|
|
||||||
@ -24,7 +21,6 @@ class DeploymentPort:
|
|||||||
port: int
|
port: int
|
||||||
name: str
|
name: str
|
||||||
protocol: str
|
protocol: str
|
||||||
inner_port: Optional[int] = None
|
|
||||||
|
|
||||||
|
|
||||||
class BaseController:
|
class BaseController:
|
||||||
@ -59,8 +55,3 @@ class BaseController:
|
|||||||
def get_static_deployment(self) -> str:
|
def get_static_deployment(self) -> str:
|
||||||
"""Return a static deployment configuration"""
|
"""Return a static deployment configuration"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_container_image(self) -> str:
|
|
||||||
"""Get container image to use for this outpost"""
|
|
||||||
image_name_template: str = CONFIG.y("outposts.docker_image_base")
|
|
||||||
return image_name_template % {"type": self.outpost.type, "version": __version__}
|
|
||||||
|
@ -8,6 +8,7 @@ from docker.models.containers import Container
|
|||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.outposts.controllers.base import BaseController, ControllerException
|
from authentik.outposts.controllers.base import BaseController, ControllerException
|
||||||
from authentik.outposts.models import (
|
from authentik.outposts.models import (
|
||||||
DockerServiceConnection,
|
DockerServiceConnection,
|
||||||
@ -59,14 +60,15 @@ class DockerController(BaseController):
|
|||||||
return self.client.containers.get(container_name), False
|
return self.client.containers.get(container_name), False
|
||||||
except NotFound:
|
except NotFound:
|
||||||
self.logger.info("Container does not exist, creating")
|
self.logger.info("Container does not exist, creating")
|
||||||
image_name = self.get_container_image()
|
image_prefix = CONFIG.y("outposts.docker_image_base")
|
||||||
|
image_name = f"{image_prefix}-{self.outpost.type}:{__version__}"
|
||||||
self.client.images.pull(image_name)
|
self.client.images.pull(image_name)
|
||||||
container_args = {
|
container_args = {
|
||||||
"image": image_name,
|
"image": image_name,
|
||||||
"name": f"authentik-proxy-{self.outpost.uuid.hex}",
|
"name": f"authentik-proxy-{self.outpost.uuid.hex}",
|
||||||
"detach": True,
|
"detach": True,
|
||||||
"ports": {
|
"ports": {
|
||||||
f"{port.port}/{port.protocol.lower()}": port.inner_port or port.port
|
f"{port.port}/{port.protocol.lower()}": port.port
|
||||||
for port in self.deployment_ports
|
for port in self.deployment_ports
|
||||||
},
|
},
|
||||||
"environment": self._get_env(),
|
"environment": self._get_env(),
|
||||||
@ -141,15 +143,15 @@ class DockerController(BaseController):
|
|||||||
def get_static_deployment(self) -> str:
|
def get_static_deployment(self) -> str:
|
||||||
"""Generate docker-compose yaml for proxy, version 3.5"""
|
"""Generate docker-compose yaml for proxy, version 3.5"""
|
||||||
ports = [
|
ports = [
|
||||||
f"{port.port}:{port.inner_port or port.port}/{port.protocol.lower()}"
|
f"{port.port}:{port.port}/{port.protocol.lower()}"
|
||||||
for port in self.deployment_ports
|
for port in self.deployment_ports
|
||||||
]
|
]
|
||||||
image_name = self.get_container_image()
|
image_prefix = CONFIG.y("outposts.docker_image_base")
|
||||||
compose = {
|
compose = {
|
||||||
"version": "3.5",
|
"version": "3.5",
|
||||||
"services": {
|
"services": {
|
||||||
f"authentik_{self.outpost.type}": {
|
f"authentik_{self.outpost.type}": {
|
||||||
"image": image_name,
|
"image": f"{image_prefix}-{self.outpost.type}:{__version__}",
|
||||||
"ports": ports,
|
"ports": ports,
|
||||||
"environment": {
|
"environment": {
|
||||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"""Base Kubernetes Reconciler"""
|
"""Base Kubernetes Reconciler"""
|
||||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||||
|
|
||||||
from django.utils.text import slugify
|
|
||||||
from kubernetes.client import V1ObjectMeta
|
from kubernetes.client import V1ObjectMeta
|
||||||
from kubernetes.client.models.v1_deployment import V1Deployment
|
from kubernetes.client.models.v1_deployment import V1Deployment
|
||||||
from kubernetes.client.models.v1_pod import V1Pod
|
from kubernetes.client.models.v1_pod import V1Pod
|
||||||
@ -30,11 +29,6 @@ class NeedsUpdate(ReconcileTrigger):
|
|||||||
"""Exception to trigger an update to the Kubernetes Object"""
|
"""Exception to trigger an update to the Kubernetes Object"""
|
||||||
|
|
||||||
|
|
||||||
class Disabled(SentryIgnoredException):
|
|
||||||
"""Exception which can be thrown in a reconciler to signal than an
|
|
||||||
object should not be created."""
|
|
||||||
|
|
||||||
|
|
||||||
class KubernetesObjectReconciler(Generic[T]):
|
class KubernetesObjectReconciler(Generic[T]):
|
||||||
"""Base Kubernetes Reconciler, handles the basic logic."""
|
"""Base Kubernetes Reconciler, handles the basic logic."""
|
||||||
|
|
||||||
@ -43,24 +37,17 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
def __init__(self, controller: "KubernetesController"):
|
def __init__(self, controller: "KubernetesController"):
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.namespace = controller.outpost.config.kubernetes_namespace
|
self.namespace = controller.outpost.config.kubernetes_namespace
|
||||||
self.logger = get_logger().bind(type=self.__class__.__name__)
|
self.logger = get_logger()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Get the name of the object this reconciler manages"""
|
"""Get the name of the object this reconciler manages"""
|
||||||
return self.controller.outpost.config.object_naming_template % {
|
raise NotImplementedError
|
||||||
"name": slugify(self.controller.outpost.name),
|
|
||||||
"uuid": self.controller.outpost.uuid.hex,
|
|
||||||
}
|
|
||||||
|
|
||||||
def up(self):
|
def up(self):
|
||||||
"""Create object if it doesn't exist, update if needed or recreate if needed."""
|
"""Create object if it doesn't exist, update if needed or recreate if needed."""
|
||||||
current = None
|
current = None
|
||||||
try:
|
reference = self.get_reference_object()
|
||||||
reference = self.get_reference_object()
|
|
||||||
except Disabled:
|
|
||||||
self.logger.debug("Object not required")
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
current = self.retrieve()
|
current = self.retrieve()
|
||||||
@ -71,6 +58,7 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
self.logger.debug("Other unhandled error", exc=exc)
|
self.logger.debug("Other unhandled error", exc=exc)
|
||||||
raise exc
|
raise exc
|
||||||
else:
|
else:
|
||||||
|
self.logger.debug("Got current, running reconcile")
|
||||||
self.reconcile(current, reference)
|
self.reconcile(current, reference)
|
||||||
except NeedsRecreate:
|
except NeedsRecreate:
|
||||||
self.logger.debug("Recreate requested")
|
self.logger.debug("Recreate requested")
|
||||||
@ -79,22 +67,16 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
self.delete(current)
|
self.delete(current)
|
||||||
else:
|
else:
|
||||||
self.logger.debug("No old found, creating")
|
self.logger.debug("No old found, creating")
|
||||||
self.logger.debug("Creating")
|
self.logger.debug("Created")
|
||||||
self.create(reference)
|
self.create(reference)
|
||||||
except NeedsUpdate:
|
except NeedsUpdate:
|
||||||
self.logger.debug("Updating")
|
self.logger.debug("Updating")
|
||||||
self.update(current, reference)
|
self.update(current, reference)
|
||||||
else:
|
else:
|
||||||
self.logger.debug("Object is up-to-date.")
|
self.logger.debug("Nothing to do...")
|
||||||
|
|
||||||
def down(self):
|
def down(self):
|
||||||
"""Delete object if found"""
|
"""Delete object if found"""
|
||||||
# Call self.get_reference_object to check if we even need to do anything
|
|
||||||
try:
|
|
||||||
self.get_reference_object()
|
|
||||||
except Disabled:
|
|
||||||
self.logger.debug("Object not required")
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
current = self.retrieve()
|
current = self.retrieve()
|
||||||
self.delete(current)
|
self.delete(current)
|
||||||
@ -138,7 +120,7 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
namespace=self.namespace,
|
namespace=self.namespace,
|
||||||
labels={
|
labels={
|
||||||
"app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
|
"app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
|
||||||
"app.kubernetes.io/instance": slugify(self.controller.outpost.name),
|
"app.kubernetes.io/instance": self.controller.outpost.name,
|
||||||
"app.kubernetes.io/version": __version__,
|
"app.kubernetes.io/version": __version__,
|
||||||
"app.kubernetes.io/managed-by": "goauthentik.io",
|
"app.kubernetes.io/managed-by": "goauthentik.io",
|
||||||
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
|
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
|
||||||
|
@ -16,6 +16,8 @@ from kubernetes.client import (
|
|||||||
V1SecretKeySelector,
|
V1SecretKeySelector,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from authentik import __version__
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||||
from authentik.outposts.controllers.k8s.base import (
|
from authentik.outposts.controllers.k8s.base import (
|
||||||
KubernetesObjectReconciler,
|
KubernetesObjectReconciler,
|
||||||
@ -37,6 +39,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
self.api = AppsV1Api(controller.client)
|
self.api = AppsV1Api(controller.client)
|
||||||
self.outpost = self.controller.outpost
|
self.outpost = self.controller.outpost
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
|
||||||
|
|
||||||
def reconcile(self, current: V1Deployment, reference: V1Deployment):
|
def reconcile(self, current: V1Deployment, reference: V1Deployment):
|
||||||
super().reconcile(current, reference)
|
super().reconcile(current, reference)
|
||||||
if current.spec.replicas != reference.spec.replicas:
|
if current.spec.replicas != reference.spec.replicas:
|
||||||
@ -62,13 +68,14 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
for port in self.controller.deployment_ports:
|
for port in self.controller.deployment_ports:
|
||||||
container_ports.append(
|
container_ports.append(
|
||||||
V1ContainerPort(
|
V1ContainerPort(
|
||||||
container_port=port.inner_port or port.port,
|
container_port=port.port,
|
||||||
name=port.name,
|
name=port.name,
|
||||||
protocol=port.protocol.upper(),
|
protocol=port.protocol.upper(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
meta = self.get_object_meta(name=self.name)
|
meta = self.get_object_meta(name=self.name)
|
||||||
image_name = self.controller.get_container_image()
|
secret_name = f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
|
||||||
|
image_prefix = CONFIG.y("outposts.docker_image_base")
|
||||||
return V1Deployment(
|
return V1Deployment(
|
||||||
metadata=meta,
|
metadata=meta,
|
||||||
spec=V1DeploymentSpec(
|
spec=V1DeploymentSpec(
|
||||||
@ -80,14 +87,14 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
containers=[
|
containers=[
|
||||||
V1Container(
|
V1Container(
|
||||||
name=str(self.outpost.type),
|
name=str(self.outpost.type),
|
||||||
image=image_name,
|
image=f"{image_prefix}-{self.outpost.type}:{__version__}",
|
||||||
ports=container_ports,
|
ports=container_ports,
|
||||||
env=[
|
env=[
|
||||||
V1EnvVar(
|
V1EnvVar(
|
||||||
name="AUTHENTIK_HOST",
|
name="AUTHENTIK_HOST",
|
||||||
value_from=V1EnvVarSource(
|
value_from=V1EnvVarSource(
|
||||||
secret_key_ref=V1SecretKeySelector(
|
secret_key_ref=V1SecretKeySelector(
|
||||||
name=self.name,
|
name=secret_name,
|
||||||
key="authentik_host",
|
key="authentik_host",
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -96,7 +103,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
name="AUTHENTIK_TOKEN",
|
name="AUTHENTIK_TOKEN",
|
||||||
value_from=V1EnvVarSource(
|
value_from=V1EnvVarSource(
|
||||||
secret_key_ref=V1SecretKeySelector(
|
secret_key_ref=V1SecretKeySelector(
|
||||||
name=self.name,
|
name=secret_name,
|
||||||
key="token",
|
key="token",
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@ -105,7 +112,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
name="AUTHENTIK_INSECURE",
|
name="AUTHENTIK_INSECURE",
|
||||||
value_from=V1EnvVarSource(
|
value_from=V1EnvVarSource(
|
||||||
secret_key_ref=V1SecretKeySelector(
|
secret_key_ref=V1SecretKeySelector(
|
||||||
name=self.name,
|
name=secret_name,
|
||||||
key="authentik_host_insecure",
|
key="authentik_host_insecure",
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -26,6 +26,10 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
|||||||
super().__init__(controller)
|
super().__init__(controller)
|
||||||
self.api = CoreV1Api(controller.client)
|
self.api = CoreV1Api(controller.client)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
|
||||||
|
|
||||||
def reconcile(self, current: V1Secret, reference: V1Secret):
|
def reconcile(self, current: V1Secret, reference: V1Secret):
|
||||||
super().reconcile(current, reference)
|
super().reconcile(current, reference)
|
||||||
for key in reference.data.keys():
|
for key in reference.data.keys():
|
||||||
|
@ -21,6 +21,10 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
|||||||
super().__init__(controller)
|
super().__init__(controller)
|
||||||
self.api = CoreV1Api(controller.client)
|
self.api = CoreV1Api(controller.client)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
|
||||||
|
|
||||||
def reconcile(self, current: V1Service, reference: V1Service):
|
def reconcile(self, current: V1Service, reference: V1Service):
|
||||||
super().reconcile(current, reference)
|
super().reconcile(current, reference)
|
||||||
if len(current.spec.ports) != len(reference.spec.ports):
|
if len(current.spec.ports) != len(reference.spec.ports):
|
||||||
@ -39,17 +43,13 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
|||||||
name=port.name,
|
name=port.name,
|
||||||
port=port.port,
|
port=port.port,
|
||||||
protocol=port.protocol.upper(),
|
protocol=port.protocol.upper(),
|
||||||
target_port=port.inner_port or port.port,
|
target_port=port.port,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
|
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
|
||||||
return V1Service(
|
return V1Service(
|
||||||
metadata=meta,
|
metadata=meta,
|
||||||
spec=V1ServiceSpec(
|
spec=V1ServiceSpec(ports=ports, selector=selector_labels, type="ClusterIP"),
|
||||||
ports=ports,
|
|
||||||
selector=selector_labels,
|
|
||||||
type=self.controller.outpost.config.kubernetes_service_type,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def create(self, reference: V1Service):
|
def create(self, reference: V1Service):
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
|
from kubernetes.client import OpenApiException
|
||||||
from kubernetes.client.api_client import ApiClient
|
from kubernetes.client.api_client import ApiClient
|
||||||
from kubernetes.client.exceptions import ApiException
|
|
||||||
from structlog.testing import capture_logs
|
from structlog.testing import capture_logs
|
||||||
|
from urllib3.exceptions import HTTPError
|
||||||
from yaml import dump_all
|
from yaml import dump_all
|
||||||
|
|
||||||
from authentik.outposts.controllers.base import BaseController, ControllerException
|
from authentik.outposts.controllers.base import BaseController, ControllerException
|
||||||
from authentik.outposts.controllers.k8s.base import Disabled, KubernetesObjectReconciler
|
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||||
from authentik.outposts.controllers.k8s.secret import SecretReconciler
|
from authentik.outposts.controllers.k8s.secret import SecretReconciler
|
||||||
from authentik.outposts.controllers.k8s.service import ServiceReconciler
|
from authentik.outposts.controllers.k8s.service import ServiceReconciler
|
||||||
@ -42,8 +43,8 @@ class KubernetesController(BaseController):
|
|||||||
reconciler = self.reconcilers[reconcile_key](self)
|
reconciler = self.reconcilers[reconcile_key](self)
|
||||||
reconciler.up()
|
reconciler.up()
|
||||||
|
|
||||||
except ApiException as exc:
|
except (OpenApiException, HTTPError) as exc:
|
||||||
raise ControllerException(str(exc)) from exc
|
raise ControllerException from exc
|
||||||
|
|
||||||
def up_with_logs(self) -> list[str]:
|
def up_with_logs(self) -> list[str]:
|
||||||
try:
|
try:
|
||||||
@ -54,8 +55,8 @@ class KubernetesController(BaseController):
|
|||||||
reconciler.up()
|
reconciler.up()
|
||||||
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
||||||
return all_logs
|
return all_logs
|
||||||
except ApiException as exc:
|
except (OpenApiException, HTTPError) as exc:
|
||||||
raise ControllerException(str(exc)) from exc
|
raise ControllerException from exc
|
||||||
|
|
||||||
def down(self):
|
def down(self):
|
||||||
try:
|
try:
|
||||||
@ -63,17 +64,14 @@ class KubernetesController(BaseController):
|
|||||||
reconciler = self.reconcilers[reconcile_key](self)
|
reconciler = self.reconcilers[reconcile_key](self)
|
||||||
reconciler.down()
|
reconciler.down()
|
||||||
|
|
||||||
except ApiException as exc:
|
except OpenApiException as exc:
|
||||||
raise ControllerException(str(exc)) from exc
|
raise ControllerException from exc
|
||||||
|
|
||||||
def get_static_deployment(self) -> str:
|
def get_static_deployment(self) -> str:
|
||||||
documents = []
|
documents = []
|
||||||
for reconcile_key in self.reconcile_order:
|
for reconcile_key in self.reconcile_order:
|
||||||
reconciler = self.reconcilers[reconcile_key](self)
|
reconciler = self.reconcilers[reconcile_key](self)
|
||||||
try:
|
documents.append(reconciler.get_reference_object().to_dict())
|
||||||
documents.append(reconciler.get_reference_object().to_dict())
|
|
||||||
except Disabled:
|
|
||||||
continue
|
|
||||||
|
|
||||||
with StringIO() as _str:
|
with StringIO() as _str:
|
||||||
dump_all(
|
dump_all(
|
||||||
|
@ -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"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -5,7 +5,6 @@ from typing import Iterable, Optional, Union
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from dacite import from_dict
|
from dacite import from_dict
|
||||||
from django.contrib.auth.models import Permission
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models.base import Model
|
from django.db.models.base import Model
|
||||||
@ -32,7 +31,6 @@ from authentik.crypto.models import CertificateKeyPair
|
|||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.models import InheritanceForeignKey
|
from authentik.lib.models import InheritanceForeignKey
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.lib.utils.http import USER_ATTRIBUTE_CAN_OVERRIDE_IP
|
|
||||||
from authentik.outposts.docker_tls import DockerInlineTLS
|
from authentik.outposts.docker_tls import DockerInlineTLS
|
||||||
|
|
||||||
OUR_VERSION = parse(__version__)
|
OUR_VERSION = parse(__version__)
|
||||||
@ -57,18 +55,16 @@ class OutpostConfig:
|
|||||||
"error_reporting.environment", "customer"
|
"error_reporting.environment", "customer"
|
||||||
)
|
)
|
||||||
|
|
||||||
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
|
||||||
kubernetes_replicas: int = field(default=1)
|
kubernetes_replicas: int = field(default=1)
|
||||||
kubernetes_namespace: str = field(default="default")
|
kubernetes_namespace: str = field(default="default")
|
||||||
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
||||||
kubernetes_ingress_secret_name: str = field(default="authentik-outpost-tls")
|
kubernetes_ingress_secret_name: str = field(default="authentik-outpost")
|
||||||
kubernetes_service_type: str = field(default="ClusterIP")
|
|
||||||
|
|
||||||
|
|
||||||
class OutpostModel(Model):
|
class OutpostModel(Model):
|
||||||
"""Base model for providers that need more objects than just themselves"""
|
"""Base model for providers that need more objects than just themselves"""
|
||||||
|
|
||||||
def get_required_objects(self) -> Iterable[Union[models.Model, str]]:
|
def get_required_objects(self) -> Iterable[models.Model]:
|
||||||
"""Return a list of all required objects"""
|
"""Return a list of all required objects"""
|
||||||
return [self]
|
return [self]
|
||||||
|
|
||||||
@ -81,7 +77,6 @@ class OutpostType(models.TextChoices):
|
|||||||
"""Outpost types, currently only the reverse proxy is available"""
|
"""Outpost types, currently only the reverse proxy is available"""
|
||||||
|
|
||||||
PROXY = "proxy"
|
PROXY = "proxy"
|
||||||
LDAP = "ldap"
|
|
||||||
|
|
||||||
|
|
||||||
def default_outpost_config(host: Optional[str] = None):
|
def default_outpost_config(host: Optional[str] = None):
|
||||||
@ -331,7 +326,6 @@ class Outpost(models.Model):
|
|||||||
if not users.exists():
|
if not users.exists():
|
||||||
user: User = User.objects.create(username=self.user_identifier)
|
user: User = User.objects.create(username=self.user_identifier)
|
||||||
user.attributes[USER_ATTRIBUTE_SA] = True
|
user.attributes[USER_ATTRIBUTE_SA] = True
|
||||||
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
|
||||||
user.set_unusable_password()
|
user.set_unusable_password()
|
||||||
user.save()
|
user.save()
|
||||||
else:
|
else:
|
||||||
@ -340,29 +334,9 @@ class Outpost(models.Model):
|
|||||||
# the ones the user needs
|
# the ones the user needs
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
UserObjectPermission.objects.filter(user=user).delete()
|
UserObjectPermission.objects.filter(user=user).delete()
|
||||||
user.user_permissions.clear()
|
for model in self.get_required_objects():
|
||||||
for model_or_perm in self.get_required_objects():
|
code_name = f"{model._meta.app_label}.view_{model._meta.model_name}"
|
||||||
if isinstance(model_or_perm, models.Model):
|
assign_perm(code_name, user, model)
|
||||||
model_or_perm: models.Model
|
|
||||||
code_name = (
|
|
||||||
f"{model_or_perm._meta.app_label}."
|
|
||||||
f"view_{model_or_perm._meta.model_name}"
|
|
||||||
)
|
|
||||||
assign_perm(code_name, user, model_or_perm)
|
|
||||||
else:
|
|
||||||
app_label, perm = model_or_perm.split(".")
|
|
||||||
permission = Permission.objects.filter(
|
|
||||||
codename=perm,
|
|
||||||
content_type__app_label=app_label,
|
|
||||||
)
|
|
||||||
if not permission.exists():
|
|
||||||
LOGGER.warning("permission doesn't exist", perm=model_or_perm)
|
|
||||||
continue
|
|
||||||
user.user_permissions.add(permission.first())
|
|
||||||
LOGGER.debug(
|
|
||||||
"Updated service account's permissions",
|
|
||||||
perms=UserObjectPermission.objects.filter(user=user),
|
|
||||||
)
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -385,9 +359,9 @@ class Outpost(models.Model):
|
|||||||
managed=f"goauthentik.io/outpost/{self.token_identifier}",
|
managed=f"goauthentik.io/outpost/{self.token_identifier}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_required_objects(self) -> Iterable[Union[models.Model, str]]:
|
def get_required_objects(self) -> Iterable[models.Model]:
|
||||||
"""Get an iterator of all objects the user needs read access to"""
|
"""Get an iterator of all objects the user needs read access to"""
|
||||||
objects: list[Union[models.Model, str]] = [self]
|
objects = [self]
|
||||||
for provider in (
|
for provider in (
|
||||||
Provider.objects.filter(outpost=self).select_related().select_subclasses()
|
Provider.objects.filter(outpost=self).select_related().select_subclasses()
|
||||||
):
|
):
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
"""authentik outpost signals"""
|
"""authentik outpost signals"""
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.utils.reflection import class_to_path
|
from authentik.lib.utils.reflection import class_to_path
|
||||||
from authentik.outposts.controllers.base import ControllerException
|
|
||||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
from authentik.outposts.models import Outpost, OutpostServiceConnection
|
||||||
from authentik.outposts.tasks import outpost_controller_down, outpost_post_save
|
from authentik.outposts.tasks import outpost_post_save, outpost_pre_delete
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
UPDATE_TRIGGERING_MODELS = (
|
UPDATE_TRIGGERING_MODELS = (
|
||||||
@ -21,27 +20,6 @@ UPDATE_TRIGGERING_MODELS = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=Outpost)
|
|
||||||
# pylint: disable=unused-argument
|
|
||||||
def pre_save_outpost(sender, instance: Outpost, **_):
|
|
||||||
"""Pre-save checks for an outpost, if the name or config.kubernetes_namespace changes,
|
|
||||||
we call down and then wait for the up after save"""
|
|
||||||
old_instances = Outpost.objects.filter(pk=instance.pk)
|
|
||||||
if not old_instances.exists():
|
|
||||||
return
|
|
||||||
old_instance = old_instances.first()
|
|
||||||
dirty = False
|
|
||||||
# Name changes the deployment name, need to recreate
|
|
||||||
dirty += old_instance.name != instance.name
|
|
||||||
# namespace requires re-create
|
|
||||||
dirty += (
|
|
||||||
old_instance.config.kubernetes_namespace != instance.config.kubernetes_namespace
|
|
||||||
)
|
|
||||||
if bool(dirty):
|
|
||||||
LOGGER.info("Outpost needs re-deployment due to changes", instance=instance)
|
|
||||||
outpost_controller_down_wrapper(old_instance)
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
@receiver(post_save)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def post_save_update(sender, instance: Model, **_):
|
def post_save_update(sender, instance: Model, **_):
|
||||||
@ -63,23 +41,15 @@ def post_save_update(sender, instance: Model, **_):
|
|||||||
def pre_delete_cleanup(sender, instance: Outpost, **_):
|
def pre_delete_cleanup(sender, instance: Outpost, **_):
|
||||||
"""Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
|
"""Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
|
||||||
instance.user.delete()
|
instance.user.delete()
|
||||||
outpost_controller_down_wrapper(instance)
|
# To ensure that deployment is cleaned up *consistently* we call the controller, and wait
|
||||||
|
# for it to finish. We don't want to call it in this thread, as we don't have the Outpost
|
||||||
|
# Service connection here
|
||||||
def outpost_controller_down_wrapper(instance: Outpost):
|
|
||||||
"""To ensure that deployment is cleaned up *consistently* we call the controller, and wait
|
|
||||||
for it to finish. We don't want to call it in this thread, as we don't have the Outpost
|
|
||||||
Service connection here"""
|
|
||||||
try:
|
try:
|
||||||
outpost_controller_down.delay(instance.pk.hex).get()
|
outpost_pre_delete.delay(instance.pk.hex).get()
|
||||||
except RuntimeError: # pragma: no cover
|
except RuntimeError:
|
||||||
# In e2e/integration tests, this might run inside a thread/process and
|
# In e2e/integration tests, this might run inside a thread/process and
|
||||||
# trigger the celery `Never call result.get() within a task` detection
|
# trigger the celery `Never call result.get() within a task` detection
|
||||||
if settings.TEST:
|
if settings.TEST:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
except ControllerException as exc:
|
|
||||||
LOGGER.warning(
|
|
||||||
"failed to cleanup outpost deployment", exc=exc, instance=instance
|
|
||||||
)
|
|
||||||
|
@ -3,7 +3,7 @@ from os import R_OK, access
|
|||||||
from os.path import expanduser
|
from os.path import expanduser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from socket import gethostname
|
from socket import gethostname
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
@ -19,7 +19,7 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
from authentik.lib.utils.reflection import path_to_class
|
from authentik.lib.utils.reflection import path_to_class
|
||||||
from authentik.outposts.controllers.base import BaseController, ControllerException
|
from authentik.outposts.controllers.base import ControllerException
|
||||||
from authentik.outposts.models import (
|
from authentik.outposts.models import (
|
||||||
DockerServiceConnection,
|
DockerServiceConnection,
|
||||||
KubernetesServiceConnection,
|
KubernetesServiceConnection,
|
||||||
@ -29,8 +29,6 @@ from authentik.outposts.models import (
|
|||||||
OutpostState,
|
OutpostState,
|
||||||
OutpostType,
|
OutpostType,
|
||||||
)
|
)
|
||||||
from authentik.providers.ldap.controllers.docker import LDAPDockerController
|
|
||||||
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
|
|
||||||
from authentik.providers.proxy.controllers.docker import ProxyDockerController
|
from authentik.providers.proxy.controllers.docker import ProxyDockerController
|
||||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
@ -38,24 +36,6 @@ from authentik.root.celery import CELERY_APP
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
def controller_for_outpost(outpost: Outpost) -> Optional[BaseController]:
|
|
||||||
"""Get a controller for the outpost, when a service connection is defined"""
|
|
||||||
if not outpost.service_connection:
|
|
||||||
return None
|
|
||||||
service_connection = outpost.service_connection
|
|
||||||
if outpost.type == OutpostType.PROXY:
|
|
||||||
if isinstance(service_connection, DockerServiceConnection):
|
|
||||||
return ProxyDockerController(outpost, service_connection)
|
|
||||||
if isinstance(service_connection, KubernetesServiceConnection):
|
|
||||||
return ProxyKubernetesController(outpost, service_connection)
|
|
||||||
if outpost.type == OutpostType.LDAP:
|
|
||||||
if isinstance(service_connection, DockerServiceConnection):
|
|
||||||
return LDAPDockerController(outpost, service_connection)
|
|
||||||
if isinstance(service_connection, KubernetesServiceConnection):
|
|
||||||
return LDAPKubernetesController(outpost, service_connection)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task()
|
||||||
def outpost_controller_all():
|
def outpost_controller_all():
|
||||||
"""Launch Controller for all Outposts which support it"""
|
"""Launch Controller for all Outposts which support it"""
|
||||||
@ -96,10 +76,16 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str):
|
|||||||
outpost: Outpost = Outpost.objects.get(pk=outpost_pk)
|
outpost: Outpost = Outpost.objects.get(pk=outpost_pk)
|
||||||
self.set_uid(slugify(outpost.name))
|
self.set_uid(slugify(outpost.name))
|
||||||
try:
|
try:
|
||||||
controller = controller_for_outpost(outpost)
|
if not outpost.service_connection:
|
||||||
if not controller:
|
|
||||||
return
|
return
|
||||||
logs = controller.up_with_logs()
|
if outpost.type == OutpostType.PROXY:
|
||||||
|
service_connection = outpost.service_connection
|
||||||
|
if isinstance(service_connection, DockerServiceConnection):
|
||||||
|
logs = ProxyDockerController(outpost, service_connection).up_with_logs()
|
||||||
|
if isinstance(service_connection, KubernetesServiceConnection):
|
||||||
|
logs = ProxyKubernetesController(
|
||||||
|
outpost, service_connection
|
||||||
|
).up_with_logs()
|
||||||
LOGGER.debug("---------------Outpost Controller logs starting----------------")
|
LOGGER.debug("---------------Outpost Controller logs starting----------------")
|
||||||
for log in logs:
|
for log in logs:
|
||||||
LOGGER.debug(log)
|
LOGGER.debug(log)
|
||||||
@ -111,13 +97,15 @@ def outpost_controller(self: MonitoredTask, outpost_pk: str):
|
|||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task()
|
||||||
def outpost_controller_down(outpost_pk: str):
|
def outpost_pre_delete(outpost_pk: str):
|
||||||
"""Delete outpost objects before deleting the DB Object"""
|
"""Delete outpost objects before deleting the DB Object"""
|
||||||
outpost = Outpost.objects.get(pk=outpost_pk)
|
outpost = Outpost.objects.get(pk=outpost_pk)
|
||||||
controller = controller_for_outpost(outpost)
|
if outpost.type == OutpostType.PROXY:
|
||||||
if not controller:
|
service_connection = outpost.service_connection
|
||||||
return
|
if isinstance(service_connection, DockerServiceConnection):
|
||||||
controller.down()
|
ProxyDockerController(outpost, service_connection).down()
|
||||||
|
if isinstance(service_connection, KubernetesServiceConnection):
|
||||||
|
ProxyKubernetesController(outpost, service_connection).down()
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||||
|
@ -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.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans 'Permission denied' %} - {{ config.authentik.branding.title }}
|
{% trans 'Permission denied - authentik' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card_title %}
|
{% block card_title %}
|
||||||
|
@ -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"]
|
|
@ -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"
|
|
@ -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),
|
|
||||||
]
|
|
@ -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),
|
|
||||||
]
|
|
@ -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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -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")
|
|
@ -38,7 +38,6 @@ class OAuth2ProviderSerializer(ProviderSerializer):
|
|||||||
"client_type",
|
"client_type",
|
||||||
"client_id",
|
"client_id",
|
||||||
"client_secret",
|
"client_secret",
|
||||||
"access_code_validity",
|
|
||||||
"token_validity",
|
"token_validity",
|
||||||
"include_claims_in_id_token",
|
"include_claims_in_id_token",
|
||||||
"jwt_alg",
|
"jwt_alg",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""OAuth2Provider API Views"""
|
"""OAuth2Provider API Views"""
|
||||||
from guardian.utils import get_anonymous_user
|
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.fields import CharField, ListField
|
from rest_framework.fields import CharField, ListField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
@ -39,10 +38,11 @@ class AuthorizationCodeViewSet(
|
|||||||
ordering = ["provider", "expires"]
|
ordering = ["provider", "expires"]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
if not self.request:
|
||||||
if user.is_superuser:
|
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
return super().get_queryset().filter(user=user.pk)
|
if self.request.user.is_superuser:
|
||||||
|
return super().get_queryset()
|
||||||
|
return super().get_queryset().filter(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenViewSet(
|
class RefreshTokenViewSet(
|
||||||
@ -59,7 +59,8 @@ class RefreshTokenViewSet(
|
|||||||
ordering = ["provider", "expires"]
|
ordering = ["provider", "expires"]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
if not self.request:
|
||||||
if user.is_superuser:
|
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
return super().get_queryset().filter(user=user.pk)
|
if self.request.user.is_superuser:
|
||||||
|
return super().get_queryset()
|
||||||
|
return super().get_queryset().filter(user=self.request.user)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"""authentik oauth provider app config"""
|
"""authentik auth oauth provider app config"""
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class AuthentikProviderOAuth2Config(AppConfig):
|
class AuthentikProviderOAuth2Config(AppConfig):
|
||||||
"""authentik oauth provider app config"""
|
"""authentik auth oauth provider app config"""
|
||||||
|
|
||||||
name = "authentik.providers.oauth2"
|
name = "authentik.providers.oauth2"
|
||||||
label = "authentik_providers_oauth2"
|
label = "authentik_providers_oauth2"
|
||||||
|
@ -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],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -6,18 +6,18 @@ import time
|
|||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from hashlib import sha256
|
from hashlib import sha256
|
||||||
from typing import Any, Optional, Type, Union
|
from typing import Any, Optional, Type
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
|
||||||
from dacite import from_dict
|
from dacite import from_dict
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils import dateformat, timezone
|
from django.utils import dateformat, timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from jwt import encode
|
from jwkest.jwk import Key, RSAKey, SYMKey, import_rsa_key
|
||||||
|
from jwkest.jws import JWS
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
|
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
|
||||||
@ -175,16 +175,6 @@ class OAuth2Provider(Provider):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
access_code_validity = models.TextField(
|
|
||||||
default="minutes=1",
|
|
||||||
validators=[timedelta_string_validator],
|
|
||||||
help_text=_(
|
|
||||||
(
|
|
||||||
"Access codes not valid on or after current time + this value "
|
|
||||||
"(Format: hours=1;minutes=2;seconds=3)."
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
token_validity = models.TextField(
|
token_validity = models.TextField(
|
||||||
default="minutes=10",
|
default="minutes=10",
|
||||||
validators=[timedelta_string_validator],
|
validators=[timedelta_string_validator],
|
||||||
@ -239,7 +229,7 @@ class OAuth2Provider(Provider):
|
|||||||
token.access_token = token.create_access_token(user, request)
|
token.access_token = token.create_access_token(user, request)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def get_jwt_keys(self) -> Union[RSAPrivateKey, str]:
|
def get_jwt_keys(self) -> list[Key]:
|
||||||
"""
|
"""
|
||||||
Takes a provider and returns the set of keys associated with it.
|
Takes a provider and returns the set of keys associated with it.
|
||||||
Returns a list of keys.
|
Returns a list of keys.
|
||||||
@ -256,10 +246,17 @@ class OAuth2Provider(Provider):
|
|||||||
self.jwt_alg = JWTAlgorithms.HS256
|
self.jwt_alg = JWTAlgorithms.HS256
|
||||||
self.save()
|
self.save()
|
||||||
else:
|
else:
|
||||||
return self.rsa_key.private_key
|
# Because the JWT Library uses python cryptodome,
|
||||||
|
# we can't directly pass the RSAPublicKey
|
||||||
|
# object, but have to load it ourselves
|
||||||
|
key = import_rsa_key(self.rsa_key.key_data)
|
||||||
|
keys = [RSAKey(key=key, kid=self.rsa_key.kid)]
|
||||||
|
if not keys:
|
||||||
|
raise Exception("You must add at least one RSA Key.")
|
||||||
|
return keys
|
||||||
|
|
||||||
if self.jwt_alg == JWTAlgorithms.HS256:
|
if self.jwt_alg == JWTAlgorithms.HS256:
|
||||||
return self.client_secret
|
return [SYMKey(key=self.client_secret, alg=self.jwt_alg)]
|
||||||
|
|
||||||
raise Exception("Unsupported key algorithm.")
|
raise Exception("Unsupported key algorithm.")
|
||||||
|
|
||||||
@ -300,11 +297,11 @@ class OAuth2Provider(Provider):
|
|||||||
|
|
||||||
def encode(self, payload: dict[str, Any]) -> str:
|
def encode(self, payload: dict[str, Any]) -> str:
|
||||||
"""Represent the ID Token as a JSON Web Token (JWT)."""
|
"""Represent the ID Token as a JSON Web Token (JWT)."""
|
||||||
key = self.get_jwt_keys()
|
keys = self.get_jwt_keys()
|
||||||
# If the provider does not have an RSA Key assigned, it was switched to Symmetric
|
# If the provider does not have an RSA Key assigned, it was switched to Symmetric
|
||||||
self.refresh_from_db()
|
self.refresh_from_db()
|
||||||
# pyright: reportGeneralTypeIssues=false
|
jws = JWS(payload, alg=self.jwt_alg)
|
||||||
return encode(payload, key, algorithm=self.jwt_alg)
|
return jws.sign_compact(keys)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
{% trans 'End session' %} - {{ config.authentik.branding.title }}
|
{% trans 'End session' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block card_title %}
|
{% block card_title %}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""Test authorize view"""
|
"""Test authorize view"""
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
@ -11,21 +11,17 @@ from authentik.providers.oauth2.errors import (
|
|||||||
ClientIdError,
|
ClientIdError,
|
||||||
RedirectUriError,
|
RedirectUriError,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.generators import (
|
from authentik.providers.oauth2.generators import generate_client_id
|
||||||
generate_client_id,
|
|
||||||
generate_client_secret,
|
|
||||||
)
|
|
||||||
from authentik.providers.oauth2.models import (
|
from authentik.providers.oauth2.models import (
|
||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
GrantTypes,
|
GrantTypes,
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
|
||||||
from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams
|
from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams
|
||||||
|
|
||||||
|
|
||||||
class TestAuthorize(OAuthTestCase):
|
class TestViewsAuthorize(TestCase):
|
||||||
"""Test authorize view"""
|
"""Test authorize view"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
@ -204,7 +200,6 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_client_secret(),
|
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris="http://localhost",
|
||||||
)
|
)
|
||||||
@ -238,4 +233,3 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.validate_jwt(token, provider)
|
|
@ -1,11 +1,11 @@
|
|||||||
"""Test token view"""
|
"""Test token view"""
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
|
||||||
from django.test import RequestFactory
|
from django.test import RequestFactory, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import User
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.providers.oauth2.constants import (
|
from authentik.providers.oauth2.constants import (
|
||||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||||
@ -20,17 +20,15 @@ from authentik.providers.oauth2.models import (
|
|||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
)
|
)
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
|
||||||
from authentik.providers.oauth2.views.token import TokenParams
|
from authentik.providers.oauth2.views.token import TokenParams
|
||||||
|
|
||||||
|
|
||||||
class TestToken(OAuthTestCase):
|
class TestViewsToken(TestCase):
|
||||||
"""Test token view"""
|
"""Test token view"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.app = Application.objects.create(name="test", slug="test")
|
|
||||||
|
|
||||||
def test_request_auth_code(self):
|
def test_request_auth_code(self):
|
||||||
"""test request param"""
|
"""test request param"""
|
||||||
@ -99,15 +97,12 @@ class TestToken(OAuthTestCase):
|
|||||||
authorization_flow=Flow.objects.first(),
|
authorization_flow=Flow.objects.first(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
)
|
)
|
||||||
# Needs to be assigned to an application for iss to be set
|
|
||||||
self.app.provider = provider
|
|
||||||
self.app.save()
|
|
||||||
header = b64encode(
|
header = b64encode(
|
||||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||||
).decode()
|
).decode()
|
||||||
user = User.objects.get(username="akadmin")
|
user = User.objects.get(username="akadmin")
|
||||||
code = AuthorizationCode.objects.create(
|
code = AuthorizationCode.objects.create(
|
||||||
code="foobar", provider=provider, user=user, is_open_id=True
|
code="foobar", provider=provider, user=user
|
||||||
)
|
)
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_providers_oauth2:token"),
|
reverse("authentik_providers_oauth2:token"),
|
||||||
@ -131,7 +126,6 @@ class TestToken(OAuthTestCase):
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.validate_jwt(new_token, provider)
|
|
||||||
|
|
||||||
def test_refresh_token_view(self):
|
def test_refresh_token_view(self):
|
||||||
"""test request param"""
|
"""test request param"""
|
||||||
@ -142,9 +136,6 @@ class TestToken(OAuthTestCase):
|
|||||||
authorization_flow=Flow.objects.first(),
|
authorization_flow=Flow.objects.first(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
)
|
)
|
||||||
# Needs to be assigned to an application for iss to be set
|
|
||||||
self.app.provider = provider
|
|
||||||
self.app.save()
|
|
||||||
header = b64encode(
|
header = b64encode(
|
||||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||||
).decode()
|
).decode()
|
||||||
@ -183,7 +174,6 @@ class TestToken(OAuthTestCase):
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.validate_jwt(new_token, provider)
|
|
||||||
|
|
||||||
def test_refresh_token_view_invalid_origin(self):
|
def test_refresh_token_view_invalid_origin(self):
|
||||||
"""test request param"""
|
"""test request param"""
|
@ -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")
|
|
@ -219,7 +219,7 @@ class OAuthAuthorizationParams:
|
|||||||
code.code_challenge_method = self.code_challenge_method
|
code.code_challenge_method = self.code_challenge_method
|
||||||
|
|
||||||
code.expires_at = timezone.now() + timedelta_from_string(
|
code.expires_at = timezone.now() + timedelta_from_string(
|
||||||
self.provider.access_code_validity
|
self.provider.token_validity
|
||||||
)
|
)
|
||||||
code.scope = self.scope
|
code.scope = self.scope
|
||||||
code.nonce = self.nonce
|
code.nonce = self.nonce
|
||||||
@ -291,7 +291,7 @@ class OAuthFulfillmentStage(StageView):
|
|||||||
GrantTypes.HYBRID,
|
GrantTypes.HYBRID,
|
||||||
]:
|
]:
|
||||||
code = self.params.create_code(self.request)
|
code = self.params.create_code(self.request)
|
||||||
code.save(force_insert=True)
|
code.save()
|
||||||
|
|
||||||
if self.params.grant_type == GrantTypes.AUTHORIZATION_CODE:
|
if self.params.grant_type == GrantTypes.AUTHORIZATION_CODE:
|
||||||
query_params["code"] = code.code
|
query_params["code"] = code.code
|
||||||
|
@ -104,6 +104,7 @@ class TokenIntrospectionView(View):
|
|||||||
token: RefreshToken
|
token: RefreshToken
|
||||||
params: TokenIntrospectionParams
|
params: TokenIntrospectionParams
|
||||||
provider: OAuth2Provider
|
provider: OAuth2Provider
|
||||||
|
id_token: IDToken
|
||||||
|
|
||||||
def post(self, request: HttpRequest) -> HttpResponse:
|
def post(self, request: HttpRequest) -> HttpResponse:
|
||||||
"""Introspection handler"""
|
"""Introspection handler"""
|
||||||
|
@ -1,23 +1,14 @@
|
|||||||
"""authentik OAuth2 JWKS Views"""
|
"""authentik OAuth2 JWKS Views"""
|
||||||
from base64 import urlsafe_b64encode
|
|
||||||
|
|
||||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
|
||||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from jwkest import long_to_base64
|
||||||
|
from jwkest.jwk import import_rsa_key
|
||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
|
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
|
||||||
|
|
||||||
|
|
||||||
def b64_enc(number: int) -> str:
|
|
||||||
"""Convert number to base64-encoded octet-value"""
|
|
||||||
length = ((number).bit_length() + 7) // 8
|
|
||||||
number_bytes = number.to_bytes(length, "big")
|
|
||||||
final = urlsafe_b64encode(number_bytes).rstrip(b"=")
|
|
||||||
return final.decode("ascii")
|
|
||||||
|
|
||||||
|
|
||||||
class JWKSView(View):
|
class JWKSView(View):
|
||||||
"""Show RSA Key data for Provider"""
|
"""Show RSA Key data for Provider"""
|
||||||
|
|
||||||
@ -31,16 +22,15 @@ class JWKSView(View):
|
|||||||
response_data = {}
|
response_data = {}
|
||||||
|
|
||||||
if provider.jwt_alg == JWTAlgorithms.RS256:
|
if provider.jwt_alg == JWTAlgorithms.RS256:
|
||||||
public_key: RSAPublicKey = provider.rsa_key.private_key.public_key()
|
public_key = import_rsa_key(provider.rsa_key.key_data).publickey()
|
||||||
public_numbers = public_key.public_numbers()
|
|
||||||
response_data["keys"] = [
|
response_data["keys"] = [
|
||||||
{
|
{
|
||||||
"kty": "RSA",
|
"kty": "RSA",
|
||||||
"alg": "RS256",
|
"alg": "RS256",
|
||||||
"use": "sig",
|
"use": "sig",
|
||||||
"kid": provider.rsa_key.kid,
|
"kid": provider.rsa_key.kid,
|
||||||
"n": b64_enc(public_numbers.n),
|
"n": long_to_base64(public_key.n),
|
||||||
"e": b64_enc(public_numbers.e),
|
"e": long_to_base64(public_key.e),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ from authentik.providers.oauth2.constants import (
|
|||||||
from authentik.providers.oauth2.errors import TokenError, UserAuthError
|
from authentik.providers.oauth2.errors import TokenError, UserAuthError
|
||||||
from authentik.providers.oauth2.models import (
|
from authentik.providers.oauth2.models import (
|
||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
ClientTypes,
|
|
||||||
OAuth2Provider,
|
OAuth2Provider,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
)
|
)
|
||||||
@ -76,7 +75,7 @@ class TokenParams:
|
|||||||
LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id)
|
LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id)
|
||||||
raise TokenError("invalid_client")
|
raise TokenError("invalid_client")
|
||||||
|
|
||||||
if self.provider.client_type == ClientTypes.CONFIDENTIAL:
|
if self.provider.client_type == "confidential":
|
||||||
if self.provider.client_secret != self.client_secret:
|
if self.provider.client_secret != self.client_secret:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Invalid client secret: client does not have secret",
|
"Invalid client secret: client does not have secret",
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
"""ProxyProvider API Views"""
|
"""ProxyProvider API Views"""
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from drf_yasg.utils import swagger_serializer_method
|
from drf_yasg.utils import swagger_serializer_method
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
@ -33,17 +30,6 @@ class OpenIDConnectConfigurationSerializer(PassiveSerializer):
|
|||||||
class ProxyProviderSerializer(ProviderSerializer):
|
class ProxyProviderSerializer(ProviderSerializer):
|
||||||
"""ProxyProvider Serializer"""
|
"""ProxyProvider Serializer"""
|
||||||
|
|
||||||
def validate(self, attrs) -> dict[Any, str]:
|
|
||||||
"""Check that internal_host is set when forward_auth_mode is disabled"""
|
|
||||||
if (
|
|
||||||
not attrs.get("forward_auth_mode", False)
|
|
||||||
and attrs.get("internal_host", "") == ""
|
|
||||||
):
|
|
||||||
raise ValidationError(
|
|
||||||
"Internal host cannot be empty when forward auth is disabled."
|
|
||||||
)
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
instance: ProxyProvider = super().create(validated_data)
|
instance: ProxyProvider = super().create(validated_data)
|
||||||
instance.set_oauth_defaults()
|
instance.set_oauth_defaults()
|
||||||
@ -66,7 +52,6 @@ class ProxyProviderSerializer(ProviderSerializer):
|
|||||||
"basic_auth_enabled",
|
"basic_auth_enabled",
|
||||||
"basic_auth_password_attribute",
|
"basic_auth_password_attribute",
|
||||||
"basic_auth_user_attribute",
|
"basic_auth_user_attribute",
|
||||||
"forward_auth_mode",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -101,7 +86,6 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
|
|||||||
"basic_auth_enabled",
|
"basic_auth_enabled",
|
||||||
"basic_auth_password_attribute",
|
"basic_auth_password_attribute",
|
||||||
"basic_auth_user_attribute",
|
"basic_auth_user_attribute",
|
||||||
"forward_auth_mode",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=OpenIDConnectConfigurationSerializer)
|
@swagger_serializer_method(serializer_or_field=OpenIDConnectConfigurationSerializer)
|
||||||
|
@ -17,7 +17,6 @@ from kubernetes.client.models.networking_v1beta1_ingress_rule import (
|
|||||||
|
|
||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||||
from authentik.outposts.controllers.k8s.base import (
|
from authentik.outposts.controllers.k8s.base import (
|
||||||
Disabled,
|
|
||||||
KubernetesObjectReconciler,
|
KubernetesObjectReconciler,
|
||||||
NeedsUpdate,
|
NeedsUpdate,
|
||||||
)
|
)
|
||||||
@ -34,6 +33,10 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
|
|||||||
super().__init__(controller)
|
super().__init__(controller)
|
||||||
self.api = NetworkingV1beta1Api(controller.client)
|
self.api = NetworkingV1beta1Api(controller.client)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
|
||||||
|
|
||||||
def _check_annotations(self, reference: NetworkingV1beta1Ingress):
|
def _check_annotations(self, reference: NetworkingV1beta1Ingress):
|
||||||
"""Check that all annotations *we* set are correct"""
|
"""Check that all annotations *we* set are correct"""
|
||||||
for key, value in self.get_ingress_annotations().items():
|
for key, value in self.get_ingress_annotations().items():
|
||||||
@ -51,8 +54,7 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
|
|||||||
expected_hosts = []
|
expected_hosts = []
|
||||||
expected_hosts_tls = []
|
expected_hosts_tls = []
|
||||||
for proxy_provider in ProxyProvider.objects.filter(
|
for proxy_provider in ProxyProvider.objects.filter(
|
||||||
outpost__in=[self.controller.outpost],
|
outpost__in=[self.controller.outpost]
|
||||||
forward_auth_mode=False,
|
|
||||||
):
|
):
|
||||||
proxy_provider: ProxyProvider
|
proxy_provider: ProxyProvider
|
||||||
external_host_name = urlparse(proxy_provider.external_host)
|
external_host_name = urlparse(proxy_provider.external_host)
|
||||||
@ -100,46 +102,27 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
|
|||||||
rules = []
|
rules = []
|
||||||
tls_hosts = []
|
tls_hosts = []
|
||||||
for proxy_provider in ProxyProvider.objects.filter(
|
for proxy_provider in ProxyProvider.objects.filter(
|
||||||
outpost__in=[self.controller.outpost],
|
outpost__in=[self.controller.outpost]
|
||||||
):
|
):
|
||||||
proxy_provider: ProxyProvider
|
proxy_provider: ProxyProvider
|
||||||
external_host_name = urlparse(proxy_provider.external_host)
|
external_host_name = urlparse(proxy_provider.external_host)
|
||||||
if external_host_name.scheme == "https":
|
if external_host_name.scheme == "https":
|
||||||
tls_hosts.append(external_host_name.hostname)
|
tls_hosts.append(external_host_name.hostname)
|
||||||
if proxy_provider.forward_auth_mode:
|
rule = NetworkingV1beta1IngressRule(
|
||||||
rule = NetworkingV1beta1IngressRule(
|
host=external_host_name.hostname,
|
||||||
host=external_host_name.hostname,
|
http=NetworkingV1beta1HTTPIngressRuleValue(
|
||||||
http=NetworkingV1beta1HTTPIngressRuleValue(
|
paths=[
|
||||||
paths=[
|
NetworkingV1beta1HTTPIngressPath(
|
||||||
NetworkingV1beta1HTTPIngressPath(
|
backend=NetworkingV1beta1IngressBackend(
|
||||||
backend=NetworkingV1beta1IngressBackend(
|
service_name=self.name,
|
||||||
service_name=self.name,
|
service_port="http",
|
||||||
service_port="http",
|
),
|
||||||
),
|
path="/",
|
||||||
path="/akprox",
|
)
|
||||||
)
|
]
|
||||||
]
|
),
|
||||||
),
|
)
|
||||||
)
|
|
||||||
else:
|
|
||||||
rule = NetworkingV1beta1IngressRule(
|
|
||||||
host=external_host_name.hostname,
|
|
||||||
http=NetworkingV1beta1HTTPIngressRuleValue(
|
|
||||||
paths=[
|
|
||||||
NetworkingV1beta1HTTPIngressPath(
|
|
||||||
backend=NetworkingV1beta1IngressBackend(
|
|
||||||
service_name=self.name,
|
|
||||||
service_port="http",
|
|
||||||
),
|
|
||||||
path="/",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
rules.append(rule)
|
rules.append(rule)
|
||||||
if not rules:
|
|
||||||
self.logger.debug("No providers use proxying, no ingress needed")
|
|
||||||
raise Disabled()
|
|
||||||
tls_config = None
|
tls_config = None
|
||||||
if tls_hosts:
|
if tls_hosts:
|
||||||
tls_config = NetworkingV1beta1IngressTLS(
|
tls_config = NetworkingV1beta1IngressTLS(
|
||||||
|
@ -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,
|
|
||||||
)
|
|
@ -3,9 +3,6 @@ from authentik.outposts.controllers.base import DeploymentPort
|
|||||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost
|
from authentik.outposts.models import KubernetesServiceConnection, Outpost
|
||||||
from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler
|
from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler
|
||||||
from authentik.providers.proxy.controllers.k8s.traefik import (
|
|
||||||
TraefikMiddlewareReconciler,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyKubernetesController(KubernetesController):
|
class ProxyKubernetesController(KubernetesController):
|
||||||
@ -18,6 +15,4 @@ class ProxyKubernetesController(KubernetesController):
|
|||||||
DeploymentPort(4443, "https", "tcp"),
|
DeploymentPort(4443, "https", "tcp"),
|
||||||
]
|
]
|
||||||
self.reconcilers["ingress"] = IngressReconciler
|
self.reconcilers["ingress"] = IngressReconciler
|
||||||
self.reconcilers["traefik middleware"] = TraefikMiddlewareReconciler
|
|
||||||
self.reconcile_order.append("ingress")
|
self.reconcile_order.append("ingress")
|
||||||
self.reconcile_order.append("traefik middleware")
|
|
||||||
|
@ -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")
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,7 +1,7 @@
|
|||||||
"""authentik proxy models"""
|
"""authentik proxy models"""
|
||||||
import string
|
import string
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from typing import Iterable, Optional, Type, Union
|
from typing import Iterable, Optional, Type
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -42,8 +42,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
|||||||
Protocols by using a Reverse-Proxy."""
|
Protocols by using a Reverse-Proxy."""
|
||||||
|
|
||||||
internal_host = models.TextField(
|
internal_host = models.TextField(
|
||||||
validators=[DomainlessURLValidator(schemes=("http", "https"))],
|
validators=[DomainlessURLValidator(schemes=("http", "https"))]
|
||||||
blank=True,
|
|
||||||
)
|
)
|
||||||
external_host = models.TextField(
|
external_host = models.TextField(
|
||||||
validators=[DomainlessURLValidator(schemes=("http", "https"))]
|
validators=[DomainlessURLValidator(schemes=("http", "https"))]
|
||||||
@ -53,13 +52,6 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
|||||||
help_text=_("Validate SSL Certificates of upstream servers"),
|
help_text=_("Validate SSL Certificates of upstream servers"),
|
||||||
verbose_name=_("Internal host SSL Validation"),
|
verbose_name=_("Internal host SSL Validation"),
|
||||||
)
|
)
|
||||||
forward_auth_mode = models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text=_(
|
|
||||||
"Enable support for forwardAuth in traefik and nginx auth_request. Exclusive with "
|
|
||||||
"internal_host."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
skip_path_regex = models.TextField(
|
skip_path_regex = models.TextField(
|
||||||
default="",
|
default="",
|
||||||
@ -147,7 +139,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Proxy Provider {self.name}"
|
return f"Proxy Provider {self.name}"
|
||||||
|
|
||||||
def get_required_objects(self) -> Iterable[Union[models.Model, str]]:
|
def get_required_objects(self) -> Iterable[models.Model]:
|
||||||
required_models = [self]
|
required_models = [self]
|
||||||
if self.certificate is not None:
|
if self.certificate is not None:
|
||||||
required_models.append(self.certificate)
|
required_models.append(self.certificate)
|
||||||
|
@ -102,13 +102,11 @@ INSTALLED_APPS = [
|
|||||||
"authentik.policies.password",
|
"authentik.policies.password",
|
||||||
"authentik.policies.reputation",
|
"authentik.policies.reputation",
|
||||||
"authentik.providers.proxy",
|
"authentik.providers.proxy",
|
||||||
"authentik.providers.ldap",
|
|
||||||
"authentik.providers.oauth2",
|
"authentik.providers.oauth2",
|
||||||
"authentik.providers.saml",
|
"authentik.providers.saml",
|
||||||
"authentik.recovery",
|
"authentik.recovery",
|
||||||
"authentik.sources.ldap",
|
"authentik.sources.ldap",
|
||||||
"authentik.sources.oauth",
|
"authentik.sources.oauth",
|
||||||
"authentik.sources.plex",
|
|
||||||
"authentik.sources.saml",
|
"authentik.sources.saml",
|
||||||
"authentik.stages.authenticator_static",
|
"authentik.stages.authenticator_static",
|
||||||
"authentik.stages.authenticator_totp",
|
"authentik.stages.authenticator_totp",
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""authentik URL Configuration"""
|
"""authentik URL Configuration"""
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
@ -47,3 +49,11 @@ urlpatterns += [
|
|||||||
path("-/health/live/", LiveView.as_view(), name="health-live"),
|
path("-/health/live/", LiveView.as_view(), name="health-live"),
|
||||||
path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
|
path("-/health/ready/", ReadyView.as_view(), name="health-ready"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG: # pragma: no cover
|
||||||
|
|
||||||
|
urlpatterns = (
|
||||||
|
static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
+ urlpatterns
|
||||||
|
)
|
||||||
|
@ -19,7 +19,7 @@ class BaseLDAPSynchronizer:
|
|||||||
|
|
||||||
def __init__(self, source: LDAPSource):
|
def __init__(self, source: LDAPSource):
|
||||||
self._source = source
|
self._source = source
|
||||||
self._logger = get_logger().bind(source=source, syncer=self.__class__.__name__)
|
self._logger = get_logger().bind(source=source)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_dn_users(self) -> str:
|
def base_dn_users(self) -> str:
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""OAuth Source Serializer"""
|
"""OAuth Source Serializer"""
|
||||||
from guardian.utils import get_anonymous_user
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.sources import SourceSerializer
|
from authentik.core.api.sources import SourceSerializer
|
||||||
@ -27,7 +26,8 @@ class UserOAuthSourceConnectionViewSet(ModelViewSet):
|
|||||||
filterset_fields = ["source__slug"]
|
filterset_fields = ["source__slug"]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
if not self.request:
|
||||||
if user.is_superuser:
|
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
return super().get_queryset().filter(user=user.pk)
|
if self.request.user.is_superuser:
|
||||||
|
return super().get_queryset()
|
||||||
|
return super().get_queryset().filter(user=self.request.user)
|
||||||
|
@ -2,21 +2,11 @@
|
|||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.conf import settings
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
AUTHENTIK_SOURCES_OAUTH_TYPES = [
|
|
||||||
"authentik.sources.oauth.types.discord",
|
|
||||||
"authentik.sources.oauth.types.facebook",
|
|
||||||
"authentik.sources.oauth.types.github",
|
|
||||||
"authentik.sources.oauth.types.google",
|
|
||||||
"authentik.sources.oauth.types.reddit",
|
|
||||||
"authentik.sources.oauth.types.twitter",
|
|
||||||
"authentik.sources.oauth.types.azure_ad",
|
|
||||||
"authentik.sources.oauth.types.oidc",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AuthentikSourceOAuthConfig(AppConfig):
|
class AuthentikSourceOAuthConfig(AppConfig):
|
||||||
"""authentik source.oauth config"""
|
"""authentik source.oauth config"""
|
||||||
@ -28,7 +18,7 @@ class AuthentikSourceOAuthConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
"""Load source_types from config file"""
|
"""Load source_types from config file"""
|
||||||
for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES:
|
for source_type in settings.AUTHENTIK_SOURCES_OAUTH_TYPES:
|
||||||
try:
|
try:
|
||||||
import_module(source_type)
|
import_module(source_type)
|
||||||
LOGGER.debug("Loaded OAuth Source Type", type=source_type)
|
LOGGER.debug("Loaded OAuth Source Type", type=source_type)
|
||||||
|
23
authentik/sources/oauth/auth.py
Normal file
23
authentik/sources/oauth/auth.py
Normal 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
|
@ -9,7 +9,6 @@ from rest_framework.serializers import Serializer
|
|||||||
|
|
||||||
from authentik.core.models import Source, UserSourceConnection
|
from authentik.core.models import Source, UserSourceConnection
|
||||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||||
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.sources.oauth.types.manager import SourceType
|
from authentik.sources.oauth.types.manager import SourceType
|
||||||
@ -68,14 +67,9 @@ class OAuthSource(Source):
|
|||||||
@property
|
@property
|
||||||
def ui_login_button(self) -> UILoginButton:
|
def ui_login_button(self) -> UILoginButton:
|
||||||
return UILoginButton(
|
return UILoginButton(
|
||||||
challenge=RedirectChallenge(
|
url=reverse(
|
||||||
instance={
|
"authentik_sources_oauth:oauth-client-login",
|
||||||
"type": ChallengeTypes.REDIRECT.value,
|
kwargs={"source_slug": self.slug},
|
||||||
"to": reverse(
|
|
||||||
"authentik_sources_oauth:oauth-client-login",
|
|
||||||
kwargs={"source_slug": self.slug},
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
icon_url=static(f"authentik/sources/{self.provider_type}.svg"),
|
icon_url=static(f"authentik/sources/{self.provider_type}.svg"),
|
||||||
name=self.name,
|
name=self.name,
|
||||||
@ -169,6 +163,16 @@ class OpenIDOAuthSource(OAuthSource):
|
|||||||
verbose_name_plural = _("OpenID OAuth Sources")
|
verbose_name_plural = _("OpenID OAuth Sources")
|
||||||
|
|
||||||
|
|
||||||
|
class PlexOAuthSource(OAuthSource):
|
||||||
|
"""Login using plex.tv."""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
abstract = True
|
||||||
|
verbose_name = _("Plex OAuth Source")
|
||||||
|
verbose_name_plural = _("Plex OAuth Sources")
|
||||||
|
|
||||||
|
|
||||||
class UserOAuthSourceConnection(UserSourceConnection):
|
class UserOAuthSourceConnection(UserSourceConnection):
|
||||||
"""Authorized remote OAuth provider."""
|
"""Authorized remote OAuth provider."""
|
||||||
|
|
||||||
|
13
authentik/sources/oauth/settings.py
Normal file
13
authentik/sources/oauth/settings.py
Normal 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",
|
||||||
|
]
|
@ -1,7 +1,7 @@
|
|||||||
"""Discord Type tests"""
|
"""Discord Type tests"""
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
from authentik.sources.oauth.types.discord import DiscordOAuth2Callback
|
from authentik.sources.oauth.types.discord import DiscordOAuth2Callback
|
||||||
|
|
||||||
# https://discord.com/developers/docs/resources/user#user-object
|
# https://discord.com/developers/docs/resources/user#user-object
|
||||||
@ -18,7 +18,7 @@ DISCORD_USER = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestTypeDiscord(TestCase):
|
class TestTypeGitHub(TestCase):
|
||||||
"""OAuth Source tests"""
|
"""OAuth Source tests"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -32,8 +32,10 @@ class TestTypeDiscord(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_enroll_context(self):
|
def test_enroll_context(self):
|
||||||
"""Test discord Enrollment context"""
|
"""Test GitHub Enrollment context"""
|
||||||
ak_context = DiscordOAuth2Callback().get_user_enroll_context(DISCORD_USER)
|
ak_context = DiscordOAuth2Callback().get_user_enroll_context(
|
||||||
|
self.source, UserOAuthSourceConnection(), DISCORD_USER
|
||||||
|
)
|
||||||
self.assertEqual(ak_context["username"], DISCORD_USER["username"])
|
self.assertEqual(ak_context["username"], DISCORD_USER["username"])
|
||||||
self.assertEqual(ak_context["email"], DISCORD_USER["email"])
|
self.assertEqual(ak_context["email"], DISCORD_USER["email"])
|
||||||
self.assertEqual(ak_context["name"], DISCORD_USER["username"])
|
self.assertEqual(ak_context["name"], DISCORD_USER["username"])
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
"""GitHub Type tests"""
|
"""GitHub Type tests"""
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
from authentik.sources.oauth.types.github import GitHubOAuth2Callback
|
from authentik.sources.oauth.types.github import GitHubOAuth2Callback
|
||||||
|
|
||||||
# https://developer.github.com/v3/users/#get-the-authenticated-user
|
# https://developer.github.com/v3/users/#get-the-authenticated-user
|
||||||
@ -63,7 +63,9 @@ class TestTypeGitHub(TestCase):
|
|||||||
|
|
||||||
def test_enroll_context(self):
|
def test_enroll_context(self):
|
||||||
"""Test GitHub Enrollment context"""
|
"""Test GitHub Enrollment context"""
|
||||||
ak_context = GitHubOAuth2Callback().get_user_enroll_context(GITHUB_USER)
|
ak_context = GitHubOAuth2Callback().get_user_enroll_context(
|
||||||
|
self.source, UserOAuthSourceConnection(), GITHUB_USER
|
||||||
|
)
|
||||||
self.assertEqual(ak_context["username"], GITHUB_USER["login"])
|
self.assertEqual(ak_context["username"], GITHUB_USER["login"])
|
||||||
self.assertEqual(ak_context["email"], GITHUB_USER["email"])
|
self.assertEqual(ak_context["email"], GITHUB_USER["email"])
|
||||||
self.assertEqual(ak_context["name"], GITHUB_USER["name"])
|
self.assertEqual(ak_context["name"], GITHUB_USER["name"])
|
||||||
|
@ -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"])
|
|
@ -1,7 +1,7 @@
|
|||||||
"""Twitter Type tests"""
|
"""Twitter Type tests"""
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
|
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
|
||||||
|
|
||||||
# https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \
|
# https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \
|
||||||
@ -104,7 +104,9 @@ class TestTypeGitHub(TestCase):
|
|||||||
|
|
||||||
def test_enroll_context(self):
|
def test_enroll_context(self):
|
||||||
"""Test Twitter Enrollment context"""
|
"""Test Twitter Enrollment context"""
|
||||||
ak_context = TwitterOAuthCallback().get_user_enroll_context(TWITTER_USER)
|
ak_context = TwitterOAuthCallback().get_user_enroll_context(
|
||||||
|
self.source, UserOAuthSourceConnection(), TWITTER_USER
|
||||||
|
)
|
||||||
self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"])
|
self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"])
|
||||||
self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None))
|
self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None))
|
||||||
self.assertEqual(ak_context["name"], TWITTER_USER["name"])
|
self.assertEqual(ak_context["name"], TWITTER_USER["name"])
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||||
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
from authentik.sources.oauth.types.manager import MANAGER, SourceType
|
||||||
from authentik.sources.oauth.views.callback import OAuthCallback
|
from authentik.sources.oauth.views.callback import OAuthCallback
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ from authentik.sources.oauth.views.callback import OAuthCallback
|
|||||||
class AzureADOAuthCallback(OAuthCallback):
|
class AzureADOAuthCallback(OAuthCallback):
|
||||||
"""AzureAD OAuth2 Callback"""
|
"""AzureAD OAuth2 Callback"""
|
||||||
|
|
||||||
def get_user_id(self, info: dict[str, Any]) -> Optional[str]:
|
def get_user_id(self, source: OAuthSource, info: dict[str, Any]) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
return str(UUID(info.get("objectId")).int)
|
return str(UUID(info.get("objectId")).int)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
@ -17,6 +18,8 @@ class AzureADOAuthCallback(OAuthCallback):
|
|||||||
|
|
||||||
def get_user_enroll_context(
|
def get_user_enroll_context(
|
||||||
self,
|
self,
|
||||||
|
source: OAuthSource,
|
||||||
|
access: UserOAuthSourceConnection,
|
||||||
info: dict[str, Any],
|
info: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
|
mail = info.get("mail", None) or info.get("otherMails", [None])[0]
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user