Compare commits

..

47 Commits

Author SHA1 Message Date
59401e07ad release: 2024.12.5 2025-04-08 14:54:17 -03:00
4008f0f28f Revert "core: fix non-exploitable open redirect (#13696)" (#13827) 2025-04-08 19:16:35 +02:00
210fbb0067 release: 2024.12.4 2025-03-28 14:48:01 +01:00
9c081d084d security: fix CVE-2025-29928 (cherry-pick #13695) (#13701)
security: fix CVE-2025-29928 (#13695)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-03-28 14:45:42 +01:00
c7c75dc195 core: fix non-exploitable open redirect (#13696)
discovered by @dominic-r

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	authentik/core/sources/flow_manager.py
2025-03-28 14:19:55 +01:00
5448dc93db flows: fix inspector permission check (#12907)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-02-01 01:54:55 +01:00
6d152bcc60 core: fix generic sources not being fetchable by pk (#12896)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-02-01 00:40:53 +01:00
f1b7a9f934 release: 2024.12.3 2025-01-29 21:47:30 +01:00
4af75d0979 ci: fix missing dockerhub login
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-29 21:47:23 +01:00
af0a314e0b ci: fix permissions for release-publish pipeline
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-29 19:24:17 +01:00
6c7f901220 ci: fix test_docker.sh (cherry-pick #12880) (#12881)
ci: fix test_docker.sh (#12880)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-01-29 18:52:54 +01:00
3570bfa39d ci: fix test_docker.sh (cherry-pick #12878) (#12879)
ci: fix test_docker.sh (#12878)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-01-29 18:42:20 +01:00
35ab51e4c5 ci: fix test_docker.sh failing due to empty .env (cherry-pick #12876) (#12877)
ci: fix test_docker.sh failing due to empty .env (#12876)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-01-29 18:33:04 +01:00
22eaf97d62 ci: fix test_docker.sh failing due to missing .env (cherry-pick #12873) (#12874)
ci: fix test_docker.sh failing due to missing .env (#12873)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-29 17:10:29 +01:00
764b211bd4 lifecycle: better pre release test (cherry-pick #12806) (#12808)
lifecycle: better pre release test (#12806)

* move pre-release docker test to script



* set pipefail in ak



* don't reinstall wheels since they don't exist anymore



* fix image



* fix config error on startup



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-25 02:28:23 +01:00
7afc59d691 rbac: exclude permissions for internal models (cherry-pick #12803) (#12807)
rbac: exclude permissions for internal models (#12803)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-25 02:07:16 +01:00
349572bfe4 flows: clear flow state before redirecting to final URL (cherry-pick #12788) (#12801)
flows: clear flow state before redirecting to final URL (#12788)

* providers/oauth2: clear flow state before redirecting to final URL



* make flow executor invocation correct



* actually we can do this centrally



* make sure the state is really clean



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-24 18:40:02 +01:00
bef55bc3a5 core: fix permissions for admin device listing (cherry-pick #12787) (#12791)
core: fix permissions for admin device listing (#12787)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-24 15:05:23 +01:00
d86da24c01 lifecycle: update python to 3.12.8 (cherry-pick #12783) (#12786)
lifecycle: update python to 3.12.8 (#12783)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-23 23:37:48 +01:00
25d0ee02a8 core: fix application entitlements not createable with blueprints (cherry-pick #12673) (#12784)
core: fix application entitlements not createable with blueprints (#12673)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-23 16:55:16 +01:00
bccfb0b48c sources: allow uuid or slug to be used for retrieving a source (2024.12 fix) (#12772)
sources: allow uuid or slug to be used for retrieving a source

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-23 12:26:48 +01:00
4b14eca2da ci: fix missing build args for dev and release (cherry-pick #12760) (#12761)
ci: fix missing build args for dev and release (#12760)

* ci: fix missing build args for dev and release



* fix?



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-22 14:32:15 +01:00
7d5cfb6356 lifecycle: fix cryptography's OpenSSL path (cherry-pick #12753) (#12759)
lifecycle: fix cryptography's OpenSSL path (#12753)

* lifecycle: make it work



* sigh



* I dont know why this works but it works



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-22 02:32:44 +01:00
7ce46ccbe0 stages/redirect: fix query parameter when redirecting to flow (cherry-pick #12750) (#12752)
stages/redirect: fix query parameter when redirecting to flow (#12750)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-21 18:07:59 +01:00
d0217c9135 lifecycle: build binary dependencies which link against SSL directly (#12724)
* lifecycle: install binary dependencies in dockerfile directly

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* install ua-parser-builtins manually as its only distributed as binary

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* build duo_client from scratch, sigh

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* deps for kadmin

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ok fine

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* run on arm runner?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix yaml format

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rewrite release pipeline to use re-usable workflows

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix typo

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* re-usable multi-arch build?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* also add suffix for amd64

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* parameterise image name

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* re-use workflow for CI images...?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing checkout

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* inherit secrets

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* temp build directly

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* get cache-to from python script

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* better name?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* matrix for merging images?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* re-add build dep

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* use multi-image tag

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* include arch in buildcache

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	.github/workflows/ci-main.yml
#	.github/workflows/release-publish.yml
2025-01-21 15:39:32 +01:00
d82ba344d9 ci: release: fix AWS cfn template permissions (cherry-pick #12576) (#12739)
ci: release: fix AWS cfn template permissions (#12576)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-01-20 17:28:36 +01:00
01959132e8 enterprise/rac: Improve client connection status & bugfixes (cherry-pick #12684) (#12727)
enterprise/rac: Improve client connection status & bugfixes (#12684)

* enterprise/rac: improve status message when connecting/connection failed



* set fixed DPI



* automatically set resize method for RDP



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-17 18:27:31 +01:00
9d81f0598c release: 2024.12.2 2025-01-09 17:43:00 +01:00
cbe429f3fa providers/saml: fix invalid SAML Response when assertion and response are signed (cherry-pick #12611) (#12613)
providers/saml: fix invalid SAML Response when assertion and response are signed (#12611)

* providers/saml: fix invalid SAML Response when assertion and response are signed



* validate against schema too



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-09 16:20:49 +01:00
1cf0f57608 core: fix error when creating new user with default path (cherry-pick #12609) (#12612)
core: fix error when creating new user with default path (#12609)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-01-09 15:32:18 +01:00
052da72acf rbac: permissions endpoint: allow authenticated users (cherry-pick #12608) (#12610)
rbac: permissions endpoint: allow authenticated users (#12608)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-01-09 15:01:54 +01:00
9a1c76efe7 sources/kerberos: authenticate with the user's username instead of the first username in authentik (cherry-pick #12497) (#12579)
sources/kerberos: authenticate with the user's username instead of the first username in authentik (#12497)

Co-authored-by: natural-hair <github@natural-hair.net>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-01-06 15:22:15 +01:00
96b5bee912 web: fix source selection and outpost integration health (#12530)
* fix source selector

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix service connection health not updating fully

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix logo alt not translated

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/src/admin/AdminInterface/AboutModal.ts
2025-01-03 01:38:29 +01:00
09b3a1d0bd internal: fix missing trailing slash in outpost websocket (cherry-pick #12470) (#12471)
internal: fix missing trailing slash in outpost websocket (#12470)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-12-24 00:56:31 +01:00
e87a17fd81 release: 2024.12.1 2024-12-23 14:08:59 +01:00
bb1bcb29cd internal: fix URL generation for websocket connection (cherry-pick #12439) (#12440)
internal: fix URL generation for websocket connection (#12439)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-12-20 20:08:25 +01:00
0a5bdad972 website/docs: add content about bindings (cherry-pick #11787) (#12428)
website/docs: add content about bindings (#11787)

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-12-19 20:36:29 +01:00
d313225956 website/docs: add new section about impersonation (cherry-pick #12328) (#12424)
website/docs: add new section about impersonation (#12328)

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-19 19:59:44 +01:00
249dc276d4 release: 2024.12.0 2024-12-19 19:18:31 +01:00
5fb7dc4cb3 website/docs: prepare for 2024.12.0 (cherry-pick #12420) (#12422)
website/docs: prepare for 2024.12.0 (#12420)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-19 19:18:03 +01:00
82930ee807 root: expose CONN_MAX_AGE, CONN_HEALTH_CHECKS and DISABLE_SERVER_SIDE_CURSORS for PostgreSQL config (cherry-pick #10159) (#12419)
root: expose CONN_MAX_AGE, CONN_HEALTH_CHECKS and DISABLE_SERVER_SIDE_CURSORS for PostgreSQL config (#10159)

Co-authored-by: Tomás Farías Santana <tomas@tomasfarias.dev>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
2024-12-19 19:01:06 +01:00
ac25fbab54 events: notification_cleanup: avoid unnecessary loop (cherry-pick #12417) (#12418)
events: notification_cleanup: avoid unnecessary loop (#12417)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-19 18:49:30 +01:00
15cb6b18f6 translate: Updates for file web/xliff/en.xlf in zh_CN (cherry-pick #12402) (#12411)
* translate: Updates for file web/xliff/en.xlf in zh_CN (#12402)

Translate web/xliff/en.xlf in zh_CN

100% translated source file: 'web/xliff/en.xlf'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>

* ci: dont run codeql on cherry-picked prs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-12-19 13:17:46 +01:00
fdd39b4b4c translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (cherry-pick #12399) (#12408)
translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#12399)

Translate locale/en/LC_MESSAGES/django.po in zh_CN

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-19 12:43:26 +01:00
589304df4f translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (cherry-pick #12400) (#12409)
translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#12400)

Translate django.po in zh-Hans

100% translated source file: 'django.po'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-19 12:43:16 +01:00
4d920ff477 translate: Updates for file web/xliff/en.xlf in zh-Hans (cherry-pick #12401) (#12410)
translate: Updates for file web/xliff/en.xlf in zh-Hans (#12401)

Translate web/xliff/en.xlf in zh-Hans

100% translated source file: 'web/xliff/en.xlf'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-19 12:43:04 +01:00
88dc616c5e release: 2024.12.0-rc1 2024-12-18 19:35:21 +01:00
391 changed files with 6305 additions and 20970 deletions

View File

@ -1,16 +1,16 @@
[bumpversion]
current_version = 2024.12.2
current_version = 2024.12.5
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
serialize =
serialize =
{major}.{minor}.{patch}-{rc_t}{rc_n}
{major}.{minor}.{patch}
message = release: {new_version}
tag_name = version/{new_version}
[bumpversion:part:rc_t]
values =
values =
rc
final
optional_value = final
@ -31,4 +31,4 @@ optional_value = final
[bumpversion:file:web/src/common/constants.ts]
[bumpversion:file:lifecycle/aws/template.yaml]
[bumpversion:file:website/docs/install-config/install/aws/template.yaml]

View File

@ -82,16 +82,6 @@ updates:
docusaurus:
patterns:
- "@docusaurus/*"
- package-ecosystem: npm
directory: "/lifecycle/aws"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
commit-message:
prefix: "lifecycle/aws:"
labels:
- dependencies
- package-ecosystem: pip
directory: "/"
schedule:

View File

@ -25,10 +25,10 @@ jobs:
uses: ./.github/actions/setup
- uses: actions/setup-node@v4
with:
node-version-file: lifecycle/aws/package.json
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: lifecycle/aws/package-lock.json
- working-directory: lifecycle/aws/
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: |
npm ci
- name: Check changes have been applied

View File

@ -134,7 +134,7 @@ jobs:
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.12.0
uses: helm/kind-action@v1.11.0
- name: run integration
run: |
poetry run coverage run manage.py test tests/integration

View File

@ -82,7 +82,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables

View File

@ -2,7 +2,7 @@ name: "CodeQL"
on:
push:
branches: [main, "*", next, version*]
branches: [main, next, version*]
pull_request:
branches: [main]
schedule:

View File

@ -9,9 +9,17 @@ jobs:
build-server:
uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit
permissions:
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
with:
image_name: ghcr.io/goauthentik/server,beryju/authentik
release: true
registry_dockerhub: true
registry_ghcr: true
build-outpost:
runs-on: ubuntu-latest
permissions:
@ -34,7 +42,7 @@ jobs:
with:
go-version-file: "go.mod"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -139,8 +147,8 @@ jobs:
aws-region: ${{ env.AWS_REGION }}
- name: Upload template
run: |
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
test-release:
needs:
- build-server

View File

@ -14,16 +14,7 @@ jobs:
- uses: actions/checkout@v4
- name: Pre-release test
run: |
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
docker buildx install
mkdir -p ./gen-ts-api
docker build -t testing:latest .
echo "AUTHENTIK_IMAGE=testing" >> .env
echo "AUTHENTIK_TAG=latest" >> .env
docker compose up --no-start
docker compose start postgresql redis
docker compose run -u root server test-all
make test-docker
- id: generate_token
uses: tibdex/github-app-token@v2
with:

View File

@ -15,7 +15,6 @@ go.mod @goauthentik/backend
go.sum @goauthentik/backend
# Infrastructure
.github/ @goauthentik/infrastructure
lifecycle/aws/ @goauthentik/infrastructure
Dockerfile @goauthentik/infrastructure
*Dockerfile @goauthentik/infrastructure
.dockerignore @goauthentik/infrastructure

View File

@ -94,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Python dependencies
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips AS python-deps
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-deps
ARG TARGETARCH
ARG TARGETVARIANT
@ -139,7 +139,7 @@ RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
poetry install --only=main --no-ansi --no-interaction --no-root"
# Stage 6: Run
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips AS final-image
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS final-image
ARG VERSION
ARG GIT_BUILD_HASH
@ -155,12 +155,10 @@ WORKDIR /
# We cannot cache this layer otherwise we'll end up with a bigger image
RUN apt-get update && \
apt-get upgrade -y && \
# Required for runtime
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 libltdl7 libxslt1.1 && \
# Required for bootstrap & healtcheck
apt-get install -y --no-install-recommends runit && \
pip3 install --no-cache-dir --upgrade pip && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \

View File

@ -5,7 +5,7 @@ PWD = $(shell pwd)
UID = $(shell id -u)
GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.npm_version)
PY_SOURCES = authentik tests scripts lifecycle .github
PY_SOURCES = authentik tests scripts lifecycle .github website/docs/install-config/install/aws
DOCKER_IMAGE ?= "authentik:test"
GEN_API_TS = "gen-ts-api"
@ -45,15 +45,6 @@ help: ## Show this help
go-test:
go test -timeout 0 -v -race -cover ./...
test-docker: ## Run all tests in a docker-compose
echo "PG_PASS=$(shell openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64 -w 0)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis
docker compose run -u root server test-all
rm -f .env
test: ## Run the server tests and produce a coverage report (locally)
coverage run manage.py test --keepdb authentik
coverage html
@ -78,9 +69,6 @@ migrate: ## Run the Authentik Django server's migrations
i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service
aws-cfn:
cd lifecycle/aws && npm run aws-cfn
core-i18n-extract:
ak makemessages \
--add-location file \
@ -152,7 +140,7 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
-i /local/schema.yml \
-g typescript-fetch \
-o /local/${GEN_API_TS} \
@ -255,6 +243,9 @@ website-build:
website-watch: ## Build and watch the documentation website, updating automatically
cd website && npm run watch
aws-cfn:
cd website && npm run aws-cfn
#########################
## Docker
#########################
@ -263,6 +254,9 @@ docker: ## Build a docker image of the current source tree
mkdir -p ${GEN_API_TS}
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
test-docker:
./scripts/test_docker.sh
#########################
## CI
#########################

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.12.2"
__version__ = "2024.12.5"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
@ -16,5 +16,5 @@ def get_full_version() -> str:
"""Get full version, with build hash appended"""
version = __version__
if (build_hash := get_build_hash()) != "":
return f"{version}+{build_hash}"
version += "." + build_hash
return version

View File

@ -7,9 +7,7 @@ from sys import version as python_version
from typing import TypedDict
from cryptography.hazmat.backends.openssl.backend import backend
from django.conf import settings
from django.utils.timezone import now
from django.views.debug import SafeExceptionReporterFilter
from drf_spectacular.utils import extend_schema
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request
@ -54,16 +52,10 @@ class SystemInfoSerializer(PassiveSerializer):
def get_http_headers(self, request: Request) -> dict[str, str]:
"""Get HTTP Request headers"""
headers = {}
raw_session = request._request.COOKIES.get(settings.SESSION_COOKIE_NAME)
for key, value in request.META.items():
if not isinstance(value, str):
continue
actual_value = value
if raw_session in actual_value:
actual_value = actual_value.replace(
raw_session, SafeExceptionReporterFilter.cleansed_substitute
)
headers[key] = actual_value
headers[key] = value
return headers
def get_http_host(self, request: Request) -> str:

View File

@ -1,16 +1,12 @@
"""authentik administration overview"""
from socket import gethostname
from django.conf import settings
from drf_spectacular.utils import extend_schema, inline_serializer
from packaging.version import parse
from rest_framework.fields import BooleanField, CharField
from rest_framework.fields import IntegerField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from authentik import get_full_version
from authentik.rbac.permissions import HasPermission
from authentik.root.celery import CELERY_APP
@ -20,38 +16,11 @@ class WorkerView(APIView):
permission_classes = [HasPermission("authentik_rbac.view_system_info")]
@extend_schema(
responses=inline_serializer(
"Worker",
fields={
"worker_id": CharField(),
"version": CharField(),
"version_matching": BooleanField(),
},
many=True,
)
)
@extend_schema(responses=inline_serializer("Workers", fields={"count": IntegerField()}))
def get(self, request: Request) -> Response:
"""Get currently connected worker count."""
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5)
our_version = parse(get_full_version())
response = []
for worker in raw:
key = list(worker.keys())[0]
version = worker[key].get("version")
version_matching = False
if version:
version_matching = parse(version) == our_version
response.append(
{"worker_id": key, "version": version, "version_matching": version_matching}
)
count = len(CELERY_APP.control.ping(timeout=0.5))
# In debug we run with `task_always_eager`, so tasks are ran on the main process
if settings.DEBUG: # pragma: no cover
response.append(
{
"worker_id": f"authentik-debug@{gethostname()}",
"version": get_full_version(),
"version_matching": True,
}
)
return Response(response)
count += 1
return Response({"count": count})

View File

@ -1,10 +1,11 @@
"""authentik admin app config"""
from prometheus_client import Info
from prometheus_client import Gauge, Info
from authentik.blueprints.apps import ManagedAppConfig
PROM_INFO = Info("authentik_version", "Currently running authentik version")
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
class AuthentikAdminConfig(ManagedAppConfig):

View File

@ -1,35 +1,14 @@
"""admin signals"""
from django.dispatch import receiver
from packaging.version import parse
from prometheus_client import Gauge
from authentik import get_full_version
from authentik.admin.apps import GAUGE_WORKERS
from authentik.root.celery import CELERY_APP
from authentik.root.monitoring import monitoring_set
GAUGE_WORKERS = Gauge(
"authentik_admin_workers",
"Currently connected workers, their versions and if they are the same version as authentik",
["version", "version_matched"],
)
_version = parse(get_full_version())
@receiver(monitoring_set)
def monitoring_set_workers(sender, **kwargs):
"""Set worker gauge"""
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5)
worker_version_count = {}
for worker in raw:
key = list(worker.keys())[0]
version = worker[key].get("version")
version_matching = False
if version:
version_matching = parse(version) == _version
worker_version_count.setdefault(version, {"count": 0, "matching": version_matching})
worker_version_count[version]["count"] += 1
for version, stats in worker_version_count.items():
GAUGE_WORKERS.labels(version, stats["matching"]).set(stats["count"])
count = len(CELERY_APP.control.ping(timeout=0.5))
GAUGE_WORKERS.set(count)

View File

@ -34,7 +34,7 @@ class TestAdminAPI(TestCase):
response = self.client.get(reverse("authentik_api:admin_workers"))
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body), 0)
self.assertEqual(body["count"], 0)
def test_metrics(self):
"""Test metrics API"""

View File

@ -0,0 +1,67 @@
"""API Authorization"""
from django.conf import settings
from django.db.models import Model
from django.db.models.query import QuerySet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.authentication import get_authorization_header
from rest_framework.filters import BaseFilterBackend
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
from authentik.api.authentication import validate_auth
from authentik.rbac.filters import ObjectFilter
class OwnerFilter(BaseFilterBackend):
"""Filter objects by their owner"""
owner_key = "user"
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
if request.user.is_superuser:
return queryset
return queryset.filter(**{self.owner_key: request.user})
class SecretKeyFilter(DjangoFilterBackend):
"""Allow access to all objects when authenticated with secret key as token.
Replaces both DjangoFilterBackend and ObjectFilter"""
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
auth_header = get_authorization_header(request)
token = validate_auth(auth_header)
if token and token == settings.SECRET_KEY:
return queryset
queryset = ObjectFilter().filter_queryset(request, queryset, view)
return super().filter_queryset(request, queryset, view)
class OwnerPermissions(BasePermission):
"""Authorize requests by an object's owner matching the requesting user"""
owner_key = "user"
def has_permission(self, request: Request, view) -> bool:
"""If the user is authenticated, we allow all requests here. For listing, the
object-level permissions are done by the filter backend"""
return request.user.is_authenticated
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
"""Check if the object's owner matches the currently logged in user"""
if not hasattr(obj, self.owner_key):
return False
owner = getattr(obj, self.owner_key)
if owner != request.user:
return False
return True
class OwnerSuperuserPermissions(OwnerPermissions):
"""Similar to OwnerPermissions, except always allow access for superusers"""
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
if request.user.is_superuser:
return True
return super().has_object_permission(request, view, obj)

View File

@ -1,68 +0,0 @@
"""Test and debug Blueprints"""
import atexit
import readline
from pathlib import Path
from pprint import pformat
from sys import exit as sysexit
from textwrap import indent
from django.core.management.base import BaseCommand, no_translations
from structlog.stdlib import get_logger
from yaml import load
from authentik.blueprints.v1.common import BlueprintLoader, EntryInvalidError
from authentik.core.management.commands.shell import get_banner_text
from authentik.lib.utils.errors import exception_to_string
LOGGER = get_logger()
class Command(BaseCommand):
"""Test and debug Blueprints"""
lines = []
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
histfolder = Path("~").expanduser() / Path(".local/share/authentik")
histfolder.mkdir(parents=True, exist_ok=True)
histfile = histfolder / Path("blueprint_shell_history")
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set editing-mode vi")
try:
readline.read_history_file(str(histfile))
except FileNotFoundError:
pass
atexit.register(readline.write_history_file, str(histfile))
@no_translations
def handle(self, *args, **options):
"""Interactively debug blueprint files"""
self.stdout.write(get_banner_text("Blueprint shell"))
self.stdout.write("Type '.eval' to evaluate previously entered statement(s).")
def do_eval():
yaml_input = "\n".join([line for line in self.lines if line])
data = load(yaml_input, BlueprintLoader)
self.stdout.write(pformat(data))
self.lines = []
while True:
try:
line = input("> ")
if line == ".eval":
do_eval()
else:
self.lines.append(line)
except EntryInvalidError as exc:
self.stdout.write("Failed to evaluate expression:")
self.stdout.write(indent(exception_to_string(exc), prefix=" "))
except EOFError:
break
except KeyboardInterrupt:
self.stdout.write()
sysexit(0)
self.stdout.write()

View File

@ -126,7 +126,7 @@ class Command(BaseCommand):
def_name_perm = f"model_{model_path}_permissions"
def_path_perm = f"#/$defs/{def_name_perm}"
self.schema["$defs"][def_name_perm] = self.model_permissions(model)
template = {
return {
"type": "object",
"required": ["model", "identifiers"],
"properties": {
@ -143,11 +143,6 @@ class Command(BaseCommand):
"identifiers": {"$ref": def_path},
},
}
# Meta models don't require identifiers, as there's no matching database model to find
if issubclass(model, BaseMetaModel):
del template["properties"]["identifiers"]
template["required"].remove("identifiers")
return template
def field_to_jsonschema(self, field: Field) -> dict:
"""Convert a single field to json schema"""

View File

@ -202,9 +202,6 @@ class Blueprint:
class YAMLTag:
"""Base class for all YAML Tags"""
def __repr__(self) -> str:
return str(self.resolve(BlueprintEntry(""), Blueprint()))
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
"""Implement yaml tag logic"""
raise NotImplementedError

View File

@ -14,10 +14,10 @@ from rest_framework.response import Response
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import SecretKeyFilter
from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.rbac.filters import SecretKeyFilter
from authentik.tenants.utils import get_current_tenant

View File

@ -2,12 +2,16 @@
from typing import TypedDict
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.fields import SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.viewsets import GenericViewSet
from ua_parser import user_agent_parser
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import AuthenticatedSession
@ -106,4 +110,11 @@ class AuthenticatedSessionViewSet(
search_fields = ["user__username", "last_ip", "last_user_agent"]
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
ordering = ["user__username"]
owner_field = "user"
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)

View File

@ -3,6 +3,7 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.fields import (
BooleanField,
CharField,
@ -16,7 +17,6 @@ from rest_framework.viewsets import ViewSet
from authentik.core.api.utils import MetaNameSerializer
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
from authentik.rbac.decorators import permission_required
from authentik.stages.authenticator import device_classes, devices_for_user
from authentik.stages.authenticator.models import Device
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
@ -73,7 +73,9 @@ class AdminDeviceViewSet(ViewSet):
def get_devices(self, **kwargs):
"""Get all devices in all child classes"""
for model in device_classes():
device_set = model.objects.filter(**kwargs)
device_set = get_objects_for_user(
self.request.user, f"{model._meta.app_label}.view_{model._meta.model_name}", model
).filter(**kwargs)
yield from device_set
@extend_schema(
@ -86,10 +88,6 @@ class AdminDeviceViewSet(ViewSet):
],
responses={200: DeviceSerializer(many=True)},
)
@permission_required(
None,
[f"{model._meta.app_label}.view_{model._meta.model_name}" for model in device_classes()],
)
def list(self, request: Request) -> Response:
"""Get all devices for current user"""
kwargs = {}

View File

@ -2,22 +2,26 @@
from collections.abc import Iterable
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
from authentik.core.models import GroupSourceConnection, Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer
from authentik.lib.api import MultipleFieldLookupMixin
from authentik.lib.utils.file import (
FilePathSerializer,
FileUploadSerializer,
@ -72,6 +76,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
class SourceViewSet(
MultipleFieldLookupMixin,
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
@ -84,6 +89,7 @@ class SourceViewSet(
queryset = Source.objects.none()
serializer_class = SourceSerializer
lookup_field = "slug"
lookup_fields = ["slug", "pbm_uuid"]
search_fields = ["slug", "name"]
filterset_fields = ["slug", "name", "managed"]
@ -186,10 +192,11 @@ class UserSourceConnectionViewSet(
queryset = UserSourceConnection.objects.all()
serializer_class = UserSourceConnectionSerializer
permission_classes = [OwnerSuperuserPermissions]
filterset_fields = ["user", "source__slug"]
search_fields = ["source__slug"]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug", "pk"]
owner_field = "user"
class GroupSourceConnectionSerializer(SourceSerializer):
@ -224,7 +231,8 @@ class GroupSourceConnectionViewSet(
queryset = GroupSourceConnection.objects.all()
serializer_class = GroupSourceConnectionSerializer
permission_classes = [OwnerSuperuserPermissions]
filterset_fields = ["group", "source__slug"]
search_fields = ["source__slug"]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug", "pk"]
owner_field = "user"

View File

@ -3,15 +3,18 @@
from typing import Any
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
@ -135,8 +138,8 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
"managed",
]
ordering = ["identifier", "expires"]
owner_field = "user"
rbac_allow_create_without_perm = True
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()

View File

@ -1,13 +1,14 @@
"""User API Views"""
from datetime import timedelta
from importlib import import_module
from json import loads
from typing import Any
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.contrib.sessions.backends.base import SessionBase
from django.db.models.functions import ExtractHour
from django.db.transaction import atomic
from django.db.utils import IntegrityError
@ -91,6 +92,7 @@ from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
class UserGroupSerializer(ModelSerializer):
@ -427,7 +429,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
queryset = User.objects.none()
ordering = ["username"]
serializer_class = UserSerializer
search_fields = ["username", "name", "is_active", "email", "uuid", "attributes"]
search_fields = ["username", "name", "is_active", "email", "uuid"]
filterset_class = UsersFilter
def get_queryset(self):
@ -585,7 +587,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"""Set password for user"""
user: User = self.get_object()
try:
user.set_password(request.data.get("password"), request=request)
user.set_password(request.data.get("password"))
user.save()
except (ValidationError, IntegrityError) as exc:
LOGGER.debug("Failed to set password", exc=exc)
@ -767,7 +769,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not instance.is_active:
sessions = AuthenticatedSession.objects.filter(user=instance)
session_ids = sessions.values_list("session_key", flat=True)
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
for session in session_ids:
SessionStore(session).delete()
sessions.delete()
LOGGER.debug("Deleted user's sessions", user=instance.username)
return response

View File

@ -44,12 +44,13 @@ class TokenBackend(InbuiltBackend):
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
) -> User | None:
try:
user = User._default_manager.get_by_natural_key(username)
except User.DoesNotExist:
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
User().set_password(password, request=request)
User().set_password(password)
return None
tokens = Token.filter_not_expired(

View File

@ -58,7 +58,6 @@ class PropertyMappingEvaluator(BaseEvaluator):
self._context["user"] = user
if request:
req.http_request = request
self._context["http_request"] = request
req.context.update(**kwargs)
self._context["request"] = req
self._context.update(**kwargs)

View File

@ -17,9 +17,7 @@ from authentik.events.middleware import should_log_model
from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict
def get_banner_text(shell_type="shell") -> str:
return f"""### authentik {shell_type} ({get_full_version()})
BANNER_TEXT = f"""### authentik shell ({get_full_version()})
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
@ -116,4 +114,4 @@ class Command(BaseCommand):
readline.parse_and_bind("tab: complete")
# Run interactive shell
code.interact(banner=get_banner_text(), local=namespace)
code.interact(banner=BANNER_TEXT, local=namespace)

View File

@ -1,45 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0041_applicationentitlement"),
]
operations = [
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(fields=["expires"], name="authentik_c_expires_08251d_idx"),
),
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(fields=["expiring"], name="authentik_c_expirin_9cd839_idx"),
),
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(
fields=["expiring", "expires"], name="authentik_c_expirin_195a84_idx"
),
),
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(fields=["session_key"], name="authentik_c_session_d0f005_idx"),
),
migrations.AddIndex(
model_name="token",
index=models.Index(fields=["expires"], name="authentik_c_expires_a62b4b_idx"),
),
migrations.AddIndex(
model_name="token",
index=models.Index(fields=["expiring"], name="authentik_c_expirin_a1b838_idx"),
),
migrations.AddIndex(
model_name="token",
index=models.Index(
fields=["expiring", "expires"], name="authentik_c_expirin_ba04d9_idx"
),
),
]

View File

@ -356,13 +356,13 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
"""superuser == staff user"""
return self.is_superuser # type: ignore
def set_password(self, raw_password, signal=True, sender=None, request=None):
def set_password(self, raw_password, signal=True, sender=None):
if self.pk and signal:
from authentik.core.signals import password_changed
if not sender:
sender = self
password_changed.send(sender=sender, user=self, password=raw_password, request=request)
password_changed.send(sender=sender, user=self, password=raw_password)
self.password_change_date = now()
return super().set_password(raw_password)
@ -846,11 +846,6 @@ class ExpiringModel(models.Model):
class Meta:
abstract = True
indexes = [
models.Index(fields=["expires"]),
models.Index(fields=["expiring"]),
models.Index(fields=["expiring", "expires"]),
]
def expire_action(self, *args, **kwargs):
"""Handler which is called when this object is expired. By
@ -906,7 +901,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
class Meta:
verbose_name = _("Token")
verbose_name_plural = _("Tokens")
indexes = ExpiringModel.Meta.indexes + [
indexes = [
models.Index(fields=["identifier"]),
models.Index(fields=["key"]),
]
@ -1006,9 +1001,6 @@ class AuthenticatedSession(ExpiringModel):
class Meta:
verbose_name = _("Authenticated Session")
verbose_name_plural = _("Authenticated Sessions")
indexes = ExpiringModel.Meta.indexes + [
models.Index(fields=["session_key"]),
]
def __str__(self) -> str:
return f"Authenticated Session {self.session_key[:10]}"

View File

@ -1,7 +1,10 @@
"""authentik core signals"""
from importlib import import_module
from django.conf import settings
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.contrib.sessions.backends.base import SessionBase
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
@ -25,6 +28,7 @@ password_changed = Signal()
login_failed = Signal()
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
@receiver(post_save, sender=Application)
@ -60,8 +64,7 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
@receiver(pre_delete, sender=AuthenticatedSession)
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
"""Delete session when authenticated session is deleted"""
cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)
SessionStore(instance.session_key).delete()
@receiver(pre_save)

View File

@ -28,6 +28,7 @@ from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import SecretKeyFilter
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY
@ -35,7 +36,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter, SecretKeyFilter
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()

View File

@ -1,27 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_enterprise", "0003_remove_licenseusage_within_limits_and_more"),
]
operations = [
migrations.AddIndex(
model_name="licenseusage",
index=models.Index(fields=["expires"], name="authentik_e_expires_3f2956_idx"),
),
migrations.AddIndex(
model_name="licenseusage",
index=models.Index(fields=["expiring"], name="authentik_e_expirin_11d3d7_idx"),
),
migrations.AddIndex(
model_name="licenseusage",
index=models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_4d558f_idx"
),
),
]

View File

@ -93,4 +93,3 @@ class LicenseUsage(ExpiringModel):
class Meta:
verbose_name = _("License Usage")
verbose_name_plural = _("License Usage Records")
indexes = ExpiringModel.Meta.indexes

View File

@ -1,8 +1,11 @@
"""RAC Provider API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
@ -31,6 +34,12 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
]
class ConnectionTokenOwnerFilter(OwnerFilter):
"""Owner filter for connection tokens (checks session's user)"""
owner_key = "session__user"
class ConnectionTokenViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
@ -46,4 +55,10 @@ class ConnectionTokenViewSet(
filterset_fields = ["endpoint", "session__user", "provider"]
search_fields = ["endpoint__name", "provider__name"]
ordering = ["endpoint__name", "provider__name"]
owner_field = "session__user"
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [
ConnectionTokenOwnerFilter,
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]

View File

@ -1,28 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
("authentik_providers_rac", "0005_alter_racpropertymapping_options"),
]
operations = [
migrations.AddIndex(
model_name="connectiontoken",
index=models.Index(fields=["expires"], name="authentik_p_expires_91f148_idx"),
),
migrations.AddIndex(
model_name="connectiontoken",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_59a5a7_idx"),
),
migrations.AddIndex(
model_name="connectiontoken",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_aed3ca_idx"
),
),
]

View File

@ -211,4 +211,3 @@ class ConnectionToken(ExpiringModel):
class Meta:
verbose_name = _("RAC Connection token")
verbose_name_plural = _("RAC Connection tokens")
indexes = ExpiringModel.Meta.indexes

View File

@ -1,11 +1,14 @@
"""AuthenticatorEndpointGDTCStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
@ -64,7 +67,8 @@ class EndpointDeviceViewSet(
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
owner_field = "user"
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class EndpointAdminDeviceViewSet(ModelViewSet):

View File

@ -1,15 +1,17 @@
"""Notification API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField
from rest_framework.permissions import IsAuthenticated
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.events.api.events import EventSerializer
@ -55,7 +57,8 @@ class NotificationViewSet(
"seen",
"user",
]
owner_field = "user"
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
@extend_schema(
request=OpenApiTypes.NONE,
@ -63,7 +66,7 @@ class NotificationViewSet(
204: OpenApiResponse(description="Marked tasks as read successfully."),
},
)
@action(detail=False, methods=["post"], permission_classes=[IsAuthenticated])
@action(detail=False, methods=["post"])
def mark_all_seen(self, request: Request) -> Response:
"""Mark all the user's notifications as seen"""
Notification.objects.filter(user=request.user, seen=False).update(seen=True)

View File

@ -1,41 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0007_event_authentik_e_action_9a9dd9_idx_and_more"),
]
operations = [
migrations.AddIndex(
model_name="event",
index=models.Index(fields=["expires"], name="authentik_e_expires_8c73a8_idx"),
),
migrations.AddIndex(
model_name="event",
index=models.Index(fields=["expiring"], name="authentik_e_expirin_b5cb5e_idx"),
),
migrations.AddIndex(
model_name="event",
index=models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_e37180_idx"
),
),
migrations.AddIndex(
model_name="systemtask",
index=models.Index(fields=["expires"], name="authentik_e_expires_4d3985_idx"),
),
migrations.AddIndex(
model_name="systemtask",
index=models.Index(fields=["expiring"], name="authentik_e_expirin_81d649_idx"),
),
migrations.AddIndex(
model_name="systemtask",
index=models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_eb3598_idx"
),
),
]

View File

@ -306,7 +306,7 @@ class Event(SerializerModel, ExpiringModel):
class Meta:
verbose_name = _("Event")
verbose_name_plural = _("Events")
indexes = ExpiringModel.Meta.indexes + [
indexes = [
models.Index(fields=["action"]),
models.Index(fields=["user"]),
models.Index(fields=["app"]),
@ -694,4 +694,3 @@ class SystemTask(SerializerModel, ExpiringModel):
permissions = [("run_task", _("Run task"))]
verbose_name = _("System Task")
verbose_name_plural = _("System Tasks")
indexes = ExpiringModel.Meta.indexes

View File

@ -106,9 +106,9 @@ def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_
@receiver(password_changed)
def on_password_changed(sender, user: User, password: str, request: HttpRequest | None, **_):
def on_password_changed(sender, user: User, password: str, **_):
"""Log password change"""
Event.new(EventAction.PASSWORD_SET).from_http(request, user=user)
Event.new(EventAction.PASSWORD_SET).from_http(None, user=user)
@receiver(post_save, sender=Event)

View File

@ -1,7 +1,5 @@
"""Flow Stage API Views"""
from uuid import uuid4
from django.urls.base import reverse
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
@ -29,11 +27,6 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
component = SerializerMethodField()
flow_set = FlowSetSerializer(many=True, required=False)
def to_representation(self, instance: Stage):
if isinstance(instance, Stage) and instance.is_in_memory:
instance.stage_uuid = uuid4()
return super().to_representation(instance)
def get_component(self, obj: Stage) -> str:
"""Get object type so that we know how to edit the object"""
if obj.__class__ == Stage:

View File

@ -88,8 +88,7 @@ class Migration(migrations.Migration):
model_name="flowstagebinding",
name="re_evaluate_policies",
field=models.BooleanField(
default=False,
help_text="Evaluate policies when the Stage is presented to the user.",
default=False, help_text="Evaluate policies when the Stage is present to the user."
),
),
migrations.AddField(

View File

@ -20,7 +20,7 @@ class Migration(migrations.Migration):
model_name="flowstagebinding",
name="re_evaluate_policies",
field=models.BooleanField(
default=True, help_text="Evaluate policies when the Stage is presented to the user."
default=True, help_text="Evaluate policies when the Stage is present to the user."
),
),
]

View File

@ -102,12 +102,8 @@ class Stage(SerializerModel):
user settings are available, or a challenge."""
return None
@property
def is_in_memory(self):
return hasattr(self, "__in_memory_type")
def __str__(self):
if self.is_in_memory:
if hasattr(self, "__in_memory_type"):
return f"In-memory Stage {getattr(self, '__in_memory_type')}"
return f"Stage {self.name}"
@ -231,7 +227,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
)
re_evaluate_policies = models.BooleanField(
default=True,
help_text=_("Evaluate policies when the Stage is presented to the user."),
help_text=_("Evaluate policies when the Stage is present to the user."),
)
invalid_response_action = models.TextField(

View File

@ -109,6 +109,8 @@ class FlowPlan:
def pop(self):
"""Pop next pending stage from bottom of list"""
if not self.markers and not self.bindings:
return
self.markers.pop(0)
self.bindings.pop(0)
@ -156,20 +158,17 @@ class FlowPlan:
final_stage: type[StageView] = self.bindings[-1].stage.view
temp_exec = FlowExecutorView(flow=flow, request=request, plan=self)
temp_exec.current_stage = self.bindings[-1].stage
temp_exec.current_stage_view = final_stage
temp_exec.setup(request, flow.slug)
stage = final_stage(request=request, executor=temp_exec)
return stage.dispatch(request)
get_qs = request.GET.copy()
if request.user.is_authenticated and (
# Object-scoped permission or global permission
request.user.has_perm("authentik_flows.inspect_flow", flow)
or request.user.has_perm("authentik_flows.inspect_flow")
):
get_qs["inspector"] = "available"
response = stage.dispatch(request)
# Ensure we clean the flow state we have in the session before we redirect away
temp_exec.stage_ok()
return response
return redirect_with_qs(
"authentik_core:if-flow",
get_qs,
request.GET,
flow_slug=flow.slug,
)

View File

@ -7,8 +7,8 @@ from django.http import HttpRequest, HttpResponse
from django.test.client import RequestFactory
from django.urls import reverse
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.core.models import User
from authentik.core.tests.utils import create_test_flow
from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import (
FlowDeniedAction,
@ -255,11 +255,7 @@ class TestFlowExecutor(FlowTestCase):
)
binding = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
)
binding2 = FlowStageBinding.objects.create(
target=flow,
@ -282,8 +278,8 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.markers[0].__class__, StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
# Second request, this passes the first dummy stage
response = self.client.post(exec_url)
@ -305,11 +301,7 @@ class TestFlowExecutor(FlowTestCase):
)
binding = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
)
binding2 = FlowStageBinding.objects.create(
target=flow,
@ -318,11 +310,7 @@ class TestFlowExecutor(FlowTestCase):
re_evaluate_policies=True,
)
binding3 = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
order=2,
evaluate_on_plan=True,
re_evaluate_policies=False,
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
)
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
@ -340,9 +328,9 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.markers[0].__class__, StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
self.assertEqual(plan.markers[2].__class__, StageMarker)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertIsInstance(plan.markers[2], StageMarker)
# Second request, this passes the first dummy stage
response = self.client.post(exec_url)
@ -353,8 +341,8 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.bindings[1], binding3)
self.assertEqual(plan.markers[0].__class__, ReevaluateMarker)
self.assertEqual(plan.markers[1].__class__, StageMarker)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], StageMarker)
# third request, this should trigger the re-evaluate
# We do this request without the patch, so the policy results in false
@ -372,11 +360,7 @@ class TestFlowExecutor(FlowTestCase):
)
binding = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
)
binding2 = FlowStageBinding.objects.create(
target=flow,
@ -385,11 +369,7 @@ class TestFlowExecutor(FlowTestCase):
re_evaluate_policies=True,
)
binding3 = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
order=2,
evaluate_on_plan=True,
re_evaluate_policies=False,
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
)
PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0)
@ -407,9 +387,9 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.markers[0].__class__, StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
self.assertEqual(plan.markers[2].__class__, StageMarker)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertIsInstance(plan.markers[2], StageMarker)
# Second request, this passes the first dummy stage
response = self.client.post(exec_url)
@ -420,8 +400,8 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.bindings[1], binding3)
self.assertEqual(plan.markers[0].__class__, ReevaluateMarker)
self.assertEqual(plan.markers[1].__class__, StageMarker)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], StageMarker)
# Third request, this passes the first dummy stage
response = self.client.post(exec_url)
@ -431,7 +411,7 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding3)
self.assertEqual(plan.markers[0].__class__, StageMarker)
self.assertIsInstance(plan.markers[0], StageMarker)
# third request, this should trigger the re-evaluate
# We do this request without the patch, so the policy results in false
@ -449,11 +429,7 @@ class TestFlowExecutor(FlowTestCase):
)
binding = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
)
binding2 = FlowStageBinding.objects.create(
target=flow,
@ -468,11 +444,7 @@ class TestFlowExecutor(FlowTestCase):
re_evaluate_policies=True,
)
binding4 = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
order=2,
evaluate_on_plan=True,
re_evaluate_policies=False,
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
)
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
@ -493,10 +465,10 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.bindings[3], binding4)
self.assertEqual(plan.markers[0].__class__, StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
self.assertEqual(plan.markers[2].__class__, ReevaluateMarker)
self.assertEqual(plan.markers[3].__class__, StageMarker)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertIsInstance(plan.markers[2], ReevaluateMarker)
self.assertIsInstance(plan.markers[3], StageMarker)
# Second request, this passes the first dummy stage
response = self.client.post(exec_url)
@ -547,9 +519,9 @@ class TestFlowExecutor(FlowTestCase):
)
# Stage 0 is a deny stage that is added dynamically
# when the reputation policy says so
deny_stage = DenyStage.objects.create(name=generate_id())
deny_stage = DenyStage.objects.create(name="deny")
reputation_policy = ReputationPolicy.objects.create(
name=generate_id(), threshold=-1, check_ip=False
name="reputation", threshold=-1, check_ip=False
)
deny_binding = FlowStageBinding.objects.create(
target=flow,
@ -562,7 +534,7 @@ class TestFlowExecutor(FlowTestCase):
# Stage 1 is an identification stage
ident_stage = IdentificationStage.objects.create(
name=generate_id(),
name="ident",
user_fields=[UserFields.E_MAIL],
pretend_user_exists=False,
)
@ -587,64 +559,3 @@ class TestFlowExecutor(FlowTestCase):
)
response = self.client.post(exec_url, {"uid_field": "invalid-string"}, follow=True)
self.assertStageResponse(response, flow, component="ak-stage-access-denied")
def test_re_evaluate_group_binding(self):
"""Test re-evaluate stage binding that has a policy binding to a group"""
flow = create_test_flow()
user_group_membership = create_test_user()
user_direct_binding = create_test_user()
user_other = create_test_user()
group_a = Group.objects.create(name=generate_id())
user_group_membership.ak_groups.add(group_a)
# Stage 0 is an identification stage
ident_stage = IdentificationStage.objects.create(
name=generate_id(),
user_fields=[UserFields.USERNAME],
pretend_user_exists=False,
)
FlowStageBinding.objects.create(
target=flow,
stage=ident_stage,
order=0,
)
# Stage 1 is a dummy stage that is only shown for users in group_a
dummy_stage = DummyStage.objects.create(name=generate_id())
dummy_binding = FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1)
PolicyBinding.objects.create(group=group_a, target=dummy_binding, order=0)
PolicyBinding.objects.create(user=user_direct_binding, target=dummy_binding, order=0)
# Stage 2 is a deny stage that (in this case) only user_b will see
deny_stage = DenyStage.objects.create(name=generate_id())
FlowStageBinding.objects.create(target=flow, stage=deny_stage, order=2)
exec_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
with self.subTest(f"Test user access through group: {user_group_membership}"):
self.client.logout()
# First request, run the planner
response = self.client.get(exec_url)
self.assertStageResponse(response, flow, component="ak-stage-identification")
response = self.client.post(
exec_url, {"uid_field": user_group_membership.username}, follow=True
)
self.assertStageResponse(response, flow, component="ak-stage-dummy")
with self.subTest(f"Test user access through user: {user_direct_binding}"):
self.client.logout()
# First request, run the planner
response = self.client.get(exec_url)
self.assertStageResponse(response, flow, component="ak-stage-identification")
response = self.client.post(
exec_url, {"uid_field": user_direct_binding.username}, follow=True
)
self.assertStageResponse(response, flow, component="ak-stage-dummy")
with self.subTest(f"Test user has no access: {user_other}"):
self.client.logout()
# First request, run the planner
response = self.client.get(exec_url)
self.assertStageResponse(response, flow, component="ak-stage-identification")
response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True)
self.assertStageResponse(response, flow, component="ak-stage-access-denied")

View File

@ -8,7 +8,6 @@ from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction
from authentik.lib.generators import generate_id
from authentik.stages.dummy.models import DummyStage
from authentik.stages.identification.models import IdentificationStage, UserFields
@ -27,7 +26,7 @@ class TestFlowInspector(APITestCase):
# Stage 1 is an identification stage
ident_stage = IdentificationStage.objects.create(
name=generate_id(),
name="ident",
user_fields=[UserFields.USERNAME],
)
FlowStageBinding.objects.create(
@ -36,8 +35,9 @@ class TestFlowInspector(APITestCase):
order=1,
invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
)
dummy_stage = DummyStage.objects.create(name=generate_id())
FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1)
FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
)
res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
@ -68,11 +68,9 @@ class TestFlowInspector(APITestCase):
)
content = loads(ins.content)
self.assertEqual(content["is_completed"], False)
self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "ident")
self.assertEqual(
content["current_plan"]["current_stage"]["stage_obj"]["name"], ident_stage.name
)
self.assertEqual(
content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], dummy_stage.name
content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], "dummy2"
)
self.client.post(
@ -86,12 +84,8 @@ class TestFlowInspector(APITestCase):
)
content = loads(ins.content)
self.assertEqual(content["is_completed"], False)
self.assertEqual(
content["plans"][0]["current_stage"]["stage_obj"]["name"], ident_stage.name
)
self.assertEqual(
content["current_plan"]["current_stage"]["stage_obj"]["name"], dummy_stage.name
)
self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident")
self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2")
self.assertEqual(
content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username
)

View File

@ -29,7 +29,6 @@ from authentik.flows.planner import (
cache_key,
)
from authentik.flows.stage import StageView
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost
@ -154,7 +153,7 @@ class TestFlowPlanner(TestCase):
"""Test planner cache"""
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
)
request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
@ -173,7 +172,7 @@ class TestFlowPlanner(TestCase):
"""Test planner with default_context"""
flow = create_test_flow()
FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
)
user = User.objects.create(username="test-user")
@ -192,7 +191,7 @@ class TestFlowPlanner(TestCase):
FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
stage=DummyStage.objects.create(name="dummy1"),
order=0,
re_evaluate_policies=True,
)
@ -205,7 +204,7 @@ class TestFlowPlanner(TestCase):
planner = FlowPlanner(flow)
plan = planner.plan(request)
self.assertEqual(plan.markers[0].__class__, ReevaluateMarker)
self.assertIsInstance(plan.markers[0], ReevaluateMarker)
def test_planner_reevaluate_actual(self):
"""Test planner with re-evaluate"""
@ -213,14 +212,11 @@ class TestFlowPlanner(TestCase):
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
order=0,
re_evaluate_policies=False,
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
target=flow,
stage=DummyStage.objects.create(name=generate_id()),
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,
)
@ -244,8 +240,6 @@ class TestFlowPlanner(TestCase):
self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.markers[0].__class__, StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)

View File

@ -103,7 +103,7 @@ class FlowExecutorView(APIView):
permission_classes = [AllowAny]
flow: Flow
flow: Flow = None
plan: FlowPlan | None = None
current_binding: FlowStageBinding | None = None
@ -114,7 +114,8 @@ class FlowExecutorView(APIView):
def setup(self, request: HttpRequest, flow_slug: str):
super().setup(request, flow_slug=flow_slug)
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
if not self.flow:
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
self._logger = get_logger().bind(flow_slug=flow_slug)
set_tag("authentik.flow", self.flow.slug)

View File

@ -78,9 +78,9 @@ class FlowInspectorView(APIView):
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
if settings.DEBUG:
return
if request.user.has_perm(
if request.user.has_perm("authentik_flows.inspect_flow") or request.user.has_perm(
"authentik_flows.inspect_flow", self.flow
) or request.user.has_perm("authentik_flows.inspect_flow"):
):
return
raise Http404
@ -96,9 +96,6 @@ class FlowInspectorView(APIView):
"""Get current flow state and record it"""
plans = []
for plan in request.session.get(SESSION_KEY_HISTORY, []):
plan: FlowPlan
if plan.flow_pk != self.flow.pk.hex:
continue
plan_serializer = FlowInspectorPlanSerializer(
instance=plan, context={"request": request}
)

37
authentik/lib/api.py Normal file
View File

@ -0,0 +1,37 @@
from collections.abc import Callable, Sequence
from typing import Self
from uuid import UUID
from django.db.models import Model, Q, QuerySet, UUIDField
from django.shortcuts import get_object_or_404
class MultipleFieldLookupMixin:
"""Helper mixin class to add support for multiple lookup_fields.
`lookup_fields` needs to be set which specifies the actual fields to query, `lookup_field`
is only used to generate the URL."""
lookup_field: str
lookup_fields: str | Sequence[str]
get_queryset: Callable[[Self], QuerySet]
filter_queryset: Callable[[Self, QuerySet], QuerySet]
def get_object(self):
queryset: QuerySet = self.get_queryset()
queryset = self.filter_queryset(queryset)
if isinstance(self.lookup_fields, str):
self.lookup_fields = [self.lookup_fields]
query = Q()
model: Model = queryset.model
for field in self.lookup_fields:
field_inst = model._meta.get_field(field)
# Sanity check, if the field we're filtering again, only apply the filter if
# our value looks like a UUID
if isinstance(field_inst, UUIDField):
try:
UUID(self.kwargs[self.lookup_field])
except ValueError:
continue
query |= Q(**{field: self.kwargs[self.lookup_field]})
return get_object_or_404(queryset, query)

View File

@ -283,7 +283,8 @@ class ConfigLoader:
def get_optional_int(self, path: str, default=None) -> int | None:
"""Wrapper for get that converts value into int or None if set"""
value = self.get(path, default)
if value is UNSET:
return default
try:
return int(value)
except (ValueError, TypeError) as exc:

View File

@ -9,25 +9,20 @@ from typing import Any
from cachetools import TLRUCache, cached
from django.core.exceptions import FieldError
from django.http import HttpRequest
from django.utils.text import slugify
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import ValidationError
from sentry_sdk import start_span
from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, User
from authentik.core.models import User
from authentik.events.models import Event
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
from authentik.stages.authenticator import devices_for_user
LOGGER = get_logger()
@ -61,7 +56,6 @@ class BaseEvaluator:
"ak_logger": get_logger(self._filename).bind(),
"ak_user_by": BaseEvaluator.expr_user_by,
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
"ak_create_jwt": self.expr_create_jwt,
"ip_address": ip_address,
"ip_network": ip_network,
"list_flatten": BaseEvaluator.expr_flatten,
@ -188,36 +182,6 @@ class BaseEvaluator:
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
return proc.profiling_wrapper()
def expr_create_jwt(
self,
user: User,
provider: OAuth2Provider | str,
scopes: list[str],
validity: str = "seconds=60",
) -> str | None:
"""Issue a JWT for a given provider"""
request: HttpRequest = self._context.get("http_request")
if not request:
return None
if not isinstance(provider, OAuth2Provider):
provider = OAuth2Provider.objects.get(name=provider)
session = None
if hasattr(request, "session") and request.session.session_key:
session = AuthenticatedSession.objects.filter(
session_key=request.session.session_key
).first()
access_token = AccessToken(
provider=provider,
user=user,
expires=now() + timedelta_from_string(validity),
scope=scopes,
auth_time=now(),
session=session,
)
access_token.id_token = IDToken.new(provider, access_token, request)
access_token.save()
return access_token.token
def wrap_expression(self, expression: str) -> str:
"""Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())

View File

@ -1,15 +1,11 @@
"""Test Evaluator base functions"""
from django.test import RequestFactory, TestCase
from django.urls import reverse
from jwt import decode
from django.test import TestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event
from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
class TestEvaluator(TestCase):
@ -45,35 +41,3 @@ class TestEvaluator(TestCase):
event = Event.objects.filter(action="custom_foo").first()
self.assertIsNotNone(event)
self.assertEqual(event.context, {"bar": "baz", "foo": "bar"})
@apply_blueprint("system/providers-oauth2.yaml")
def test_expr_create_jwt(self):
"""Test expr_create_jwt"""
rf = RequestFactory()
user = create_test_user()
provider = OAuth2Provider.objects.create(
name=generate_id(),
authorization_flow=create_test_flow(),
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
managed__in=[
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-profile",
]
)
)
evaluator = BaseEvaluator(generate_id())
evaluator._context = {
"http_request": rf.get(reverse("authentik_core:root-redirect")),
"user": user,
"provider": provider.name,
}
jwt = evaluator.evaluate(
"return ak_create_jwt(user, provider, ['openid', 'email', 'profile'])"
)
decoded = decode(
jwt, provider.client_secret, algorithms=["HS256"], audience=provider.client_id
)
self.assertEqual(decoded["preferred_username"], user.username)

View File

@ -207,7 +207,7 @@ class KubernetesObjectReconciler(Generic[T]):
"app.kubernetes.io/instance": slugify(self.controller.outpost.name),
"app.kubernetes.io/managed-by": "goauthentik.io",
"app.kubernetes.io/name": f"authentik-{self.controller.outpost.type.lower()}",
"app.kubernetes.io/version": get_version().replace("+", "-"),
"app.kubernetes.io/version": get_version(),
"goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
"goauthentik.io/outpost-type": str(self.controller.outpost.type),
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,

View File

@ -94,7 +94,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
meta = self.get_object_meta(name=self.name)
image_name = self.controller.get_container_image()
image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets
version = get_full_version().replace("+", "-")
version = get_full_version()
return V1Deployment(
metadata=meta,
spec=V1DeploymentSpec(

View File

@ -13,7 +13,7 @@ if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController
@dataclass(slots=True)
@dataclass
class PrometheusServiceMonitorSpecEndpoint:
"""Prometheus ServiceMonitor endpoint spec"""
@ -21,14 +21,14 @@ class PrometheusServiceMonitorSpecEndpoint:
path: str = field(default="/metrics")
@dataclass(slots=True)
@dataclass
class PrometheusServiceMonitorSpecSelector:
"""Prometheus ServiceMonitor selector spec"""
matchLabels: dict
@dataclass(slots=True)
@dataclass
class PrometheusServiceMonitorSpec:
"""Prometheus ServiceMonitor spec"""
@ -37,7 +37,7 @@ class PrometheusServiceMonitorSpec:
selector: PrometheusServiceMonitorSpecSelector
@dataclass(slots=True)
@dataclass
class PrometheusServiceMonitorMetadata:
"""Prometheus ServiceMonitor metadata"""
@ -46,7 +46,7 @@ class PrometheusServiceMonitorMetadata:
labels: dict = field(default_factory=dict)
@dataclass(slots=True)
@dataclass
class PrometheusServiceMonitor:
"""Prometheus ServiceMonitor"""

View File

@ -1,30 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_policies_reputation",
"0007_reputation_authentik_p_identif_9434d7_idx_and_more",
),
]
operations = [
migrations.AddIndex(
model_name="reputation",
index=models.Index(fields=["expires"], name="authentik_p_expires_da493f_idx"),
),
migrations.AddIndex(
model_name="reputation",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_2ab34f_idx"),
),
migrations.AddIndex(
model_name="reputation",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_2a8ec7_idx"
),
),
]

View File

@ -96,7 +96,7 @@ class Reputation(ExpiringModel, SerializerModel):
verbose_name = _("Reputation Score")
verbose_name_plural = _("Reputation Scores")
unique_together = ("identifier", "ip")
indexes = ExpiringModel.Meta.indexes + [
indexes = [
models.Index(fields=["identifier"]),
models.Index(fields=["ip"]),
models.Index(fields=["ip", "identifier"]),

View File

@ -1,72 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
("authentik_providers_oauth2", "0026_alter_accesstoken_session_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddIndex(
model_name="accesstoken",
index=models.Index(fields=["expires"], name="authentik_p_expires_9f24a5_idx"),
),
migrations.AddIndex(
model_name="accesstoken",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_2d9205_idx"),
),
migrations.AddIndex(
model_name="accesstoken",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_c74005_idx"
),
),
migrations.AddIndex(
model_name="authorizationcode",
index=models.Index(fields=["expires"], name="authentik_p_expires_f594b2_idx"),
),
migrations.AddIndex(
model_name="authorizationcode",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_6a5e2c_idx"),
),
migrations.AddIndex(
model_name="authorizationcode",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_c0f353_idx"
),
),
migrations.AddIndex(
model_name="devicetoken",
index=models.Index(fields=["expires"], name="authentik_p_expires_961437_idx"),
),
migrations.AddIndex(
model_name="devicetoken",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_4fd278_idx"),
),
migrations.AddIndex(
model_name="devicetoken",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_cd6b1c_idx"
),
),
migrations.AddIndex(
model_name="refreshtoken",
index=models.Index(fields=["expires"], name="authentik_p_expires_c479a7_idx"),
),
migrations.AddIndex(
model_name="refreshtoken",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_d4d17f_idx"),
),
migrations.AddIndex(
model_name="refreshtoken",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_acb4a5_idx"
),
),
]

View File

@ -425,7 +425,6 @@ class AuthorizationCode(SerializerModel, ExpiringModel, BaseGrantModel):
class Meta:
verbose_name = _("Authorization Code")
verbose_name_plural = _("Authorization Codes")
indexes = ExpiringModel.Meta.indexes
def __str__(self):
return f"Authorization code for {self.provider_id} for user {self.user_id}"
@ -454,7 +453,7 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
_id_token = models.TextField()
class Meta:
indexes = ExpiringModel.Meta.indexes + [
indexes = [
HashIndex(fields=["token"]),
]
verbose_name = _("OAuth2 Access Token")
@ -505,7 +504,7 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
)
class Meta:
indexes = ExpiringModel.Meta.indexes + [
indexes = [
HashIndex(fields=["token"]),
]
verbose_name = _("OAuth2 Refresh Token")
@ -557,7 +556,6 @@ class DeviceToken(ExpiringModel):
class Meta:
verbose_name = _("Device Token")
verbose_name_plural = _("Device Tokens")
indexes = ExpiringModel.Meta.indexes
def __str__(self):
return f"Device Token for {self.provider_id}"

View File

@ -49,9 +49,7 @@ class TesOAuth2DeviceInit(OAuthTestCase):
kwargs={
"flow_slug": self.device_flow.slug,
},
)
+ "?"
+ urlencode({"inspector": "available"}),
),
)
def test_device_init_post(self):
@ -65,9 +63,7 @@ class TesOAuth2DeviceInit(OAuthTestCase):
kwargs={
"flow_slug": self.device_flow.slug,
},
)
+ "?"
+ urlencode({"inspector": "available"}),
),
)
res = self.api_client.get(
reverse(
@ -122,9 +118,7 @@ class TesOAuth2DeviceInit(OAuthTestCase):
kwargs={
"flow_slug": provider.authorization_flow.slug,
},
)
+ "?"
+ urlencode({"inspector": "available"}),
),
},
)
@ -156,7 +150,7 @@ class TesOAuth2DeviceInit(OAuthTestCase):
},
)
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code, "inspector": "available"}),
+ urlencode({QS_KEY_CODE: token.user_code}),
)
def test_device_init_denied(self):

View File

@ -12,7 +12,6 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_cert,
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import (
AccessToken,
ClientTypes,
IDToken,
OAuth2Provider,
RedirectURI,
@ -109,29 +108,3 @@ class TesOAuth2Revoke(OAuthTestCase):
},
)
self.assertEqual(res.status_code, 401)
def test_revoke_public(self):
"""Test revoke public client"""
self.provider.client_type = ClientTypes.PUBLIC
self.provider.save()
token: AccessToken = AccessToken.objects.create(
provider=self.provider,
user=self.user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
),
)
auth_public = b64encode(f"{self.provider.client_id}:{generate_id()}".encode()).decode()
res = self.client.post(
reverse("authentik_providers_oauth2:token-revoke"),
HTTP_AUTHORIZATION=f"Basic {auth_public}",
data={
"token": token.token,
},
)
self.assertEqual(res.status_code, 200)

View File

@ -178,18 +178,12 @@ def protected_resource_view(scopes: list[str]):
return wrapper
def provider_from_request(request: HttpRequest) -> tuple[OAuth2Provider | None, str, str]:
"""Get provider from Basic auth of client_id:client_secret. Does not perform authentication"""
client_id, client_secret = extract_client_auth(request)
if client_id == client_secret == "":
return None, "", ""
provider: OAuth2Provider | None = OAuth2Provider.objects.filter(client_id=client_id).first()
return provider, client_id, client_secret
def authenticate_provider(request: HttpRequest) -> OAuth2Provider | None:
"""Attempt to authenticate via Basic auth of client_id:client_secret"""
provider, client_id, client_secret = provider_from_request(request)
client_id, client_secret = extract_client_auth(request)
if client_id == client_secret == "":
return None
provider: OAuth2Provider | None = OAuth2Provider.objects.filter(client_id=client_id).first()
if not provider:
return None
if client_id != provider.client_id or client_secret != provider.client_secret:

View File

@ -499,11 +499,11 @@ class OAuthFulfillmentStage(StageView):
)
challenge.is_valid()
self.executor.stage_ok()
return HttpChallengeResponse(
challenge=challenge,
)
self.executor.stage_ok()
return HttpResponseRedirectScheme(uri, allowed_schemes=[parsed.scheme])
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@ -9,12 +9,8 @@ from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger
from authentik.providers.oauth2.errors import TokenRevocationError
from authentik.providers.oauth2.models import AccessToken, ClientTypes, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.utils import (
TokenResponse,
authenticate_provider,
provider_from_request,
)
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, RefreshToken
from authentik.providers.oauth2.utils import TokenResponse, authenticate_provider
LOGGER = get_logger()
@ -31,9 +27,7 @@ class TokenRevocationParams:
"""Extract required Parameters from HTTP Request"""
raw_token = request.POST.get("token")
provider, _, _ = provider_from_request(request)
if provider and provider.client_type == ClientTypes.CONFIDENTIAL:
provider = authenticate_provider(request)
provider = authenticate_provider(request)
if not provider:
raise TokenRevocationError("invalid_client")

View File

@ -15,7 +15,7 @@ if TYPE_CHECKING:
from authentik.outposts.controllers.kubernetes import KubernetesController
@dataclass(slots=True)
@dataclass
class TraefikMiddlewareSpecForwardAuth:
"""traefik middleware forwardAuth spec"""
@ -28,14 +28,14 @@ class TraefikMiddlewareSpecForwardAuth:
trustForwardHeader: bool = field(default=True)
@dataclass(slots=True)
@dataclass
class TraefikMiddlewareSpec:
"""Traefik middleware spec"""
forwardAuth: TraefikMiddlewareSpecForwardAuth
@dataclass(slots=True)
@dataclass
class TraefikMiddlewareMetadata:
"""Traefik Middleware metadata"""
@ -44,7 +44,7 @@ class TraefikMiddlewareMetadata:
labels: dict = field(default_factory=dict)
@dataclass(slots=True)
@dataclass
class TraefikMiddleware:
"""Traefik Middleware"""

View File

@ -16,7 +16,6 @@ from rest_framework.decorators import action
from rest_framework.fields import CharField, FileField, SerializerMethodField
from rest_framework.parsers import MultiPartParser
from rest_framework.permissions import AllowAny
from rest_framework.renderers import BaseRenderer, JSONRenderer
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import PrimaryKeyRelatedField, ValidationError
@ -39,16 +38,6 @@ from authentik.sources.saml.processors.constants import SAML_BINDING_POST, SAML_
LOGGER = get_logger()
class RawXMLDataRenderer(BaseRenderer):
"""Renderer to allow application/xml as value for 'Accept' in the metadata endpoint."""
media_type = "application/xml"
format = "xml"
def render(self, data, accepted_media_type=None, renderer_context=None):
return data
class SAMLProviderSerializer(ProviderSerializer):
"""SAMLProvider Serializer"""
@ -65,23 +54,9 @@ class SAMLProviderSerializer(ProviderSerializer):
if "request" not in self._context:
return ""
request: HttpRequest = self._context["request"]._request
try:
return request.build_absolute_uri(
reverse(
"authentik_providers_saml:metadata-download",
kwargs={"application_slug": instance.application.slug},
)
)
except Provider.application.RelatedObjectDoesNotExist:
return request.build_absolute_uri(
reverse(
"authentik_api:samlprovider-metadata",
kwargs={
"pk": instance.pk,
},
)
+ "?download"
)
return request.build_absolute_uri(
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": instance.pk}) + "?download"
)
def get_url_sso_post(self, instance: SAMLProvider) -> str:
"""Get SSO Post URL"""
@ -249,21 +224,9 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
],
description="Optionally force the metadata to only include one binding.",
),
# Explicitly excluded, because otherwise spectacular automatically
# add it when using multiple renderer_classes
OpenApiParameter(
name="format",
exclude=True,
required=False,
),
],
)
@action(
methods=["GET"],
detail=True,
permission_classes=[AllowAny],
renderer_classes=[JSONRenderer, RawXMLDataRenderer],
)
@action(methods=["GET"], detail=True, permission_classes=[AllowAny])
def metadata(self, request: Request, pk: int) -> Response:
"""Return metadata as XML string"""
# We don't use self.get_object() on purpose as this view is un-authenticated
@ -281,9 +244,9 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
f'attachment; filename="{provider.name}_authentik_meta.xml"'
)
return response
return Response({"metadata": metadata}, content_type="application/json")
return Response({"metadata": metadata})
except Provider.application.RelatedObjectDoesNotExist:
return Response({"metadata": ""}, content_type="application/json")
return Response({"metadata": ""})
@permission_required(
None,

View File

@ -104,22 +104,6 @@ class TestSAMLProviderAPI(APITestCase):
)
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
# Test download with Accept: application/xml
response = self.client.get(
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk})
+ "?download",
HTTP_ACCEPT="application/xml",
)
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
response = self.client.get(
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk})
+ "?download",
HTTP_ACCEPT="application/xml;charset=UTF-8",
)
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
def test_metadata_invalid(self):
"""Test metadata export (invalid)"""
@ -137,11 +121,6 @@ class TestSAMLProviderAPI(APITestCase):
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": "abc"}),
)
self.assertEqual(404, response.status_code)
response = self.client.get(
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk}),
HTTP_ACCEPT="application/invalid-mime-type",
)
self.assertEqual(406, response.status_code)
def test_import_success(self):
"""Test metadata import (success case)"""

View File

@ -2,7 +2,7 @@
from django.apps import apps
from django.contrib.auth.models import Permission
from django.db.models import QuerySet
from django.db.models import Q, QuerySet
from django_filters.filters import ModelChoiceFilter
from django_filters.filterset import FilterSet
from django_filters.rest_framework import DjangoFilterBackend
@ -18,6 +18,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.blueprints.v1.importer import excluded_models
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User
from authentik.lib.validators import RequiredTogetherValidator
@ -105,13 +106,13 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
]
def get_queryset(self) -> QuerySet:
return (
Permission.objects.all()
.select_related("content_type")
.filter(
content_type__app_label__startswith="authentik",
query = Q()
for model in excluded_models():
query |= Q(
content_type__app_label=model._meta.app_label,
content_type__model=model._meta.model_name,
)
)
return Permission.objects.all().select_related("content_type").exclude(query)
class PermissionAssignSerializer(PassiveSerializer):

View File

@ -1,15 +1,10 @@
"""RBAC API Filter"""
from django.conf import settings
from django.db.models import QuerySet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.authentication import get_authorization_header
from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request
from rest_framework.views import APIView
from rest_framework_guardian.filters import ObjectPermissionsFilter
from authentik.api.authentication import validate_auth
from authentik.core.models import UserTypes
@ -17,7 +12,7 @@ class ObjectFilter(ObjectPermissionsFilter):
"""Object permission filter that grants global permission higher priority than
per-object permissions"""
def filter_queryset(self, request: Request, queryset: QuerySet, view: APIView) -> QuerySet:
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
permission = self.perm_format % {
"app_label": queryset.model._meta.app_label,
"model_name": queryset.model._meta.model_name,
@ -26,9 +21,6 @@ class ObjectFilter(ObjectPermissionsFilter):
# per-object permissions
if request.user.has_perm(permission):
return queryset
# User does not have permissions, but we have an owner field defined, so filter by that
if owner_field := getattr(view, "owner_field", None):
return queryset.filter(**{owner_field: request.user})
queryset = super().filter_queryset(request, queryset, view)
# Outposts (which are the only objects using internal service accounts)
# except requests to return an empty list when they have no objects
@ -40,17 +32,3 @@ class ObjectFilter(ObjectPermissionsFilter):
# and also no object permissions assigned (directly or via role)
raise PermissionDenied()
return queryset
class SecretKeyFilter(DjangoFilterBackend):
"""Allow access to all objects when authenticated with secret key as token.
Replaces both DjangoFilterBackend and ObjectFilter"""
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
auth_header = get_authorization_header(request)
token = validate_auth(auth_header)
if token and token == settings.SECRET_KEY:
return queryset
queryset = ObjectFilter().filter_queryset(request, queryset, view)
return super().filter_queryset(request, queryset, view)

View File

@ -15,17 +15,6 @@ class ObjectPermissions(DjangoObjectPermissions):
lookup = getattr(view, "lookup_url_kwarg", None) or getattr(view, "lookup_field", None)
if lookup and lookup in view.kwargs:
return True
# Legacy behaviour:
# Allow creation of objects even without explicit permission
queryset = self._queryset(view)
required_perms = self.get_required_permissions(request.method, queryset.model)
if (
len(required_perms) == 1
and f"{queryset.model._meta.app_label}.add_{queryset.model._meta.model_name}"
in required_perms
and getattr(view, "rbac_allow_create_without_perm", False)
):
return True
return super().has_permission(request, view)
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
@ -35,10 +24,6 @@ class ObjectPermissions(DjangoObjectPermissions):
# Rank global permissions higher than per-object permissions
if request.user.has_perms(perms):
return True
# Allow access for owners if configured
if owner_field := getattr(view, "owner_field", None):
if getattr(obj, owner_field) == request.user:
return True
return super().has_object_permission(request, view, obj)

View File

@ -18,7 +18,6 @@ from celery.signals import (
task_prerun,
worker_ready,
)
from celery.worker.control import inspect_command
from django.conf import settings
from django.db import ProgrammingError
from django_tenants.utils import get_public_schema_name
@ -26,7 +25,6 @@ from structlog.contextvars import STRUCTLOG_KEY_PREFIX
from structlog.stdlib import get_logger
from tenant_schemas_celery.app import CeleryApp as TenantAwareCeleryApp
from authentik import get_full_version
from authentik.lib.sentry import before_send
from authentik.lib.utils.errors import exception_to_string
@ -161,12 +159,6 @@ class LivenessProbe(bootsteps.StartStopStep):
HEARTBEAT_FILE.touch()
@inspect_command(default_timeout=0.2)
def ping(state, **kwargs):
"""Ping worker(s)."""
return {"ok": "pong", "version": get_full_version()}
CELERY_APP.config_from_object(settings.CELERY)
# Load task modules from all registered Django app configs.

View File

@ -13,6 +13,7 @@ from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.events.api.tasks import SystemTaskSerializer
from authentik.lib.api import MultipleFieldLookupMixin
from authentik.sources.kerberos.models import KerberosSource
from authentik.sources.kerberos.tasks import CACHE_KEY_STATUS
@ -59,12 +60,13 @@ class KerberosSyncStatusSerializer(PassiveSerializer):
tasks = SystemTaskSerializer(many=True, read_only=True)
class KerberosSourceViewSet(UsedByMixin, ModelViewSet):
class KerberosSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
"""Kerberos Source Viewset"""
queryset = KerberosSource.objects.all()
serializer_class = KerberosSourceSerializer
lookup_field = "slug"
lookup_fields = ["slug", "pbm_uuid"]
filterset_fields = [
"name",
"slug",

View File

@ -1,7 +1,10 @@
"""Kerberos Source Serializer"""
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.sources import (
GroupSourceConnectionSerializer,
GroupSourceConnectionViewSet,
@ -29,8 +32,9 @@ class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
serializer_class = UserKerberosSourceConnectionSerializer
filterset_fields = ["source__slug"]
search_fields = ["source__slug"]
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug"]
owner_field = "user"
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):

View File

@ -28,14 +28,14 @@ class KerberosBackend(InbuiltBackend):
if "@" in username:
username, realm = username.rsplit("@", 1)
user, source = self.auth_user(request, username, realm, **kwargs)
user, source = self.auth_user(username, realm, **kwargs)
if user:
self.set_method("kerberos", request, source=source)
return user
return None
def auth_user(
self, request: HttpRequest, username: str, realm: str | None, password: str, **filters
self, username: str, realm: str | None, password: str, **filters
) -> tuple[User | None, KerberosSource | None]:
sources = KerberosSource.objects.filter(enabled=True)
user = User.objects.filter(
@ -76,7 +76,7 @@ class KerberosBackend(InbuiltBackend):
user=user_source_connection.user,
)
user_source_connection.user.set_password(
password, sender=user_source_connection.source, request=request
password, sender=user_source_connection.source
)
user_source_connection.user.save()
return user_source_connection.user, user_source_connection.source

View File

@ -12,7 +12,6 @@ from django.db.models.fields import b64decode
from django.http import HttpRequest
from django.shortcuts import reverse
from django.templatetags.static import static
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from kadmin import KAdmin, KAdminApiVersion
from kadmin.exceptions import PyKAdminException
@ -174,18 +173,12 @@ class KerberosSource(Source):
def get_base_user_properties(self, principal: str, **kwargs):
localpart, _ = principal.rsplit("@", 1)
properties = {
return {
"username": localpart,
"type": UserTypes.INTERNAL,
"path": self.get_user_path(),
}
if "principal_obj" in kwargs:
princ_expiry = kwargs["principal_obj"].expire_time
properties["is_active"] = princ_expiry is None or princ_expiry > now()
return properties
def get_base_group_properties(self, group_id: str, **kwargs):
return {
"name": group_id,

View File

@ -18,6 +18,7 @@ from authentik.core.api.property_mappings import PropertyMappingFilterSet, Prope
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.api import MultipleFieldLookupMixin
from authentik.lib.sync.outgoing.api import SyncStatusSerializer
from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping
from authentik.sources.ldap.tasks import CACHE_KEY_STATUS, SYNC_CLASSES
@ -103,12 +104,13 @@ class LDAPSourceSerializer(SourceSerializer):
extra_kwargs = {"bind_password": {"write_only": True}}
class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
class LDAPSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
"""LDAP Source Viewset"""
queryset = LDAPSource.objects.all()
serializer_class = LDAPSourceSerializer
lookup_field = "slug"
lookup_fields = ["slug", "pbm_uuid"]
filterset_fields = [
"name",
"slug",

View File

@ -20,15 +20,13 @@ class LDAPBackend(InbuiltBackend):
return None
for source in LDAPSource.objects.filter(enabled=True):
LOGGER.debug("LDAP Auth attempt", source=source)
user = self.auth_user(request, source, **kwargs)
user = self.auth_user(source, **kwargs)
if user:
self.set_method("ldap", request, source=source)
return user
return None
def auth_user(
self, request: HttpRequest, source: LDAPSource, password: str, **filters: str
) -> User | None:
def auth_user(self, source: LDAPSource, password: str, **filters: str) -> User | None:
"""Try to bind as either user_dn or mail with password.
Returns True on success, otherwise False"""
users = User.objects.filter(**filters)
@ -45,7 +43,7 @@ class LDAPBackend(InbuiltBackend):
if source.password_login_update_internal_password:
# Password given successfully binds to LDAP, so we save it in our Database
LOGGER.debug("Updating user's password in DB", user=user)
user.set_password(password, sender=source, request=request)
user.set_password(password, sender=source)
user.save()
return user
# Password doesn't match

View File

@ -16,6 +16,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.lib.api import MultipleFieldLookupMixin
from authentik.lib.utils.http import get_http_session
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.registry import SourceType, registry
@ -170,12 +171,13 @@ class OAuthSourceFilter(FilterSet):
]
class OAuthSourceViewSet(UsedByMixin, ModelViewSet):
class OAuthSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
"""Source Viewset"""
queryset = OAuthSource.objects.all()
serializer_class = OAuthSourceSerializer
lookup_field = "slug"
lookup_fields = ["slug", "pbm_uuid"]
filterset_class = OAuthSourceFilter
search_fields = ["name", "slug"]
ordering = ["name"]

View File

@ -81,12 +81,7 @@ class OAuth2Client(BaseOAuthClient):
if self.source.source_type.urls_customizable and self.source.access_token_url:
access_token_url = self.source.access_token_url
response = self.do_request(
"post",
access_token_url,
auth=(self.get_client_id(), self.get_client_secret()),
data=args,
headers=self._default_headers,
**request_kwargs,
"post", access_token_url, data=args, headers=self._default_headers, **request_kwargs
)
response.raise_for_status()
except RequestException as exc:

View File

@ -18,6 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views.executor import to_stage_response
from authentik.lib.api import MultipleFieldLookupMixin
from authentik.rbac.decorators import permission_required
from authentik.sources.plex.models import PlexSource, UserPlexSourceConnection
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager
@ -45,12 +46,13 @@ class PlexTokenRedeemSerializer(PassiveSerializer):
plex_token = CharField()
class PlexSourceViewSet(UsedByMixin, ModelViewSet):
class PlexSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
"""Plex source Viewset"""
queryset = PlexSource.objects.all()
serializer_class = PlexSourceSerializer
lookup_field = "slug"
lookup_fields = ["slug", "pbm_uuid"]
filterset_fields = [
"name",
"slug",

View File

@ -9,6 +9,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.lib.api import MultipleFieldLookupMixin
from authentik.providers.saml.api.providers import SAMLMetadataSerializer
from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.metadata import MetadataProcessor
@ -37,12 +38,13 @@ class SAMLSourceSerializer(SourceSerializer):
]
class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
class SAMLSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
"""SAMLSource Viewset"""
queryset = SAMLSource.objects.all()
serializer_class = SAMLSourceSerializer
lookup_field = "slug"
lookup_fields = ["slug", "pbm_uuid"]
filterset_fields = [
"name",
"slug",

View File

@ -7,6 +7,7 @@ from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.tokens import TokenSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.lib.api import MultipleFieldLookupMixin
from authentik.sources.scim.models import SCIMSource
@ -47,12 +48,13 @@ class SCIMSourceSerializer(SourceSerializer):
]
class SCIMSourceViewSet(UsedByMixin, ModelViewSet):
class SCIMSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
"""SCIMSource Viewset"""
queryset = SCIMSource.objects.all()
serializer_class = SCIMSourceSerializer
lookup_field = "slug"
lookup_fields = ["slug", "pbm_uuid"]
filterset_fields = ["name", "slug"]
search_fields = ["name", "slug", "token__identifier", "token__user__username"]
ordering = ["name"]

View File

@ -88,55 +88,6 @@ class TestSCIMUsers(APITestCase):
).exists()
)
def test_user_create_duplicate_by_username(self):
"""Test user create"""
user = create_test_user()
username = generate_id()
obj1 = {
"userName": username,
"externalId": generate_id(),
"emails": [
{
"primary": True,
"value": user.email,
}
],
}
obj2 = obj1.copy()
obj2.update({"externalId": generate_id()})
response = self.client.post(
reverse(
"authentik_sources_scim:v2-users",
kwargs={
"source_slug": self.source.slug,
},
),
data=dumps(obj1),
content_type=SCIM_CONTENT_TYPE,
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
)
self.assertEqual(response.status_code, 201)
self.assertTrue(
SCIMSourceUser.objects.filter(source=self.source, user__username=username).exists()
)
self.assertTrue(
Event.objects.filter(
action=EventAction.MODEL_CREATED, user__username=self.source.token.user.username
).exists()
)
response = self.client.post(
reverse(
"authentik_sources_scim:v2-users",
kwargs={
"source_slug": self.source.slug,
},
),
data=dumps(obj2),
content_type=SCIM_CONTENT_TYPE,
HTTP_AUTHORIZATION=f"Bearer {self.source.token.key}",
)
self.assertEqual(response.status_code, 409)
def test_user_property_mappings(self):
"""Test user property_mappings"""
self.source.user_property_mappings.set(

View File

@ -2,7 +2,6 @@
from uuid import uuid4
from django.db.models import Q
from django.db.transaction import atomic
from django.http import Http404, QueryDict
from django.urls import reverse
@ -114,11 +113,8 @@ class UsersView(SCIMObjectView):
def post(self, request: Request, **kwargs) -> Response:
"""Create user handler"""
connection = SCIMSourceUser.objects.filter(
Q(
Q(user__uuid=request.data.get("id"))
| Q(user__username=request.data.get("userName"))
),
source=self.source,
user__uuid=request.data.get("id"),
).first()
if connection:
self.logger.debug("Found existing user")

View File

@ -1,18 +1,20 @@
"""AuthenticatorDuoStage API Views"""
from django.http import Http404
from django_filters.rest_framework.backends import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import CharField, ChoiceField, IntegerField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger
from authentik.core.api.groups import GroupMemberSerializer
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer
@ -166,11 +168,9 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
class DuoDeviceSerializer(ModelSerializer):
"""Serializer for Duo authenticator devices"""
user = GroupMemberSerializer(read_only=True)
class Meta:
model = DuoDevice
fields = ["pk", "name", "user"]
fields = ["pk", "name"]
depth = 2
@ -189,7 +189,8 @@ class DuoDeviceViewSet(
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
owner_field = "user"
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class DuoAdminDeviceViewSet(ModelViewSet):

View File

@ -1,9 +1,11 @@
"""AuthenticatorSMSStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer
@ -42,11 +44,9 @@ class AuthenticatorSMSStageViewSet(UsedByMixin, ModelViewSet):
class SMSDeviceSerializer(ModelSerializer):
"""Serializer for sms authenticator devices"""
user = GroupMemberSerializer(read_only=True)
class Meta:
model = SMSDevice
fields = ["name", "pk", "phone_number", "user"]
fields = ["name", "pk", "phone_number"]
depth = 2
extra_kwargs = {
"phone_number": {"read_only": True},
@ -65,10 +65,11 @@ class SMSDeviceViewSet(
queryset = SMSDevice.objects.all()
serializer_class = SMSDeviceSerializer
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
owner_field = "user"
class SMSAdminDeviceViewSet(ModelViewSet):

View File

@ -1,9 +1,11 @@
"""AuthenticatorStaticStage API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer
@ -49,11 +51,10 @@ class StaticDeviceSerializer(ModelSerializer):
"""Serializer for static authenticator devices"""
token_set = StaticDeviceTokenSerializer(many=True, read_only=True)
user = GroupMemberSerializer(read_only=True)
class Meta:
model = StaticDevice
fields = ["name", "token_set", "pk", "user"]
fields = ["name", "token_set", "pk"]
class StaticDeviceViewSet(
@ -68,10 +69,11 @@ class StaticDeviceViewSet(
queryset = StaticDevice.objects.filter(confirmed=True)
serializer_class = StaticDeviceSerializer
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
owner_field = "user"
class StaticAdminDeviceViewSet(ModelViewSet):

View File

@ -1,10 +1,12 @@
"""AuthenticatorTOTPStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.fields import ChoiceField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer
@ -38,14 +40,11 @@ class AuthenticatorTOTPStageViewSet(UsedByMixin, ModelViewSet):
class TOTPDeviceSerializer(ModelSerializer):
"""Serializer for totp authenticator devices"""
user = GroupMemberSerializer(read_only=True)
class Meta:
model = TOTPDevice
fields = [
"name",
"pk",
"user",
]
depth = 2
@ -62,10 +61,11 @@ class TOTPDeviceViewSet(
queryset = TOTPDevice.objects.filter(confirmed=True)
serializer_class = TOTPDeviceSerializer
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
owner_field = "user"
class TOTPAdminDeviceViewSet(ModelViewSet):

View File

@ -1,9 +1,11 @@
"""AuthenticatorWebAuthnStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.stages.authenticator_webauthn.api.device_types import WebAuthnDeviceTypeSerializer
@ -14,11 +16,10 @@ class WebAuthnDeviceSerializer(ModelSerializer):
"""Serializer for WebAuthn authenticator devices"""
device_type = WebAuthnDeviceTypeSerializer(read_only=True, allow_null=True)
user = GroupMemberSerializer(read_only=True)
class Meta:
model = WebAuthnDevice
fields = ["pk", "name", "created_on", "device_type", "aaguid", "user"]
fields = ["pk", "name", "created_on", "device_type", "aaguid"]
extra_kwargs = {
"aaguid": {"read_only": True},
}
@ -39,7 +40,8 @@ class WebAuthnDeviceViewSet(
search_fields = ["name"]
filterset_fields = ["name"]
ordering = ["name"]
owner_field = "user"
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class WebAuthnAdminDeviceViewSet(ModelViewSet):

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,12 @@
"""ConsentStage API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.applications import ApplicationSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
@ -53,4 +57,11 @@ class UserConsentViewSet(
filterset_fields = ["user", "application"]
ordering = ["application", "expires"]
search_fields = ["user__username"]
owner_field = "user"
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)

View File

@ -1,30 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
("authentik_stages_consent", "0006_alter_userconsent_expires"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddIndex(
model_name="userconsent",
index=models.Index(fields=["expires"], name="authentik_s_expires_0e99e8_idx"),
),
migrations.AddIndex(
model_name="userconsent",
index=models.Index(fields=["expiring"], name="authentik_s_expirin_8f51e5_idx"),
),
migrations.AddIndex(
model_name="userconsent",
index=models.Index(
fields=["expiring", "expires"], name="authentik_s_expirin_e50090_idx"
),
),
]

View File

@ -71,4 +71,3 @@ class UserConsent(SerializerModel, ExpiringModel):
unique_together = (("user", "application", "permissions"),)
verbose_name = _("User Consent")
verbose_name_plural = _("User Consents")
indexes = ExpiringModel.Meta.indexes

View File

@ -1,30 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0027_auto_20231028_1424"),
("authentik_stages_invitation", "0008_alter_invitation_expires"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddIndex(
model_name="invitation",
index=models.Index(fields=["expires"], name="authentik_s_expires_96f4b8_idx"),
),
migrations.AddIndex(
model_name="invitation",
index=models.Index(fields=["expiring"], name="authentik_s_expirin_4f8f35_idx"),
),
migrations.AddIndex(
model_name="invitation",
index=models.Index(
fields=["expiring", "expires"], name="authentik_s_expirin_4f8096_idx"
),
),
]

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