Compare commits
107 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
e0f48a30b7 | |||
973f14d911 | |||
e8978adc1b | |||
3ca8d9c968 | |||
42636142fa | |||
57c459348f | |||
493b34cf0d | |||
f0493f418b | |||
d45a292652 | |||
b21ea360db | |||
6816f8b851 | |||
de714f0390 | |||
800df332b5 | |||
16c194d2dc | |||
53100a72fe | |||
ec4c3f44cb | |||
f10bd432b3 | |||
4de927ba5b | |||
74e578c2bf | |||
e584fd1344 | |||
0e02925a3d | |||
5b837c3ccc | |||
2580371f94 | |||
4e9be85353 | |||
79508e1965 | |||
3a88dde545 | |||
31fc4d1cb9 | |||
09cd8f8f63 | |||
d824b09365 | |||
cabbd18880 | |||
c9dda17c68 | |||
bb8559ee18 | |||
5ae32e525c | |||
0832145a01 | |||
4167276c8f | |||
afb84c7bc5 | |||
82b2c7e3f0 | |||
fc8004db2b | |||
ddfc943bba | |||
8c0c12292e | |||
803490d98b | |||
16835ab478 | |||
572b8d87b5 | |||
31d2ea65fd | |||
f4ac2f50e2 | |||
969a3f0ddd | |||
4e18f47f28 | |||
f10286edf8 | |||
d789dcc28f | |||
715a71427e | |||
84c21d16cf | |||
2e4e17adb7 | |||
00cbaaf672 | |||
74e4e8f6aa | |||
d78fda990a | |||
10d949f7a9 | |||
6661af032d | |||
fb5e4a3af8 | |||
1dfad83a34 | |||
70025c648c | |||
676b77aa7c | |||
e35e096266 | |||
7af12d4fec | |||
8d6db0fabf | |||
8ddcf99bf7 | |||
e25f6aea8c | |||
b1a9eda1d3 | |||
2c15ab9995 | |||
b3c51e426d | |||
71578af47f | |||
6c985acb36 | |||
d878d2140e | |||
4766d6ff3d | |||
3a64d97040 | |||
2275ba3add | |||
9f7c941426 | |||
34ae9e6dab | |||
bf683514ee | |||
9b58bdb447 | |||
4237f20ccd | |||
2408719a47 | |||
b33fef7929 | |||
73b9847e7d | |||
a7e4eb021d | |||
11306770ad | |||
5235e00d3c | |||
7834146efc | |||
d4379ecd31 | |||
7492608ace | |||
7eef501446 | |||
b73de96aa6 | |||
a7adeb917e | |||
4ee2f951da | |||
01c5235e82 | |||
0ce4f9fe12 | |||
2f4f951818 | |||
a6c214e8fa | |||
57f8b108c4 | |||
7c1fe1243f | |||
3f69dd34ba | |||
c81431895a | |||
560c979d26 | |||
c5cc8842ec | |||
2a881d241d | |||
6291834573 | |||
eeea36acea | |||
e95b9da586 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.6.1-rc1
|
||||
current_version = 2021.6.1-rc6
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
|
@ -1,5 +1,4 @@
|
||||
env
|
||||
helm
|
||||
static
|
||||
htmlcov
|
||||
*.env.yml
|
||||
|
46
.github/workflows/release.yml
vendored
46
.github/workflows/release.yml
vendored
@ -33,12 +33,22 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.6.1-rc1,
|
||||
beryju/authentik:2021.6.1-rc6,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.6.1-rc1,
|
||||
ghcr.io/goauthentik/server:2021.6.1-rc6,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
uses: docker/build-push-action@v2
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.1-rc6', 'rc') }}
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
beryju/authentik:stable,
|
||||
ghcr.io/goauthentik/server:stable
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
build-proxy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -66,12 +76,22 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-proxy:2021.6.1-rc1,
|
||||
beryju/authentik-proxy:2021.6.1-rc6,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.6.1-rc1,
|
||||
ghcr.io/goauthentik/proxy:2021.6.1-rc6,
|
||||
ghcr.io/goauthentik/proxy:latest
|
||||
file: outpost/proxy.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
uses: docker/build-push-action@v2
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.1-rc6', 'rc') }}
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
beryju/authentik-proxy:stable,
|
||||
ghcr.io/goauthentik/proxy:stable
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
build-ldap:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -99,12 +119,22 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-ldap:2021.6.1-rc1,
|
||||
beryju/authentik-ldap:2021.6.1-rc6,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.6.1-rc1,
|
||||
ghcr.io/goauthentik/ldap:2021.6.1-rc6,
|
||||
ghcr.io/goauthentik/ldap:latest
|
||||
file: outpost/ldap.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
uses: docker/build-push-action@v2
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.1-rc6', 'rc') }}
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
beryju/authentik-ldap:stable,
|
||||
ghcr.io/goauthentik/ldap:stable
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
test-release:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
needs:
|
||||
@ -122,7 +152,7 @@ jobs:
|
||||
docker-compose pull -q
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
|
||||
docker-compose run -u root server test
|
||||
sentry-release:
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
needs:
|
||||
@ -138,5 +168,5 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.6.1-rc1
|
||||
version: authentik@2021.6.1-rc6
|
||||
environment: beryjuorg-prod
|
||||
|
4
.github/workflows/tag.yml
vendored
4
.github/workflows/tag.yml
vendored
@ -20,11 +20,11 @@ jobs:
|
||||
docker-compose pull -q
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/authentik:latest \
|
||||
-t ghcr.io/goauthentik/server:latest \
|
||||
-f Dockerfile .
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root --entrypoint /bin/bash server -c "apt-get update && apt-get install -y --no-install-recommends git && pip install --no-cache -r requirements-dev.txt && ./manage.py test authentik"
|
||||
docker-compose run -u root server test
|
||||
- name: Extract version number
|
||||
id: get_version
|
||||
uses: actions/github-script@v4.0.2
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -193,10 +193,6 @@ pip-selfcheck.json
|
||||
local.env.yml
|
||||
.vscode/
|
||||
|
||||
### Helm ###
|
||||
# Chart dependencies
|
||||
**/charts/*.tgz
|
||||
|
||||
# Selenium Screenshots
|
||||
selenium_screenshots/
|
||||
backups/
|
||||
|
@ -8,7 +8,7 @@ WORKDIR /app/
|
||||
|
||||
RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
pipenv lock -r --dev-only > requirements-dev.txt
|
||||
|
||||
# Stage 2: Build web API
|
||||
FROM openapitools/openapi-generator-cli as api-builder
|
||||
@ -28,7 +28,7 @@ COPY ./web /static/
|
||||
COPY --from=api-builder /local/web/api /static/api
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i --production=false && npm run build
|
||||
RUN cd /static && npm i && npm run build
|
||||
|
||||
# Stage 4: Build go proxy
|
||||
FROM golang:1.16.5 AS builder
|
||||
@ -76,6 +76,7 @@ RUN apt-get update && \
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pyproject.toml /
|
||||
COPY ./xml /xml
|
||||
COPY ./tests /tests
|
||||
COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
COPY --from=builder /work/authentik /authentik-proxy
|
||||
|
118
Pipfile.lock
generated
118
Pipfile.lock
generated
@ -56,6 +56,7 @@
|
||||
"sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
|
||||
"sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.7.4.post0"
|
||||
},
|
||||
"aioredis": {
|
||||
@ -70,6 +71,7 @@
|
||||
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
|
||||
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.6"
|
||||
},
|
||||
"asgiref": {
|
||||
@ -77,6 +79,7 @@
|
||||
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
|
||||
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.3.4"
|
||||
},
|
||||
"async-timeout": {
|
||||
@ -84,6 +87,7 @@
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.3'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
@ -91,6 +95,7 @@
|
||||
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
||||
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==21.2.0"
|
||||
},
|
||||
"autobahn": {
|
||||
@ -98,6 +103,7 @@
|
||||
"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
|
||||
"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==21.3.1"
|
||||
},
|
||||
"automat": {
|
||||
@ -116,24 +122,26 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:2ade860f66fa6b9a9886d7ff2e5118e5efebc4807b863ef735d358ef730234ed",
|
||||
"sha256:bbf727d770a9844834bfbf3f811db1d3438320897f67cfb21cdca5bb8fc23c13"
|
||||
"sha256:6180272094030bda3ee5c242881892cd3d9d19c05cb513945f530e396c7de1e4",
|
||||
"sha256:95d814d16fe55ae55e1e4a3db248596f9647a0c42f4796c6e05be0bfaffb1830"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.90"
|
||||
"version": "==1.17.94"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:6ae4ff3405cc4fc69ff3673a8dd234bf869aa556ae1e0da050d7f2aa3c3edab6",
|
||||
"sha256:b301810c4bd6cab1b6eaf6bfd9f25abb27959b586c2e1689bbce035b3fb8ae66"
|
||||
"sha256:60a382a5b2f7d77b1b575d54fba819097526e3fdd0f3004f4d1142d50af0d642",
|
||||
"sha256:ba8a7951be535e25219a82dea15c30d7bdf0c51e7c1623c3306248493c1616ac"
|
||||
],
|
||||
"version": "==1.20.90"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.20.94"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
|
||||
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
|
||||
],
|
||||
"markers": "python_version ~= '3.5'",
|
||||
"version": "==4.2.2"
|
||||
},
|
||||
"cbor2": {
|
||||
@ -152,6 +160,7 @@
|
||||
"sha256:ce6219986385778b1ab7f9b542f160bb4d3558f52975e914a27b774e47016fb7",
|
||||
"sha256:d562b2773e14ee1d65ea5b85351a83a64d4f3fd011bc2b4c70a6e813e78203ce"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.4.0"
|
||||
},
|
||||
"celery": {
|
||||
@ -244,6 +253,7 @@
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"click": {
|
||||
@ -251,6 +261,7 @@
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"click-didyoumean": {
|
||||
@ -310,6 +321,7 @@
|
||||
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
|
||||
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"defusedxml": {
|
||||
@ -414,11 +426,11 @@
|
||||
},
|
||||
"drf-spectacular": {
|
||||
"hashes": [
|
||||
"sha256:4d35e890b8139e1c056588c5529a2f2066615635482563f0840b96d3b879d7d2",
|
||||
"sha256:f552476dfde647963c21615249672e7f4f9ece3788036b5ee5c6cc5ad50748ab"
|
||||
"sha256:146e8c21dc806a20c84c687811c30163970fbf620213ab87280f7403469d80bb",
|
||||
"sha256:8a028d251a6d0b39739ebdec487fd43ee4ecba244d8ffaaac43ff06430728dd8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.17.0"
|
||||
"version": "==0.17.1"
|
||||
},
|
||||
"duo-client": {
|
||||
"hashes": [
|
||||
@ -440,6 +452,7 @@
|
||||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"geoip2": {
|
||||
@ -452,10 +465,11 @@
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:9b235dbc876e49454cbedc52ae0abd540ef705ebccdf4fbe93553bb13f26b1a4",
|
||||
"sha256:eb017521276a75492282c6ca4b718f26de112ed3bcbeaeeb02c1b82de425f909"
|
||||
"sha256:154f7889c5d679a6f626f36adb12afbd4dbb0a9a04ec575d989d6ba79c4fd65e",
|
||||
"sha256:6d47c79b5d09fbc7e8355fd9594cc4cf65fdde5d401c63951eaac4baa1ba2ae1"
|
||||
],
|
||||
"version": "==1.30.2"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.31.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@ -470,6 +484,7 @@
|
||||
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.12.0"
|
||||
},
|
||||
"hiredis": {
|
||||
@ -516,6 +531,7 @@
|
||||
"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
|
||||
"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"httptools": {
|
||||
@ -564,6 +580,7 @@
|
||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"jmespath": {
|
||||
@ -571,6 +588,7 @@
|
||||
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
|
||||
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"jsonschema": {
|
||||
@ -585,6 +603,7 @@
|
||||
"sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d",
|
||||
"sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.1.0"
|
||||
},
|
||||
"kubernetes": {
|
||||
@ -598,6 +617,9 @@
|
||||
"ldap3": {
|
||||
"hashes": [
|
||||
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
|
||||
"sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
|
||||
"sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
|
||||
"sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056",
|
||||
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
|
||||
],
|
||||
"index": "pypi",
|
||||
@ -659,6 +681,7 @@
|
||||
"hashes": [
|
||||
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.0.3"
|
||||
},
|
||||
"msgpack": {
|
||||
@ -734,6 +757,7 @@
|
||||
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
|
||||
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.1.0"
|
||||
},
|
||||
"oauthlib": {
|
||||
@ -741,6 +765,7 @@
|
||||
"sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc",
|
||||
"sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.1.1"
|
||||
},
|
||||
"packaging": {
|
||||
@ -756,6 +781,7 @@
|
||||
"sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86",
|
||||
"sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.11.0"
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
@ -763,6 +789,7 @@
|
||||
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04",
|
||||
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==3.0.18"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
@ -808,15 +835,37 @@
|
||||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
|
||||
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
|
||||
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
|
||||
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
|
||||
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
|
||||
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
|
||||
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
|
||||
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
|
||||
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
|
||||
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
|
||||
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
|
||||
],
|
||||
"version": "==0.4.8"
|
||||
},
|
||||
"pyasn1-modules": {
|
||||
"hashes": [
|
||||
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
|
||||
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
|
||||
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
|
||||
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
|
||||
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
|
||||
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
|
||||
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
|
||||
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
|
||||
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
|
||||
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
|
||||
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
|
||||
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
|
||||
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
|
||||
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
|
||||
],
|
||||
"version": "==0.2.8"
|
||||
},
|
||||
@ -825,6 +874,7 @@
|
||||
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.20"
|
||||
},
|
||||
"pycryptodome": {
|
||||
@ -868,6 +918,7 @@
|
||||
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
||||
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.0.2"
|
||||
},
|
||||
"pyjwt": {
|
||||
@ -890,12 +941,14 @@
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"python-dateutil": {
|
||||
@ -903,6 +956,7 @@
|
||||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"python-dotenv": {
|
||||
@ -959,6 +1013,7 @@
|
||||
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
|
||||
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.5.3"
|
||||
},
|
||||
"requests": {
|
||||
@ -966,12 +1021,14 @@
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.1"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
|
||||
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0"
|
||||
@ -1012,6 +1069,7 @@
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
@ -1019,6 +1077,7 @@
|
||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"structlog": {
|
||||
@ -1074,6 +1133,7 @@
|
||||
"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",
|
||||
"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==21.2.1"
|
||||
},
|
||||
"typing-extensions": {
|
||||
@ -1097,6 +1157,7 @@
|
||||
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
|
||||
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"urllib3": {
|
||||
@ -1141,6 +1202,7 @@
|
||||
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
||||
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.0"
|
||||
},
|
||||
"watchgod": {
|
||||
@ -1167,10 +1229,11 @@
|
||||
},
|
||||
"websocket-client": {
|
||||
"hashes": [
|
||||
"sha256:3e2bf58191d4619b161389a95bdce84ce9e0b24eb8107e7e590db682c2d0ca81",
|
||||
"sha256:abf306dc6351dcef07f4d40453037e51cc5d9da2ef60d0fc5d0fe3bcda255372"
|
||||
"sha256:b68e4959d704768fa20e35c9d508c8dc2bbc041fd8d267c0d7345cffe2824568",
|
||||
"sha256:e5c333bfa9fa739538b652b6f8c8fc2559f1d364243c8a689d7c0e1d41c2e611"
|
||||
],
|
||||
"version": "==1.0.1"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
@ -1267,6 +1330,7 @@
|
||||
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
|
||||
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.6.3"
|
||||
},
|
||||
"zope.interface": {
|
||||
@ -1323,6 +1387,7 @@
|
||||
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
|
||||
"sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==5.4.0"
|
||||
}
|
||||
},
|
||||
@ -1339,6 +1404,7 @@
|
||||
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
|
||||
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
|
||||
],
|
||||
"markers": "python_version ~= '3.6'",
|
||||
"version": "==2.5.6"
|
||||
},
|
||||
"attrs": {
|
||||
@ -1346,6 +1412,7 @@
|
||||
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
||||
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==21.2.0"
|
||||
},
|
||||
"bandit": {
|
||||
@ -1384,6 +1451,7 @@
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"click": {
|
||||
@ -1391,6 +1459,7 @@
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"colorama": {
|
||||
@ -1464,6 +1533,7 @@
|
||||
"sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
|
||||
"sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==4.0.7"
|
||||
},
|
||||
"gitpython": {
|
||||
@ -1471,6 +1541,7 @@
|
||||
"sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135",
|
||||
"sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.1.17"
|
||||
},
|
||||
"idna": {
|
||||
@ -1492,6 +1563,7 @@
|
||||
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6",
|
||||
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"
|
||||
],
|
||||
"markers": "python_version >= '3.6' and python_version < '4'",
|
||||
"version": "==5.8.0"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
@ -1519,6 +1591,7 @@
|
||||
"sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
|
||||
"sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.6.0"
|
||||
},
|
||||
"mccabe": {
|
||||
@ -1555,6 +1628,7 @@
|
||||
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd",
|
||||
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"
|
||||
],
|
||||
"markers": "python_version >= '2.6'",
|
||||
"version": "==5.6.0"
|
||||
},
|
||||
"pluggy": {
|
||||
@ -1562,6 +1636,7 @@
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"py": {
|
||||
@ -1569,6 +1644,7 @@
|
||||
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.10.0"
|
||||
},
|
||||
"pylint": {
|
||||
@ -1599,6 +1675,7 @@
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
@ -1703,6 +1780,7 @@
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.1"
|
||||
},
|
||||
"requests-mock": {
|
||||
@ -1726,6 +1804,7 @@
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"smmap": {
|
||||
@ -1733,6 +1812,7 @@
|
||||
"sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182",
|
||||
"sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"stevedore": {
|
||||
@ -1740,6 +1820,7 @@
|
||||
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
|
||||
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"toml": {
|
||||
@ -1747,6 +1828,7 @@
|
||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"urllib3": {
|
||||
|
@ -21,7 +21,7 @@ authentik is an open-source Identity Provider focused on flexibility and versati
|
||||
|
||||
For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/)
|
||||
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://goauthentik.io/docs/installation/kubernetes/)
|
||||
For bigger setups, there is a Helm Chart [here])(https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.6.1-rc1"
|
||||
__version__ = "2021.6.1-rc6"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
@ -10,3 +10,25 @@ class AuthentikAPIConfig(AppConfig):
|
||||
label = "authentik_api"
|
||||
mountpoint = "api/"
|
||||
verbose_name = "authentik API"
|
||||
|
||||
def ready(self) -> None:
|
||||
from drf_spectacular.extensions import OpenApiAuthenticationExtension
|
||||
|
||||
from authentik.api.authentication import TokenAuthentication
|
||||
|
||||
# Class is defined here as it needs to be created early enough that drf-spectacular will
|
||||
# find it, but also won't cause any import issues
|
||||
# pylint: disable=unused-variable
|
||||
class TokenSchema(OpenApiAuthenticationExtension):
|
||||
"""Auth schema"""
|
||||
|
||||
target_class = TokenAuthentication
|
||||
name = "authentik"
|
||||
|
||||
def get_security_definition(self, auto_schema):
|
||||
"""Auth schema"""
|
||||
return {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "Authorization",
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ from base64 import b64decode
|
||||
from binascii import Error
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from drf_spectacular.authentication import OpenApiAuthenticationExtension
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework.request import Request
|
||||
@ -56,18 +55,3 @@ class TokenAuthentication(BaseAuthentication):
|
||||
return None
|
||||
|
||||
return (token.user, None) # pragma: no cover
|
||||
|
||||
|
||||
class TokenSchema(OpenApiAuthenticationExtension):
|
||||
"""Auth schema"""
|
||||
|
||||
target_class = TokenAuthentication
|
||||
name = "authentik"
|
||||
|
||||
def get_security_definition(self, auto_schema):
|
||||
"""Auth schema"""
|
||||
return {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "Authorization",
|
||||
}
|
||||
|
@ -11,13 +11,7 @@ from drf_spectacular.utils import (
|
||||
inline_serializer,
|
||||
)
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
FileField,
|
||||
IntegerField,
|
||||
ReadOnlyField,
|
||||
)
|
||||
from rest_framework.fields import BooleanField, CharField, FileField, ReadOnlyField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -29,6 +23,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.events.models import EventAction
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
@ -73,7 +68,7 @@ class ApplicationSerializer(ModelSerializer):
|
||||
}
|
||||
|
||||
|
||||
class ApplicationViewSet(ModelViewSet):
|
||||
class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Application Viewset"""
|
||||
|
||||
queryset = Application.objects.all()
|
||||
@ -106,15 +101,19 @@ class ApplicationViewSet(ModelViewSet):
|
||||
return applications
|
||||
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
"CheckAccessRequest", fields={"for_user": IntegerField(required=False)}
|
||||
),
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="for_user",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
)
|
||||
],
|
||||
responses={
|
||||
200: PolicyTestResultSerializer(),
|
||||
404: OpenApiResponse(description="for_user user not found"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, methods=["POST"])
|
||||
@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"""
|
||||
@ -203,7 +202,7 @@ class ApplicationViewSet(ModelViewSet):
|
||||
"""Set application icon"""
|
||||
app: Application = self.get_object()
|
||||
icon = request.FILES.get("file", None)
|
||||
clear = request.data.get("clear", False)
|
||||
clear = request.data.get("clear", "false").lower() == "true"
|
||||
if clear:
|
||||
# .delete() saves the model by default
|
||||
app.meta_icon.delete()
|
||||
|
@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from ua_parser import user_agent_parser
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.models import AuthenticatedSession
|
||||
from authentik.events.geo import GEOIP_READER, GeoIPDict
|
||||
|
||||
@ -92,6 +93,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
|
||||
class AuthenticatedSessionViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -5,6 +5,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import is_dict
|
||||
from authentik.core.models import Group
|
||||
|
||||
@ -20,7 +21,7 @@ class GroupSerializer(ModelSerializer):
|
||||
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
|
||||
|
||||
|
||||
class GroupViewSet(ModelViewSet):
|
||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Group Viewset"""
|
||||
|
||||
queryset = Group.objects.all()
|
||||
|
@ -14,6 +14,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import (
|
||||
MetaNameSerializer,
|
||||
PassiveSerializer,
|
||||
@ -65,6 +66,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
||||
class PropertyMappingViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -9,6 +9,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.models import Provider
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
@ -48,6 +49,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
class ProviderViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -10,6 +10,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.models import Source
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
@ -52,6 +53,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
class SourceViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
@ -43,7 +44,7 @@ class TokenViewSerializer(PassiveSerializer):
|
||||
key = CharField(read_only=True)
|
||||
|
||||
|
||||
class TokenViewSet(ModelViewSet):
|
||||
class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Token Viewset"""
|
||||
|
||||
lookup_field = "identifier"
|
||||
|
102
authentik/core/api/used_by.py
Normal file
102
authentik/core/api/used_by.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""used_by mixin"""
|
||||
from enum import Enum
|
||||
from inspect import getmembers
|
||||
|
||||
from django.db.models.base import Model
|
||||
from django.db.models.deletion import SET_DEFAULT, SET_NULL
|
||||
from django.db.models.manager import Manager
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, ChoiceField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
|
||||
|
||||
class DeleteAction(Enum):
|
||||
"""Which action a delete will have on a used object"""
|
||||
|
||||
CASCADE = "cascade"
|
||||
CASCADE_MANY = "cascade_many"
|
||||
SET_NULL = "set_null"
|
||||
SET_DEFAULT = "set_default"
|
||||
|
||||
|
||||
class UsedBySerializer(PassiveSerializer):
|
||||
"""A list of all objects referencing the queried object"""
|
||||
|
||||
app = CharField()
|
||||
model_name = CharField()
|
||||
pk = CharField()
|
||||
name = CharField()
|
||||
action = ChoiceField(choices=[(x.name, x.name) for x in DeleteAction])
|
||||
|
||||
|
||||
def get_delete_action(manager: Manager) -> str:
|
||||
"""Get the delete action from the Foreign key, falls back to cascade"""
|
||||
if hasattr(manager, "field"):
|
||||
if manager.field.remote_field.on_delete.__name__ == SET_NULL.__name__:
|
||||
return DeleteAction.SET_NULL.name
|
||||
if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__:
|
||||
return DeleteAction.SET_DEFAULT.name
|
||||
if hasattr(manager, "source_field"):
|
||||
return DeleteAction.CASCADE_MANY.name
|
||||
return DeleteAction.CASCADE.name
|
||||
|
||||
|
||||
class UsedByMixin:
|
||||
"""Mixin to add a used_by endpoint to return a list of all objects using this object"""
|
||||
|
||||
@extend_schema(
|
||||
responses={200: UsedBySerializer(many=True)},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||
# pylint: disable=invalid-name, unused-argument, too-many-locals
|
||||
def used_by(self, request: Request, *args, **kwargs) -> Response:
|
||||
"""Get a list of all objects that use this object"""
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
model: Model = self.get_object()
|
||||
used_by = []
|
||||
shadows = []
|
||||
for attr_name, manager in getmembers(model, lambda x: isinstance(x, Manager)):
|
||||
if attr_name == "objects": # pragma: no cover
|
||||
continue
|
||||
manager: Manager
|
||||
if manager.model._meta.abstract:
|
||||
continue
|
||||
app = manager.model._meta.app_label
|
||||
model_name = manager.model._meta.model_name
|
||||
delete_action = get_delete_action(manager)
|
||||
|
||||
# To make sure we only apply shadows when there are any objects,
|
||||
# but so we only apply them once, have a simple flag for the first object
|
||||
first_object = True
|
||||
|
||||
for obj in get_objects_for_user(
|
||||
request.user, f"{app}.view_{model_name}", manager
|
||||
).all():
|
||||
# Only merge shadows on first object
|
||||
if first_object:
|
||||
shadows += getattr(
|
||||
manager.model._meta, "authentik_used_by_shadows", []
|
||||
)
|
||||
first_object = False
|
||||
serializer = UsedBySerializer(
|
||||
data={
|
||||
"app": app,
|
||||
"model_name": model_name,
|
||||
"pk": str(obj.pk),
|
||||
"name": str(obj),
|
||||
"action": delete_action,
|
||||
}
|
||||
)
|
||||
serializer.is_valid()
|
||||
used_by.append(serializer.data)
|
||||
# Check the shadows map and remove anything that should be shadowed
|
||||
for idx, user in enumerate(used_by):
|
||||
full_model_name = f"{user['app']}.{user['model_name']}"
|
||||
if full_model_name in shadows:
|
||||
del used_by[idx]
|
||||
return Response(used_by)
|
@ -25,6 +25,7 @@ from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.groups import GroupSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
||||
from authentik.core.middleware import (
|
||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||
@ -131,7 +132,7 @@ class UsersFilter(FilterSet):
|
||||
fields = ["username", "name", "is_active", "is_superuser", "attributes"]
|
||||
|
||||
|
||||
class UserViewSet(ModelViewSet):
|
||||
class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
"""User Viewset"""
|
||||
|
||||
queryset = User.objects.none()
|
||||
|
@ -26,6 +26,8 @@ class ImpersonateMiddleware:
|
||||
|
||||
if SESSION_IMPERSONATE_USER in request.session:
|
||||
request.user = request.session[SESSION_IMPERSONATE_USER]
|
||||
# Ensure that the user is active, otherwise nothing will work
|
||||
request.user.is_active = True
|
||||
|
||||
return self.get_response(request)
|
||||
|
||||
|
@ -5,6 +5,7 @@ from typing import Any, Optional, Type
|
||||
from urllib.parse import urlencode
|
||||
from uuid import uuid4
|
||||
|
||||
import django.db.models.options as options
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
@ -41,6 +42,9 @@ GRAVATAR_URL = "https://secure.gravatar.com"
|
||||
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
||||
|
||||
|
||||
options.DEFAULT_NAMES = options.DEFAULT_NAMES + ("authentik_used_by_shadows",)
|
||||
|
||||
|
||||
def default_token_duration():
|
||||
"""Default duration a Token is valid"""
|
||||
return now() + timedelta(minutes=30)
|
||||
|
@ -1,11 +1,12 @@
|
||||
"""authentik core signals"""
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
from django.core.signals import Signal
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.http.request import HttpRequest
|
||||
from prometheus_client import Gauge
|
||||
@ -18,7 +19,7 @@ GAUGE_MODELS = Gauge(
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.core.models import User
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
@ -60,3 +61,17 @@ def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
|
||||
AuthenticatedSession.objects.filter(
|
||||
session_key=request.session.session_key
|
||||
).delete()
|
||||
|
||||
|
||||
@receiver(pre_delete)
|
||||
def authenticated_session_delete(
|
||||
sender: Type[Model], instance: "AuthenticatedSession", **_
|
||||
):
|
||||
"""Delete session when authenticated session is deleted"""
|
||||
from authentik.core.models import AuthenticatedSession
|
||||
|
||||
if sender != AuthenticatedSession:
|
||||
return
|
||||
|
||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||
cache.delete(cache_key)
|
||||
|
@ -33,6 +33,7 @@ from authentik.flows.planner import (
|
||||
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 import BACKEND_DJANGO
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
@ -198,7 +199,7 @@ class SourceFlowManager:
|
||||
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_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_SOURCE: self.source,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
|
@ -3,16 +3,6 @@
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.pf-c-background-image::before {
|
||||
background-image: url("{% static 'dist/assets/images/flow_background.jpg' %}");
|
||||
background-position: center;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans 'End session' %} - {{ tenant.branding_title }}
|
||||
{% endblock %}
|
||||
|
@ -26,7 +26,7 @@ class TestApplicationsAPI(APITestCase):
|
||||
def test_check_access(self):
|
||||
"""Test check_access operation"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:application-check-access",
|
||||
kwargs={"slug": self.allowed.slug},
|
||||
@ -36,7 +36,7 @@ class TestApplicationsAPI(APITestCase):
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content), {"messages": [], "passing": True}
|
||||
)
|
||||
response = self.client.post(
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:application-check-access",
|
||||
kwargs={"slug": self.denied.slug},
|
||||
|
@ -17,6 +17,9 @@ class TestImpersonation(TestCase):
|
||||
|
||||
def test_impersonate_simple(self):
|
||||
"""test simple impersonation and un-impersonation"""
|
||||
# test with an inactive user to ensure that still works
|
||||
self.other_user.is_active = False
|
||||
self.other_user.save()
|
||||
self.client.force_login(self.akadmin)
|
||||
|
||||
self.client.get(
|
||||
|
@ -2,7 +2,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from rest_framework.fields import CharField, DictField
|
||||
from rest_framework.fields import CharField
|
||||
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.flows.challenge import Challenge
|
||||
@ -22,18 +22,10 @@ class UILoginButton:
|
||||
icon_url: Optional[str] = None
|
||||
|
||||
|
||||
class UILoginButtonSerializer(PassiveSerializer):
|
||||
"""Serializer for Login buttons of sources"""
|
||||
|
||||
name = CharField()
|
||||
challenge = DictField()
|
||||
icon_url = CharField(required=False, allow_null=True)
|
||||
|
||||
|
||||
class UserSettingSerializer(PassiveSerializer):
|
||||
"""Serializer for User settings for stages and sources"""
|
||||
|
||||
object_uid = CharField()
|
||||
component = CharField()
|
||||
title = CharField()
|
||||
configure_url = CharField()
|
||||
configure_url = CharField(required=False)
|
||||
|
@ -1,10 +1,12 @@
|
||||
"""Crypto API Views"""
|
||||
import django_filters
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.x509 import load_pem_x509_certificate
|
||||
from django.http.response import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_filters import FilterSet
|
||||
from django_filters.filters import BooleanFilter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
@ -20,6 +22,7 @@ from rest_framework.serializers import ModelSerializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
@ -33,6 +36,9 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
cert_subject = SerializerMethodField()
|
||||
private_key_available = SerializerMethodField()
|
||||
|
||||
certificate_download_url = SerializerMethodField()
|
||||
private_key_download_url = SerializerMethodField()
|
||||
|
||||
def get_cert_subject(self, instance: CertificateKeyPair) -> str:
|
||||
"""Get certificate subject as full rfc4514"""
|
||||
return instance.certificate.subject.rfc4514_string()
|
||||
@ -41,6 +47,26 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"""Show if this keypair has a private key configured or not"""
|
||||
return instance.key_data != "" and instance.key_data is not None
|
||||
|
||||
def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
|
||||
"""Get URL to download certificate"""
|
||||
return (
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-view-certificate",
|
||||
kwargs={"pk": instance.pk},
|
||||
)
|
||||
+ "?download"
|
||||
)
|
||||
|
||||
def get_private_key_download_url(self, instance: CertificateKeyPair) -> str:
|
||||
"""Get URL to download private key"""
|
||||
return (
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-view-private-key",
|
||||
kwargs={"pk": instance.pk},
|
||||
)
|
||||
+ "?download"
|
||||
)
|
||||
|
||||
def validate_certificate_data(self, value: str) -> str:
|
||||
"""Verify that input is a valid PEM x509 Certificate"""
|
||||
try:
|
||||
@ -77,6 +103,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"cert_expiry",
|
||||
"cert_subject",
|
||||
"private_key_available",
|
||||
"certificate_download_url",
|
||||
"private_key_download_url",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"key_data": {"write_only": True},
|
||||
@ -100,10 +128,10 @@ class CertificateGenerationSerializer(PassiveSerializer):
|
||||
validity_days = IntegerField(initial=365)
|
||||
|
||||
|
||||
class CertificateKeyPairFilter(django_filters.FilterSet):
|
||||
class CertificateKeyPairFilter(FilterSet):
|
||||
"""Filter for certificates"""
|
||||
|
||||
has_key = django_filters.BooleanFilter(
|
||||
has_key = BooleanFilter(
|
||||
label="Only return certificate-key pairs with keys", method="filter_has_key"
|
||||
)
|
||||
|
||||
@ -117,7 +145,7 @@ class CertificateKeyPairFilter(django_filters.FilterSet):
|
||||
fields = ["name"]
|
||||
|
||||
|
||||
class CertificateKeyPairViewSet(ModelViewSet):
|
||||
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
"""CertificateKeyPair Viewset"""
|
||||
|
||||
queryset = CertificateKeyPair.objects.all()
|
||||
|
@ -4,10 +4,14 @@ import datetime
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.api.used_by import DeleteAction
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.api import CertificateKeyPairSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
|
||||
|
||||
class TestCrypto(TestCase):
|
||||
@ -91,3 +95,35 @@ class TestCrypto(TestCase):
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertIn("Content-Disposition", response)
|
||||
|
||||
def test_used_by(self):
|
||||
"""Test used_by endpoint"""
|
||||
self.client.force_login(User.objects.get(username="akadmin"))
|
||||
keypair = CertificateKeyPair.objects.first()
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://localhost",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-used-by",
|
||||
kwargs={"pk": keypair.pk},
|
||||
)
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertJSONEqual(
|
||||
response.content.decode(),
|
||||
[
|
||||
{
|
||||
"app": "authentik_providers_oauth2",
|
||||
"model_name": "oauth2provider",
|
||||
"pk": str(provider.pk),
|
||||
"name": str(provider),
|
||||
"action": DeleteAction.SET_NULL.name,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
@ -36,6 +36,7 @@ class EventSerializer(ModelSerializer):
|
||||
"client_ip",
|
||||
"created",
|
||||
"expires",
|
||||
"tenant",
|
||||
]
|
||||
|
||||
|
||||
@ -76,6 +77,11 @@ class EventsFilter(django_filters.FilterSet):
|
||||
field_name="action",
|
||||
lookup_expr="icontains",
|
||||
)
|
||||
tenant_name = django_filters.CharFilter(
|
||||
field_name="tenant",
|
||||
lookup_expr="name",
|
||||
label="Tenant name",
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def filter_context_model_pk(self, queryset, name, value):
|
||||
|
@ -7,6 +7,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.events.api.event import EventSerializer
|
||||
from authentik.events.models import Notification
|
||||
|
||||
@ -35,6 +36,7 @@ class NotificationViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -3,6 +3,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.groups import GroupSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.events.models import NotificationRule
|
||||
|
||||
|
||||
@ -24,7 +25,7 @@ class NotificationRuleSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class NotificationRuleViewSet(ModelViewSet):
|
||||
class NotificationRuleViewSet(UsedByMixin, ModelViewSet):
|
||||
"""NotificationRule Viewset"""
|
||||
|
||||
queryset = NotificationRule.objects.all()
|
||||
|
@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer, Serializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.events.models import (
|
||||
Notification,
|
||||
NotificationSeverity,
|
||||
@ -52,7 +53,7 @@ class NotificationTransportTestSerializer(Serializer):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NotificationTransportViewSet(ModelViewSet):
|
||||
class NotificationTransportViewSet(UsedByMixin, ModelViewSet):
|
||||
"""NotificationTransport Viewset"""
|
||||
|
||||
queryset = NotificationTransport.objects.all()
|
||||
|
@ -40,9 +40,9 @@ class GeoIPReader:
|
||||
return
|
||||
try:
|
||||
reader = Reader(path)
|
||||
LOGGER.info("Loaded GeoIP database")
|
||||
self.__reader = reader
|
||||
self.__last_mtime = stat(path).st_mtime
|
||||
LOGGER.info("Loaded GeoIP database", last_write=self.__last_mtime)
|
||||
except OSError as exc:
|
||||
LOGGER.warning("Failed to load GeoIP database", exc=exc)
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
from functools import partial
|
||||
from typing import Callable
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
@ -12,6 +13,7 @@ from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction, Notification
|
||||
from authentik.events.signals import EventNewThread
|
||||
from authentik.events.utils import model_to_dict
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
|
||||
|
||||
class AuditMiddleware:
|
||||
@ -54,10 +56,19 @@ class AuditMiddleware:
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def process_exception(self, request: HttpRequest, exception: Exception):
|
||||
"""Unregister handlers in case of exception"""
|
||||
"""Disconnect handlers in case of exception"""
|
||||
post_save.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
|
||||
pre_delete.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
|
||||
|
||||
if settings.DEBUG:
|
||||
return
|
||||
thread = EventNewThread(
|
||||
EventAction.SYSTEM_EXCEPTION,
|
||||
request,
|
||||
message=exception_to_string(exception),
|
||||
)
|
||||
thread.run()
|
||||
|
||||
@staticmethod
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_handler(
|
||||
|
55
authentik/events/migrations/0016_add_tenant.py
Normal file
55
authentik/events/migrations/0016_add_tenant.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-14 15:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.events.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0015_alter_event_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="tenant",
|
||||
field=models.JSONField(
|
||||
blank=True, default=authentik.events.models.default_tenant
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("secret_view", "Secret View"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("system_exception", "System Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("email_sent", "Email Sent"),
|
||||
("update_available", "Update Available"),
|
||||
("custom_", "Custom Prefix"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
@ -21,11 +21,12 @@ from authentik.core.middleware import (
|
||||
)
|
||||
from authentik.core.models import ExpiringModel, Group, User
|
||||
from authentik.events.geo import GEOIP_READER
|
||||
from authentik.events.utils import cleanse_dict, get_user, sanitize_dict
|
||||
from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
from authentik.tenants.utils import DEFAULT_TENANT
|
||||
|
||||
LOGGER = get_logger("authentik.events")
|
||||
GAUGE_EVENTS = Gauge(
|
||||
@ -40,6 +41,11 @@ def default_event_duration():
|
||||
return now() + timedelta(days=365)
|
||||
|
||||
|
||||
def default_tenant():
|
||||
"""Get a default value for tenant"""
|
||||
return sanitize_dict(model_to_dict(DEFAULT_TENANT))
|
||||
|
||||
|
||||
class NotificationTransportError(SentryIgnoredException):
|
||||
"""Error raised when a notification fails to be delivered"""
|
||||
|
||||
@ -71,6 +77,7 @@ class EventAction(models.TextChoices):
|
||||
|
||||
SYSTEM_TASK_EXECUTION = "system_task_execution"
|
||||
SYSTEM_TASK_EXCEPTION = "system_task_exception"
|
||||
SYSTEM_EXCEPTION = "system_exception"
|
||||
|
||||
CONFIGURATION_ERROR = "configuration_error"
|
||||
|
||||
@ -94,6 +101,7 @@ class Event(ExpiringModel):
|
||||
context = models.JSONField(default=dict, blank=True)
|
||||
client_ip = models.GenericIPAddressField(null=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
tenant = models.JSONField(default=default_tenant, blank=True)
|
||||
|
||||
# Shadow the expires attribute from ExpiringModel to override the default duration
|
||||
expires = models.DateTimeField(default=default_event_duration)
|
||||
@ -132,6 +140,13 @@ class Event(ExpiringModel):
|
||||
"""Add data from a Django-HttpRequest, allowing the creation of
|
||||
Events independently from requests.
|
||||
`user` arguments optionally overrides user from requests."""
|
||||
if request:
|
||||
self.context["http_request"] = {
|
||||
"path": request.get_full_path(),
|
||||
"method": request.method,
|
||||
}
|
||||
if hasattr(request, "tenant"):
|
||||
self.tenant = sanitize_dict(model_to_dict(request.tenant))
|
||||
if hasattr(request, "user"):
|
||||
original_user = None
|
||||
if hasattr(request, "session"):
|
||||
|
@ -2,6 +2,7 @@
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.flows.models import FlowStageBinding
|
||||
|
||||
@ -27,7 +28,7 @@ class FlowStageBindingSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class FlowStageBindingViewSet(ModelViewSet):
|
||||
class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""FlowStageBinding Viewset"""
|
||||
|
||||
queryset = FlowStageBinding.objects.all()
|
||||
|
@ -24,6 +24,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import CacheSerializer, LinkSerializer
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import Flow
|
||||
@ -44,10 +45,16 @@ class FlowSerializer(ModelSerializer):
|
||||
|
||||
background = ReadOnlyField(source="background_url")
|
||||
|
||||
export_url = SerializerMethodField()
|
||||
|
||||
def get_cache_count(self, flow: Flow) -> int:
|
||||
"""Get count of cached flows"""
|
||||
return len(cache.keys(f"{cache_key(flow)}*"))
|
||||
|
||||
def get_export_url(self, flow: Flow) -> str:
|
||||
"""Get export URL for flow"""
|
||||
return reverse("authentik_api:flow-export", kwargs={"slug": flow.slug})
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Flow
|
||||
@ -64,6 +71,7 @@ class FlowSerializer(ModelSerializer):
|
||||
"cache_count",
|
||||
"policy_engine_mode",
|
||||
"compatibility_mode",
|
||||
"export_url",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"background": {"read_only": True},
|
||||
@ -94,7 +102,7 @@ class DiagramElement:
|
||||
return f"{self.identifier}=>{self.type}: {self.rest}"
|
||||
|
||||
|
||||
class FlowViewSet(ModelViewSet):
|
||||
class FlowViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Flow Viewset"""
|
||||
|
||||
queryset = Flow.objects.all()
|
||||
@ -293,10 +301,14 @@ class FlowViewSet(ModelViewSet):
|
||||
"""Set Flow background"""
|
||||
flow: Flow = self.get_object()
|
||||
background = request.FILES.get("file", None)
|
||||
clear = request.data.get("clear", False)
|
||||
clear = request.data.get("clear", "false").lower() == "true"
|
||||
if clear:
|
||||
# .delete() saves the model by default
|
||||
flow.background.delete()
|
||||
if flow.background_url.startswith("/media"):
|
||||
# .delete() saves the model by default
|
||||
flow.background.delete()
|
||||
else:
|
||||
flow.background = None
|
||||
flow.save()
|
||||
return Response({})
|
||||
if background:
|
||||
flow.background = background
|
||||
|
@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.flows.api.flows import FlowSerializer
|
||||
@ -49,6 +50,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
|
||||
class StageViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
@ -91,10 +93,10 @@ class StageViewSet(
|
||||
if not user_settings:
|
||||
continue
|
||||
user_settings.initial_data["object_uid"] = str(stage.pk)
|
||||
if hasattr(stage, "configure_url"):
|
||||
if hasattr(stage, "configure_flow") and stage.configure_flow:
|
||||
user_settings.initial_data["configure_url"] = reverse(
|
||||
"authentik_flows:configure",
|
||||
kwargs={"stage_uuid": stage.uuid.hex},
|
||||
kwargs={"stage_uuid": stage.pk},
|
||||
)
|
||||
if not user_settings.is_valid():
|
||||
LOGGER.warning(user_settings.errors)
|
||||
|
@ -6,6 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.stages.identification.models import UserFields
|
||||
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
|
||||
|
||||
|
||||
def create_default_authentication_flow(
|
||||
@ -31,7 +32,7 @@ def create_default_authentication_flow(
|
||||
|
||||
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authentication-password",
|
||||
defaults={"backends": ["django.contrib.auth.backends.ModelBackend"]},
|
||||
defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP]},
|
||||
)
|
||||
|
||||
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||
|
@ -15,9 +15,6 @@ PREFILL_POLICY_EXPRESSION = """# This policy sets the user for the currently run
|
||||
# by injecting "pending_user"
|
||||
akadmin = ak_user_by(username="akadmin")
|
||||
context["pending_user"] = akadmin
|
||||
# We're also setting the backend for the user, so we can
|
||||
# directly login without having to identify again
|
||||
context["user_backend"] = "django.contrib.auth.backends.ModelBackend"
|
||||
return True"""
|
||||
|
||||
|
||||
|
@ -72,7 +72,7 @@ class Stage(SerializerModel):
|
||||
def __str__(self):
|
||||
if hasattr(self, "__in_memory_type"):
|
||||
return f"In-memory Stage {getattr(self, '__in_memory_type')}"
|
||||
return self.name
|
||||
return f"Stage {self.name}"
|
||||
|
||||
|
||||
def in_memory_stage(view: Type["StageView"]) -> Stage:
|
||||
@ -212,7 +212,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
||||
return FlowStageBindingSerializer
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.target} #{self.order}"
|
||||
return f"Flow-stage binding #{self.order} to {self.target}"
|
||||
|
||||
class Meta:
|
||||
|
||||
|
@ -50,14 +50,17 @@ class StageView(View):
|
||||
self.executor = executor
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_pending_user(self) -> User:
|
||||
def get_pending_user(self, for_display=False) -> User:
|
||||
"""Either show the matched User object or show what the user entered,
|
||||
based on what the earlier stage (mostly IdentificationStage) set.
|
||||
_USER_IDENTIFIER overrides the first User, as PENDING_USER is used for
|
||||
other things besides the form display.
|
||||
|
||||
If no user is pending, returns request.user"""
|
||||
if PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context:
|
||||
if (
|
||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER in self.executor.plan.context
|
||||
and for_display
|
||||
):
|
||||
return User(
|
||||
username=self.executor.plan.context.get(
|
||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER
|
||||
@ -109,7 +112,7 @@ class ChallengeStageView(StageView):
|
||||
# If there's a pending user, update the `username` field
|
||||
# this field is only used by password managers.
|
||||
# If there's no user set, an error is raised later.
|
||||
if user := self.get_pending_user():
|
||||
if user := self.get_pending_user(for_display=True):
|
||||
challenge.initial_data["pending_user"] = user.username
|
||||
challenge.initial_data["pending_user_avatar"] = DEFAULT_AVATAR
|
||||
if not isinstance(user, AnonymousUser):
|
||||
|
@ -511,4 +511,4 @@ class TestFlowExecutor(TestCase):
|
||||
executor.flow = flow
|
||||
|
||||
stage_view = StageView(executor)
|
||||
self.assertEqual(ident, stage_view.get_pending_user().username)
|
||||
self.assertEqual(ident, stage_view.get_pending_user(for_display=True).username)
|
||||
|
@ -174,7 +174,7 @@ class FlowExecutorView(APIView):
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: PolymorphicProxySerializer(
|
||||
component_name="FlowChallengeRequest",
|
||||
component_name="ChallengeTypes",
|
||||
serializers=challenge_types(),
|
||||
resource_type_field_name="component",
|
||||
),
|
||||
@ -214,7 +214,7 @@ class FlowExecutorView(APIView):
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: PolymorphicProxySerializer(
|
||||
component_name="FlowChallengeRequest",
|
||||
component_name="ChallengeTypes",
|
||||
serializers=challenge_types(),
|
||||
resource_type_field_name="component",
|
||||
),
|
||||
|
10
authentik/lib/utils/errors.py
Normal file
10
authentik/lib/utils/errors.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""error utils"""
|
||||
from traceback import format_tb
|
||||
|
||||
TRACEBACK_HEADER = "Traceback (most recent call last):\n"
|
||||
|
||||
|
||||
def exception_to_string(exc: Exception) -> str:
|
||||
"""Convert exception to string stackrace"""
|
||||
# Either use passed original exception or whatever we have
|
||||
return TRACEBACK_HEADER + "".join(format_tb(exc.__traceback__)) + str(exc)
|
@ -11,6 +11,7 @@ from rest_framework.serializers import JSONField, ModelSerializer, ValidationErr
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer, is_dict
|
||||
from authentik.core.models import Provider
|
||||
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
|
||||
@ -95,7 +96,7 @@ class OutpostHealthSerializer(PassiveSerializer):
|
||||
version_outdated = BooleanField(read_only=True)
|
||||
|
||||
|
||||
class OutpostViewSet(ModelViewSet):
|
||||
class OutpostViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Outpost Viewset"""
|
||||
|
||||
queryset = Outpost.objects.all()
|
||||
|
@ -14,6 +14,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import (
|
||||
MetaNameSerializer,
|
||||
PassiveSerializer,
|
||||
@ -32,6 +33,13 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
|
||||
|
||||
component = ReadOnlyField()
|
||||
|
||||
def get_component(self, obj: OutpostServiceConnection) -> str:
|
||||
"""Get object type so that we know how to edit the object"""
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
if obj.__class__ == OutpostServiceConnection:
|
||||
return ""
|
||||
return obj.component
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OutpostServiceConnection
|
||||
@ -55,6 +63,7 @@ class ServiceConnectionStateSerializer(PassiveSerializer):
|
||||
class ServiceConnectionViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
@ -105,7 +114,7 @@ class DockerServiceConnectionSerializer(ServiceConnectionSerializer):
|
||||
]
|
||||
|
||||
|
||||
class DockerServiceConnectionViewSet(ModelViewSet):
|
||||
class DockerServiceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
"""DockerServiceConnection Viewset"""
|
||||
|
||||
queryset = DockerServiceConnection.objects.all()
|
||||
@ -139,7 +148,7 @@ class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer):
|
||||
fields = ServiceConnectionSerializer.Meta.fields + ["kubeconfig"]
|
||||
|
||||
|
||||
class KubernetesServiceConnectionViewSet(ModelViewSet):
|
||||
class KubernetesServiceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||
"""KubernetesServiceConnection Viewset"""
|
||||
|
||||
queryset = KubernetesServiceConnection.objects.all()
|
||||
|
@ -63,7 +63,7 @@ class DockerController(BaseController):
|
||||
self.client.images.pull(image_name)
|
||||
container_args = {
|
||||
"image": image_name,
|
||||
"name": f"authentik-proxy-{self.outpost.uuid.hex}",
|
||||
"name": container_name,
|
||||
"detach": True,
|
||||
"ports": {
|
||||
f"{port.port}/{port.protocol.lower()}": port.inner_port or port.port
|
||||
|
@ -8,7 +8,7 @@ from uuid import uuid4
|
||||
from dacite import from_dict
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.core.cache import cache
|
||||
from django.db import models, transaction
|
||||
from django.db import IntegrityError, models, transaction
|
||||
from django.db.models.base import Model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from docker.client import DockerClient
|
||||
@ -50,6 +50,8 @@ class ServiceConnectionInvalid(SentryIgnoredException):
|
||||
class OutpostConfig:
|
||||
"""Configuration an outpost uses to configure it self"""
|
||||
|
||||
# update website/docs/outposts/outposts.md
|
||||
|
||||
authentik_host: str
|
||||
authentik_host_insecure: bool = False
|
||||
|
||||
@ -141,7 +143,9 @@ class OutpostServiceConnection(models.Model):
|
||||
@property
|
||||
def component(self) -> str:
|
||||
"""Return component used to edit this object"""
|
||||
raise NotImplementedError
|
||||
# This is called when creating an outpost with a service connection
|
||||
# since the response doesn't use the correct inheritance
|
||||
return ""
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -380,21 +384,24 @@ class Outpost(models.Model):
|
||||
tokens = Token.filter_not_expired(
|
||||
identifier=self.token_identifier,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
)
|
||||
if tokens.exists():
|
||||
token = tokens.first()
|
||||
if not token.managed:
|
||||
token.managed = managed
|
||||
token.save()
|
||||
return token
|
||||
return Token.objects.create(
|
||||
user=self.user,
|
||||
identifier=self.token_identifier,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
description=f"Autogenerated by authentik for Outpost {self.name}",
|
||||
expiring=False,
|
||||
managed=managed,
|
||||
)
|
||||
if tokens.exists():
|
||||
return tokens.first()
|
||||
try:
|
||||
return Token.objects.create(
|
||||
user=self.user,
|
||||
identifier=self.token_identifier,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
description=f"Autogenerated by authentik for Outpost {self.name}",
|
||||
expiring=False,
|
||||
managed=managed,
|
||||
)
|
||||
except IntegrityError:
|
||||
# Integrity error happens mostly when managed is re-used
|
||||
Token.objects.filter(managed=managed).delete()
|
||||
Token.objects.filter(identifier=self.token_identifier).delete()
|
||||
return self.token
|
||||
|
||||
def get_required_objects(self) -> Iterable[Union[models.Model, str]]:
|
||||
"""Get an iterator of all objects the user needs read access to"""
|
||||
|
@ -11,6 +11,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.groups import GroupSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.policies.api.policies import PolicySerializer
|
||||
from authentik.policies.models import PolicyBinding, PolicyBindingModel
|
||||
@ -99,7 +100,7 @@ class PolicyBindingSerializer(ModelSerializer):
|
||||
return data
|
||||
|
||||
|
||||
class PolicyBindingViewSet(ModelViewSet):
|
||||
class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""PolicyBinding Viewset"""
|
||||
|
||||
queryset = (
|
||||
|
@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.applications import user_app_cache_key
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import (
|
||||
CacheSerializer,
|
||||
MetaNameSerializer,
|
||||
@ -79,6 +80,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer):
|
||||
class PolicyViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Dummy Policy API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.policies.api.policies import PolicySerializer
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
|
||||
@ -13,7 +14,7 @@ class DummyPolicySerializer(PolicySerializer):
|
||||
fields = PolicySerializer.Meta.fields + ["result", "wait_min", "wait_max"]
|
||||
|
||||
|
||||
class DummyPolicyViewSet(ModelViewSet):
|
||||
class DummyPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Dummy Viewset"""
|
||||
|
||||
queryset = DummyPolicy.objects.all()
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Event Matcher Policy API"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.policies.api.policies import PolicySerializer
|
||||
from authentik.policies.event_matcher.models import EventMatcherPolicy
|
||||
|
||||
@ -17,7 +18,7 @@ class EventMatcherPolicySerializer(PolicySerializer):
|
||||
]
|
||||
|
||||
|
||||
class EventMatcherPolicyViewSet(ModelViewSet):
|
||||
class EventMatcherPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Event Matcher Policy Viewset"""
|
||||
|
||||
queryset = EventMatcherPolicy.objects.all()
|
||||
|
@ -0,0 +1,48 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-14 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_event_matcher", "0016_alter_eventmatcherpolicy_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventmatcherpolicy",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("secret_view", "Secret View"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("system_exception", "System Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("email_sent", "Email Sent"),
|
||||
("update_available", "Update Available"),
|
||||
("custom_", "Custom Prefix"),
|
||||
],
|
||||
help_text="Match created events with this action type. When left empty, all action types will be matched.",
|
||||
),
|
||||
),
|
||||
]
|
@ -1,6 +1,7 @@
|
||||
"""Password Expiry Policy API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.policies.api.policies import PolicySerializer
|
||||
from authentik.policies.expiry.models import PasswordExpiryPolicy
|
||||
|
||||
@ -13,7 +14,7 @@ class PasswordExpiryPolicySerializer(PolicySerializer):
|
||||
fields = PolicySerializer.Meta.fields + ["days", "deny_only"]
|
||||
|
||||
|
||||
class PasswordExpiryPolicyViewSet(ModelViewSet):
|
||||
class PasswordExpiryPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Password Expiry Viewset"""
|
||||
|
||||
queryset = PasswordExpiryPolicy.objects.all()
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Expression Policy API"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.policies.api.policies import PolicySerializer
|
||||
from authentik.policies.expression.evaluator import PolicyEvaluator
|
||||
from authentik.policies.expression.models import ExpressionPolicy
|
||||
@ -20,7 +21,7 @@ class ExpressionPolicySerializer(PolicySerializer):
|
||||
fields = PolicySerializer.Meta.fields + ["expression"]
|
||||
|
||||
|
||||
class ExpressionPolicyViewSet(ModelViewSet):
|
||||
class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = ExpressionPolicy.objects.all()
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Source API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.policies.api.policies import PolicySerializer
|
||||
from authentik.policies.hibp.models import HaveIBeenPwendPolicy
|
||||
|
||||
@ -13,7 +14,7 @@ class HaveIBeenPwendPolicySerializer(PolicySerializer):
|
||||
fields = PolicySerializer.Meta.fields + ["password_field", "allowed_count"]
|
||||
|
||||
|
||||
class HaveIBeenPwendPolicyViewSet(ModelViewSet):
|
||||
class HaveIBeenPwendPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = HaveIBeenPwendPolicy.objects.all()
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Password Policy API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.policies.api.policies import PolicySerializer
|
||||
from authentik.policies.password.models import PasswordPolicy
|
||||
|
||||
@ -21,7 +22,7 @@ class PasswordPolicySerializer(PolicySerializer):
|
||||
]
|
||||
|
||||
|
||||
class PasswordPolicyViewSet(ModelViewSet):
|
||||
class PasswordPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Password Policy Viewset"""
|
||||
|
||||
queryset = PasswordPolicy.objects.all()
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""authentik policy task"""
|
||||
from multiprocessing import get_context
|
||||
from multiprocessing.connection import Connection
|
||||
from traceback import format_tb
|
||||
from typing import Optional
|
||||
|
||||
from django.core.cache import cache
|
||||
@ -11,12 +10,12 @@ from sentry_sdk.tracing import Span
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.policies.exceptions import PolicyException
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
TRACEBACK_HEADER = "Traceback (most recent call last):\n"
|
||||
|
||||
FORK_CTX = get_context("fork")
|
||||
PROCESS_CLASS = FORK_CTX.Process
|
||||
@ -106,11 +105,7 @@ class PolicyProcess(PROCESS_CLASS):
|
||||
except PolicyException as exc:
|
||||
# Either use passed original exception or whatever we have
|
||||
src_exc = exc.src_exc if exc.src_exc else exc
|
||||
error_string = (
|
||||
TRACEBACK_HEADER
|
||||
+ "".join(format_tb(src_exc.__traceback__))
|
||||
+ str(src_exc)
|
||||
)
|
||||
error_string = exception_to_string(src_exc)
|
||||
# Create policy exception event, only when we're not debugging
|
||||
if not self.request.debug:
|
||||
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
||||
|
@ -3,6 +3,7 @@ from rest_framework import mixins
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.policies.api.policies import PolicySerializer
|
||||
from authentik.policies.reputation.models import (
|
||||
IPReputation,
|
||||
@ -23,7 +24,7 @@ class ReputationPolicySerializer(PolicySerializer):
|
||||
]
|
||||
|
||||
|
||||
class ReputationPolicyViewSet(ModelViewSet):
|
||||
class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Reputation Policy Viewset"""
|
||||
|
||||
queryset = ReputationPolicy.objects.all()
|
||||
@ -46,6 +47,7 @@ class IPReputationSerializer(ModelSerializer):
|
||||
class IPReputationViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
@ -74,6 +76,7 @@ class UserReputationSerializer(ModelSerializer):
|
||||
class UserReputationViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -3,11 +3,13 @@ from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.policies.models import Policy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
CACHE_KEY_IP_PREFIX = "authentik_reputation_ip_"
|
||||
CACHE_KEY_USER_PREFIX = "authentik_reputation_user_"
|
||||
|
||||
@ -35,9 +37,16 @@ class ReputationPolicy(Policy):
|
||||
if self.check_ip:
|
||||
score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
|
||||
passing = passing and score <= self.threshold
|
||||
LOGGER.debug("Score for IP", ip=remote_ip, score=score, passing=passing)
|
||||
if self.check_username:
|
||||
score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0)
|
||||
passing = passing and score <= self.threshold
|
||||
LOGGER.debug(
|
||||
"Score for Username",
|
||||
username=request.user.username,
|
||||
score=score,
|
||||
passing=passing,
|
||||
)
|
||||
return PolicyResult(passing)
|
||||
|
||||
class Meta:
|
||||
|
@ -1,9 +1,10 @@
|
||||
"""test reputation signals and policy"""
|
||||
from django.contrib.auth import authenticate
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.utils.http import DEFAULT_IP
|
||||
from authentik.policies.reputation.models import (
|
||||
CACHE_KEY_IP_PREFIX,
|
||||
CACHE_KEY_USER_PREFIX,
|
||||
@ -19,9 +20,12 @@ class TestReputationPolicy(TestCase):
|
||||
"""test reputation signals and policy"""
|
||||
|
||||
def setUp(self):
|
||||
self.test_ip = "255.255.255.255"
|
||||
self.request_factory = RequestFactory()
|
||||
self.request = self.request_factory.get("/")
|
||||
self.test_ip = "127.0.0.1"
|
||||
self.test_username = "test"
|
||||
cache.delete(CACHE_KEY_IP_PREFIX + self.test_ip)
|
||||
cache.delete(CACHE_KEY_IP_PREFIX + DEFAULT_IP)
|
||||
cache.delete(CACHE_KEY_USER_PREFIX + self.test_username)
|
||||
# We need a user for the one-to-one in userreputation
|
||||
self.user = User.objects.create(username=self.test_username)
|
||||
@ -29,7 +33,9 @@ class TestReputationPolicy(TestCase):
|
||||
def test_ip_reputation(self):
|
||||
"""test IP reputation"""
|
||||
# Trigger negative reputation
|
||||
authenticate(None, username=self.test_username, password=self.test_username)
|
||||
authenticate(
|
||||
self.request, username=self.test_username, password=self.test_username
|
||||
)
|
||||
# Test value in cache
|
||||
self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1)
|
||||
# Save cache and check db values
|
||||
@ -39,7 +45,9 @@ class TestReputationPolicy(TestCase):
|
||||
def test_user_reputation(self):
|
||||
"""test User reputation"""
|
||||
# Trigger negative reputation
|
||||
authenticate(None, username=self.test_username, password=self.test_username)
|
||||
authenticate(
|
||||
self.request, username=self.test_username, password=self.test_username
|
||||
)
|
||||
# Test value in cache
|
||||
self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1)
|
||||
# Save cache and check db values
|
||||
|
@ -4,6 +4,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.providers.ldap.models import LDAPProvider
|
||||
|
||||
|
||||
@ -19,7 +20,7 @@ class LDAPProviderSerializer(ProviderSerializer):
|
||||
]
|
||||
|
||||
|
||||
class LDAPProviderViewSet(ModelViewSet):
|
||||
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"""LDAPProvider Viewset"""
|
||||
|
||||
queryset = LDAPProvider.objects.all()
|
||||
|
@ -11,6 +11,7 @@ from rest_framework.serializers import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import Provider
|
||||
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
|
||||
@ -61,7 +62,7 @@ class OAuth2ProviderSetupURLs(PassiveSerializer):
|
||||
logout = ReadOnlyField()
|
||||
|
||||
|
||||
class OAuth2ProviderViewSet(ModelViewSet):
|
||||
class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"""OAuth2Provider Viewset"""
|
||||
|
||||
queryset = OAuth2Provider.objects.all()
|
||||
|
@ -2,6 +2,7 @@
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.providers.oauth2.models import ScopeMapping
|
||||
|
||||
|
||||
@ -17,7 +18,7 @@ class ScopeMappingSerializer(PropertyMappingSerializer):
|
||||
]
|
||||
|
||||
|
||||
class ScopeMappingViewSet(ModelViewSet):
|
||||
class ScopeMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""ScopeMapping Viewset"""
|
||||
|
||||
queryset = ScopeMapping.objects.all()
|
||||
|
@ -10,6 +10,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.providers.oauth2.api.provider import OAuth2ProviderSerializer
|
||||
@ -57,6 +58,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
|
||||
class AuthorizationCodeViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
@ -82,6 +84,7 @@ class AuthorizationCodeViewSet(
|
||||
class RefreshTokenViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-09 21:52
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_crypto", "0002_create_self_signed_kp"),
|
||||
("authentik_providers_oauth2", "0013_alter_authorizationcode_nonce"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="oauth2provider",
|
||||
name="rsa_key",
|
||||
field=models.ForeignKey(
|
||||
help_text="Key used to sign the tokens. Only required when JWT Algorithm is set to RS256.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="authentik_crypto.certificatekeypair",
|
||||
verbose_name="RSA Key",
|
||||
),
|
||||
),
|
||||
]
|
@ -215,8 +215,7 @@ class OAuth2Provider(Provider):
|
||||
rsa_key = models.ForeignKey(
|
||||
CertificateKeyPair,
|
||||
verbose_name=_("RSA Key"),
|
||||
on_delete=models.CASCADE,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
help_text=_(
|
||||
"Key used to sign the tokens. Only required when JWT Algorithm is set to RS256."
|
||||
|
@ -1,13 +1,14 @@
|
||||
"""ProxyProvider API Views"""
|
||||
from typing import Any
|
||||
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||
@ -76,7 +77,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
||||
]
|
||||
|
||||
|
||||
class ProxyProviderViewSet(ModelViewSet):
|
||||
class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"""ProxyProvider Viewset"""
|
||||
|
||||
queryset = ProxyProvider.objects.all()
|
||||
@ -84,6 +85,7 @@ class ProxyProviderViewSet(ModelViewSet):
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
@extend_schema_serializer(deprecate_fields=["forward_auth_mode"])
|
||||
class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
"""Proxy provider serializer for outposts"""
|
||||
|
||||
|
@ -167,3 +167,4 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
|
||||
verbose_name = _("Proxy Provider")
|
||||
verbose_name_plural = _("Proxy Providers")
|
||||
authentik_used_by_shadows = ["authentik_providers_oauth2.oauth2provider"]
|
||||
|
@ -4,11 +4,17 @@ from xml.etree.ElementTree import ParseError # nosec
|
||||
from defusedxml.ElementTree import fromstring
|
||||
from django.http.response import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, FileField, ReadOnlyField
|
||||
from rest_framework.fields import (
|
||||
CharField,
|
||||
FileField,
|
||||
ReadOnlyField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
@ -21,6 +27,7 @@ from structlog.stdlib import get_logger
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import Provider
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
@ -36,6 +43,15 @@ LOGGER = get_logger()
|
||||
class SAMLProviderSerializer(ProviderSerializer):
|
||||
"""SAMLProvider Serializer"""
|
||||
|
||||
metadata_download_url = SerializerMethodField()
|
||||
|
||||
def get_metadata_download_url(self, instance: SAMLProvider) -> str:
|
||||
"""Get metadata download URL"""
|
||||
return (
|
||||
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": instance.pk})
|
||||
+ "?download"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = SAMLProvider
|
||||
@ -53,6 +69,7 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
"signing_kp",
|
||||
"verification_kp",
|
||||
"sp_binding",
|
||||
"metadata_download_url",
|
||||
]
|
||||
|
||||
|
||||
@ -75,7 +92,7 @@ class SAMLProviderImportSerializer(PassiveSerializer):
|
||||
file = FileField()
|
||||
|
||||
|
||||
class SAMLProviderViewSet(ModelViewSet):
|
||||
class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
"""SAMLProvider Viewset"""
|
||||
|
||||
queryset = SAMLProvider.objects.all()
|
||||
@ -166,7 +183,7 @@ class SAMLPropertyMappingSerializer(PropertyMappingSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SAMLPropertyMappingViewSet(ModelViewSet):
|
||||
class SAMLPropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""SAMLPropertyMapping Viewset"""
|
||||
|
||||
queryset = SAMLPropertyMapping.objects.all()
|
||||
|
@ -120,7 +120,7 @@ class ServiceProviderMetadataParser:
|
||||
)
|
||||
ctx.key = key
|
||||
ctx.verify(signature_node)
|
||||
except xmlsec.VerificationError as exc:
|
||||
except xmlsec.Error as exc:
|
||||
raise ValueError("Failed to verify Metadata signature") from exc
|
||||
|
||||
def parse(self, raw_xml: str) -> ServiceProviderMetadata:
|
||||
|
@ -108,7 +108,7 @@ class AuthNRequestParser:
|
||||
)
|
||||
ctx.key = key
|
||||
ctx.verify(signature_node)
|
||||
except xmlsec.VerificationError as exc:
|
||||
except xmlsec.Error as exc:
|
||||
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
||||
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
@ -160,7 +160,7 @@ class AuthNRequestParser:
|
||||
sign_algorithm_transform,
|
||||
b64decode(signature),
|
||||
)
|
||||
except xmlsec.VerificationError as exc:
|
||||
except xmlsec.Error as exc:
|
||||
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
|
||||
|
@ -2,12 +2,13 @@
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http.request import HttpRequest, QueryDict
|
||||
from django.http.request import QueryDict
|
||||
from django.test import RequestFactory, TestCase
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||
@ -58,11 +59,6 @@ qNAZMq1DqpibfCBg
|
||||
-----END CERTIFICATE-----"""
|
||||
|
||||
|
||||
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||
"""Dummy get_response for SessionMiddleware"""
|
||||
return None
|
||||
|
||||
|
||||
class TestAuthNRequest(TestCase):
|
||||
"""Test AuthN Request generator and parser"""
|
||||
|
||||
|
@ -7,6 +7,7 @@ from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
|
||||
|
||||
class UseTokenView(View):
|
||||
@ -18,7 +19,7 @@ class UseTokenView(View):
|
||||
if not tokens.exists():
|
||||
raise Http404
|
||||
token = tokens.first()
|
||||
login(request, token.user, backend="django.contrib.auth.backends.ModelBackend")
|
||||
login(request, token.user, backend=BACKEND_DJANGO)
|
||||
token.delete()
|
||||
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||
return redirect("authentik_core:if-admin")
|
||||
|
@ -375,7 +375,11 @@ if _ERROR_REPORTING:
|
||||
environment=CONFIG.y("error_reporting.environment", "customer"),
|
||||
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
|
||||
)
|
||||
set_tag("authentik.build_hash", os.environ.get(ENV_GIT_HASH_KEY, "tagged"))
|
||||
# Default to empty string as that is what docker has
|
||||
build_hash = os.environ.get(ENV_GIT_HASH_KEY, "")
|
||||
if build_hash == "":
|
||||
build_hash = "tagged"
|
||||
set_tag("authentik.build_hash", build_hash)
|
||||
set_tag(
|
||||
"authentik.env", "kubernetes" if "KUBERNETES_PORT" in os.environ else "compose"
|
||||
)
|
||||
|
@ -10,6 +10,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.admin.api.tasks import TaskSerializer
|
||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.events.monitored_tasks import TaskInfo
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
|
||||
@ -41,7 +42,7 @@ class LDAPSourceSerializer(SourceSerializer):
|
||||
extra_kwargs = {"bind_password": {"write_only": True}}
|
||||
|
||||
|
||||
class LDAPSourceViewSet(ModelViewSet):
|
||||
class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"""LDAP Source Viewset"""
|
||||
|
||||
queryset = LDAPSource.objects.all()
|
||||
@ -75,7 +76,7 @@ class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
|
||||
]
|
||||
|
||||
|
||||
class LDAPPropertyMappingViewSet(ModelViewSet):
|
||||
class LDAPPropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""LDAP PropertyMapping Viewset"""
|
||||
|
||||
queryset = LDAPPropertyMapping.objects.all()
|
||||
|
@ -9,6 +9,7 @@ from rest_framework.serializers import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.manager import MANAGER
|
||||
@ -78,7 +79,7 @@ class OAuthSourceSerializer(SourceSerializer):
|
||||
extra_kwargs = {"consumer_secret": {"write_only": True}}
|
||||
|
||||
|
||||
class OAuthSourceViewSet(ModelViewSet):
|
||||
class OAuthSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = OAuthSource.objects.all()
|
||||
|
@ -6,6 +6,7 @@ from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.sources.oauth.models import UserOAuthSourceConnection
|
||||
|
||||
|
||||
@ -26,6 +27,7 @@ class UserOAuthSourceConnectionViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.flows.challenge import RedirectChallenge
|
||||
from authentik.flows.views import to_stage_response
|
||||
@ -42,7 +43,7 @@ class PlexTokenRedeemSerializer(PassiveSerializer):
|
||||
plex_token = CharField()
|
||||
|
||||
|
||||
class PlexSourceViewSet(ModelViewSet):
|
||||
class PlexSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Plex source Viewset"""
|
||||
|
||||
queryset = PlexSource.objects.all()
|
||||
|
@ -7,6 +7,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.providers.saml.api import SAMLMetadataSerializer
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
||||
@ -33,7 +34,7 @@ class SAMLSourceSerializer(SourceSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SAMLSourceViewSet(ModelViewSet):
|
||||
class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
"""SAMLSource Viewset"""
|
||||
|
||||
queryset = SAMLSource.objects.all()
|
||||
|
@ -39,7 +39,7 @@ from authentik.sources.saml.processors.constants import (
|
||||
from authentik.sources.saml.processors.request import SESSION_REQUEST_ID
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_login.stage import DEFAULT_BACKEND
|
||||
from authentik.stages.user_login.stage import BACKEND_DJANGO
|
||||
|
||||
LOGGER = get_logger()
|
||||
if TYPE_CHECKING:
|
||||
@ -141,7 +141,7 @@ class ResponseProcessor:
|
||||
self._source.authentication_flow,
|
||||
**{
|
||||
PLAN_CONTEXT_PENDING_USER: user,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: DEFAULT_BACKEND,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
},
|
||||
)
|
||||
|
||||
@ -204,7 +204,7 @@ class ResponseProcessor:
|
||||
self._source.authentication_flow,
|
||||
**{
|
||||
PLAN_CONTEXT_PENDING_USER: matching_users.first(),
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: DEFAULT_BACKEND,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
},
|
||||
)
|
||||
|
@ -2,7 +2,7 @@
|
||||
from django.utils.timezone import now
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.root.celery import CELERY_APP
|
||||
@ -31,11 +31,13 @@ def clean_temporary_users(self: MonitoredTask):
|
||||
continue
|
||||
source = sources.first()
|
||||
source_delta = timedelta_from_string(source.temporary_user_delete_after)
|
||||
if _now - user.last_login >= source_delta:
|
||||
if (
|
||||
_now - user.last_login >= source_delta
|
||||
and not AuthenticatedSession.objects.filter(user=user).exists()
|
||||
):
|
||||
LOGGER.debug(
|
||||
"User is expired and will be deleted.", user=user, delta=source_delta
|
||||
)
|
||||
# TODO: Check if user is signed in anywhere?
|
||||
user.delete()
|
||||
deleted_users += 1
|
||||
messages.append(f"Successfully deleted {deleted_users} users.")
|
||||
|
@ -12,6 +12,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_duo.stage import (
|
||||
@ -37,7 +38,7 @@ class AuthenticatorDuoStageSerializer(StageSerializer):
|
||||
}
|
||||
|
||||
|
||||
class AuthenticatorDuoStageViewSet(ModelViewSet):
|
||||
class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AuthenticatorDuoStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorDuoStage.objects.all()
|
||||
@ -78,6 +79,7 @@ class DuoDeviceViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -1,44 +1,6 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-25 14:32
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.flows.models import FlowDesignation
|
||||
|
||||
|
||||
def create_default_setup_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
Flow = apps.get_model("authentik_flows", "Flow")
|
||||
FlowStageBinding = apps.get_model("authentik_flows", "FlowStageBinding")
|
||||
|
||||
AuthenticatorDuoStage = apps.get_model(
|
||||
"authentik_stages_authenticator_duo", "AuthenticatorDuoStage"
|
||||
)
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
|
||||
flow, _ = Flow.objects.using(db_alias).update_or_create(
|
||||
slug="default-authenticator-duo-setup",
|
||||
designation=FlowDesignation.STAGE_CONFIGURATION,
|
||||
defaults={
|
||||
"name": "default-authenticator-duo-setup",
|
||||
"title": "Setup Duo",
|
||||
},
|
||||
)
|
||||
|
||||
stage, _ = AuthenticatorDuoStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authenticator-duo-setup"
|
||||
)
|
||||
|
||||
FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||
target=flow, stage=stage, defaults={"order": 0}
|
||||
)
|
||||
|
||||
for stage in AuthenticatorDuoStage.objects.using(db_alias).filter(
|
||||
configure_flow=None
|
||||
):
|
||||
stage.configure_flow = flow
|
||||
stage.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -50,6 +12,4 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(create_default_setup_flow),
|
||||
]
|
||||
operations = []
|
||||
|
@ -8,6 +8,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
|
||||
|
||||
@ -21,7 +22,7 @@ class AuthenticatorStaticStageSerializer(StageSerializer):
|
||||
fields = StageSerializer.Meta.fields + ["configure_flow", "token_count"]
|
||||
|
||||
|
||||
class AuthenticatorStaticStageViewSet(ModelViewSet):
|
||||
class AuthenticatorStaticStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AuthenticatorStaticStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorStaticStage.objects.all()
|
||||
@ -52,6 +53,7 @@ class StaticDeviceViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -8,6 +8,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||
|
||||
@ -21,7 +22,7 @@ class AuthenticatorTOTPStageSerializer(StageSerializer):
|
||||
fields = StageSerializer.Meta.fields + ["configure_flow", "digits"]
|
||||
|
||||
|
||||
class AuthenticatorTOTPStageViewSet(ModelViewSet):
|
||||
class AuthenticatorTOTPStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AuthenticatorTOTPStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorTOTPStage.objects.all()
|
||||
@ -45,6 +46,7 @@ class TOTPDeviceViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -3,4 +3,4 @@
|
||||
INSTALLED_APPS = [
|
||||
"django_otp.plugins.otp_totp",
|
||||
]
|
||||
OTP_TOTP_ISSUER = "authentik"
|
||||
OTP_TOTP_ISSUER = "__to_replace__"
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""TOTP Setup stage"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
@ -16,6 +17,7 @@ from authentik.flows.challenge import (
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||
from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER
|
||||
|
||||
LOGGER = get_logger()
|
||||
SESSION_TOTP_DEVICE = "totp_device"
|
||||
@ -54,7 +56,9 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
|
||||
return AuthenticatorTOTPChallenge(
|
||||
data={
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"config_url": device.config_url,
|
||||
"config_url": device.config_url.replace(
|
||||
OTP_TOTP_ISSUER, slugify(self.request.tenant.branding_title)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.flows.models import NotConfiguredAction
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage
|
||||
@ -32,7 +33,7 @@ class AuthenticatorValidateStageSerializer(StageSerializer):
|
||||
]
|
||||
|
||||
|
||||
class AuthenticatorValidateStageViewSet(ModelViewSet):
|
||||
class AuthenticatorValidateStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AuthenticatorValidateStage Viewset"""
|
||||
|
||||
queryset = AuthenticatorValidateStage.objects.all()
|
||||
|
@ -91,6 +91,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
"""Get a list of all device challenges applicable for the current stage"""
|
||||
challenges = []
|
||||
user_devices = devices_for_user(self.get_pending_user())
|
||||
LOGGER.debug("Got devices for user", devices=user_devices)
|
||||
|
||||
# static and totp are only shown once
|
||||
# since their challenges are device-independant
|
||||
@ -101,6 +102,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
for device in user_devices:
|
||||
device_class = device.__class__.__name__.lower().replace("device", "")
|
||||
if device_class not in stage.device_classes:
|
||||
LOGGER.debug("device class not allowed", device_class=device_class)
|
||||
continue
|
||||
# Ensure only classes in PER_DEVICE_CLASSES are returned per device
|
||||
# otherwise only return a single challenge
|
||||
@ -108,15 +110,16 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
continue
|
||||
if device_class not in seen_classes:
|
||||
seen_classes.append(device_class)
|
||||
challenges.append(
|
||||
DeviceChallenge(
|
||||
data={
|
||||
"device_class": device_class,
|
||||
"device_uid": device.pk,
|
||||
"challenge": get_challenge_for_device(self.request, device),
|
||||
}
|
||||
).initial_data
|
||||
challenge = DeviceChallenge(
|
||||
data={
|
||||
"device_class": device_class,
|
||||
"device_uid": device.pk,
|
||||
"challenge": get_challenge_for_device(self.request, device),
|
||||
}
|
||||
)
|
||||
challenge.is_valid()
|
||||
challenges.append(challenge.data)
|
||||
LOGGER.debug("adding challenge for device", challenge=challenge)
|
||||
return challenges
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
|
127
authentik/stages/authenticator_validate/tests.py
Normal file
127
authentik/stages/authenticator_validate/tests.py
Normal file
@ -0,0 +1,127 @@
|
||||
"""Test validator stage"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.models import NotConfiguredAction
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.providers.oauth2.generators import (
|
||||
generate_client_id,
|
||||
generate_client_secret,
|
||||
)
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_validate.api import (
|
||||
AuthenticatorValidateStageSerializer,
|
||||
)
|
||||
from authentik.stages.authenticator_validate.challenge import (
|
||||
get_challenge_for_device,
|
||||
validate_challenge_code,
|
||||
validate_challenge_duo,
|
||||
validate_challenge_webauthn,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
|
||||
|
||||
class AuthenticatorValidateStageTests(TestCase):
|
||||
"""Test validator stage"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.request_factory = RequestFactory()
|
||||
|
||||
def test_stage_validation(self):
|
||||
"""Test serializer validation"""
|
||||
self.client.force_login(self.user)
|
||||
serializer = AuthenticatorValidateStageSerializer(
|
||||
data={"name": "foo", "not_configured_action": NotConfiguredAction.CONFIGURE}
|
||||
)
|
||||
self.assertFalse(serializer.is_valid())
|
||||
self.assertIn("not_configured_action", serializer.errors)
|
||||
|
||||
def test_device_challenge_totp(self):
|
||||
"""Test device challenge"""
|
||||
request = self.request_factory.get("/")
|
||||
totp_device = TOTPDevice.objects.create(
|
||||
user=self.user, confirmed=True, digits=6
|
||||
)
|
||||
self.assertEqual(get_challenge_for_device(request, totp_device), {})
|
||||
with self.assertRaises(ValidationError):
|
||||
validate_challenge_code("1234", request, self.user)
|
||||
|
||||
def test_device_challenge_webauthn(self):
|
||||
"""Test webauthn"""
|
||||
request = self.request_factory.get("/")
|
||||
request.user = self.user
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
|
||||
webauthn_device = WebAuthnDevice.objects.create(
|
||||
user=self.user,
|
||||
public_key="qwerqwerqre",
|
||||
credential_id="foobarbaz",
|
||||
sign_count=0,
|
||||
rp_id="foo",
|
||||
)
|
||||
challenge = get_challenge_for_device(request, webauthn_device)
|
||||
del challenge["challenge"]
|
||||
self.assertEqual(
|
||||
challenge,
|
||||
{
|
||||
"allowCredentials": [
|
||||
{
|
||||
"id": "foobarbaz",
|
||||
"transports": ["usb", "nfc", "ble", "internal"],
|
||||
"type": "public-key",
|
||||
}
|
||||
],
|
||||
"rpId": "foo",
|
||||
"timeout": 60000,
|
||||
"userVerification": "discouraged",
|
||||
},
|
||||
)
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
validate_challenge_webauthn({}, request, self.user)
|
||||
|
||||
def test_device_challenge_duo(self):
|
||||
"""Test duo"""
|
||||
request = self.request_factory.get("/")
|
||||
stage = AuthenticatorDuoStage.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
api_hostname="",
|
||||
)
|
||||
duo_device = DuoDevice.objects.create(
|
||||
user=self.user,
|
||||
stage=stage,
|
||||
)
|
||||
duo_mock = MagicMock(
|
||||
auth=MagicMock(
|
||||
return_value={
|
||||
"result": "allow",
|
||||
"status": "allow",
|
||||
"status_msg": "Success. Logging you in...",
|
||||
}
|
||||
)
|
||||
)
|
||||
failed_duo_mock = MagicMock(auth=MagicMock(return_value={"result": "deny"}))
|
||||
with patch(
|
||||
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
|
||||
duo_mock,
|
||||
):
|
||||
self.assertEqual(
|
||||
duo_device.pk, validate_challenge_duo(duo_device.pk, request, self.user)
|
||||
)
|
||||
with patch(
|
||||
"authentik.stages.authenticator_duo.models.AuthenticatorDuoStage.client",
|
||||
failed_duo_mock,
|
||||
):
|
||||
with self.assertRaises(ValidationError):
|
||||
validate_challenge_duo(duo_device.pk, request, self.user)
|
@ -7,6 +7,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.authenticator_webauthn.models import (
|
||||
AuthenticateWebAuthnStage,
|
||||
@ -23,7 +24,7 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer):
|
||||
fields = StageSerializer.Meta.fields + ["configure_flow"]
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStageViewSet(ModelViewSet):
|
||||
class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""AuthenticateWebAuthnStage Viewset"""
|
||||
|
||||
queryset = AuthenticateWebAuthnStage.objects.all()
|
||||
@ -44,6 +45,7 @@ class WebAuthnDeviceViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -28,8 +28,6 @@ from authentik.stages.authenticator_webauthn.utils import (
|
||||
get_rp_id,
|
||||
)
|
||||
|
||||
RP_NAME = "authentik"
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
SESSION_KEY_WEBAUTHN_AUTHENTICATED = (
|
||||
@ -119,7 +117,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||
user = self.get_pending_user()
|
||||
make_credential_options = WebAuthnMakeCredentialOptions(
|
||||
challenge,
|
||||
RP_NAME,
|
||||
self.request.tenant.branding_title,
|
||||
get_rp_id(self.request),
|
||||
user.uid,
|
||||
user.username,
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""CaptchaStage API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.captcha.models import CaptchaStage
|
||||
|
||||
@ -15,7 +16,7 @@ class CaptchaStageSerializer(StageSerializer):
|
||||
extra_kwargs = {"private_key": {"write_only": True}}
|
||||
|
||||
|
||||
class CaptchaStageViewSet(ModelViewSet):
|
||||
class CaptchaStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""CaptchaStage Viewset"""
|
||||
|
||||
queryset = CaptchaStage.objects.all()
|
||||
|
@ -6,6 +6,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.core.api.applications import ApplicationSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.consent.models import ConsentStage, UserConsent
|
||||
@ -20,7 +21,7 @@ class ConsentStageSerializer(StageSerializer):
|
||||
fields = StageSerializer.Meta.fields + ["mode", "consent_expire_in"]
|
||||
|
||||
|
||||
class ConsentStageViewSet(ModelViewSet):
|
||||
class ConsentStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""ConsentStage Viewset"""
|
||||
|
||||
queryset = ConsentStage.objects.all()
|
||||
@ -42,6 +43,7 @@ class UserConsentSerializer(StageSerializer):
|
||||
class UserConsentViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
UsedByMixin,
|
||||
mixins.ListModelMixin,
|
||||
GenericViewSet,
|
||||
):
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""deny Stage API Views"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.deny.models import DenyStage
|
||||
|
||||
@ -14,7 +15,7 @@ class DenyStageSerializer(StageSerializer):
|
||||
fields = StageSerializer.Meta.fields
|
||||
|
||||
|
||||
class DenyStageViewSet(ModelViewSet):
|
||||
class DenyStageViewSet(UsedByMixin, ModelViewSet):
|
||||
"""DenyStage Viewset"""
|
||||
|
||||
queryset = DenyStage.objects.all()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user