Compare commits
188 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
31a58e2c25 | |||
229715acb2 | |||
2a670afd02 | |||
b69248dd55 | |||
5ff5edf769 | |||
939889e0ec | |||
19ae6585dc | |||
a81c847392 | |||
c6ede78fba | |||
cea1289186 | |||
c297f28552 | |||
35b25bd76e | |||
64d7610b13 | |||
2c8fcff832 | |||
054e76d02a | |||
80fa132dd9 | |||
4c59c3abef | |||
22d319c0e7 | |||
89edd77484 | |||
04e52d8ba6 | |||
9b5e3921cb | |||
2bbad64dc3 | |||
f6026fdb13 | |||
49def45ca3 | |||
a4856969f4 | |||
2aa7266688 | |||
25817cae6b | |||
5383ae2c19 | |||
c0c246edab | |||
831b32c279 | |||
70ccc63702 | |||
de954250e5 | |||
f268bd4c69 | |||
57a48b6350 | |||
9aac114115 | |||
66e3cbdc46 | |||
2d76d23f7b | |||
4327b35bc3 | |||
f7047df40e | |||
ef77a4b64e | |||
5d7d21076f | |||
ede072889e | |||
9cb7e6c606 | |||
e7d36c095d | |||
b88eb430c1 | |||
641872a33a | |||
405c690193 | |||
932cf48d2b | |||
402819107d | |||
41f135126b | |||
591a339302 | |||
35f2c5d96a | |||
fe6963c428 | |||
19cac4bf43 | |||
4ca564490e | |||
fcb795c273 | |||
14c70b3e4a | |||
ac880c28d7 | |||
f3c6b9a4f6 | |||
cba0cf0d76 | |||
73b67cf0f0 | |||
23a8052cc8 | |||
57c49c3865 | |||
cbea51ae5b | |||
8962081d92 | |||
e743f13f81 | |||
b20a8b7c17 | |||
b53c94d76a | |||
d4419d66c1 | |||
79044368d2 | |||
426686957d | |||
28cb803fd9 | |||
85c3a36b62 | |||
9ba8a715b1 | |||
358750f66e | |||
b9918529b8 | |||
a5673b4ec8 | |||
d9287d0c0e | |||
d9c2b64116 | |||
2b150d3077 | |||
dec7a9cfb9 | |||
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]
|
[bumpversion]
|
||||||
current_version = 2021.6.1-rc1
|
current_version = 2021.6.2
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
env
|
env
|
||||||
helm
|
|
||||||
static
|
static
|
||||||
htmlcov
|
htmlcov
|
||||||
*.env.yml
|
*.env.yml
|
||||||
|
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@ -6,6 +6,7 @@ daysUntilClose: 7
|
|||||||
exemptLabels:
|
exemptLabels:
|
||||||
- pinned
|
- pinned
|
||||||
- security
|
- security
|
||||||
|
- pr_wanted
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: >
|
||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
57
.github/workflows/release.yml
vendored
57
.github/workflows/release.yml
vendored
@ -33,12 +33,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik:2021.6.1-rc1,
|
beryju/authentik:2021.6.2,
|
||||||
beryju/authentik:latest,
|
beryju/authentik:latest,
|
||||||
ghcr.io/goauthentik/server:2021.6.1-rc1,
|
ghcr.io/goauthentik/server:2021.6.2,
|
||||||
ghcr.io/goauthentik/server:latest
|
ghcr.io/goauthentik/server:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
context: .
|
context: .
|
||||||
|
- name: Building Docker Image (stable)
|
||||||
|
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
|
||||||
|
run: |
|
||||||
|
docker pull beryju/authentik:latest
|
||||||
|
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||||
|
docker push beryju/authentik:stable
|
||||||
|
docker pull ghcr.io/goauthentik/server:latest
|
||||||
|
docker tag ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:stable
|
||||||
|
docker push ghcr.io/goauthentik/server:stable
|
||||||
build-proxy:
|
build-proxy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -66,12 +75,21 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-proxy:2021.6.1-rc1,
|
beryju/authentik-proxy:2021.6.2,
|
||||||
beryju/authentik-proxy:latest,
|
beryju/authentik-proxy:latest,
|
||||||
ghcr.io/goauthentik/proxy:2021.6.1-rc1,
|
ghcr.io/goauthentik/proxy:2021.6.2,
|
||||||
ghcr.io/goauthentik/proxy:latest
|
ghcr.io/goauthentik/proxy:latest
|
||||||
file: outpost/proxy.Dockerfile
|
file: outpost/proxy.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
- name: Building Docker Image (stable)
|
||||||
|
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
|
||||||
|
run: |
|
||||||
|
docker pull beryju/authentik-proxy:latest
|
||||||
|
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||||
|
docker push beryju/authentik-proxy:stable
|
||||||
|
docker pull ghcr.io/goauthentik/proxy:latest
|
||||||
|
docker tag ghcr.io/goauthentik/proxy:latest ghcr.io/goauthentik/proxy:stable
|
||||||
|
docker push ghcr.io/goauthentik/proxy:stable
|
||||||
build-ldap:
|
build-ldap:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -99,14 +117,22 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-ldap:2021.6.1-rc1,
|
beryju/authentik-ldap:2021.6.2,
|
||||||
beryju/authentik-ldap:latest,
|
beryju/authentik-ldap:latest,
|
||||||
ghcr.io/goauthentik/ldap:2021.6.1-rc1,
|
ghcr.io/goauthentik/ldap:2021.6.2,
|
||||||
ghcr.io/goauthentik/ldap:latest
|
ghcr.io/goauthentik/ldap:latest
|
||||||
file: outpost/ldap.Dockerfile
|
file: outpost/ldap.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
- name: Building Docker Image (stable)
|
||||||
|
if: ${{ github.event_name == 'release' && !contains('2021.6.2', 'rc') }}
|
||||||
|
run: |
|
||||||
|
docker pull beryju/authentik-ldap:latest
|
||||||
|
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||||
|
docker push beryju/authentik-ldap:stable
|
||||||
|
docker pull ghcr.io/goauthentik/ldap:latest
|
||||||
|
docker tag ghcr.io/goauthentik/ldap:latest ghcr.io/goauthentik/ldap:stable
|
||||||
|
docker push ghcr.io/goauthentik/ldap:stable
|
||||||
test-release:
|
test-release:
|
||||||
if: ${{ github.event_name == 'release' }}
|
|
||||||
needs:
|
needs:
|
||||||
- build-server
|
- build-server
|
||||||
- build-proxy
|
- build-proxy
|
||||||
@ -122,7 +148,7 @@ jobs:
|
|||||||
docker-compose pull -q
|
docker-compose pull -q
|
||||||
docker-compose up --no-start
|
docker-compose up --no-start
|
||||||
docker-compose start postgresql redis
|
docker-compose start postgresql redis
|
||||||
docker-compose run -u root --entrypoint /bin/bash server -c "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:
|
sentry-release:
|
||||||
if: ${{ github.event_name == 'release' }}
|
if: ${{ github.event_name == 'release' }}
|
||||||
needs:
|
needs:
|
||||||
@ -130,13 +156,26 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v2.1.5
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- name: Build web api client and web ui
|
||||||
|
run: |
|
||||||
|
export NODE_ENV=production
|
||||||
|
make gen-web
|
||||||
|
cd web
|
||||||
|
npm i
|
||||||
|
npm run build
|
||||||
- name: Create a Sentry.io release
|
- name: Create a Sentry.io release
|
||||||
uses: getsentry/action-release@v1
|
uses: getsentry/action-release@v1
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
SENTRY_ORG: beryjuorg
|
SENTRY_ORG: beryjuorg
|
||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
version: authentik@2021.6.1-rc1
|
version: authentik@2021.6.2
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
|
sourcemaps: './web/dist'
|
||||||
|
4
.github/workflows/tag.yml
vendored
4
.github/workflows/tag.yml
vendored
@ -20,11 +20,11 @@ jobs:
|
|||||||
docker-compose pull -q
|
docker-compose pull -q
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/authentik:latest \
|
-t ghcr.io/goauthentik/server:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
docker-compose up --no-start
|
docker-compose up --no-start
|
||||||
docker-compose start postgresql redis
|
docker-compose start postgresql redis
|
||||||
docker-compose run -u root --entrypoint /bin/bash server -c "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
|
- name: Extract version number
|
||||||
id: get_version
|
id: get_version
|
||||||
uses: actions/github-script@v4.0.2
|
uses: actions/github-script@v4.0.2
|
||||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -193,10 +193,6 @@ pip-selfcheck.json
|
|||||||
local.env.yml
|
local.env.yml
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
### Helm ###
|
|
||||||
# Chart dependencies
|
|
||||||
**/charts/*.tgz
|
|
||||||
|
|
||||||
# Selenium Screenshots
|
# Selenium Screenshots
|
||||||
selenium_screenshots/
|
selenium_screenshots/
|
||||||
backups/
|
backups/
|
||||||
|
@ -8,7 +8,7 @@ WORKDIR /app/
|
|||||||
|
|
||||||
RUN pip install pipenv && \
|
RUN pip install pipenv && \
|
||||||
pipenv lock -r > requirements.txt && \
|
pipenv lock -r > requirements.txt && \
|
||||||
pipenv lock -rd > requirements-dev.txt
|
pipenv lock -r --dev-only > requirements-dev.txt
|
||||||
|
|
||||||
# Stage 2: Build web API
|
# Stage 2: Build web API
|
||||||
FROM openapitools/openapi-generator-cli as api-builder
|
FROM openapitools/openapi-generator-cli as api-builder
|
||||||
@ -28,7 +28,7 @@ COPY ./web /static/
|
|||||||
COPY --from=api-builder /local/web/api /static/api
|
COPY --from=api-builder /local/web/api /static/api
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
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
|
# Stage 4: Build go proxy
|
||||||
FROM golang:1.16.5 AS builder
|
FROM golang:1.16.5 AS builder
|
||||||
@ -76,6 +76,7 @@ RUN apt-get update && \
|
|||||||
COPY ./authentik/ /authentik
|
COPY ./authentik/ /authentik
|
||||||
COPY ./pyproject.toml /
|
COPY ./pyproject.toml /
|
||||||
COPY ./xml /xml
|
COPY ./xml /xml
|
||||||
|
COPY ./tests /tests
|
||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
COPY --from=builder /work/authentik /authentik-proxy
|
COPY --from=builder /work/authentik /authentik-proxy
|
||||||
|
1
Pipfile
1
Pipfile
@ -46,6 +46,7 @@ webauthn = "*"
|
|||||||
xmlsec = "*"
|
xmlsec = "*"
|
||||||
duo-client = "*"
|
duo-client = "*"
|
||||||
ua-parser = "*"
|
ua-parser = "*"
|
||||||
|
deepmerge = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.9"
|
python_version = "3.9"
|
||||||
|
224
Pipfile.lock
generated
224
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "4fa1ad681762c867a95410074f31ac5d00119e187e0f38982cd59fdf301cccf5"
|
"sha256": "f90d9fb4713eaf9c5ffe6a3858e64843670f79ab5007e7debf914c1f094c8d63"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -56,6 +56,7 @@
|
|||||||
"sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
|
"sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a",
|
||||||
"sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
|
"sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.7.4.post0"
|
"version": "==3.7.4.post0"
|
||||||
},
|
},
|
||||||
"aioredis": {
|
"aioredis": {
|
||||||
@ -70,6 +71,7 @@
|
|||||||
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
|
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
|
||||||
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
|
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.0.6"
|
"version": "==5.0.6"
|
||||||
},
|
},
|
||||||
"asgiref": {
|
"asgiref": {
|
||||||
@ -77,6 +79,7 @@
|
|||||||
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
|
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
|
||||||
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
|
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.3.4"
|
"version": "==3.3.4"
|
||||||
},
|
},
|
||||||
"async-timeout": {
|
"async-timeout": {
|
||||||
@ -84,6 +87,7 @@
|
|||||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||||
],
|
],
|
||||||
|
"markers": "python_full_version >= '3.5.3'",
|
||||||
"version": "==3.0.1"
|
"version": "==3.0.1"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
@ -91,6 +95,7 @@
|
|||||||
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
||||||
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
|
"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"
|
"version": "==21.2.0"
|
||||||
},
|
},
|
||||||
"autobahn": {
|
"autobahn": {
|
||||||
@ -98,6 +103,7 @@
|
|||||||
"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
|
"sha256:9195df8af03b0ff29ccd4b7f5abbde957ee90273465942205f9a1bad6c3f07ac",
|
||||||
"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
|
"sha256:e126c1f583e872fb59e79d36977cfa1f2d0a8a79f90ae31f406faae7664b8e03"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==21.3.1"
|
"version": "==21.3.1"
|
||||||
},
|
},
|
||||||
"automat": {
|
"automat": {
|
||||||
@ -116,24 +122,26 @@
|
|||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2ade860f66fa6b9a9886d7ff2e5118e5efebc4807b863ef735d358ef730234ed",
|
"sha256:2c2f70608934b03f9c08f4cd185de223b5abd18245dd4d4800e1fbc2a2523e31",
|
||||||
"sha256:bbf727d770a9844834bfbf3f811db1d3438320897f67cfb21cdca5bb8fc23c13"
|
"sha256:fccfa81cda69bb2317ed97e7149d7d84d19e6ec3bfbe3f721139e7ac0c407c73"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.17.90"
|
"version": "==1.17.98"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6ae4ff3405cc4fc69ff3673a8dd234bf869aa556ae1e0da050d7f2aa3c3edab6",
|
"sha256:b2a49de4ee04b690142c8e7240f0f5758e3f7673dd39cf398efe893bf5e11c3f",
|
||||||
"sha256:b301810c4bd6cab1b6eaf6bfd9f25abb27959b586c2e1689bbce035b3fb8ae66"
|
"sha256:b955b23fe2fbdbbc8e66f37fe2970de6b5d8169f940b200bcf434751709d38f6"
|
||||||
],
|
],
|
||||||
"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.98"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
|
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
|
||||||
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
|
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version ~= '3.5'",
|
||||||
"version": "==4.2.2"
|
"version": "==4.2.2"
|
||||||
},
|
},
|
||||||
"cbor2": {
|
"cbor2": {
|
||||||
@ -152,15 +160,16 @@
|
|||||||
"sha256:ce6219986385778b1ab7f9b542f160bb4d3558f52975e914a27b774e47016fb7",
|
"sha256:ce6219986385778b1ab7f9b542f160bb4d3558f52975e914a27b774e47016fb7",
|
||||||
"sha256:d562b2773e14ee1d65ea5b85351a83a64d4f3fd011bc2b4c70a6e813e78203ce"
|
"sha256:d562b2773e14ee1d65ea5b85351a83a64d4f3fd011bc2b4c70a6e813e78203ce"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.4.0"
|
"version": "==5.4.0"
|
||||||
},
|
},
|
||||||
"celery": {
|
"celery": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1329de1edeaf734ef859e630cb42df2c116d53e59d2f46433b13aed196e85620",
|
"sha256:54436cd97b031bf2e08064223240e2a83d601d9414bcb1b702f94c6c33c29485",
|
||||||
"sha256:65f061c04578cf189cd7352c192e1a79fdeb370b916bff792bcc769560e81184"
|
"sha256:b5399d76cf70d5cfac3ec993f8796ec1aa90d4cef55972295751f384758a80d7"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.1.0"
|
"version": "==5.1.1"
|
||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -244,6 +253,7 @@
|
|||||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
"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"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
@ -251,6 +261,7 @@
|
|||||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
"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"
|
"version": "==7.1.2"
|
||||||
},
|
},
|
||||||
"click-didyoumean": {
|
"click-didyoumean": {
|
||||||
@ -310,8 +321,17 @@
|
|||||||
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
|
"sha256:76ffae916ba3aa66b46996c14fa713e46004788167a4873d647544e750e0e99f",
|
||||||
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
|
"sha256:a9af943c79717bc52fe64a3c236ae5d3adccc8b5be19c881b442d2c3db233393"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.0.2"
|
"version": "==3.0.2"
|
||||||
},
|
},
|
||||||
|
"deepmerge": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:87166dbe9ba1a3348a45c9d4ada6778f518d41afc0b85aa017ea3041facc3f9c",
|
||||||
|
"sha256:f6fd7f1293c535fb599e197e750dbe8674503c5d2a89759b3c72a3c46746d4fd"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.3.0"
|
||||||
|
},
|
||||||
"defusedxml": {
|
"defusedxml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
|
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
|
||||||
@ -414,11 +434,11 @@
|
|||||||
},
|
},
|
||||||
"drf-spectacular": {
|
"drf-spectacular": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4d35e890b8139e1c056588c5529a2f2066615635482563f0840b96d3b879d7d2",
|
"sha256:6ffbfde7d96a4a2febd19182cc405217e1e86a50280fc739402291c93d1a32b7",
|
||||||
"sha256:f552476dfde647963c21615249672e7f4f9ece3788036b5ee5c6cc5ad50748ab"
|
"sha256:77593024bb899f69227abedcf87def7851a11c9978f781aa4b385a10f67a38b7"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.17.0"
|
"version": "==0.17.2"
|
||||||
},
|
},
|
||||||
"duo-client": {
|
"duo-client": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -440,6 +460,7 @@
|
|||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.18.2"
|
"version": "==0.18.2"
|
||||||
},
|
},
|
||||||
"geoip2": {
|
"geoip2": {
|
||||||
@ -452,10 +473,11 @@
|
|||||||
},
|
},
|
||||||
"google-auth": {
|
"google-auth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:9b235dbc876e49454cbedc52ae0abd540ef705ebccdf4fbe93553bb13f26b1a4",
|
"sha256:b3a67fa9ba5b768861dacf374c2135eb09fa14a0e40c851c3b8ea7abe6fc8fef",
|
||||||
"sha256:eb017521276a75492282c6ca4b718f26de112ed3bcbeaeeb02c1b82de425f909"
|
"sha256:e34e5f5de5610b202f9b40ebd9f8b27571d5c5537db9afed3a72b2db5a345039"
|
||||||
],
|
],
|
||||||
"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.32.0"
|
||||||
},
|
},
|
||||||
"gunicorn": {
|
"gunicorn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -470,6 +492,7 @@
|
|||||||
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
|
||||||
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==0.12.0"
|
"version": "==0.12.0"
|
||||||
},
|
},
|
||||||
"hiredis": {
|
"hiredis": {
|
||||||
@ -516,6 +539,7 @@
|
|||||||
"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
|
"sha256:f52010e0a44e3d8530437e7da38d11fb822acfb0d5b12e9cd5ba655509937ca0",
|
||||||
"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
|
"sha256:f8196f739092a78e4f6b1b2172679ed3343c39c61a3e9d722ce6fcf1dac2824a"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.0.0"
|
"version": "==2.0.0"
|
||||||
},
|
},
|
||||||
"httptools": {
|
"httptools": {
|
||||||
@ -564,6 +588,7 @@
|
|||||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||||
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
|
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==0.5.1"
|
"version": "==0.5.1"
|
||||||
},
|
},
|
||||||
"jmespath": {
|
"jmespath": {
|
||||||
@ -571,6 +596,7 @@
|
|||||||
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
|
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
|
||||||
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
|
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.10.0"
|
"version": "==0.10.0"
|
||||||
},
|
},
|
||||||
"jsonschema": {
|
"jsonschema": {
|
||||||
@ -585,6 +611,7 @@
|
|||||||
"sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d",
|
"sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d",
|
||||||
"sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"
|
"sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.1.0"
|
"version": "==5.1.0"
|
||||||
},
|
},
|
||||||
"kubernetes": {
|
"kubernetes": {
|
||||||
@ -598,6 +625,9 @@
|
|||||||
"ldap3": {
|
"ldap3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
|
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
|
||||||
|
"sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
|
||||||
|
"sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
|
||||||
|
"sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056",
|
||||||
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
|
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
@ -659,6 +689,7 @@
|
|||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.0.3"
|
"version": "==2.0.3"
|
||||||
},
|
},
|
||||||
"msgpack": {
|
"msgpack": {
|
||||||
@ -734,6 +765,7 @@
|
|||||||
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
|
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
|
||||||
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
|
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.1.0"
|
"version": "==5.1.0"
|
||||||
},
|
},
|
||||||
"oauthlib": {
|
"oauthlib": {
|
||||||
@ -741,6 +773,7 @@
|
|||||||
"sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc",
|
"sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc",
|
||||||
"sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"
|
"sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.1.1"
|
"version": "==3.1.1"
|
||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
@ -756,67 +789,85 @@
|
|||||||
"sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86",
|
"sha256:3a8baade6cb80bcfe43297e33e7623f3118d660d41387593758e2fb1ea173a86",
|
||||||
"sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"
|
"sha256:b014bc76815eb1399da8ce5fc84b7717a3e63652b0c0f8804092c9363acab1b2"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.11.0"
|
"version": "==0.11.0"
|
||||||
},
|
},
|
||||||
"prompt-toolkit": {
|
"prompt-toolkit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:bf00f22079f5fadc949f42ae8ff7f05702826a97059ffcc6281036ad40ac6f04",
|
"sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f",
|
||||||
"sha256:e1b4f11b9336a28fa11810bc623c357420f69dfdb6d2dac41ca2c21a55c033bc"
|
"sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"
|
||||||
],
|
],
|
||||||
"version": "==3.0.18"
|
"markers": "python_full_version >= '3.6.1'",
|
||||||
|
"version": "==3.0.19"
|
||||||
},
|
},
|
||||||
"psycopg2-binary": {
|
"psycopg2-binary": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
|
"sha256:0b7dae87f0b729922e06f85f667de7bf16455d411971b2043bbd9577af9d1975",
|
||||||
"sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
|
"sha256:0f2e04bd2a2ab54fa44ee67fe2d002bb90cee1c0f1cc0ebc3148af7b02034cbd",
|
||||||
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
|
"sha256:123c3fb684e9abfc47218d3784c7b4c47c8587951ea4dd5bc38b6636ac57f616",
|
||||||
"sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
|
"sha256:1473c0215b0613dd938db54a653f68251a45a78b05f6fc21af4326f40e8360a2",
|
||||||
"sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
|
"sha256:14db1752acdd2187d99cb2ca0a1a6dfe57fc65c3281e0f20e597aac8d2a5bd90",
|
||||||
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
|
"sha256:1e3a362790edc0a365385b1ac4cc0acc429a0c0d662d829a50b6ce743ae61b5a",
|
||||||
"sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
|
"sha256:1e85b74cbbb3056e3656f1cc4781294df03383127a8114cbc6531e8b8367bf1e",
|
||||||
"sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
|
"sha256:20f1ab44d8c352074e2d7ca67dc00843067788791be373e67a0911998787ce7d",
|
||||||
"sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
|
"sha256:2f62c207d1740b0bde5c4e949f857b044818f734a3d57f1d0d0edc65050532ed",
|
||||||
"sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
|
"sha256:3242b9619de955ab44581a03a64bdd7d5e470cc4183e8fcadd85ab9d3756ce7a",
|
||||||
"sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
|
"sha256:35c4310f8febe41f442d3c65066ca93cccefd75013df3d8c736c5b93ec288140",
|
||||||
"sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
|
"sha256:4235f9d5ddcab0b8dbd723dca56ea2922b485ea00e1dafacf33b0c7e840b3d32",
|
||||||
"sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
|
"sha256:5ced67f1e34e1a450cdb48eb53ca73b60aa0af21c46b9b35ac3e581cf9f00e31",
|
||||||
"sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
|
"sha256:7360647ea04db2e7dff1648d1da825c8cf68dc5fbd80b8fb5b3ee9f068dcd21a",
|
||||||
"sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
|
"sha256:8c13d72ed6af7fd2c8acbd95661cf9477f94e381fce0792c04981a8283b52917",
|
||||||
"sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
|
"sha256:988b47ac70d204aed01589ed342303da7c4d84b56c2f4c4b8b00deda123372bf",
|
||||||
"sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
|
"sha256:995fc41ebda5a7a663a254a1dcac52638c3e847f48307b5416ee373da15075d7",
|
||||||
"sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd",
|
"sha256:a36c7eb6152ba5467fb264d73844877be8b0847874d4822b7cf2d3c0cb8cdcb0",
|
||||||
"sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
|
"sha256:aed4a9a7e3221b3e252c39d0bf794c438dc5453bc2963e8befe9d4cd324dff72",
|
||||||
"sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
|
"sha256:aef9aee84ec78af51107181d02fe8773b100b01c5dfde351184ad9223eab3698",
|
||||||
"sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
|
"sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773",
|
||||||
"sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
|
"sha256:b4d7679a08fea64573c969f6994a2631908bb2c0e69a7235648642f3d2e39a68",
|
||||||
"sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
|
"sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76",
|
||||||
"sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
|
"sha256:ca86db5b561b894f9e5f115d6a159fff2a2570a652e07889d8a383b5fae66eb4",
|
||||||
"sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
|
"sha256:cfc523edecddaef56f6740d7de1ce24a2fdf94fd5e704091856a201872e37f9f",
|
||||||
"sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
|
"sha256:da113b70f6ec40e7d81b43d1b139b9db6a05727ab8be1ee559f3a69854a69d34",
|
||||||
"sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66",
|
"sha256:f6fac64a38f6768e7bc7b035b9e10d8a538a9fadce06b983fb3e6fa55ac5f5ce",
|
||||||
"sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
|
"sha256:f8559617b1fcf59a9aedba2c9838b5b6aa211ffedecabca412b92a1ff75aac1a",
|
||||||
"sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449",
|
"sha256:fbb42a541b1093385a2d8c7eec94d26d30437d0e77c1d25dae1dcc46741a385e"
|
||||||
"sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
|
|
||||||
"sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
|
|
||||||
"sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
|
|
||||||
"sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
|
|
||||||
"sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
|
|
||||||
"sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.8.6"
|
"version": "==2.9.1"
|
||||||
},
|
},
|
||||||
"pyasn1": {
|
"pyasn1": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||||
|
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
|
||||||
|
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
|
||||||
|
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
|
||||||
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
||||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
|
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
|
||||||
|
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
|
||||||
|
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
|
||||||
|
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
|
||||||
|
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
|
||||||
|
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
|
||||||
|
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
|
||||||
|
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
|
||||||
],
|
],
|
||||||
"version": "==0.4.8"
|
"version": "==0.4.8"
|
||||||
},
|
},
|
||||||
"pyasn1-modules": {
|
"pyasn1-modules": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
|
||||||
|
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
|
||||||
|
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
|
||||||
|
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
|
||||||
|
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
|
||||||
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
|
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
|
||||||
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
|
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
|
||||||
|
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
|
||||||
|
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
|
||||||
|
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
|
||||||
|
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
|
||||||
|
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
|
||||||
|
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
|
||||||
],
|
],
|
||||||
"version": "==0.2.8"
|
"version": "==0.2.8"
|
||||||
},
|
},
|
||||||
@ -825,6 +876,7 @@
|
|||||||
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||||
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.20"
|
"version": "==2.20"
|
||||||
},
|
},
|
||||||
"pycryptodome": {
|
"pycryptodome": {
|
||||||
@ -868,6 +920,7 @@
|
|||||||
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
||||||
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
|
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==2.0.2"
|
"version": "==2.0.2"
|
||||||
},
|
},
|
||||||
"pyjwt": {
|
"pyjwt": {
|
||||||
@ -890,12 +943,14 @@
|
|||||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.4.7"
|
"version": "==2.4.7"
|
||||||
},
|
},
|
||||||
"pyrsistent": {
|
"pyrsistent": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==0.17.3"
|
"version": "==0.17.3"
|
||||||
},
|
},
|
||||||
"python-dateutil": {
|
"python-dateutil": {
|
||||||
@ -903,14 +958,15 @@
|
|||||||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.8.1"
|
"version": "==2.8.1"
|
||||||
},
|
},
|
||||||
"python-dotenv": {
|
"python-dotenv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00aa34e92d992e9f8383730816359647f358f4a3be1ba45e5a5cefd27ee91544",
|
"sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d",
|
||||||
"sha256:b1ae5e9643d5ed987fc57cc2583021e38db531946518130777734f9589b3141f"
|
"sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d"
|
||||||
],
|
],
|
||||||
"version": "==0.17.1"
|
"version": "==0.18.0"
|
||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -959,6 +1015,7 @@
|
|||||||
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
|
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
|
||||||
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
|
"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"
|
"version": "==3.5.3"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
@ -966,12 +1023,14 @@
|
|||||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
"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"
|
"version": "==2.25.1"
|
||||||
},
|
},
|
||||||
"requests-oauthlib": {
|
"requests-oauthlib": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
||||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
|
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
|
||||||
|
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.0"
|
"version": "==1.3.0"
|
||||||
@ -1012,6 +1071,7 @@
|
|||||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.16.0"
|
"version": "==1.16.0"
|
||||||
},
|
},
|
||||||
"sqlparse": {
|
"sqlparse": {
|
||||||
@ -1019,6 +1079,7 @@
|
|||||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==0.4.1"
|
"version": "==0.4.1"
|
||||||
},
|
},
|
||||||
"structlog": {
|
"structlog": {
|
||||||
@ -1074,6 +1135,7 @@
|
|||||||
"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",
|
"sha256:7d6f89745680233f1c4db9ddb748df5e88d2a7a37962be174c0fd04c8dba1dc8",
|
||||||
"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
|
"sha256:c16b55f9a67b2419cfdf8846576e2ec9ba94fe6978a83080c352a80db31c93fb"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==21.2.1"
|
"version": "==21.2.1"
|
||||||
},
|
},
|
||||||
"typing-extensions": {
|
"typing-extensions": {
|
||||||
@ -1097,6 +1159,7 @@
|
|||||||
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
|
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
|
||||||
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
|
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==3.0.1"
|
"version": "==3.0.1"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
@ -1141,6 +1204,7 @@
|
|||||||
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
||||||
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
|
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==5.0.0"
|
"version": "==5.0.0"
|
||||||
},
|
},
|
||||||
"watchgod": {
|
"watchgod": {
|
||||||
@ -1167,10 +1231,11 @@
|
|||||||
},
|
},
|
||||||
"websocket-client": {
|
"websocket-client": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3e2bf58191d4619b161389a95bdce84ce9e0b24eb8107e7e590db682c2d0ca81",
|
"sha256:b68e4959d704768fa20e35c9d508c8dc2bbc041fd8d267c0d7345cffe2824568",
|
||||||
"sha256:abf306dc6351dcef07f4d40453037e51cc5d9da2ef60d0fc5d0fe3bcda255372"
|
"sha256:e5c333bfa9fa739538b652b6f8c8fc2559f1d364243c8a689d7c0e1d41c2e611"
|
||||||
],
|
],
|
||||||
"version": "==1.0.1"
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==1.1.0"
|
||||||
},
|
},
|
||||||
"websockets": {
|
"websockets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1267,6 +1332,7 @@
|
|||||||
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
|
"sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a",
|
||||||
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
|
"sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==1.6.3"
|
"version": "==1.6.3"
|
||||||
},
|
},
|
||||||
"zope.interface": {
|
"zope.interface": {
|
||||||
@ -1323,6 +1389,7 @@
|
|||||||
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
|
"sha256:f44e517131a98f7a76696a7b21b164bcb85291cee106a23beccce454e1f433a4",
|
||||||
"sha256:f7ee479e96f7ee350db1cf24afa5685a5899e2b34992fb99e1f7c1b0b758d263"
|
"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"
|
"version": "==5.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1339,6 +1406,7 @@
|
|||||||
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
|
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
|
||||||
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
|
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version ~= '3.6'",
|
||||||
"version": "==2.5.6"
|
"version": "==2.5.6"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
@ -1346,6 +1414,7 @@
|
|||||||
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
|
||||||
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
|
"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"
|
"version": "==21.2.0"
|
||||||
},
|
},
|
||||||
"bandit": {
|
"bandit": {
|
||||||
@ -1384,6 +1453,7 @@
|
|||||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
"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"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
@ -1391,6 +1461,7 @@
|
|||||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
"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"
|
"version": "==7.1.2"
|
||||||
},
|
},
|
||||||
"colorama": {
|
"colorama": {
|
||||||
@ -1464,14 +1535,16 @@
|
|||||||
"sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
|
"sha256:6c4cc71933456991da20917998acbe6cf4fb41eeaab7d6d67fbc05ecd4c865b0",
|
||||||
"sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
|
"sha256:96bf5c08b157a666fec41129e6d327235284cca4c81e92109260f353ba138005"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.4'",
|
||||||
"version": "==4.0.7"
|
"version": "==4.0.7"
|
||||||
},
|
},
|
||||||
"gitpython": {
|
"gitpython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:29fe82050709760081f588dd50ce83504feddbebdc4da6956d02351552b1c135",
|
"sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b",
|
||||||
"sha256:ee24bdc93dce357630764db659edaf6b8d664d4ff5447ccfeedd2dc5c253f41e"
|
"sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"
|
||||||
],
|
],
|
||||||
"version": "==3.1.17"
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.1.18"
|
||||||
},
|
},
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1489,10 +1562,11 @@
|
|||||||
},
|
},
|
||||||
"isort": {
|
"isort": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0a943902919f65c5684ac4e0154b1ad4fac6dcaa5d9f3426b732f1c8b5419be6",
|
"sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
|
||||||
"sha256:2bb1680aad211e3c9944dbce1d4ba09a989f04e238296c87fe2139faa26d655d"
|
"sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"
|
||||||
],
|
],
|
||||||
"version": "==5.8.0"
|
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
|
||||||
|
"version": "==5.9.1"
|
||||||
},
|
},
|
||||||
"lazy-object-proxy": {
|
"lazy-object-proxy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1519,6 +1593,7 @@
|
|||||||
"sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
|
"sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93",
|
||||||
"sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"
|
"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"
|
"version": "==1.6.0"
|
||||||
},
|
},
|
||||||
"mccabe": {
|
"mccabe": {
|
||||||
@ -1555,6 +1630,7 @@
|
|||||||
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd",
|
"sha256:42df03e7797b796625b1029c0400279c7c34fd7df24a7d7818a1abb5b38710dd",
|
||||||
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"
|
"sha256:c68c661ac5cc81058ac94247278eeda6d2e6aecb3e227b0387c30d277e7ef8d4"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6'",
|
||||||
"version": "==5.6.0"
|
"version": "==5.6.0"
|
||||||
},
|
},
|
||||||
"pluggy": {
|
"pluggy": {
|
||||||
@ -1562,6 +1638,7 @@
|
|||||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.13.1"
|
"version": "==0.13.1"
|
||||||
},
|
},
|
||||||
"py": {
|
"py": {
|
||||||
@ -1569,6 +1646,7 @@
|
|||||||
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||||
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.10.0"
|
"version": "==1.10.0"
|
||||||
},
|
},
|
||||||
"pylint": {
|
"pylint": {
|
||||||
@ -1599,6 +1677,7 @@
|
|||||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.4.7"
|
"version": "==2.4.7"
|
||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
@ -1703,6 +1782,7 @@
|
|||||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
"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"
|
"version": "==2.25.1"
|
||||||
},
|
},
|
||||||
"requests-mock": {
|
"requests-mock": {
|
||||||
@ -1726,6 +1806,7 @@
|
|||||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.16.0"
|
"version": "==1.16.0"
|
||||||
},
|
},
|
||||||
"smmap": {
|
"smmap": {
|
||||||
@ -1733,6 +1814,7 @@
|
|||||||
"sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182",
|
"sha256:7e65386bd122d45405ddf795637b7f7d2b532e7e401d46bbe3fb49b9986d5182",
|
||||||
"sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"
|
"sha256:a9a7479e4c572e2e775c404dcd3080c8dc49f39918c2cf74913d30c4c478e3c2"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==4.0.0"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"stevedore": {
|
"stevedore": {
|
||||||
@ -1740,6 +1822,7 @@
|
|||||||
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
|
"sha256:3a5bbd0652bf552748871eaa73a4a8dc2899786bc497a2aa1fcb4dcdb0debeee",
|
||||||
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
|
"sha256:50d7b78fbaf0d04cd62411188fa7eedcb03eb7f4c4b37005615ceebe582aa82a"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.3.0"
|
"version": "==3.3.0"
|
||||||
},
|
},
|
||||||
"toml": {
|
"toml": {
|
||||||
@ -1747,6 +1830,7 @@
|
|||||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.10.2"
|
"version": "==0.10.2"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"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 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
|
## Screenshots
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "2021.6.1-rc1"
|
__version__ = "2021.6.2"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
@ -10,3 +10,25 @@ class AuthentikAPIConfig(AppConfig):
|
|||||||
label = "authentik_api"
|
label = "authentik_api"
|
||||||
mountpoint = "api/"
|
mountpoint = "api/"
|
||||||
verbose_name = "authentik 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 binascii import Error
|
||||||
from typing import Any, Optional, Union
|
from typing import Any, Optional, Union
|
||||||
|
|
||||||
from drf_spectacular.authentication import OpenApiAuthenticationExtension
|
|
||||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
@ -56,18 +55,3 @@ class TokenAuthentication(BaseAuthentication):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return (token.user, None) # pragma: no cover
|
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,
|
inline_serializer,
|
||||||
)
|
)
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import (
|
from rest_framework.fields import BooleanField, CharField, FileField, ReadOnlyField
|
||||||
BooleanField,
|
|
||||||
CharField,
|
|
||||||
FileField,
|
|
||||||
IntegerField,
|
|
||||||
ReadOnlyField,
|
|
||||||
)
|
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
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.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application, User
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||||
@ -73,7 +68,7 @@ class ApplicationSerializer(ModelSerializer):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ApplicationViewSet(ModelViewSet):
|
class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Application Viewset"""
|
"""Application Viewset"""
|
||||||
|
|
||||||
queryset = Application.objects.all()
|
queryset = Application.objects.all()
|
||||||
@ -106,15 +101,19 @@ class ApplicationViewSet(ModelViewSet):
|
|||||||
return applications
|
return applications
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=inline_serializer(
|
parameters=[
|
||||||
"CheckAccessRequest", fields={"for_user": IntegerField(required=False)}
|
OpenApiParameter(
|
||||||
),
|
name="for_user",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.INT,
|
||||||
|
)
|
||||||
|
],
|
||||||
responses={
|
responses={
|
||||||
200: PolicyTestResultSerializer(),
|
200: PolicyTestResultSerializer(),
|
||||||
404: OpenApiResponse(description="for_user user not found"),
|
404: OpenApiResponse(description="for_user user not found"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@action(detail=True, methods=["POST"])
|
@action(detail=True, methods=["GET"])
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def check_access(self, request: Request, slug: str) -> Response:
|
def check_access(self, request: Request, slug: str) -> Response:
|
||||||
"""Check access to a single application by slug"""
|
"""Check access to a single application by slug"""
|
||||||
@ -203,7 +202,7 @@ class ApplicationViewSet(ModelViewSet):
|
|||||||
"""Set application icon"""
|
"""Set application icon"""
|
||||||
app: Application = self.get_object()
|
app: Application = self.get_object()
|
||||||
icon = request.FILES.get("file", None)
|
icon = request.FILES.get("file", None)
|
||||||
clear = request.data.get("clear", False)
|
clear = request.data.get("clear", "false").lower() == "true"
|
||||||
if clear:
|
if clear:
|
||||||
# .delete() saves the model by default
|
# .delete() saves the model by default
|
||||||
app.meta_icon.delete()
|
app.meta_icon.delete()
|
||||||
|
@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer
|
|||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from ua_parser import user_agent_parser
|
from ua_parser import user_agent_parser
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.models import AuthenticatedSession
|
from authentik.core.models import AuthenticatedSession
|
||||||
from authentik.events.geo import GEOIP_READER, GeoIPDict
|
from authentik.events.geo import GEOIP_READER, GeoIPDict
|
||||||
|
|
||||||
@ -92,6 +93,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
|
|||||||
class AuthenticatedSessionViewSet(
|
class AuthenticatedSessionViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
|
@ -5,6 +5,7 @@ from rest_framework.serializers import ModelSerializer
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
from 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.api.utils import is_dict
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ class GroupSerializer(ModelSerializer):
|
|||||||
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
|
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
|
||||||
|
|
||||||
|
|
||||||
class GroupViewSet(ModelViewSet):
|
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Group Viewset"""
|
"""Group Viewset"""
|
||||||
|
|
||||||
queryset = Group.objects.all()
|
queryset = Group.objects.all()
|
||||||
|
@ -14,6 +14,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
|||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import (
|
from authentik.core.api.utils import (
|
||||||
MetaNameSerializer,
|
MetaNameSerializer,
|
||||||
PassiveSerializer,
|
PassiveSerializer,
|
||||||
@ -65,6 +66,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
|||||||
class PropertyMappingViewSet(
|
class PropertyMappingViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
|
@ -9,6 +9,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
from rest_framework.viewsets import GenericViewSet
|
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.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
@ -48,6 +49,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
class ProviderViewSet(
|
class ProviderViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
|
@ -10,6 +10,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
|||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from structlog.stdlib import get_logger
|
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.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||||
from authentik.core.models import Source
|
from authentik.core.models import Source
|
||||||
from authentik.core.types import UserSettingSerializer
|
from authentik.core.types import UserSettingSerializer
|
||||||
@ -52,6 +53,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
class SourceViewSet(
|
class SourceViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
|
@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
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.users import UserSerializer
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import Token, TokenIntents
|
from authentik.core.models import Token, TokenIntents
|
||||||
@ -43,7 +44,7 @@ class TokenViewSerializer(PassiveSerializer):
|
|||||||
key = CharField(read_only=True)
|
key = CharField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class TokenViewSet(ModelViewSet):
|
class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Token Viewset"""
|
"""Token Viewset"""
|
||||||
|
|
||||||
lookup_field = "identifier"
|
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.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.groups import GroupSerializer
|
from authentik.core.api.groups import GroupSerializer
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
||||||
from authentik.core.middleware import (
|
from authentik.core.middleware import (
|
||||||
SESSION_IMPERSONATE_ORIGINAL_USER,
|
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||||
@ -131,7 +132,7 @@ class UsersFilter(FilterSet):
|
|||||||
fields = ["username", "name", "is_active", "is_superuser", "attributes"]
|
fields = ["username", "name", "is_active", "is_superuser", "attributes"]
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""User Viewset"""
|
"""User Viewset"""
|
||||||
|
|
||||||
queryset = User.objects.none()
|
queryset = User.objects.none()
|
||||||
|
@ -3,23 +3,33 @@ from traceback import format_tb
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from guardian.utils import get_anonymous_user
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import PropertyMapping, User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
|
from authentik.policies.types import PolicyRequest
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingEvaluator(BaseEvaluator):
|
class PropertyMappingEvaluator(BaseEvaluator):
|
||||||
"""Custom Evalautor that adds some different context variables."""
|
"""Custom Evalautor that adds some different context variables."""
|
||||||
|
|
||||||
def set_context(
|
def set_context(
|
||||||
self, user: Optional[User], request: Optional[HttpRequest], **kwargs
|
self,
|
||||||
|
user: Optional[User],
|
||||||
|
request: Optional[HttpRequest],
|
||||||
|
mapping: PropertyMapping,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Update context with context from PropertyMapping's evaluate"""
|
"""Update context with context from PropertyMapping's evaluate"""
|
||||||
|
req = PolicyRequest(user=get_anonymous_user())
|
||||||
|
req.obj = mapping
|
||||||
if user:
|
if user:
|
||||||
|
req.user = user
|
||||||
self._context["user"] = user
|
self._context["user"] = user
|
||||||
if request:
|
if request:
|
||||||
self._context["request"] = request
|
req.http_request = request
|
||||||
|
self._context["request"] = req
|
||||||
self._context.update(**kwargs)
|
self._context.update(**kwargs)
|
||||||
|
|
||||||
def handle_error(self, exc: Exception, expression_source: str):
|
def handle_error(self, exc: Exception, expression_source: str):
|
||||||
@ -30,9 +40,8 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||||||
expression=expression_source,
|
expression=expression_source,
|
||||||
message=error_string,
|
message=error_string,
|
||||||
)
|
)
|
||||||
if "user" in self._context:
|
|
||||||
event.set_user(self._context["user"])
|
|
||||||
if "request" in self._context:
|
if "request" in self._context:
|
||||||
event.from_http(self._context["request"])
|
req: PolicyRequest = self._context["request"]
|
||||||
|
event.from_http(req.http_request, req.user)
|
||||||
return
|
return
|
||||||
event.save()
|
event.save()
|
||||||
|
@ -26,6 +26,8 @@ class ImpersonateMiddleware:
|
|||||||
|
|
||||||
if SESSION_IMPERSONATE_USER in request.session:
|
if SESSION_IMPERSONATE_USER in request.session:
|
||||||
request.user = request.session[SESSION_IMPERSONATE_USER]
|
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)
|
return self.get_response(request)
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ from typing import Any, Optional, Type
|
|||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import django.db.models.options as options
|
||||||
|
from deepmerge import always_merger
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||||
@ -41,6 +43,9 @@ GRAVATAR_URL = "https://secure.gravatar.com"
|
|||||||
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
||||||
|
|
||||||
|
|
||||||
|
options.DEFAULT_NAMES = options.DEFAULT_NAMES + ("authentik_used_by_shadows",)
|
||||||
|
|
||||||
|
|
||||||
def default_token_duration():
|
def default_token_duration():
|
||||||
"""Default duration a Token is valid"""
|
"""Default duration a Token is valid"""
|
||||||
return now() + timedelta(minutes=30)
|
return now() + timedelta(minutes=30)
|
||||||
@ -110,8 +115,8 @@ class User(GuardianUserMixin, AbstractUser):
|
|||||||
including the users attributes"""
|
including the users attributes"""
|
||||||
final_attributes = {}
|
final_attributes = {}
|
||||||
for group in self.ak_groups.all().order_by("name"):
|
for group in self.ak_groups.all().order_by("name"):
|
||||||
final_attributes.update(group.attributes)
|
always_merger.merge(final_attributes, group.attributes)
|
||||||
final_attributes.update(self.attributes)
|
always_merger.merge(final_attributes, self.attributes)
|
||||||
return final_attributes
|
return final_attributes
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
@ -138,21 +143,25 @@ class User(GuardianUserMixin, AbstractUser):
|
|||||||
@property
|
@property
|
||||||
def avatar(self) -> str:
|
def avatar(self) -> str:
|
||||||
"""Get avatar, depending on authentik.avatar setting"""
|
"""Get avatar, depending on authentik.avatar setting"""
|
||||||
mode = CONFIG.raw.get("authentik").get("avatars")
|
mode: str = CONFIG.y("avatars", "none")
|
||||||
if mode == "none":
|
if mode == "none":
|
||||||
return DEFAULT_AVATAR
|
return DEFAULT_AVATAR
|
||||||
|
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
||||||
|
mail_hash = md5(self.email.encode("utf-8")).hexdigest() # nosec
|
||||||
if mode == "gravatar":
|
if mode == "gravatar":
|
||||||
parameters = [
|
parameters = [
|
||||||
("s", "158"),
|
("s", "158"),
|
||||||
("r", "g"),
|
("r", "g"),
|
||||||
]
|
]
|
||||||
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
|
||||||
mail_hash = md5(self.email.encode("utf-8")).hexdigest() # nosec
|
|
||||||
gravatar_url = (
|
gravatar_url = (
|
||||||
f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
|
f"{GRAVATAR_URL}/avatar/{mail_hash}?{urlencode(parameters, doseq=True)}"
|
||||||
)
|
)
|
||||||
return escape(gravatar_url)
|
return escape(gravatar_url)
|
||||||
raise ValueError(f"Invalid avatar mode {mode}")
|
return mode % {
|
||||||
|
"username": self.username,
|
||||||
|
"mail_hash": mail_hash,
|
||||||
|
"upn": self.attributes.get("upn", ""),
|
||||||
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -456,7 +465,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
|||||||
from authentik.core.expression import PropertyMappingEvaluator
|
from authentik.core.expression import PropertyMappingEvaluator
|
||||||
|
|
||||||
evaluator = PropertyMappingEvaluator()
|
evaluator = PropertyMappingEvaluator()
|
||||||
evaluator.set_context(user, request, **kwargs)
|
evaluator.set_context(user, request, self, **kwargs)
|
||||||
try:
|
try:
|
||||||
return evaluator.evaluate(self.expression)
|
return evaluator.evaluate(self.expression)
|
||||||
except (ValueError, SyntaxError) as exc:
|
except (ValueError, SyntaxError) as exc:
|
||||||
@ -490,8 +499,12 @@ class AuthenticatedSession(ExpiringModel):
|
|||||||
last_used = models.DateTimeField(auto_now=True)
|
last_used = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_request(request: HttpRequest, user: User) -> "AuthenticatedSession":
|
def from_request(
|
||||||
|
request: HttpRequest, user: User
|
||||||
|
) -> Optional["AuthenticatedSession"]:
|
||||||
"""Create a new session from a http request"""
|
"""Create a new session from a http request"""
|
||||||
|
if not hasattr(request, "session") or not request.session.session_key:
|
||||||
|
return None
|
||||||
return AuthenticatedSession(
|
return AuthenticatedSession(
|
||||||
session_key=request.session.session_key,
|
session_key=request.session.session_key,
|
||||||
user=user,
|
user=user,
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
"""authentik core signals"""
|
"""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.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.cache import cache
|
||||||
from django.core.signals import Signal
|
from django.core.signals import Signal
|
||||||
from django.db.models import Model
|
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.dispatch import receiver
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from prometheus_client import Gauge
|
from prometheus_client import Gauge
|
||||||
@ -18,7 +19,7 @@ GAUGE_MODELS = Gauge(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.core.models import User
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
@receiver(post_save)
|
||||||
@ -48,7 +49,9 @@ def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
|
|||||||
"""Create an AuthenticatedSession from request"""
|
"""Create an AuthenticatedSession from request"""
|
||||||
from authentik.core.models import AuthenticatedSession
|
from authentik.core.models import AuthenticatedSession
|
||||||
|
|
||||||
AuthenticatedSession.from_request(request, user).save()
|
session = AuthenticatedSession.from_request(request, user)
|
||||||
|
if session:
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
@receiver(user_logged_out)
|
||||||
@ -60,3 +63,17 @@ def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
|
|||||||
AuthenticatedSession.objects.filter(
|
AuthenticatedSession.objects.filter(
|
||||||
session_key=request.session.session_key
|
session_key=request.session.session_key
|
||||||
).delete()
|
).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.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
from authentik.policies.utils import delete_none_keys
|
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.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
|
|
||||||
@ -182,6 +183,8 @@ class SourceFlowManager:
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
|
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
|
||||||
"""Hook to override stages which are appended to the flow"""
|
"""Hook to override stages which are appended to the flow"""
|
||||||
|
if not self.source.enrollment_flow:
|
||||||
|
return []
|
||||||
if flow.slug == self.source.enrollment_flow.slug:
|
if flow.slug == self.source.enrollment_flow.slug:
|
||||||
return [
|
return [
|
||||||
in_memory_stage(PostUserEnrollmentStage),
|
in_memory_stage(PostUserEnrollmentStage),
|
||||||
@ -198,7 +201,7 @@ class SourceFlowManager:
|
|||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
# Since we authenticate the user by their token, they have no backend set
|
# 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_SSO: True,
|
||||||
PLAN_CONTEXT_SOURCE: self.source,
|
PLAN_CONTEXT_SOURCE: self.source,
|
||||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||||
|
@ -3,16 +3,6 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% 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 %}
|
{% block title %}
|
||||||
{% trans 'End session' %} - {{ tenant.branding_title }}
|
{% trans 'End session' %} - {{ tenant.branding_title }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -11,6 +11,11 @@
|
|||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script>
|
<script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script>
|
||||||
|
<style>
|
||||||
|
.pf-c-background-image::before {
|
||||||
|
background-image: url("{{ flow.background_url }}");
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
@ -7,6 +7,14 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
.pf-c-background-image::before {
|
||||||
|
background-image: url("/static/dist/assets/images/flow_background.jpg");
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="pf-c-background-image">
|
<div class="pf-c-background-image">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
|
||||||
|
@ -26,7 +26,7 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
def test_check_access(self):
|
def test_check_access(self):
|
||||||
"""Test check_access operation"""
|
"""Test check_access operation"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
response = self.client.post(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_api:application-check-access",
|
"authentik_api:application-check-access",
|
||||||
kwargs={"slug": self.allowed.slug},
|
kwargs={"slug": self.allowed.slug},
|
||||||
@ -36,7 +36,7 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
force_str(response.content), {"messages": [], "passing": True}
|
force_str(response.content), {"messages": [], "passing": True}
|
||||||
)
|
)
|
||||||
response = self.client.post(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_api:application-check-access",
|
"authentik_api:application-check-access",
|
||||||
kwargs={"slug": self.denied.slug},
|
kwargs={"slug": self.denied.slug},
|
||||||
|
@ -17,6 +17,9 @@ class TestImpersonation(TestCase):
|
|||||||
|
|
||||||
def test_impersonate_simple(self):
|
def test_impersonate_simple(self):
|
||||||
"""test simple impersonation and un-impersonation"""
|
"""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.force_login(self.akadmin)
|
||||||
|
|
||||||
self.client.get(
|
self.client.get(
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from rest_framework.fields import CharField, DictField
|
from rest_framework.fields import CharField
|
||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.flows.challenge import Challenge
|
from authentik.flows.challenge import Challenge
|
||||||
@ -22,18 +22,10 @@ class UILoginButton:
|
|||||||
icon_url: Optional[str] = None
|
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):
|
class UserSettingSerializer(PassiveSerializer):
|
||||||
"""Serializer for User settings for stages and sources"""
|
"""Serializer for User settings for stages and sources"""
|
||||||
|
|
||||||
object_uid = CharField()
|
object_uid = CharField()
|
||||||
component = CharField()
|
component = CharField()
|
||||||
title = CharField()
|
title = CharField()
|
||||||
configure_url = CharField()
|
configure_url = CharField(required=False)
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
"""Crypto API Views"""
|
"""Crypto API Views"""
|
||||||
import django_filters
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
from cryptography.x509 import load_pem_x509_certificate
|
from cryptography.x509 import load_pem_x509_certificate
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
@ -20,6 +22,7 @@ from rest_framework.serializers import ModelSerializer, ValidationError
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.crypto.builder import CertificateBuilder
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
@ -33,6 +36,9 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||||||
cert_subject = SerializerMethodField()
|
cert_subject = SerializerMethodField()
|
||||||
private_key_available = SerializerMethodField()
|
private_key_available = SerializerMethodField()
|
||||||
|
|
||||||
|
certificate_download_url = SerializerMethodField()
|
||||||
|
private_key_download_url = SerializerMethodField()
|
||||||
|
|
||||||
def get_cert_subject(self, instance: CertificateKeyPair) -> str:
|
def get_cert_subject(self, instance: CertificateKeyPair) -> str:
|
||||||
"""Get certificate subject as full rfc4514"""
|
"""Get certificate subject as full rfc4514"""
|
||||||
return instance.certificate.subject.rfc4514_string()
|
return instance.certificate.subject.rfc4514_string()
|
||||||
@ -41,6 +47,26 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||||||
"""Show if this keypair has a private key configured or not"""
|
"""Show if this keypair has a private key configured or not"""
|
||||||
return instance.key_data != "" and instance.key_data is not None
|
return instance.key_data != "" and instance.key_data is not None
|
||||||
|
|
||||||
|
def 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:
|
def validate_certificate_data(self, value: str) -> str:
|
||||||
"""Verify that input is a valid PEM x509 Certificate"""
|
"""Verify that input is a valid PEM x509 Certificate"""
|
||||||
try:
|
try:
|
||||||
@ -77,6 +103,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
|||||||
"cert_expiry",
|
"cert_expiry",
|
||||||
"cert_subject",
|
"cert_subject",
|
||||||
"private_key_available",
|
"private_key_available",
|
||||||
|
"certificate_download_url",
|
||||||
|
"private_key_download_url",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"key_data": {"write_only": True},
|
"key_data": {"write_only": True},
|
||||||
@ -100,10 +128,10 @@ class CertificateGenerationSerializer(PassiveSerializer):
|
|||||||
validity_days = IntegerField(initial=365)
|
validity_days = IntegerField(initial=365)
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairFilter(django_filters.FilterSet):
|
class CertificateKeyPairFilter(FilterSet):
|
||||||
"""Filter for certificates"""
|
"""Filter for certificates"""
|
||||||
|
|
||||||
has_key = django_filters.BooleanFilter(
|
has_key = BooleanFilter(
|
||||||
label="Only return certificate-key pairs with keys", method="filter_has_key"
|
label="Only return certificate-key pairs with keys", method="filter_has_key"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -117,7 +145,7 @@ class CertificateKeyPairFilter(django_filters.FilterSet):
|
|||||||
fields = ["name"]
|
fields = ["name"]
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairViewSet(ModelViewSet):
|
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""CertificateKeyPair Viewset"""
|
"""CertificateKeyPair Viewset"""
|
||||||
|
|
||||||
queryset = CertificateKeyPair.objects.all()
|
queryset = CertificateKeyPair.objects.all()
|
||||||
|
@ -55,11 +55,16 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
|||||||
def private_key(self) -> Optional[RSAPrivateKey]:
|
def private_key(self) -> Optional[RSAPrivateKey]:
|
||||||
"""Get python cryptography PrivateKey instance"""
|
"""Get python cryptography PrivateKey instance"""
|
||||||
if not self._private_key and self._private_key != "":
|
if not self._private_key and self._private_key != "":
|
||||||
|
try:
|
||||||
self._private_key = load_pem_private_key(
|
self._private_key = load_pem_private_key(
|
||||||
str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])),
|
str.encode(
|
||||||
|
"\n".join([x.strip() for x in self.key_data.split("\n")])
|
||||||
|
),
|
||||||
password=None,
|
password=None,
|
||||||
backend=default_backend(),
|
backend=default_backend(),
|
||||||
)
|
)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
return self._private_key
|
return self._private_key
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -4,10 +4,14 @@ import datetime
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import DeleteAction
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.crypto.api import CertificateKeyPairSerializer
|
from authentik.crypto.api import CertificateKeyPairSerializer
|
||||||
from authentik.crypto.builder import CertificateBuilder
|
from authentik.crypto.builder import CertificateBuilder
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
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):
|
class TestCrypto(TestCase):
|
||||||
@ -91,3 +95,35 @@ class TestCrypto(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(200, response.status_code)
|
self.assertEqual(200, response.status_code)
|
||||||
self.assertIn("Content-Disposition", response)
|
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",
|
"client_ip",
|
||||||
"created",
|
"created",
|
||||||
"expires",
|
"expires",
|
||||||
|
"tenant",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -76,6 +77,11 @@ class EventsFilter(django_filters.FilterSet):
|
|||||||
field_name="action",
|
field_name="action",
|
||||||
lookup_expr="icontains",
|
lookup_expr="icontains",
|
||||||
)
|
)
|
||||||
|
tenant_name = django_filters.CharFilter(
|
||||||
|
field_name="tenant",
|
||||||
|
lookup_expr="name",
|
||||||
|
label="Tenant name",
|
||||||
|
)
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def filter_context_model_pk(self, queryset, name, value):
|
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 rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
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.api.event import EventSerializer
|
||||||
from authentik.events.models import Notification
|
from authentik.events.models import Notification
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ class NotificationViewSet(
|
|||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
mixins.UpdateModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
|
@ -3,6 +3,7 @@ from rest_framework.serializers import ModelSerializer
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.groups import GroupSerializer
|
from authentik.core.api.groups import GroupSerializer
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.events.models import NotificationRule
|
from authentik.events.models import NotificationRule
|
||||||
|
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ class NotificationRuleSerializer(ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class NotificationRuleViewSet(ModelViewSet):
|
class NotificationRuleViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""NotificationRule Viewset"""
|
"""NotificationRule Viewset"""
|
||||||
|
|
||||||
queryset = NotificationRule.objects.all()
|
queryset = NotificationRule.objects.all()
|
||||||
|
@ -9,6 +9,7 @@ from rest_framework.serializers import ModelSerializer, Serializer
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.events.models import (
|
from authentik.events.models import (
|
||||||
Notification,
|
Notification,
|
||||||
NotificationSeverity,
|
NotificationSeverity,
|
||||||
@ -52,7 +53,7 @@ class NotificationTransportTestSerializer(Serializer):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportViewSet(ModelViewSet):
|
class NotificationTransportViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""NotificationTransport Viewset"""
|
"""NotificationTransport Viewset"""
|
||||||
|
|
||||||
queryset = NotificationTransport.objects.all()
|
queryset = NotificationTransport.objects.all()
|
||||||
|
@ -40,9 +40,9 @@ class GeoIPReader:
|
|||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
reader = Reader(path)
|
reader = Reader(path)
|
||||||
LOGGER.info("Loaded GeoIP database")
|
|
||||||
self.__reader = reader
|
self.__reader = reader
|
||||||
self.__last_mtime = stat(path).st_mtime
|
self.__last_mtime = stat(path).st_mtime
|
||||||
|
LOGGER.info("Loaded GeoIP database", last_write=self.__last_mtime)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
LOGGER.warning("Failed to load GeoIP database", exc=exc)
|
LOGGER.warning("Failed to load GeoIP database", exc=exc)
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.http import HttpRequest, HttpResponse
|
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.models import Event, EventAction, Notification
|
||||||
from authentik.events.signals import EventNewThread
|
from authentik.events.signals import EventNewThread
|
||||||
from authentik.events.utils import model_to_dict
|
from authentik.events.utils import model_to_dict
|
||||||
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
|
|
||||||
|
|
||||||
class AuditMiddleware:
|
class AuditMiddleware:
|
||||||
@ -54,10 +56,19 @@ class AuditMiddleware:
|
|||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def process_exception(self, request: HttpRequest, exception: Exception):
|
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"])
|
post_save.disconnect(dispatch_uid=LOCAL.authentik["request_id"])
|
||||||
pre_delete.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
|
@staticmethod
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def post_save_handler(
|
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.core.models import ExpiringModel, Group, User
|
||||||
from authentik.events.geo import GEOIP_READER
|
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.sentry import SentryIgnoredException
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
|
from authentik.tenants.utils import DEFAULT_TENANT
|
||||||
|
|
||||||
LOGGER = get_logger("authentik.events")
|
LOGGER = get_logger("authentik.events")
|
||||||
GAUGE_EVENTS = Gauge(
|
GAUGE_EVENTS = Gauge(
|
||||||
@ -40,6 +41,11 @@ def default_event_duration():
|
|||||||
return now() + timedelta(days=365)
|
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):
|
class NotificationTransportError(SentryIgnoredException):
|
||||||
"""Error raised when a notification fails to be delivered"""
|
"""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_EXECUTION = "system_task_execution"
|
||||||
SYSTEM_TASK_EXCEPTION = "system_task_exception"
|
SYSTEM_TASK_EXCEPTION = "system_task_exception"
|
||||||
|
SYSTEM_EXCEPTION = "system_exception"
|
||||||
|
|
||||||
CONFIGURATION_ERROR = "configuration_error"
|
CONFIGURATION_ERROR = "configuration_error"
|
||||||
|
|
||||||
@ -94,6 +101,7 @@ class Event(ExpiringModel):
|
|||||||
context = models.JSONField(default=dict, blank=True)
|
context = models.JSONField(default=dict, blank=True)
|
||||||
client_ip = models.GenericIPAddressField(null=True)
|
client_ip = models.GenericIPAddressField(null=True)
|
||||||
created = models.DateTimeField(auto_now_add=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
|
# Shadow the expires attribute from ExpiringModel to override the default duration
|
||||||
expires = models.DateTimeField(default=default_event_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
|
"""Add data from a Django-HttpRequest, allowing the creation of
|
||||||
Events independently from requests.
|
Events independently from requests.
|
||||||
`user` arguments optionally overrides user 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"):
|
if hasattr(request, "user"):
|
||||||
original_user = None
|
original_user = None
|
||||||
if hasattr(request, "session"):
|
if hasattr(request, "session"):
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.flows.api.stages import StageSerializer
|
from authentik.flows.api.stages import StageSerializer
|
||||||
from authentik.flows.models import FlowStageBinding
|
from authentik.flows.models import FlowStageBinding
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ class FlowStageBindingSerializer(ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class FlowStageBindingViewSet(ModelViewSet):
|
class FlowStageBindingViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""FlowStageBinding Viewset"""
|
"""FlowStageBinding Viewset"""
|
||||||
|
|
||||||
queryset = FlowStageBinding.objects.all()
|
queryset = FlowStageBinding.objects.all()
|
||||||
|
@ -24,6 +24,7 @@ from rest_framework.viewsets import ModelViewSet
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
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.core.api.utils import CacheSerializer, LinkSerializer
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
@ -44,10 +45,16 @@ class FlowSerializer(ModelSerializer):
|
|||||||
|
|
||||||
background = ReadOnlyField(source="background_url")
|
background = ReadOnlyField(source="background_url")
|
||||||
|
|
||||||
|
export_url = SerializerMethodField()
|
||||||
|
|
||||||
def get_cache_count(self, flow: Flow) -> int:
|
def get_cache_count(self, flow: Flow) -> int:
|
||||||
"""Get count of cached flows"""
|
"""Get count of cached flows"""
|
||||||
return len(cache.keys(f"{cache_key(flow)}*"))
|
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:
|
class Meta:
|
||||||
|
|
||||||
model = Flow
|
model = Flow
|
||||||
@ -64,6 +71,7 @@ class FlowSerializer(ModelSerializer):
|
|||||||
"cache_count",
|
"cache_count",
|
||||||
"policy_engine_mode",
|
"policy_engine_mode",
|
||||||
"compatibility_mode",
|
"compatibility_mode",
|
||||||
|
"export_url",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"background": {"read_only": True},
|
"background": {"read_only": True},
|
||||||
@ -94,7 +102,7 @@ class DiagramElement:
|
|||||||
return f"{self.identifier}=>{self.type}: {self.rest}"
|
return f"{self.identifier}=>{self.type}: {self.rest}"
|
||||||
|
|
||||||
|
|
||||||
class FlowViewSet(ModelViewSet):
|
class FlowViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Flow Viewset"""
|
"""Flow Viewset"""
|
||||||
|
|
||||||
queryset = Flow.objects.all()
|
queryset = Flow.objects.all()
|
||||||
@ -293,10 +301,14 @@ class FlowViewSet(ModelViewSet):
|
|||||||
"""Set Flow background"""
|
"""Set Flow background"""
|
||||||
flow: Flow = self.get_object()
|
flow: Flow = self.get_object()
|
||||||
background = request.FILES.get("file", None)
|
background = request.FILES.get("file", None)
|
||||||
clear = request.data.get("clear", False)
|
clear = request.data.get("clear", "false").lower() == "true"
|
||||||
if clear:
|
if clear:
|
||||||
|
if flow.background_url.startswith("/media"):
|
||||||
# .delete() saves the model by default
|
# .delete() saves the model by default
|
||||||
flow.background.delete()
|
flow.background.delete()
|
||||||
|
else:
|
||||||
|
flow.background = None
|
||||||
|
flow.save()
|
||||||
return Response({})
|
return Response({})
|
||||||
if background:
|
if background:
|
||||||
flow.background = background
|
flow.background = background
|
||||||
|
@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
|||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from structlog.stdlib import get_logger
|
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.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||||
from authentik.core.types import UserSettingSerializer
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.flows.api.flows import FlowSerializer
|
from authentik.flows.api.flows import FlowSerializer
|
||||||
@ -49,6 +50,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
class StageViewSet(
|
class StageViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
@ -91,10 +93,10 @@ class StageViewSet(
|
|||||||
if not user_settings:
|
if not user_settings:
|
||||||
continue
|
continue
|
||||||
user_settings.initial_data["object_uid"] = str(stage.pk)
|
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(
|
user_settings.initial_data["configure_url"] = reverse(
|
||||||
"authentik_flows:configure",
|
"authentik_flows:configure",
|
||||||
kwargs={"stage_uuid": stage.uuid.hex},
|
kwargs={"stage_uuid": stage.pk},
|
||||||
)
|
)
|
||||||
if not user_settings.is_valid():
|
if not user_settings.is_valid():
|
||||||
LOGGER.warning(user_settings.errors)
|
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.flows.models import FlowDesignation
|
||||||
from authentik.stages.identification.models import UserFields
|
from authentik.stages.identification.models import UserFields
|
||||||
|
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
|
||||||
|
|
||||||
|
|
||||||
def create_default_authentication_flow(
|
def create_default_authentication_flow(
|
||||||
@ -31,7 +32,7 @@ def create_default_authentication_flow(
|
|||||||
|
|
||||||
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
||||||
name="default-authentication-password",
|
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(
|
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"
|
# by injecting "pending_user"
|
||||||
akadmin = ak_user_by(username="akadmin")
|
akadmin = ak_user_by(username="akadmin")
|
||||||
context["pending_user"] = 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"""
|
return True"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ class Stage(SerializerModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
if hasattr(self, "__in_memory_type"):
|
if hasattr(self, "__in_memory_type"):
|
||||||
return f"In-memory Stage {getattr(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:
|
def in_memory_stage(view: Type["StageView"]) -> Stage:
|
||||||
@ -212,7 +212,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
|||||||
return FlowStageBindingSerializer
|
return FlowStageBindingSerializer
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.target} #{self.order}"
|
return f"Flow-stage binding #{self.order} to {self.target}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ from authentik.events.models import cleanse_dict
|
|||||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||||
from authentik.flows.models import Flow, FlowStageBinding, Stage
|
from authentik.flows.models import Flow, FlowStageBinding, Stage
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.root.monitoring import UpdatingGauge
|
from authentik.root.monitoring import UpdatingGauge
|
||||||
|
|
||||||
@ -33,6 +34,7 @@ HIST_FLOWS_PLAN_TIME = Histogram(
|
|||||||
"Duration to build a plan for a flow",
|
"Duration to build a plan for a flow",
|
||||||
["flow_slug"],
|
["flow_slug"],
|
||||||
)
|
)
|
||||||
|
CACHE_TIMEOUT = int(CONFIG.y("redis.cache_timeout_flows"))
|
||||||
|
|
||||||
|
|
||||||
def cache_key(flow: Flow, user: Optional[User] = None) -> str:
|
def cache_key(flow: Flow, user: Optional[User] = None) -> str:
|
||||||
@ -157,7 +159,7 @@ class FlowPlanner:
|
|||||||
"f(plan): building plan",
|
"f(plan): building plan",
|
||||||
)
|
)
|
||||||
plan = self._build_plan(user, request, default_context)
|
plan = self._build_plan(user, request, default_context)
|
||||||
cache.set(cache_key(self.flow, user), plan)
|
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
|
||||||
GAUGE_FLOWS_CACHED.update()
|
GAUGE_FLOWS_CACHED.update()
|
||||||
if not plan.stages and not self.allow_empty_flows:
|
if not plan.stages and not self.allow_empty_flows:
|
||||||
raise EmptyFlowException()
|
raise EmptyFlowException()
|
||||||
|
@ -18,27 +18,11 @@ from authentik.flows.challenge import (
|
|||||||
)
|
)
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.views import FlowExecutorView
|
from authentik.flows.views import FlowExecutorView
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
|
||||||
|
|
||||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
|
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class InvalidChallengeError(SentryIgnoredException):
|
|
||||||
"""Error raised when a challenge from a stage is not valid"""
|
|
||||||
|
|
||||||
def __init__(self, errors, stage_view: View, challenge: Challenge) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.errors = errors
|
|
||||||
self.stage_view = stage_view
|
|
||||||
self.challenge = challenge
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return (
|
|
||||||
f"Invalid challenge from {self.stage_view}: {self.errors}\n{self.challenge}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StageView(View):
|
class StageView(View):
|
||||||
"""Abstract Stage, inherits TemplateView but can be combined with FormView"""
|
"""Abstract Stage, inherits TemplateView but can be combined with FormView"""
|
||||||
|
|
||||||
@ -50,14 +34,17 @@ class StageView(View):
|
|||||||
self.executor = executor
|
self.executor = executor
|
||||||
super().__init__(**kwargs)
|
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,
|
"""Either show the matched User object or show what the user entered,
|
||||||
based on what the earlier stage (mostly IdentificationStage) set.
|
based on what the earlier stage (mostly IdentificationStage) set.
|
||||||
_USER_IDENTIFIER overrides the first User, as PENDING_USER is used for
|
_USER_IDENTIFIER overrides the first User, as PENDING_USER is used for
|
||||||
other things besides the form display.
|
other things besides the form display.
|
||||||
|
|
||||||
If no user is pending, returns request.user"""
|
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(
|
return User(
|
||||||
username=self.executor.plan.context.get(
|
username=self.executor.plan.context.get(
|
||||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER
|
PLAN_CONTEXT_PENDING_USER_IDENTIFIER
|
||||||
@ -109,7 +96,7 @@ class ChallengeStageView(StageView):
|
|||||||
# If there's a pending user, update the `username` field
|
# If there's a pending user, update the `username` field
|
||||||
# this field is only used by password managers.
|
# this field is only used by password managers.
|
||||||
# If there's no user set, an error is raised later.
|
# 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"] = user.username
|
||||||
challenge.initial_data["pending_user_avatar"] = DEFAULT_AVATAR
|
challenge.initial_data["pending_user_avatar"] = DEFAULT_AVATAR
|
||||||
if not isinstance(user, AnonymousUser):
|
if not isinstance(user, AnonymousUser):
|
||||||
|
@ -511,4 +511,4 @@ class TestFlowExecutor(TestCase):
|
|||||||
executor.flow = flow
|
executor.flow = flow
|
||||||
|
|
||||||
stage_view = StageView(executor)
|
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)
|
||||||
|
@ -44,6 +44,7 @@ from authentik.flows.planner import (
|
|||||||
FlowPlan,
|
FlowPlan,
|
||||||
FlowPlanner,
|
FlowPlanner,
|
||||||
)
|
)
|
||||||
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
||||||
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
@ -93,6 +94,10 @@ def challenge_response_types():
|
|||||||
return Inner()
|
return Inner()
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidStageError(SentryIgnoredException):
|
||||||
|
"""Error raised when a challenge from a stage is not valid"""
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
@method_decorator(xframe_options_sameorigin, name="dispatch")
|
||||||
class FlowExecutorView(APIView):
|
class FlowExecutorView(APIView):
|
||||||
"""Stage 1 Flow executor, passing requests to Stage Views"""
|
"""Stage 1 Flow executor, passing requests to Stage Views"""
|
||||||
@ -164,17 +169,24 @@ class FlowExecutorView(APIView):
|
|||||||
current_stage=self.current_stage,
|
current_stage=self.current_stage,
|
||||||
flow_slug=self.flow.slug,
|
flow_slug=self.flow.slug,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
stage_cls = self.current_stage.type
|
stage_cls = self.current_stage.type
|
||||||
|
except NotImplementedError as exc:
|
||||||
|
self._logger.debug("Error getting stage type", exc=exc)
|
||||||
|
return self.stage_invalid()
|
||||||
self.current_stage_view = stage_cls(self)
|
self.current_stage_view = stage_cls(self)
|
||||||
self.current_stage_view.args = self.args
|
self.current_stage_view.args = self.args
|
||||||
self.current_stage_view.kwargs = self.kwargs
|
self.current_stage_view.kwargs = self.kwargs
|
||||||
self.current_stage_view.request = request
|
self.current_stage_view.request = request
|
||||||
|
try:
|
||||||
return super().dispatch(request)
|
return super().dispatch(request)
|
||||||
|
except InvalidStageError as exc:
|
||||||
|
return self.stage_invalid(str(exc))
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
responses={
|
responses={
|
||||||
200: PolymorphicProxySerializer(
|
200: PolymorphicProxySerializer(
|
||||||
component_name="FlowChallengeRequest",
|
component_name="ChallengeTypes",
|
||||||
serializers=challenge_types(),
|
serializers=challenge_types(),
|
||||||
resource_type_field_name="component",
|
resource_type_field_name="component",
|
||||||
),
|
),
|
||||||
@ -214,7 +226,7 @@ class FlowExecutorView(APIView):
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
responses={
|
responses={
|
||||||
200: PolymorphicProxySerializer(
|
200: PolymorphicProxySerializer(
|
||||||
component_name="FlowChallengeRequest",
|
component_name="ChallengeTypes",
|
||||||
serializers=challenge_types(),
|
serializers=challenge_types(),
|
||||||
resource_type_field_name="component",
|
resource_type_field_name="component",
|
||||||
),
|
),
|
||||||
@ -353,8 +365,11 @@ class FlowErrorResponse(TemplateResponse):
|
|||||||
context = {}
|
context = {}
|
||||||
context["error"] = self.error
|
context["error"] = self.error
|
||||||
if self._request.user and self._request.user.is_authenticated:
|
if self._request.user and self._request.user.is_authenticated:
|
||||||
if self._request.user.is_superuser or self._request.user.attributes.get(
|
if (
|
||||||
|
self._request.user.is_superuser
|
||||||
|
or self._request.user.group_attributes().get(
|
||||||
USER_ATTRIBUTE_DEBUG, False
|
USER_ATTRIBUTE_DEBUG, False
|
||||||
|
)
|
||||||
):
|
):
|
||||||
context["tb"] = "".join(format_tb(self.error.__traceback__))
|
context["tb"] = "".join(format_tb(self.error.__traceback__))
|
||||||
return context
|
return context
|
||||||
|
@ -62,7 +62,7 @@ class ConfigLoader:
|
|||||||
output.update(kwargs)
|
output.update(kwargs)
|
||||||
print(dumps(output))
|
print(dumps(output))
|
||||||
|
|
||||||
def update(self, root, updatee):
|
def update(self, root: dict[str, Any], updatee: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Recursively update dictionary"""
|
"""Recursively update dictionary"""
|
||||||
for key, value in updatee.items():
|
for key, value in updatee.items():
|
||||||
if isinstance(value, Mapping):
|
if isinstance(value, Mapping):
|
||||||
@ -73,7 +73,7 @@ class ConfigLoader:
|
|||||||
root[key] = value
|
root[key] = value
|
||||||
return root
|
return root
|
||||||
|
|
||||||
def parse_uri(self, value):
|
def parse_uri(self, value: str) -> str:
|
||||||
"""Parse string values which start with a URI"""
|
"""Parse string values which start with a URI"""
|
||||||
url = urlparse(value)
|
url = urlparse(value)
|
||||||
if url.scheme == "env":
|
if url.scheme == "env":
|
||||||
@ -99,7 +99,10 @@ class ConfigLoader:
|
|||||||
raise ImproperlyConfigured from exc
|
raise ImproperlyConfigured from exc
|
||||||
except PermissionError as exc:
|
except PermissionError as exc:
|
||||||
self._log(
|
self._log(
|
||||||
"warning", "Permission denied while reading file", path=path, error=exc
|
"warning",
|
||||||
|
"Permission denied while reading file",
|
||||||
|
path=path,
|
||||||
|
error=str(exc),
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_from_dict(self, update: dict):
|
def update_from_dict(self, update: dict):
|
||||||
|
@ -9,6 +9,7 @@ postgresql:
|
|||||||
web:
|
web:
|
||||||
listen: 0.0.0.0:9000
|
listen: 0.0.0.0:9000
|
||||||
listen_tls: 0.0.0.0:9443
|
listen_tls: 0.0.0.0:9443
|
||||||
|
load_local_files: false
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
@ -16,6 +17,10 @@ redis:
|
|||||||
cache_db: 0
|
cache_db: 0
|
||||||
message_queue_db: 1
|
message_queue_db: 1
|
||||||
ws_db: 2
|
ws_db: 2
|
||||||
|
cache_timeout: 300
|
||||||
|
cache_timeout_flows: 300
|
||||||
|
cache_timeout_policies: 300
|
||||||
|
cache_timeout_reputation: 300
|
||||||
|
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
@ -45,11 +50,11 @@ outposts:
|
|||||||
# %(build_hash)s: Build hash if you're running a beta version
|
# %(build_hash)s: Build hash if you're running a beta version
|
||||||
docker_image_base: "ghcr.io/goauthentik/%(type)s:%(version)s"
|
docker_image_base: "ghcr.io/goauthentik/%(type)s:%(version)s"
|
||||||
|
|
||||||
authentik:
|
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
|
||||||
avatars: gravatar # gravatar or none
|
geoip: "./GeoLite2-City.mmdb"
|
||||||
geoip: "./GeoLite2-City.mmdb"
|
|
||||||
# Optionally add links to the footer on the login page
|
# Can't currently be configured via environment variables, only yaml
|
||||||
footer_links:
|
footer_links:
|
||||||
- name: Documentation
|
- name: Documentation
|
||||||
href: https://goauthentik.io/docs/
|
href: https://goauthentik.io/docs/
|
||||||
- name: authentik Website
|
- name: authentik Website
|
||||||
|
61
authentik/lib/tests/test_config.py
Normal file
61
authentik/lib/tests/test_config.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""Test config loader"""
|
||||||
|
from os import chmod, environ, unlink, write
|
||||||
|
from tempfile import mkstemp
|
||||||
|
|
||||||
|
from django.conf import ImproperlyConfigured
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.lib.config import ENV_PREFIX, ConfigLoader
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfig(TestCase):
|
||||||
|
"""Test config loader"""
|
||||||
|
|
||||||
|
def test_env(self):
|
||||||
|
"""Test simple instance"""
|
||||||
|
config = ConfigLoader()
|
||||||
|
environ[ENV_PREFIX + "_test__test"] = "bar"
|
||||||
|
config.update_from_env()
|
||||||
|
self.assertEqual(config.y("test.test"), "bar")
|
||||||
|
|
||||||
|
def test_patch(self):
|
||||||
|
"""Test patch decorator"""
|
||||||
|
config = ConfigLoader()
|
||||||
|
config.y_set("foo.bar", "bar")
|
||||||
|
self.assertEqual(config.y("foo.bar"), "bar")
|
||||||
|
with config.patch("foo.bar", "baz"):
|
||||||
|
self.assertEqual(config.y("foo.bar"), "baz")
|
||||||
|
self.assertEqual(config.y("foo.bar"), "bar")
|
||||||
|
|
||||||
|
def test_uri_env(self):
|
||||||
|
"""Test URI parsing (environment)"""
|
||||||
|
config = ConfigLoader()
|
||||||
|
environ["foo"] = "bar"
|
||||||
|
self.assertEqual(config.parse_uri("env://foo"), "bar")
|
||||||
|
self.assertEqual(config.parse_uri("env://fo?bar"), "bar")
|
||||||
|
|
||||||
|
def test_uri_file(self):
|
||||||
|
"""Test URI parsing (file load)"""
|
||||||
|
config = ConfigLoader()
|
||||||
|
file, file_name = mkstemp()
|
||||||
|
write(file, "foo".encode())
|
||||||
|
_, file2_name = mkstemp()
|
||||||
|
chmod(file2_name, 0o000) # Remove all permissions so we can't read the file
|
||||||
|
self.assertEqual(config.parse_uri(f"file://{file_name}"), "foo")
|
||||||
|
self.assertEqual(config.parse_uri(f"file://{file2_name}?def"), "def")
|
||||||
|
unlink(file_name)
|
||||||
|
unlink(file2_name)
|
||||||
|
|
||||||
|
def test_file_update(self):
|
||||||
|
"""Test update_from_file"""
|
||||||
|
config = ConfigLoader()
|
||||||
|
file, file_name = mkstemp()
|
||||||
|
write(file, "{".encode())
|
||||||
|
file2, file2_name = mkstemp()
|
||||||
|
write(file2, "{".encode())
|
||||||
|
chmod(file2_name, 0o000) # Remove all permissions so we can't read the file
|
||||||
|
with self.assertRaises(ImproperlyConfigured):
|
||||||
|
config.update_from_file(file_name)
|
||||||
|
config.update_from_file(file2_name)
|
||||||
|
unlink(file_name)
|
||||||
|
unlink(file2_name)
|
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)
|
@ -33,7 +33,7 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
|
|||||||
return None
|
return None
|
||||||
if OUTPOST_REMOTE_IP_HEADER not in request.META:
|
if OUTPOST_REMOTE_IP_HEADER not in request.META:
|
||||||
return None
|
return None
|
||||||
if request.user.attributes.get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
if request.user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
||||||
return None
|
return None
|
||||||
return request.META[OUTPOST_REMOTE_IP_HEADER]
|
return request.META[OUTPOST_REMOTE_IP_HEADER]
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from rest_framework.serializers import JSONField, ModelSerializer, ValidationErr
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
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.api.utils import PassiveSerializer, is_dict
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
|
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
|
||||||
@ -95,7 +96,7 @@ class OutpostHealthSerializer(PassiveSerializer):
|
|||||||
version_outdated = BooleanField(read_only=True)
|
version_outdated = BooleanField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
class OutpostViewSet(ModelViewSet):
|
class OutpostViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Outpost Viewset"""
|
"""Outpost Viewset"""
|
||||||
|
|
||||||
queryset = Outpost.objects.all()
|
queryset = Outpost.objects.all()
|
||||||
|
@ -14,6 +14,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import (
|
from authentik.core.api.utils import (
|
||||||
MetaNameSerializer,
|
MetaNameSerializer,
|
||||||
PassiveSerializer,
|
PassiveSerializer,
|
||||||
@ -32,6 +33,13 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
|
|
||||||
component = ReadOnlyField()
|
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:
|
class Meta:
|
||||||
|
|
||||||
model = OutpostServiceConnection
|
model = OutpostServiceConnection
|
||||||
@ -55,6 +63,7 @@ class ServiceConnectionStateSerializer(PassiveSerializer):
|
|||||||
class ServiceConnectionViewSet(
|
class ServiceConnectionViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
@ -105,7 +114,7 @@ class DockerServiceConnectionSerializer(ServiceConnectionSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class DockerServiceConnectionViewSet(ModelViewSet):
|
class DockerServiceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""DockerServiceConnection Viewset"""
|
"""DockerServiceConnection Viewset"""
|
||||||
|
|
||||||
queryset = DockerServiceConnection.objects.all()
|
queryset = DockerServiceConnection.objects.all()
|
||||||
@ -139,7 +148,7 @@ class KubernetesServiceConnectionSerializer(ServiceConnectionSerializer):
|
|||||||
fields = ServiceConnectionSerializer.Meta.fields + ["kubeconfig"]
|
fields = ServiceConnectionSerializer.Meta.fields + ["kubeconfig"]
|
||||||
|
|
||||||
|
|
||||||
class KubernetesServiceConnectionViewSet(ModelViewSet):
|
class KubernetesServiceConnectionViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""KubernetesServiceConnection Viewset"""
|
"""KubernetesServiceConnection Viewset"""
|
||||||
|
|
||||||
queryset = KubernetesServiceConnection.objects.all()
|
queryset = KubernetesServiceConnection.objects.all()
|
||||||
|
@ -67,11 +67,6 @@ class OutpostConsumer(AuthJsonConsumer):
|
|||||||
self.accept()
|
self.accept()
|
||||||
self.outpost = outpost.first()
|
self.outpost = outpost.first()
|
||||||
self.last_uid = self.channel_name
|
self.last_uid = self.channel_name
|
||||||
LOGGER.debug(
|
|
||||||
"added outpost instace to cache",
|
|
||||||
outpost=self.outpost,
|
|
||||||
channel_name=self.channel_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def disconnect(self, close_code):
|
def disconnect(self, close_code):
|
||||||
@ -108,6 +103,11 @@ class OutpostConsumer(AuthJsonConsumer):
|
|||||||
outpost=self.outpost.name,
|
outpost=self.outpost.name,
|
||||||
uid=self.last_uid,
|
uid=self.last_uid,
|
||||||
).inc()
|
).inc()
|
||||||
|
LOGGER.debug(
|
||||||
|
"added outpost instace to cache",
|
||||||
|
outpost=self.outpost,
|
||||||
|
instance_uuid=self.last_uid,
|
||||||
|
)
|
||||||
self.first_msg = True
|
self.first_msg = True
|
||||||
|
|
||||||
if msg.instruction == WebsocketMessageInstruction.HELLO:
|
if msg.instruction == WebsocketMessageInstruction.HELLO:
|
||||||
|
@ -63,10 +63,10 @@ class DockerController(BaseController):
|
|||||||
self.client.images.pull(image_name)
|
self.client.images.pull(image_name)
|
||||||
container_args = {
|
container_args = {
|
||||||
"image": image_name,
|
"image": image_name,
|
||||||
"name": f"authentik-proxy-{self.outpost.uuid.hex}",
|
"name": container_name,
|
||||||
"detach": True,
|
"detach": True,
|
||||||
"ports": {
|
"ports": {
|
||||||
f"{port.port}/{port.protocol.lower()}": port.inner_port or port.port
|
f"{port.inner_port or port.port}/{port.protocol.lower()}": port.port
|
||||||
for port in self.deployment_ports
|
for port in self.deployment_ports
|
||||||
},
|
},
|
||||||
"environment": self._get_env(),
|
"environment": self._get_env(),
|
||||||
|
@ -8,7 +8,7 @@ from uuid import uuid4
|
|||||||
from dacite import from_dict
|
from dacite import from_dict
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import models, transaction
|
from django.db import IntegrityError, models, transaction
|
||||||
from django.db.models.base import Model
|
from django.db.models.base import Model
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from docker.client import DockerClient
|
from docker.client import DockerClient
|
||||||
@ -50,6 +50,8 @@ class ServiceConnectionInvalid(SentryIgnoredException):
|
|||||||
class OutpostConfig:
|
class OutpostConfig:
|
||||||
"""Configuration an outpost uses to configure it self"""
|
"""Configuration an outpost uses to configure it self"""
|
||||||
|
|
||||||
|
# update website/docs/outposts/outposts.md
|
||||||
|
|
||||||
authentik_host: str
|
authentik_host: str
|
||||||
authentik_host_insecure: bool = False
|
authentik_host_insecure: bool = False
|
||||||
|
|
||||||
@ -141,7 +143,9 @@ class OutpostServiceConnection(models.Model):
|
|||||||
@property
|
@property
|
||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
"""Return component used to edit this object"""
|
"""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:
|
class Meta:
|
||||||
|
|
||||||
@ -380,13 +384,11 @@ class Outpost(models.Model):
|
|||||||
tokens = Token.filter_not_expired(
|
tokens = Token.filter_not_expired(
|
||||||
identifier=self.token_identifier,
|
identifier=self.token_identifier,
|
||||||
intent=TokenIntents.INTENT_API,
|
intent=TokenIntents.INTENT_API,
|
||||||
|
managed=managed,
|
||||||
)
|
)
|
||||||
if tokens.exists():
|
if tokens.exists():
|
||||||
token = tokens.first()
|
return tokens.first()
|
||||||
if not token.managed:
|
try:
|
||||||
token.managed = managed
|
|
||||||
token.save()
|
|
||||||
return token
|
|
||||||
return Token.objects.create(
|
return Token.objects.create(
|
||||||
user=self.user,
|
user=self.user,
|
||||||
identifier=self.token_identifier,
|
identifier=self.token_identifier,
|
||||||
@ -395,6 +397,11 @@ class Outpost(models.Model):
|
|||||||
expiring=False,
|
expiring=False,
|
||||||
managed=managed,
|
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]]:
|
def get_required_objects(self) -> Iterable[Union[models.Model, str]]:
|
||||||
"""Get an iterator of all objects the user needs read access to"""
|
"""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 structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.api.groups import GroupSerializer
|
from authentik.core.api.groups import GroupSerializer
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
from authentik.policies.api.policies import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.models import PolicyBinding, PolicyBindingModel
|
from authentik.policies.models import PolicyBinding, PolicyBindingModel
|
||||||
@ -99,7 +100,7 @@ class PolicyBindingSerializer(ModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingViewSet(ModelViewSet):
|
class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""PolicyBinding Viewset"""
|
"""PolicyBinding Viewset"""
|
||||||
|
|
||||||
queryset = (
|
queryset = (
|
||||||
|
@ -14,6 +14,7 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.applications import user_app_cache_key
|
from authentik.core.api.applications import user_app_cache_key
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import (
|
from authentik.core.api.utils import (
|
||||||
CacheSerializer,
|
CacheSerializer,
|
||||||
MetaNameSerializer,
|
MetaNameSerializer,
|
||||||
@ -79,6 +80,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
class PolicyViewSet(
|
class PolicyViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
|
@ -37,7 +37,9 @@ class AccessDeniedResponse(TemplateResponse):
|
|||||||
if self._request.user and self._request.user.is_authenticated:
|
if self._request.user and self._request.user.is_authenticated:
|
||||||
if (
|
if (
|
||||||
self._request.user.is_superuser
|
self._request.user.is_superuser
|
||||||
or self._request.user.attributes.get(USER_ATTRIBUTE_DEBUG, False)
|
or self._request.user.group_attributes().get(
|
||||||
|
USER_ATTRIBUTE_DEBUG, False
|
||||||
|
)
|
||||||
):
|
):
|
||||||
context["policy_result"] = self.policy_result
|
context["policy_result"] = self.policy_result
|
||||||
return context
|
return context
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Dummy Policy API Views"""
|
"""Dummy Policy API Views"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.policies.api.policies import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ class DummyPolicySerializer(PolicySerializer):
|
|||||||
fields = PolicySerializer.Meta.fields + ["result", "wait_min", "wait_max"]
|
fields = PolicySerializer.Meta.fields + ["result", "wait_min", "wait_max"]
|
||||||
|
|
||||||
|
|
||||||
class DummyPolicyViewSet(ModelViewSet):
|
class DummyPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Dummy Viewset"""
|
"""Dummy Viewset"""
|
||||||
|
|
||||||
queryset = DummyPolicy.objects.all()
|
queryset = DummyPolicy.objects.all()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Event Matcher Policy API"""
|
"""Event Matcher Policy API"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.policies.api.policies import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.event_matcher.models import EventMatcherPolicy
|
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"""
|
"""Event Matcher Policy Viewset"""
|
||||||
|
|
||||||
queryset = EventMatcherPolicy.objects.all()
|
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"""
|
"""Password Expiry Policy API Views"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.policies.api.policies import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.expiry.models import PasswordExpiryPolicy
|
from authentik.policies.expiry.models import PasswordExpiryPolicy
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ class PasswordExpiryPolicySerializer(PolicySerializer):
|
|||||||
fields = PolicySerializer.Meta.fields + ["days", "deny_only"]
|
fields = PolicySerializer.Meta.fields + ["days", "deny_only"]
|
||||||
|
|
||||||
|
|
||||||
class PasswordExpiryPolicyViewSet(ModelViewSet):
|
class PasswordExpiryPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Password Expiry Viewset"""
|
"""Password Expiry Viewset"""
|
||||||
|
|
||||||
queryset = PasswordExpiryPolicy.objects.all()
|
queryset = PasswordExpiryPolicy.objects.all()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Expression Policy API"""
|
"""Expression Policy API"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.policies.api.policies import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.expression.evaluator import PolicyEvaluator
|
from authentik.policies.expression.evaluator import PolicyEvaluator
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
@ -20,7 +21,7 @@ class ExpressionPolicySerializer(PolicySerializer):
|
|||||||
fields = PolicySerializer.Meta.fields + ["expression"]
|
fields = PolicySerializer.Meta.fields + ["expression"]
|
||||||
|
|
||||||
|
|
||||||
class ExpressionPolicyViewSet(ModelViewSet):
|
class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Source Viewset"""
|
"""Source Viewset"""
|
||||||
|
|
||||||
queryset = ExpressionPolicy.objects.all()
|
queryset = ExpressionPolicy.objects.all()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Source API Views"""
|
"""Source API Views"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.policies.api.policies import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.hibp.models import HaveIBeenPwendPolicy
|
from authentik.policies.hibp.models import HaveIBeenPwendPolicy
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ class HaveIBeenPwendPolicySerializer(PolicySerializer):
|
|||||||
fields = PolicySerializer.Meta.fields + ["password_field", "allowed_count"]
|
fields = PolicySerializer.Meta.fields + ["password_field", "allowed_count"]
|
||||||
|
|
||||||
|
|
||||||
class HaveIBeenPwendPolicyViewSet(ModelViewSet):
|
class HaveIBeenPwendPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Source Viewset"""
|
"""Source Viewset"""
|
||||||
|
|
||||||
queryset = HaveIBeenPwendPolicy.objects.all()
|
queryset = HaveIBeenPwendPolicy.objects.all()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Password Policy API Views"""
|
"""Password Policy API Views"""
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.policies.api.policies import PolicySerializer
|
from authentik.policies.api.policies import PolicySerializer
|
||||||
from authentik.policies.password.models import PasswordPolicy
|
from authentik.policies.password.models import PasswordPolicy
|
||||||
|
|
||||||
@ -21,7 +22,7 @@ class PasswordPolicySerializer(PolicySerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PasswordPolicyViewSet(ModelViewSet):
|
class PasswordPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Password Policy Viewset"""
|
"""Password Policy Viewset"""
|
||||||
|
|
||||||
queryset = PasswordPolicy.objects.all()
|
queryset = PasswordPolicy.objects.all()
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"""authentik policy task"""
|
"""authentik policy task"""
|
||||||
from multiprocessing import get_context
|
from multiprocessing import get_context
|
||||||
from multiprocessing.connection import Connection
|
from multiprocessing.connection import Connection
|
||||||
from traceback import format_tb
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@ -11,14 +10,16 @@ from sentry_sdk.tracing import Span
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.policies.exceptions import PolicyException
|
from authentik.policies.exceptions import PolicyException
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
TRACEBACK_HEADER = "Traceback (most recent call last):\n"
|
|
||||||
|
|
||||||
FORK_CTX = get_context("fork")
|
FORK_CTX = get_context("fork")
|
||||||
|
CACHE_TIMEOUT = int(CONFIG.y("redis.cache_timeout_policies"))
|
||||||
PROCESS_CLASS = FORK_CTX.Process
|
PROCESS_CLASS = FORK_CTX.Process
|
||||||
HIST_POLICIES_EXECUTION_TIME = Histogram(
|
HIST_POLICIES_EXECUTION_TIME = Histogram(
|
||||||
"authentik_policies_execution_time",
|
"authentik_policies_execution_time",
|
||||||
@ -106,11 +107,7 @@ class PolicyProcess(PROCESS_CLASS):
|
|||||||
except PolicyException as exc:
|
except PolicyException as exc:
|
||||||
# Either use passed original exception or whatever we have
|
# Either use passed original exception or whatever we have
|
||||||
src_exc = exc.src_exc if exc.src_exc else exc
|
src_exc = exc.src_exc if exc.src_exc else exc
|
||||||
error_string = (
|
error_string = exception_to_string(src_exc)
|
||||||
TRACEBACK_HEADER
|
|
||||||
+ "".join(format_tb(src_exc.__traceback__))
|
|
||||||
+ str(src_exc)
|
|
||||||
)
|
|
||||||
# Create policy exception event, only when we're not debugging
|
# Create policy exception event, only when we're not debugging
|
||||||
if not self.request.debug:
|
if not self.request.debug:
|
||||||
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
self.create_event(EventAction.POLICY_EXCEPTION, message=error_string)
|
||||||
@ -119,7 +116,7 @@ class PolicyProcess(PROCESS_CLASS):
|
|||||||
policy_result.source_binding = self.binding
|
policy_result.source_binding = self.binding
|
||||||
if not self.request.debug:
|
if not self.request.debug:
|
||||||
key = cache_key(self.binding, self.request)
|
key = cache_key(self.binding, self.request)
|
||||||
cache.set(key, policy_result)
|
cache.set(key, policy_result, CACHE_TIMEOUT)
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"P_ENG(proc): finished and cached ",
|
"P_ENG(proc): finished and cached ",
|
||||||
policy=self.binding.policy,
|
policy=self.binding.policy,
|
||||||
|
@ -3,6 +3,7 @@ from rest_framework import mixins
|
|||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
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.api.policies import PolicySerializer
|
||||||
from authentik.policies.reputation.models import (
|
from authentik.policies.reputation.models import (
|
||||||
IPReputation,
|
IPReputation,
|
||||||
@ -23,7 +24,7 @@ class ReputationPolicySerializer(PolicySerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ReputationPolicyViewSet(ModelViewSet):
|
class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Reputation Policy Viewset"""
|
"""Reputation Policy Viewset"""
|
||||||
|
|
||||||
queryset = ReputationPolicy.objects.all()
|
queryset = ReputationPolicy.objects.all()
|
||||||
@ -46,6 +47,7 @@ class IPReputationSerializer(ModelSerializer):
|
|||||||
class IPReputationViewSet(
|
class IPReputationViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
@ -74,6 +76,7 @@ class UserReputationSerializer(ModelSerializer):
|
|||||||
class UserReputationViewSet(
|
class UserReputationViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
|
@ -3,11 +3,13 @@ from django.core.cache import cache
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
from structlog import get_logger
|
||||||
|
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.models import Policy
|
from authentik.policies.models import Policy
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
CACHE_KEY_IP_PREFIX = "authentik_reputation_ip_"
|
CACHE_KEY_IP_PREFIX = "authentik_reputation_ip_"
|
||||||
CACHE_KEY_USER_PREFIX = "authentik_reputation_user_"
|
CACHE_KEY_USER_PREFIX = "authentik_reputation_user_"
|
||||||
|
|
||||||
@ -35,9 +37,16 @@ class ReputationPolicy(Policy):
|
|||||||
if self.check_ip:
|
if self.check_ip:
|
||||||
score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
|
score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
|
||||||
passing = passing and score <= self.threshold
|
passing = passing and score <= self.threshold
|
||||||
|
LOGGER.debug("Score for IP", ip=remote_ip, score=score, passing=passing)
|
||||||
if self.check_username:
|
if self.check_username:
|
||||||
score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0)
|
score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0)
|
||||||
passing = passing and score <= self.threshold
|
passing = passing and score <= self.threshold
|
||||||
|
LOGGER.debug(
|
||||||
|
"Score for Username",
|
||||||
|
username=request.user.username,
|
||||||
|
score=score,
|
||||||
|
passing=passing,
|
||||||
|
)
|
||||||
return PolicyResult(passing)
|
return PolicyResult(passing)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -5,6 +5,7 @@ from django.dispatch import receiver
|
|||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.reputation.models import (
|
from authentik.policies.reputation.models import (
|
||||||
CACHE_KEY_IP_PREFIX,
|
CACHE_KEY_IP_PREFIX,
|
||||||
@ -13,6 +14,7 @@ from authentik.policies.reputation.models import (
|
|||||||
from authentik.stages.identification.signals import identification_failed
|
from authentik.stages.identification.signals import identification_failed
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
CACHE_TIMEOUT = int(CONFIG.y("redis.cache_timeout_reputation"))
|
||||||
|
|
||||||
|
|
||||||
def update_score(request: HttpRequest, username: str, amount: int):
|
def update_score(request: HttpRequest, username: str, amount: int):
|
||||||
@ -20,10 +22,10 @@ def update_score(request: HttpRequest, username: str, amount: int):
|
|||||||
remote_ip = get_client_ip(request)
|
remote_ip = get_client_ip(request)
|
||||||
|
|
||||||
# We only update the cache here, as its faster than writing to the DB
|
# We only update the cache here, as its faster than writing to the DB
|
||||||
cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
|
cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0, CACHE_TIMEOUT)
|
||||||
cache.incr(CACHE_KEY_IP_PREFIX + remote_ip, amount)
|
cache.incr(CACHE_KEY_IP_PREFIX + remote_ip, amount)
|
||||||
|
|
||||||
cache.get_or_set(CACHE_KEY_USER_PREFIX + username, 0)
|
cache.get_or_set(CACHE_KEY_USER_PREFIX + username, 0, CACHE_TIMEOUT)
|
||||||
cache.incr(CACHE_KEY_USER_PREFIX + username, amount)
|
cache.incr(CACHE_KEY_USER_PREFIX + username, amount)
|
||||||
|
|
||||||
LOGGER.debug("Updated score", amount=amount, for_user=username, for_ip=remote_ip)
|
LOGGER.debug("Updated score", amount=amount, for_user=username, for_ip=remote_ip)
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
"""test reputation signals and policy"""
|
"""test reputation signals and policy"""
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.core.cache import cache
|
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.core.models import User
|
||||||
|
from authentik.lib.utils.http import DEFAULT_IP
|
||||||
from authentik.policies.reputation.models import (
|
from authentik.policies.reputation.models import (
|
||||||
CACHE_KEY_IP_PREFIX,
|
CACHE_KEY_IP_PREFIX,
|
||||||
CACHE_KEY_USER_PREFIX,
|
CACHE_KEY_USER_PREFIX,
|
||||||
@ -19,9 +20,12 @@ class TestReputationPolicy(TestCase):
|
|||||||
"""test reputation signals and policy"""
|
"""test reputation signals and policy"""
|
||||||
|
|
||||||
def setUp(self):
|
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"
|
self.test_username = "test"
|
||||||
cache.delete(CACHE_KEY_IP_PREFIX + self.test_ip)
|
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)
|
cache.delete(CACHE_KEY_USER_PREFIX + self.test_username)
|
||||||
# We need a user for the one-to-one in userreputation
|
# We need a user for the one-to-one in userreputation
|
||||||
self.user = User.objects.create(username=self.test_username)
|
self.user = User.objects.create(username=self.test_username)
|
||||||
@ -29,7 +33,9 @@ class TestReputationPolicy(TestCase):
|
|||||||
def test_ip_reputation(self):
|
def test_ip_reputation(self):
|
||||||
"""test IP reputation"""
|
"""test IP reputation"""
|
||||||
# Trigger negative 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
|
# Test value in cache
|
||||||
self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1)
|
self.assertEqual(cache.get(CACHE_KEY_IP_PREFIX + self.test_ip), -1)
|
||||||
# Save cache and check db values
|
# Save cache and check db values
|
||||||
@ -39,7 +45,9 @@ class TestReputationPolicy(TestCase):
|
|||||||
def test_user_reputation(self):
|
def test_user_reputation(self):
|
||||||
"""test User reputation"""
|
"""test User reputation"""
|
||||||
# Trigger negative 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
|
# Test value in cache
|
||||||
self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1)
|
self.assertEqual(cache.get(CACHE_KEY_USER_PREFIX + self.test_username), -1)
|
||||||
# Save cache and check db values
|
# Save cache and check db values
|
||||||
|
@ -105,6 +105,7 @@ class PolicyAccessView(AccessMixin, View):
|
|||||||
policy_engine = PolicyEngine(
|
policy_engine = PolicyEngine(
|
||||||
self.application, user or self.request.user, self.request
|
self.application, user or self.request.user, self.request
|
||||||
)
|
)
|
||||||
|
policy_engine.use_cache = False
|
||||||
policy_engine.build()
|
policy_engine.build()
|
||||||
result = policy_engine.result
|
result = policy_engine.result
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
|
@ -4,6 +4,7 @@ from rest_framework.serializers import ModelSerializer
|
|||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.providers.ldap.models import LDAPProvider
|
from authentik.providers.ldap.models import LDAPProvider
|
||||||
|
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ class LDAPProviderSerializer(ProviderSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class LDAPProviderViewSet(ModelViewSet):
|
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""LDAPProvider Viewset"""
|
"""LDAPProvider Viewset"""
|
||||||
|
|
||||||
queryset = LDAPProvider.objects.all()
|
queryset = LDAPProvider.objects.all()
|
||||||
|
@ -11,6 +11,7 @@ from rest_framework.serializers import ValidationError
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
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.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
|
from authentik.providers.oauth2.models import JWTAlgorithms, OAuth2Provider
|
||||||
@ -61,7 +62,7 @@ class OAuth2ProviderSetupURLs(PassiveSerializer):
|
|||||||
logout = ReadOnlyField()
|
logout = ReadOnlyField()
|
||||||
|
|
||||||
|
|
||||||
class OAuth2ProviderViewSet(ModelViewSet):
|
class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""OAuth2Provider Viewset"""
|
"""OAuth2Provider Viewset"""
|
||||||
|
|
||||||
queryset = OAuth2Provider.objects.all()
|
queryset = OAuth2Provider.objects.all()
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.providers.oauth2.models import ScopeMapping
|
from authentik.providers.oauth2.models import ScopeMapping
|
||||||
|
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ class ScopeMappingSerializer(PropertyMappingSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ScopeMappingViewSet(ModelViewSet):
|
class ScopeMappingViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""ScopeMapping Viewset"""
|
"""ScopeMapping Viewset"""
|
||||||
|
|
||||||
queryset = ScopeMapping.objects.all()
|
queryset = ScopeMapping.objects.all()
|
||||||
|
@ -10,6 +10,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
|
|||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import GenericViewSet
|
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.users import UserSerializer
|
||||||
from authentik.core.api.utils import MetaNameSerializer
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
from authentik.providers.oauth2.api.provider import OAuth2ProviderSerializer
|
from authentik.providers.oauth2.api.provider import OAuth2ProviderSerializer
|
||||||
@ -57,6 +58,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
|
|||||||
class AuthorizationCodeViewSet(
|
class AuthorizationCodeViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
@ -82,6 +84,7 @@ class AuthorizationCodeViewSet(
|
|||||||
class RefreshTokenViewSet(
|
class RefreshTokenViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.DestroyModelMixin,
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
GenericViewSet,
|
GenericViewSet,
|
||||||
):
|
):
|
||||||
|
@ -9,7 +9,7 @@ return {}
|
|||||||
"""
|
"""
|
||||||
SCOPE_EMAIL_EXPRESSION = """
|
SCOPE_EMAIL_EXPRESSION = """
|
||||||
return {
|
return {
|
||||||
"email": user.email,
|
"email": request.user.email,
|
||||||
"email_verified": True
|
"email_verified": True
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@ -17,14 +17,14 @@ SCOPE_PROFILE_EXPRESSION = """
|
|||||||
return {
|
return {
|
||||||
# Because authentik only saves the user's full name, and has no concept of first and last names,
|
# Because authentik only saves the user's full name, and has no concept of first and last names,
|
||||||
# the full name is used as given name.
|
# the full name is used as given name.
|
||||||
# You can override this behaviour in custom mappings, i.e. `user.name.split(" ")`
|
# You can override this behaviour in custom mappings, i.e. `request.user.name.split(" ")`
|
||||||
"name": user.name,
|
"name": request.user.name,
|
||||||
"given_name": user.name,
|
"given_name": request.user.name,
|
||||||
"family_name": "",
|
"family_name": "",
|
||||||
"preferred_username": user.username,
|
"preferred_username": request.user.username,
|
||||||
"nickname": user.username,
|
"nickname": request.user.username,
|
||||||
# groups is not part of the official userinfo schema, but is a quasi-standard
|
# groups is not part of the official userinfo schema, but is a quasi-standard
|
||||||
"groups": [group.name for group in user.ak_groups.all()],
|
"groups": [group.name for group in request.user.ak_groups.all()],
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -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(
|
rsa_key = models.ForeignKey(
|
||||||
CertificateKeyPair,
|
CertificateKeyPair,
|
||||||
verbose_name=_("RSA Key"),
|
verbose_name=_("RSA Key"),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.SET_NULL,
|
||||||
blank=True,
|
|
||||||
null=True,
|
null=True,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Key used to sign the tokens. Only required when JWT Algorithm is set to RS256."
|
"Key used to sign the tokens. Only required when JWT Algorithm is set to RS256."
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
"""ProxyProvider API Views"""
|
"""ProxyProvider API Views"""
|
||||||
from typing import Any
|
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.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
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.api.utils import PassiveSerializer
|
||||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
||||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||||
@ -76,7 +77,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ProxyProviderViewSet(ModelViewSet):
|
class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""ProxyProvider Viewset"""
|
"""ProxyProvider Viewset"""
|
||||||
|
|
||||||
queryset = ProxyProvider.objects.all()
|
queryset = ProxyProvider.objects.all()
|
||||||
@ -84,6 +85,7 @@ class ProxyProviderViewSet(ModelViewSet):
|
|||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_serializer(deprecate_fields=["forward_auth_mode"])
|
||||||
class ProxyOutpostConfigSerializer(ModelSerializer):
|
class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||||
"""Proxy provider serializer for outposts"""
|
"""Proxy provider serializer for outposts"""
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ SCOPE_AK_PROXY_EXPRESSION = """
|
|||||||
# which are used for example for the HTTP-Basic Authentication mapping.
|
# which are used for example for the HTTP-Basic Authentication mapping.
|
||||||
return {
|
return {
|
||||||
"ak_proxy": {
|
"ak_proxy": {
|
||||||
"user_attributes": user.group_attributes()
|
"user_attributes": request.user.group_attributes()
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
|
@ -167,3 +167,4 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
|||||||
|
|
||||||
verbose_name = _("Proxy Provider")
|
verbose_name = _("Proxy Provider")
|
||||||
verbose_name_plural = _("Proxy Providers")
|
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 defusedxml.ElementTree import fromstring
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||||
from rest_framework.decorators import action
|
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.parsers import MultiPartParser
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.relations import SlugRelatedField
|
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.api.decorators import permission_required
|
||||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
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.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
@ -36,6 +43,15 @@ LOGGER = get_logger()
|
|||||||
class SAMLProviderSerializer(ProviderSerializer):
|
class SAMLProviderSerializer(ProviderSerializer):
|
||||||
"""SAMLProvider Serializer"""
|
"""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:
|
class Meta:
|
||||||
|
|
||||||
model = SAMLProvider
|
model = SAMLProvider
|
||||||
@ -53,6 +69,7 @@ class SAMLProviderSerializer(ProviderSerializer):
|
|||||||
"signing_kp",
|
"signing_kp",
|
||||||
"verification_kp",
|
"verification_kp",
|
||||||
"sp_binding",
|
"sp_binding",
|
||||||
|
"metadata_download_url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -75,7 +92,7 @@ class SAMLProviderImportSerializer(PassiveSerializer):
|
|||||||
file = FileField()
|
file = FileField()
|
||||||
|
|
||||||
|
|
||||||
class SAMLProviderViewSet(ModelViewSet):
|
class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""SAMLProvider Viewset"""
|
"""SAMLProvider Viewset"""
|
||||||
|
|
||||||
queryset = SAMLProvider.objects.all()
|
queryset = SAMLProvider.objects.all()
|
||||||
@ -166,7 +183,7 @@ class SAMLPropertyMappingSerializer(PropertyMappingSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class SAMLPropertyMappingViewSet(ModelViewSet):
|
class SAMLPropertyMappingViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""SAMLPropertyMapping Viewset"""
|
"""SAMLPropertyMapping Viewset"""
|
||||||
|
|
||||||
queryset = SAMLPropertyMapping.objects.all()
|
queryset = SAMLPropertyMapping.objects.all()
|
||||||
|
@ -3,7 +3,7 @@ from authentik.managed.manager import EnsureExists, ObjectManager
|
|||||||
from authentik.providers.saml.models import SAMLPropertyMapping
|
from authentik.providers.saml.models import SAMLPropertyMapping
|
||||||
|
|
||||||
GROUP_EXPRESSION = """
|
GROUP_EXPRESSION = """
|
||||||
for group in user.ak_groups.all():
|
for group in request.user.ak_groups.all():
|
||||||
yield group.name
|
yield group.name
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ class SAMLProviderManager(ObjectManager):
|
|||||||
"goauthentik.io/providers/saml/upn",
|
"goauthentik.io/providers/saml/upn",
|
||||||
name="authentik default SAML Mapping: UPN",
|
name="authentik default SAML Mapping: UPN",
|
||||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
|
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
|
||||||
expression="return user.attributes.get('upn', user.email)",
|
expression="return request.user.attributes.get('upn', request.user.email)",
|
||||||
friendly_name="",
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
@ -26,7 +26,7 @@ class SAMLProviderManager(ObjectManager):
|
|||||||
"goauthentik.io/providers/saml/name",
|
"goauthentik.io/providers/saml/name",
|
||||||
name="authentik default SAML Mapping: Name",
|
name="authentik default SAML Mapping: Name",
|
||||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
|
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
|
||||||
expression="return user.name",
|
expression="return request.user.name",
|
||||||
friendly_name="",
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
@ -34,7 +34,7 @@ class SAMLProviderManager(ObjectManager):
|
|||||||
"goauthentik.io/providers/saml/email",
|
"goauthentik.io/providers/saml/email",
|
||||||
name="authentik default SAML Mapping: Email",
|
name="authentik default SAML Mapping: Email",
|
||||||
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
saml_name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
||||||
expression="return user.email",
|
expression="return request.user.email",
|
||||||
friendly_name="",
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
@ -42,7 +42,7 @@ class SAMLProviderManager(ObjectManager):
|
|||||||
"goauthentik.io/providers/saml/username",
|
"goauthentik.io/providers/saml/username",
|
||||||
name="authentik default SAML Mapping: Username",
|
name="authentik default SAML Mapping: Username",
|
||||||
saml_name="http://schemas.goauthentik.io/2021/02/saml/username",
|
saml_name="http://schemas.goauthentik.io/2021/02/saml/username",
|
||||||
expression="return user.username",
|
expression="return request.user.username",
|
||||||
friendly_name="",
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
@ -50,7 +50,7 @@ class SAMLProviderManager(ObjectManager):
|
|||||||
"goauthentik.io/providers/saml/uid",
|
"goauthentik.io/providers/saml/uid",
|
||||||
name="authentik default SAML Mapping: User ID",
|
name="authentik default SAML Mapping: User ID",
|
||||||
saml_name="http://schemas.goauthentik.io/2021/02/saml/uid",
|
saml_name="http://schemas.goauthentik.io/2021/02/saml/uid",
|
||||||
expression="return user.pk",
|
expression="return request.user.pk",
|
||||||
friendly_name="",
|
friendly_name="",
|
||||||
),
|
),
|
||||||
EnsureExists(
|
EnsureExists(
|
||||||
@ -68,7 +68,7 @@ class SAMLProviderManager(ObjectManager):
|
|||||||
saml_name=(
|
saml_name=(
|
||||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||||
),
|
),
|
||||||
expression="return user.username",
|
expression="return request.user.username",
|
||||||
friendly_name="",
|
friendly_name="",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -24,6 +24,7 @@ from authentik.sources.saml.processors.constants import (
|
|||||||
SAML_NAME_ID_FORMAT_EMAIL,
|
SAML_NAME_ID_FORMAT_EMAIL,
|
||||||
SAML_NAME_ID_FORMAT_PERSISTENT,
|
SAML_NAME_ID_FORMAT_PERSISTENT,
|
||||||
SAML_NAME_ID_FORMAT_TRANSIENT,
|
SAML_NAME_ID_FORMAT_TRANSIENT,
|
||||||
|
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||||
SAML_NAME_ID_FORMAT_WINDOWS,
|
SAML_NAME_ID_FORMAT_WINDOWS,
|
||||||
SAML_NAME_ID_FORMAT_X509,
|
SAML_NAME_ID_FORMAT_X509,
|
||||||
SIGN_ALGORITHM_TRANSFORM_MAP,
|
SIGN_ALGORITHM_TRANSFORM_MAP,
|
||||||
@ -165,7 +166,10 @@ class AssertionProcessor:
|
|||||||
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL:
|
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL:
|
||||||
name_id.text = self.http_request.user.email
|
name_id.text = self.http_request.user.email
|
||||||
return name_id
|
return name_id
|
||||||
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_PERSISTENT:
|
if name_id.attrib["Format"] in [
|
||||||
|
SAML_NAME_ID_FORMAT_PERSISTENT,
|
||||||
|
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||||
|
]:
|
||||||
name_id.text = persistent
|
name_id.text = persistent
|
||||||
return name_id
|
return name_id
|
||||||
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509:
|
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_X509:
|
||||||
@ -180,7 +184,7 @@ class AssertionProcessor:
|
|||||||
return name_id
|
return name_id
|
||||||
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
|
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_TRANSIENT:
|
||||||
# Use the hash of the user's session, which changes every session
|
# Use the hash of the user's session, which changes every session
|
||||||
session_key: str = self.http_request.user.session.session_key
|
session_key: str = self.http_request.session.session_key
|
||||||
name_id.text = sha256(session_key.encode()).hexdigest()
|
name_id.text = sha256(session_key.encode()).hexdigest()
|
||||||
return name_id
|
return name_id
|
||||||
raise UnsupportedNameIDFormat(
|
raise UnsupportedNameIDFormat(
|
||||||
|
@ -120,7 +120,7 @@ class ServiceProviderMetadataParser:
|
|||||||
)
|
)
|
||||||
ctx.key = key
|
ctx.key = key
|
||||||
ctx.verify(signature_node)
|
ctx.verify(signature_node)
|
||||||
except xmlsec.VerificationError as exc:
|
except xmlsec.Error as exc:
|
||||||
raise ValueError("Failed to verify Metadata signature") from exc
|
raise ValueError("Failed to verify Metadata signature") from exc
|
||||||
|
|
||||||
def parse(self, raw_xml: str) -> ServiceProviderMetadata:
|
def parse(self, raw_xml: str) -> ServiceProviderMetadata:
|
||||||
|
@ -20,10 +20,11 @@ from authentik.sources.saml.processors.constants import (
|
|||||||
RSA_SHA256,
|
RSA_SHA256,
|
||||||
RSA_SHA384,
|
RSA_SHA384,
|
||||||
RSA_SHA512,
|
RSA_SHA512,
|
||||||
SAML_NAME_ID_FORMAT_EMAIL,
|
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||||
)
|
)
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
ERROR_CANNOT_DECODE_REQUEST = "Cannot decode SAML request."
|
||||||
ERROR_SIGNATURE_REQUIRED_BUT_ABSENT = (
|
ERROR_SIGNATURE_REQUIRED_BUT_ABSENT = (
|
||||||
"Verification Certificate configured, but request is not signed."
|
"Verification Certificate configured, but request is not signed."
|
||||||
)
|
)
|
||||||
@ -42,7 +43,7 @@ class AuthNRequest:
|
|||||||
|
|
||||||
relay_state: Optional[str] = None
|
relay_state: Optional[str] = None
|
||||||
|
|
||||||
name_id_policy: str = SAML_NAME_ID_FORMAT_EMAIL
|
name_id_policy: str = SAML_NAME_ID_FORMAT_UNSPECIFIED
|
||||||
|
|
||||||
|
|
||||||
class AuthNRequestParser:
|
class AuthNRequestParser:
|
||||||
@ -69,16 +70,21 @@ class AuthNRequestParser:
|
|||||||
auth_n_request = AuthNRequest(id=root.attrib["ID"], relay_state=relay_state)
|
auth_n_request = AuthNRequest(id=root.attrib["ID"], relay_state=relay_state)
|
||||||
|
|
||||||
# Check if AuthnRequest has a NameID Policy object
|
# Check if AuthnRequest has a NameID Policy object
|
||||||
name_id_policies = root.findall(f"{{{NS_SAML_PROTOCOL}}}:NameIDPolicy")
|
name_id_policies = root.findall(f"{{{NS_SAML_PROTOCOL}}}NameIDPolicy")
|
||||||
if len(name_id_policies) > 0:
|
if len(name_id_policies) > 0:
|
||||||
name_id_policy = name_id_policies[0]
|
name_id_policy = name_id_policies[0]
|
||||||
auth_n_request.name_id_policy = name_id_policy.attrib["Format"]
|
auth_n_request.name_id_policy = name_id_policy.attrib.get(
|
||||||
|
"Format", SAML_NAME_ID_FORMAT_UNSPECIFIED
|
||||||
|
)
|
||||||
|
|
||||||
return auth_n_request
|
return auth_n_request
|
||||||
|
|
||||||
def parse(self, saml_request: str, relay_state: Optional[str]) -> AuthNRequest:
|
def parse(self, saml_request: str, relay_state: Optional[str]) -> AuthNRequest:
|
||||||
"""Validate and parse raw request with enveloped signautre."""
|
"""Validate and parse raw request with enveloped signautre."""
|
||||||
|
try:
|
||||||
decoded_xml = b64decode(saml_request.encode()).decode()
|
decoded_xml = b64decode(saml_request.encode()).decode()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise CannotHandleAssertion(ERROR_CANNOT_DECODE_REQUEST)
|
||||||
|
|
||||||
verifier = self.provider.verification_kp
|
verifier = self.provider.verification_kp
|
||||||
|
|
||||||
@ -108,7 +114,7 @@ class AuthNRequestParser:
|
|||||||
)
|
)
|
||||||
ctx.key = key
|
ctx.key = key
|
||||||
ctx.verify(signature_node)
|
ctx.verify(signature_node)
|
||||||
except xmlsec.VerificationError as exc:
|
except xmlsec.Error as exc:
|
||||||
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
||||||
|
|
||||||
return self._parse_xml(decoded_xml, relay_state)
|
return self._parse_xml(decoded_xml, relay_state)
|
||||||
@ -121,7 +127,10 @@ class AuthNRequestParser:
|
|||||||
sig_alg: Optional[str] = None,
|
sig_alg: Optional[str] = None,
|
||||||
) -> AuthNRequest:
|
) -> AuthNRequest:
|
||||||
"""Validate and parse raw request with detached signature"""
|
"""Validate and parse raw request with detached signature"""
|
||||||
|
try:
|
||||||
decoded_xml = decode_base64_and_inflate(saml_request)
|
decoded_xml = decode_base64_and_inflate(saml_request)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
raise CannotHandleAssertion(ERROR_CANNOT_DECODE_REQUEST)
|
||||||
|
|
||||||
verifier = self.provider.verification_kp
|
verifier = self.provider.verification_kp
|
||||||
|
|
||||||
@ -160,7 +169,7 @@ class AuthNRequestParser:
|
|||||||
sign_algorithm_transform,
|
sign_algorithm_transform,
|
||||||
b64decode(signature),
|
b64decode(signature),
|
||||||
)
|
)
|
||||||
except xmlsec.VerificationError as exc:
|
except xmlsec.Error as exc:
|
||||||
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
||||||
return self._parse_xml(decoded_xml, relay_state)
|
return self._parse_xml(decoded_xml, relay_state)
|
||||||
|
|
||||||
|
@ -2,18 +2,19 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
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 django.test import RequestFactory, TestCase
|
||||||
from guardian.utils import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.flows.models import Flow
|
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.models import SAMLPropertyMapping, SAMLProvider
|
||||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||||
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||||
from authentik.sources.saml.exceptions import MismatchedRequestID
|
from authentik.sources.saml.exceptions import MismatchedRequestID
|
||||||
from authentik.sources.saml.models import SAMLSource
|
from authentik.sources.saml.models import SAMLSource
|
||||||
from authentik.sources.saml.processors.constants import SAML_NAME_ID_FORMAT_EMAIL
|
from authentik.sources.saml.processors.constants import SAML_NAME_ID_FORMAT_UNSPECIFIED
|
||||||
from authentik.sources.saml.processors.request import (
|
from authentik.sources.saml.processors.request import (
|
||||||
SESSION_REQUEST_ID,
|
SESSION_REQUEST_ID,
|
||||||
RequestProcessor,
|
RequestProcessor,
|
||||||
@ -58,11 +59,6 @@ qNAZMq1DqpibfCBg
|
|||||||
-----END CERTIFICATE-----"""
|
-----END CERTIFICATE-----"""
|
||||||
|
|
||||||
|
|
||||||
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
|
||||||
"""Dummy get_response for SessionMiddleware"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class TestAuthNRequest(TestCase):
|
class TestAuthNRequest(TestCase):
|
||||||
"""Test AuthN Request generator and parser"""
|
"""Test AuthN Request generator and parser"""
|
||||||
|
|
||||||
@ -210,5 +206,5 @@ class TestAuthNRequest(TestCase):
|
|||||||
REDIRECT_REQUEST, REDIRECT_RELAY_STATE, REDIRECT_SIGNATURE, REDIRECT_SIG_ALG
|
REDIRECT_REQUEST, REDIRECT_RELAY_STATE, REDIRECT_SIGNATURE, REDIRECT_SIG_ALG
|
||||||
)
|
)
|
||||||
self.assertEqual(parsed_request.id, "_dcf55fcd27a887e60a7ef9ee6fd3adab")
|
self.assertEqual(parsed_request.id, "_dcf55fcd27a887e60a7ef9ee6fd3adab")
|
||||||
self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL)
|
self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_UNSPECIFIED)
|
||||||
self.assertEqual(parsed_request.relay_state, REDIRECT_RELAY_STATE)
|
self.assertEqual(parsed_request.relay_state, REDIRECT_RELAY_STATE)
|
||||||
|
@ -17,6 +17,7 @@ from authentik.providers.saml.models import SAMLBindings, SAMLProvider
|
|||||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||||
from authentik.providers.saml.processors.request_parser import AuthNRequest
|
from authentik.providers.saml.processors.request_parser import AuthNRequest
|
||||||
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64
|
from authentik.providers.saml.utils.encoding import deflate_and_base64_encode, nice64
|
||||||
|
from authentik.sources.saml.exceptions import SAMLException
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
|
URL_VALIDATOR = URLValidator(schemes=("http", "https"))
|
||||||
@ -56,22 +57,30 @@ class SAMLFlowFinalView(ChallengeStageView):
|
|||||||
provider: SAMLProvider = get_object_or_404(
|
provider: SAMLProvider = get_object_or_404(
|
||||||
SAMLProvider, pk=application.provider_id
|
SAMLProvider, pk=application.provider_id
|
||||||
)
|
)
|
||||||
# Log Application Authorization
|
|
||||||
Event.new(
|
|
||||||
EventAction.AUTHORIZE_APPLICATION,
|
|
||||||
authorized_application=application,
|
|
||||||
flow=self.executor.plan.flow_pk,
|
|
||||||
).from_http(self.request)
|
|
||||||
|
|
||||||
if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
|
if SESSION_KEY_AUTH_N_REQUEST not in self.request.session:
|
||||||
return self.executor.stage_invalid()
|
return self.executor.stage_invalid()
|
||||||
|
|
||||||
auth_n_request: AuthNRequest = self.request.session.pop(
|
auth_n_request: AuthNRequest = self.request.session.pop(
|
||||||
SESSION_KEY_AUTH_N_REQUEST
|
SESSION_KEY_AUTH_N_REQUEST
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
response = AssertionProcessor(
|
response = AssertionProcessor(
|
||||||
provider, request, auth_n_request
|
provider, request, auth_n_request
|
||||||
).build_response()
|
).build_response()
|
||||||
|
except SAMLException as exc:
|
||||||
|
Event.new(
|
||||||
|
EventAction.CONFIGURATION_ERROR,
|
||||||
|
message=f"Failed to process SAML assertion: {str(exc)}",
|
||||||
|
provider=provider,
|
||||||
|
).from_http(self.request)
|
||||||
|
return self.executor.stage_invalid()
|
||||||
|
|
||||||
|
# Log Application Authorization
|
||||||
|
Event.new(
|
||||||
|
EventAction.AUTHORIZE_APPLICATION,
|
||||||
|
authorized_application=application,
|
||||||
|
flow=self.executor.plan.flow_pk,
|
||||||
|
).from_http(self.request)
|
||||||
|
|
||||||
if provider.sp_binding == SAMLBindings.POST:
|
if provider.sp_binding == SAMLBindings.POST:
|
||||||
form_attrs = {
|
form_attrs = {
|
||||||
|
@ -44,7 +44,7 @@ class Command(BaseCommand):
|
|||||||
user=user,
|
user=user,
|
||||||
intent=TokenIntents.INTENT_RECOVERY,
|
intent=TokenIntents.INTENT_RECOVERY,
|
||||||
description=f"Recovery Token generated by {getuser()} on {_now}",
|
description=f"Recovery Token generated by {getuser()} on {_now}",
|
||||||
identifier=f"ak-recovery-{user}",
|
identifier=f"ak-recovery-{user}-{_now}",
|
||||||
)
|
)
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
(
|
(
|
||||||
|
@ -7,6 +7,7 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from authentik.core.models import Token, TokenIntents
|
from authentik.core.models import Token, TokenIntents
|
||||||
|
from authentik.stages.password import BACKEND_DJANGO
|
||||||
|
|
||||||
|
|
||||||
class UseTokenView(View):
|
class UseTokenView(View):
|
||||||
@ -18,7 +19,7 @@ class UseTokenView(View):
|
|||||||
if not tokens.exists():
|
if not tokens.exists():
|
||||||
raise Http404
|
raise Http404
|
||||||
token = tokens.first()
|
token = tokens.first()
|
||||||
login(request, token.user, backend="django.contrib.auth.backends.ModelBackend")
|
login(request, token.user, backend=BACKEND_DJANGO)
|
||||||
token.delete()
|
token.delete()
|
||||||
messages.warning(request, _("Used recovery-link to authenticate."))
|
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||||
return redirect("authentik_core:if-admin")
|
return redirect("authentik_core:if-admin")
|
||||||
|
@ -15,6 +15,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from json import dumps
|
from json import dumps
|
||||||
|
from tempfile import gettempdir
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
@ -193,6 +194,7 @@ CACHES = {
|
|||||||
f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379"
|
f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379"
|
||||||
f"/{CONFIG.y('redis.cache_db')}"
|
f"/{CONFIG.y('redis.cache_db')}"
|
||||||
),
|
),
|
||||||
|
"TIMEOUT": int(CONFIG.y("redis.cache_timeout", 300)),
|
||||||
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
|
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -341,7 +343,7 @@ DBBACKUP_FILENAME_TEMPLATE = "authentik-backup-{datetime}.sql"
|
|||||||
DBBACKUP_CONNECTOR_MAPPING = {
|
DBBACKUP_CONNECTOR_MAPPING = {
|
||||||
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
|
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
|
||||||
}
|
}
|
||||||
|
DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp" # nosec
|
||||||
if CONFIG.y("postgresql.s3_backup"):
|
if CONFIG.y("postgresql.s3_backup"):
|
||||||
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||||
DBBACKUP_STORAGE_OPTIONS = {
|
DBBACKUP_STORAGE_OPTIONS = {
|
||||||
@ -375,7 +377,11 @@ if _ERROR_REPORTING:
|
|||||||
environment=CONFIG.y("error_reporting.environment", "customer"),
|
environment=CONFIG.y("error_reporting.environment", "customer"),
|
||||||
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
|
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(
|
set_tag(
|
||||||
"authentik.env", "kubernetes" if "KUBERNETES_PORT" in os.environ else "compose"
|
"authentik.env", "kubernetes" if "KUBERNETES_PORT" in os.environ else "compose"
|
||||||
)
|
)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user