Compare commits

..

29 Commits

Author SHA1 Message Date
1bc8fa0a9d Merge branch 'main' into web/legibility/dual-select-events 2025-02-25 09:32:42 +01:00
3ec0d30965 web: restructure ak-dual-select to avoid using CustomEvent
# What

- Replace all `CustomEvent<T>` handlers with a child class Event.
- Use the standard `dispatchEvent()` and `addEventListener()` clauses.
- Move common methods of the two panes into an abstract parent class.

# Why

CustomEvent is a bit of a side-show in JavaScript; it's perfectly acceptable to inherit from Event,
and there's much better support for type management while using it, plus deconstructing the received
event object is cleaner with child Events than unpacking the `details` object.

This codebase is now more open to change, and is closer to our still-evolving style.

Doing it this way mean that, once you have an event defined, you don't have to remember if you're
sending a custom event or a normal event, and you don't need all that infrastructure for making your
Lit objects sensitive to custom event handling.  There's enough mixin clutter already.

And just like I hate clutter, I hate duplication.  The two panes had a lot in common with ARIA
handling and storing, clearing, and assigning selected items to the "pending move" table.  Moving
all that into a parent class exposed the differences: one is a source and the other a sink; one
reflects changes made, the other possible changes to be made.

# Testing

The Storybook tests have all been updated to match the codebase, and there are standalone tests for
the various components (pagination, pane control, search) that can be exercised before deployment.
2025-01-15 16:20:12 -08:00
50d2f69332 Merge branch 'main' into dev
* main:
  website: revise full development environment instructions (#12638)
  website: bump typescript from 5.7.2 to 5.7.3 in /website (#12620)
  website: bump aws-cdk from 2.174.1 to 2.175.0 in /website (#12621)
  ci: bump docker/setup-qemu-action from 3.2.0 to 3.3.0 (#12622)
  core: bump twilio from 9.4.1 to 9.4.2 (#12623)
  core: bump python-kadmin-rs from 0.5.2 to 0.5.3 (#12624)
  core: bump ruff from 0.8.6 to 0.9.0 (#12625)
  core: bump pydantic from 2.10.4 to 2.10.5 (#12626)
  core: bump google-api-python-client from 2.157.0 to 2.158.0 (#12628)
  core: bump goauthentik.io/api/v3 from 3.2024121.3 to 3.2024122.1 (#12629)
  web: bump API Client version (#12617)
  release: 2024.12.2 (#12615)
  website/docs: prepare 2024.12.2 release notes (#12614)
  providers/saml: fix invalid SAML Response when assertion and response are signed (#12611)
  core: fix error when creating new user with default path (#12609)
  rbac: permissions endpoint: allow authenticated users (#12608)
  website/docs: update customer portal (#12603)
  website/docs: policy for email whitelist: modernize (#12558)
2025-01-10 16:26:36 -08:00
7d972ec711 Merge branch 'main' into dev
* main:
  lib: add expression helper ak_create_jwt to create JWTs (#12599)
  api: cleanup owner permissions (#12598)
  website: bump aws-cdk from 2.174.0 to 2.174.1 in /website (#12593)
  core: bump aws-cdk-lib from 2.174.0 to 2.174.1 (#12594)
  website/integrations: portainer: group config steps (#12548)
  translate: Updates for file web/xliff/en.xlf in fi (#12586)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in fi (#12584)
  website/docs: fix Nginx redirection example (#12561)
2025-01-08 10:13:59 -08:00
854427e463 Merge branch 'main' into dev
* main:
  core: bump golang.org/x/oauth2 from 0.24.0 to 0.25.0 (#12571)
  website: bump the docusaurus group in /website with 9 updates (#12569)
  core: bump github.com/coreos/go-oidc/v3 from 3.11.0 to 3.12.0 (#12572)
  core: bump ruff from 0.8.5 to 0.8.6 (#12573)
  ci: release: fix AWS cfn template permissions (#12576)
  translate: Updates for file web/xliff/en.xlf in fr (#12578)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#12577)
  sources/kerberos: authenticate with the user's username instead of the first username in authentik (#12497)
  website/integrations: Fix deprecated terraform ressource authentik_scope_mapping in docs (#12554)
  website/user-sources Fix Free IPA docs page (#12549)
  core: bump aws-cdk-lib from 2.173.4 to 2.174.0 (#12574)
  website/integrations: semaphore: fix formatting (#12567)
  website: bump aws-cdk from 2.173.4 to 2.174.0 in /website (#12570)
  website/integrations: Update Frappe Application index.md (#12527)
  website: add api reference docs to redirect file (#12551)
2025-01-06 08:32:50 -08:00
be349e2e14 Merge branch 'main' into dev
* main:
  core: bump github.com/getsentry/sentry-go from 0.30.0 to 0.31.1 (#12543)
  core: bump google-api-python-client from 2.156.0 to 2.157.0 (#12544)
  core: bump ruff from 0.8.4 to 0.8.5 (#12545)
  core: bump msgraph-sdk from 1.15.0 to 1.16.0 (#12546)
  Update index.mdx (#12542)
  web: fix source selection and outpost integration health (#12530)
  Ading a step to paperless guide (#12539)
  website/integrations: Semaphore (#12515)
  website/integrations: komga: document (#12476)
  website/integrations: fix missing quote in paperless-ngx (#12537)
  website/integrations: cloudflare access: upd placeholder for saas (#12536)
  website/integrations: veeam-enterprise-manager: don't hardcode helpcenter doc version (#12538)
2025-01-03 08:09:25 -08:00
bd0e81b8ad Merge branch 'main' into dev
* main:
  website/integrations: meshcentral: document (#12509)
  stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#12524)
  core: bump goauthentik.io/api/v3 from 3.2024121.2 to 3.2024121.3 (#12522)
  web: bump API Client version (#12520)
  website/integrations: chronograf: document (#12474)
  website/integrations: update preparation placeholder (#12507)
  providers/saml: fix handle Accept: application/xml for SAML Metadata endpoint (#12483) (#12518)
  core: bump aws-cdk-lib from 2.173.3 to 2.173.4 (#12513)
  website: bump aws-cdk from 2.173.3 to 2.173.4 in /website (#12514)
  core: bump coverage from 7.6.9 to 7.6.10 (#12499)
  core: bump aws-cdk-lib from 2.173.2 to 2.173.3 (#12500)
  website: bump aws-cdk from 2.173.2 to 2.173.3 in /website (#12501)
  core: bump github.com/go-ldap/ldap/v3 from 3.4.9 to 3.4.10 (#12502)
  website/docs: New "Whats Up Docker" URL (#12488)
2025-01-02 10:01:26 -08:00
f6afb59515 Revert "This (temporary) change is needed to prevent the unit tests from failing."
This reverts commit dddde09be5.
2024-12-26 13:59:53 -08:00
dddde09be5 This (temporary) change is needed to prevent the unit tests from failing.
\# What

\# Why

\# How

\# Designs

\# Test Steps

\# Other Notes
2024-12-26 13:59:00 -08:00
6d7fc94698 Merge branch 'main' into dev
* main: (118 commits)
  outposts: fix version label (#12486)
  web: only load version context when authenticated (#12482)
  core: bump goauthentik.io/api/v3 from 3.2024120.2 to 3.2024121.2 (#12478)
  ci: bump helm/kind-action from 1.11.0 to 1.12.0 (#12479)
  web: fix build dev build (#12473)
  root: fix dev build version being invalid semver (#12472)
  internal: fix missing trailing slash in outpost websocket (#12470)
  web: bump API Client version (#12469)
  admin: monitor worker version (#12463)
  core: bump jinja2 from 3.1.4 to 3.1.5 (#12467)
  web: bump API Client version (#12468)
  release: 2024.12.1 (#12466)
  web: misc fixes for admin and flow inspector (#12461)
  website/docs: 2024.12.1 release notes (#12462)
  core: bump goauthentik.io/api/v3 from 3.2024120.1 to 3.2024120.2 (#12456)
  core: bump urllib3 from 2.2.3 to 2.3.0 (#12457)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#12454)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#12453)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#12455)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#12458)
  ...
2024-12-26 08:49:23 -08:00
1dcf9108ad Merge branch 'main' into dev
* main:
  flows: better test stage's challenge responses (#12316)
  enterprise/stages/authenticator_endpoint_gdtc: don't set frame options globally (#12311)
  stages/identification: fix invalid challenge warning when no captcha stage is set (#12312)
  website/docs: prepare 2024.10.5 release notes (#12309)
  website: bump nanoid from 3.3.7 to 3.3.8 in /website (#12307)
  flows: silent authz flow (#12213)
  root:  use healthcheck in depends_on for postgres and redis (#12301)
  ci: ensure mark jobs always run and reflect correct status (#12288)
  enterprise: allow deletion/modification of users when in read-only mode (#12289)
  web/flows: resize captcha iframes (#12260)
2024-12-10 09:07:45 -08:00
7bb6a3dfe6 Merge branch 'main' into dev
* main:
  website/docs: add page about the Cobalt pentest (#12249)
  core: bump aws-cdk-lib from 2.171.1 to 2.172.0 (#12296)
  website: bump aws-cdk from 2.171.1 to 2.172.0 in /website (#12295)
  core: bump sentry-sdk from 2.19.1 to 2.19.2 (#12297)
  core: bump coverage from 7.6.8 to 7.6.9 (#12299)
  core, web: update translations (#12290)
  root: fix override locale only if it is not empty (#12283)
  translate: Updates for file web/xliff/en.xlf in fr (#12276)
  core: bump twilio from 9.3.7 to 9.3.8 (#12282)
  website: bump path-to-regexp and express in /website (#12279)
  core: bump sentry-sdk from 2.19.0 to 2.19.1 (#12280)
  core: bump ruff from 0.8.1 to 0.8.2 (#12281)
  website/docs: fix lint (#12287)
  website/integrations: netbird: fix redirect URI regex (#12284)
2024-12-09 08:28:52 -08:00
9cc440eee1 Merge branch 'main' into dev
* main:
  web: simplify `?inline` handler for Storybook (#12246)
  website/docs: Update Traefik middleware example to reflect latest version of Traefik (#12267)
  website/docs: add . in https://netbird.company* (#12166)
  core: bump goauthentik.io/api/v3 from 3.2024104.1 to 3.2024104.2 (#12263)
  core: bump pydantic from 2.10.2 to 2.10.3 (#12262)
  core: bump github.com/getsentry/sentry-go from 0.29.1 to 0.30.0 (#12264)
  core, web: update translations (#12268)
  website: bump @types/react from 18.3.12 to 18.3.13 in /website (#12269)
  website: bump prettier from 3.4.1 to 3.4.2 in /website (#12270)
  ci: bump actions/attest-build-provenance from 1 to 2 (#12271)
  core: bump golang.org/x/sync from 0.9.0 to 0.10.0 (#12272)
  core: bump django from 5.0.9 to 5.0.10 (#12273)
  core: bump webauthn from 2.3.0 to 2.4.0 (#12274)
  website/integrations: add The Lounge (#11971)
  core: bump python-kadmin-rs from 0.3.0 to 0.4.0 (#12257)
  root: fix health status code (#12255)
  ci: fix should_push always being false (#12252)
  web: bump API Client version (#12251)
  providers/oauth2: Add provider federation between OAuth2 Providers (#12083)
  website/integrations: mastodon: set correct uid field (#11945)
2024-12-05 10:47:00 -08:00
fe9e4526ac Merge branch 'main' into dev
* main: (31 commits)
  web/admin: bugfix: dual select initialization revision (#12051)
  web: update tests for Chromedriver 131 (#12199)
  website/integrations: add Aruba Orchestrator (#12220)
  core: bump aws-cdk-lib from 2.167.1 to 2.171.1 (#12237)
  website: bump aws-cdk from 2.167.1 to 2.171.1 in /website (#12241)
  core, web: update translations (#12236)
  core: bump python-kadmin-rs from 0.2.0 to 0.3.0 (#12238)
  core: bump pytest from 8.3.3 to 8.3.4 (#12239)
  core: bump drf-spectacular from 0.27.2 to 0.28.0 (#12240)
  core, web: update translations (#12222)
  core: Bump ruff from 0.8.0 to 0.8.1 (#12224)
  core: Bump ua-parser from 0.18.0 to 1.0.0 (#12225)
  core: Bump msgraph-sdk from 1.13.0 to 1.14.0 (#12226)
  stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#12234)
  website/docs: install: add aws (#12082)
  core: Bump pyjwt from 2.10.0 to 2.10.1 (#12217)
  core: Bump fido2 from 1.1.3 to 1.2.0 (#12218)
  core: Bump cryptography from 43.0.3 to 44.0.0 (#12219)
  providers/oauth2: allow m2m for JWKS without alg in keys (#12196)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#12210)
  ...
2024-12-02 09:10:36 -08:00
20b66f850c Merge branch 'main' into dev
* main:
  website/docs: Add note about single group per role (#12169)
  website/docs: Fix documentation about attribute merging for indirect membership (#12168)
  root: support running authentik in subpath (#8675)
  docs: fix contribution link (#12189)
  core, web: update translations (#12190)
  core: Bump msgraph-sdk from 1.12.0 to 1.13.0 (#12191)
  core: Bump selenium from 4.26.1 to 4.27.0 (#12192)
2024-11-26 09:27:51 -08:00
67b327414b Merge branch 'main' into dev
* main:
  website/docs: Fix CSP syntax (#12124)
2024-11-25 13:11:45 -08:00
5b8d86b5a9 Merge branch 'main' into dev
* main:
  ci: only mirror if secret is available (#12181)
  root: fix database ssl options not set correctly (#12180)
  core, web: update translations (#12145)
  core: bump tornado from 6.4.1 to 6.4.2 (#12165)
  website: bump the docusaurus group in /website with 9 updates (#12172)
  website: bump typescript from 5.6.3 to 5.7.2 in /website (#12173)
  ci: bump actions/checkout from 3 to 4 (#12174)
  core: bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#12175)
  core: bump coverage from 7.6.7 to 7.6.8 (#12176)
  core: bump ruff from 0.7.4 to 0.8.0 (#12177)
2024-11-25 09:21:19 -08:00
67aed3e318 Merge branch 'main' into dev
* main: (33 commits)
  ci: mirror repo to internal repo (#12160)
  core: bump goauthentik.io/api/v3 from 3.2024102.2 to 3.2024104.1 (#12149)
  core: bump debugpy from 1.8.8 to 1.8.9 (#12150)
  core: bump webauthn from 2.2.0 to 2.3.0 (#12151)
  core: bump pydantic from 2.10.0 to 2.10.1 (#12152)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#12156)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#12157)
  core: bump sentry-sdk from 2.18.0 to 2.19.0 (#12153)
  web: bump API Client version (#12147)
  root: Backport version change (#12146)
  website/docs: update info about footer links to match new UI (#12120)
  website/docs: prepare release notes (#12142)
  providers/oauth2: fix migration (#12138)
  providers/oauth2: fix migration dependencies (#12123)
  web: bump API Client version (#12129)
  providers/oauth2: fix redirect uri input (#12122)
  providers/proxy: fix redirect_uri (#12121)
  website/docs: prepare release notes (#12119)
  web: bump API Client version (#12118)
  security: fix CVE 2024 52289 (#12113)
  ...
2024-11-22 13:56:13 -08:00
9809b94030 Merge branch 'main' into dev
* main: (28 commits)
  providers/scim: accept string and int for SCIM IDs (#12093)
  website: bump the docusaurus group in /website with 9 updates (#12086)
  core: fix source_flow_manager throwing error when authenticated user attempts to re-authenticate with existing link (#12080)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in de (#12079)
  scripts: remove read_replicas from generated dev config (#12078)
  core: bump geoip2 from 4.8.0 to 4.8.1 (#12071)
  core: bump goauthentik.io/api/v3 from 3.2024100.2 to 3.2024102.2 (#12072)
  core: bump maxmind/geoipupdate from v7.0.1 to v7.1.0 (#12073)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#12074)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#12075)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#12076)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#12077)
  web/admin: auto-prefill user path for new users based on selected path (#12070)
  core: bump aiohttp from 3.10.2 to 3.10.11 (#12069)
  web/admin: fix brand title not respected in application list (#12068)
  core: bump pyjwt from 2.9.0 to 2.10.0 (#12063)
  web: add italian locale (#11958)
  web/admin: better footer links (#12004)
  core, web: update translations (#12052)
  core: bump twilio from 9.3.6 to 9.3.7 (#12061)
  ...
2024-11-20 10:23:54 -08:00
e7527c551b Merge branch 'main' into dev
* main:
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#12045)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#12047)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#12044)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#12046)
  web/flows: fix invisible captcha call (#12048)
  rbac: fix incorrect object_description for object-level permissions (#12029)
  stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#12036)
  core: bump coverage from 7.6.4 to 7.6.5 (#12037)
  ci: bump codecov/codecov-action from 4 to 5 (#12038)
  release: 2024.10.2 (#12031)
2024-11-15 12:47:17 -08:00
36b10b434a :wqge branch 'main' into dev
* main:
  providers/ldap: fix global search_full_directory permission not being sufficient (#12028)
  website/docs: 2024.10.2 release notes (#12025)
  lifecycle: fix ak exit status not being passed (#12024)
  core: use versioned_script for path only (#12003)
  core, web: update translations (#12020)
  core: bump google-api-python-client from 2.152.0 to 2.153.0 (#12021)
  providers/oauth2: fix manual device code entry (#12017)
  crypto: validate that generated certificate's name is unique (#12015)
  core, web: update translations (#12006)
  core: bump google-api-python-client from 2.151.0 to 2.152.0 (#12007)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#12011)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#12010)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#12012)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#12013)
  providers/proxy: fix Issuer when AUTHENTIK_HOST_BROWSER is set (#11968)
  website/docs: move S3 ad GeoIP to System Management/Operations (#11998)
  website/integrations: nextcloud: add SSE warning (#11976)
2024-11-14 11:29:13 -08:00
831797b871 Merge branch 'main' into dev
* main: (21 commits)
  web: bump API Client version (#11997)
  sources/kerberos: use new python-kadmin implementation (#11932)
  core: add ability to provide reason for impersonation (#11951)
  website/integrations:  update vcenter integration docs (#11768)
  core, web: update translations (#11995)
  website: bump postcss from 8.4.48 to 8.4.49 in /website (#11996)
  web: bump API Client version (#11992)
  blueprints: add default Password policy (#11793)
  stages/captcha: Run interactive captcha in Frame (#11857)
  core, web: update translations (#11979)
  core: bump packaging from 24.1 to 24.2 (#11985)
  core: bump ruff from 0.7.2 to 0.7.3 (#11986)
  core: bump msgraph-sdk from 1.11.0 to 1.12.0 (#11987)
  website: bump the docusaurus group in /website with 9 updates (#11988)
  website: bump postcss from 8.4.47 to 8.4.48 in /website (#11989)
  stages/password: use recovery flow from brand (#11953)
  core: bump golang.org/x/sync from 0.8.0 to 0.9.0 (#11962)
  web: bump cookie, swagger-client and express in /web (#11966)
  core, web: update translations (#11959)
  core: bump debugpy from 1.8.7 to 1.8.8 (#11961)
  ...
2024-11-12 10:08:48 -08:00
5cc2c0f45f Merge branch 'main' into dev
* main:
  ci: fix dockerfile warning (#11956)
2024-11-07 12:58:15 -08:00
32442766f4 Merge branch 'main' into dev
* main:
  website/docs: fix slug matching redirect URI causing broken refresh (#11950)
  website/integrations: jellyfin: update plugin catalog location (#11948)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in de (#11942)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#11946)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#11947)
  website/docs: clarify traefik ingress setup (#11938)
  core: bump importlib-metadata from 8.4.0 to 8.5.0 (#11934)
  web: bump API Client version (#11930)
  root: backport version bump `2024.10.1` (#11929)
  website/docs: `2024.10.1` Release Notes (#11926)
  website: bump path-to-regexp from 1.8.0 to 1.9.0 in /website (#11924)
  core: bump sentry-sdk from 2.17.0 to 2.18.0 (#11918)
  website: bump the docusaurus group in /website with 9 updates (#11917)
  core: bump goauthentik.io/api/v3 from 3.2024100.1 to 3.2024100.2 (#11915)
  core, web: update translations (#11914)
2024-11-07 08:08:46 -08:00
75790909a8 Merge branch 'main' into dev
* main:
  web: bump API Client version (#11909)
  enterprise/rac: fix API Schema for invalidation_flow (#11907)
2024-11-04 13:20:15 -08:00
e0d5df89ca Merge branch 'main' into dev
* main:
  core: add `None` check to a device's `extra_description` (#11904)
  providers/oauth2: fix size limited index for tokens (#11879)
  web: fix missing status code on failed build (#11903)
  website: bump docusaurus-theme-openapi-docs from 4.1.0 to 4.2.0 in /website (#11897)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in de (#11891)
  stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#11884)
  translate: Updates for file web/xliff/en.xlf in tr (#11878)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in tr (#11866)
  core: bump google-api-python-client from 2.149.0 to 2.151.0 (#11885)
  core: bump selenium from 4.26.0 to 4.26.1 (#11886)
  core, web: update translations (#11896)
  website: bump docusaurus-plugin-openapi-docs from 4.1.0 to 4.2.0 in /website (#11898)
  core: bump watchdog from 5.0.3 to 6.0.0 (#11899)
  core: bump ruff from 0.7.1 to 0.7.2 (#11900)
  core: bump django-pglock from 1.6.2 to 1.7.0 (#11901)
  website/docs: fix release notes to say Federation (#11889)
2024-11-04 10:25:52 -08:00
f25a9c624e Merge branch 'main' into dev
* main:
  website: bump elliptic from 6.5.7 to 6.6.0 in /website (#11869)
  core: bump selenium from 4.25.0 to 4.26.0 (#11875)
  core: bump goauthentik.io/api/v3 from 3.2024083.14 to 3.2024100.1 (#11876)
  website/docs: add info about invalidation flow, default flows in general (#11800)
  website: fix docs redirect (#11873)
  website: remove RC disclaimer for version 2024.10 (#11871)
  website: update supported versions (#11841)
  web: bump API Client version (#11870)
  root: backport version bump 2024.10.0 (#11868)
  website/docs: 2024.8.4 release notes (#11862)
  web/admin: provide default invalidation flows for LDAP and Radius (#11861)
2024-11-04 10:25:45 -08:00
914993a788 Merge branch 'main' into dev
* main: (43 commits)
  core, web: update translations (#11858)
  web/admin: fix code-based MFA toggle not working in wizard (#11854)
  sources/kerberos: add kiprop to ignored system principals (#11852)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#11846)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in it (#11845)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#11847)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11848)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#11849)
  translate: Updates for file web/xliff/en.xlf in it (#11850)
  website: 2024.10 Release Notes (#11839)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11814)
  core, web: update translations (#11821)
  core: bump goauthentik.io/api/v3 from 3.2024083.13 to 3.2024083.14 (#11830)
  core: bump service-identity from 24.1.0 to 24.2.0 (#11831)
  core: bump twilio from 9.3.5 to 9.3.6 (#11832)
  core: bump pytest-randomly from 3.15.0 to 3.16.0 (#11833)
  website/docs: Update social-logins github (#11822)
  website/docs: remove � (#11823)
  lifecycle: fix kdc5-config missing (#11826)
  website/docs: update preview status of different features (#11817)
  ...
2024-10-30 10:04:59 -07:00
89dad07a66 web: Add InvalidationFlow to Radius Provider dialogues
## What

- Bugfix: adds the InvalidationFlow to the Radius Provider dialogues
  - Repairs: `{"invalidation_flow":["This field is required."]}` message, which was *not* propagated
    to the Notification.
- Nitpick: Pretties `?foo=${true}` expressions: `s/\?([^=]+)=\$\{true\}/\1/`

## Note

Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.
2024-10-23 14:17:30 -07:00
612 changed files with 11730 additions and 23151 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2025.2.3
current_version = 2025.2.0
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*))?
@ -17,8 +17,6 @@ optional_value = final
[bumpversion:file:pyproject.toml]
[bumpversion:file:uv.lock]
[bumpversion:file:package.json]
[bumpversion:file:docker-compose.yml]

View File

@ -1,22 +0,0 @@
---
name: Documentation issue
about: Suggest an improvement or report a problem
title: ""
labels: documentation
assignees: ""
---
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
**Provide the URL or link to the exact page in the documentation to which you are referring.**
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the documentation issue here.
**Consider opening a PR!**
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).

View File

@ -44,6 +44,7 @@ if is_release:
]
if not prerelease:
image_tags += [
f"{name}:latest",
f"{name}:{version_family}",
]
else:

View File

@ -9,22 +9,17 @@ inputs:
runs:
using: "composite"
steps:
- name: Install apt deps
- name: Install poetry & deps
shell: bash
run: |
pipx install poetry || true
sudo apt-get update
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Setup python
- name: Setup python and restore poetry
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"
- name: Install Python deps
shell: bash
run: uv sync --all-extras --dev --frozen
cache: "poetry"
- name: Setup node
uses: actions/setup-node@v4
with:
@ -35,18 +30,15 @@ runs:
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Setup docker cache
uses: ScribeMD/docker-cache@0.5.0
with:
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
- name: Setup dependencies
shell: bash
run: |
export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/docker-compose.yml up -d
poetry sync
cd web && npm ci
- name: Generate config
shell: uv run python {0}
shell: poetry run python {0}
run: |
from authentik.lib.generators import generate_id
from yaml import safe_dump

View File

@ -11,7 +11,7 @@ services:
- 5432:5432
restart: always
redis:
image: docker.io/library/redis:7
image: docker.io/library/redis
ports:
- 6379:6379
restart: always

View File

@ -1,32 +1,7 @@
akadmin
asgi
assertIn
authentik
authn
crate
docstrings
entra
goauthentik
gunicorn
hass
jwe
jwks
keypair
keypairs
kubernetes
oidc
ontext
openid
passwordless
plex
saml
scim
singed
slo
sso
totp
traefik
# https://github.com/codespell-project/codespell/issues/1224
upToDate
hass
warmup
webauthn
ontext
singed
assertIn

View File

@ -82,12 +82,6 @@ updates:
docusaurus:
patterns:
- "@docusaurus/*"
build:
patterns:
- "@swc/*"
- "swc-*"
- "lightningcss*"
- "@rspack/binding*"
- package-ecosystem: npm
directory: "/lifecycle/aws"
schedule:
@ -98,7 +92,7 @@ updates:
prefix: "lifecycle/aws:"
labels:
- dependencies
- package-ecosystem: uv
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily

View File

@ -40,7 +40,7 @@ jobs:
attestations: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3.6.0
- uses: docker/setup-qemu-action@v3.4.0
- uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables

View File

@ -33,7 +33,7 @@ jobs:
npm ci
- name: Check changes have been applied
run: |
uv run make aws-cfn
poetry run make aws-cfn
git diff --exit-code
ci-aws-cfn-mark:
if: always()

View File

@ -15,8 +15,8 @@ jobs:
matrix:
version:
- docs
- version-2025-2
- version-2024-12
- version-2024-10
steps:
- uses: actions/checkout@v4
- run: |

View File

@ -34,7 +34,7 @@ jobs:
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run job
run: uv run make ci-${{ matrix.job }}
run: poetry run make ci-${{ matrix.job }}
test-migrations:
runs-on: ubuntu-latest
steps:
@ -42,7 +42,7 @@ jobs:
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run migrations
run: uv run python -m lifecycle.migrate
run: poetry run python -m lifecycle.migrate
test-make-seed:
runs-on: ubuntu-latest
steps:
@ -69,21 +69,19 @@ jobs:
fetch-depth: 0
- name: checkout stable
run: |
# Delete all poetry envs
rm -rf /home/runner/.cache/pypoetry
# Copy current, latest config to local
# Temporarly comment the .github backup while migrating to uv
cp authentik/lib/default.yml local.env.yml
# cp -R .github ..
cp -R .github ..
cp -R scripts ..
git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
# rm -rf .github/ scripts/
# mv ../.github ../scripts .
rm -rf scripts/
mv ../scripts .
rm -rf .github/ scripts/
mv ../.github ../scripts .
- name: Setup authentik env (stable)
uses: ./.github/actions/setup
with:
postgresql_version: ${{ matrix.psql }}
continue-on-error: true
- name: run migrations to stable
run: poetry run python -m lifecycle.migrate
- name: checkout current code
@ -93,13 +91,15 @@ jobs:
git reset --hard HEAD
git clean -d -fx .
git checkout $GITHUB_SHA
# Delete previous poetry env
rm -rf /home/runner/.cache/pypoetry/virtualenvs/*
- name: Setup authentik env (ensure latest deps are installed)
uses: ./.github/actions/setup
with:
postgresql_version: ${{ matrix.psql }}
- name: migrate to latest
run: |
uv run python -m lifecycle.migrate
poetry run python -m lifecycle.migrate
- name: run tests
env:
# Test in the main database that we just migrated from the previous stable version
@ -108,7 +108,7 @@ jobs:
CI_RUN_ID: ${{ matrix.run_id }}
CI_TOTAL_RUNS: "5"
run: |
uv run make ci-test
poetry run make ci-test
test-unittest:
name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
runs-on: ubuntu-latest
@ -133,7 +133,7 @@ jobs:
CI_RUN_ID: ${{ matrix.run_id }}
CI_TOTAL_RUNS: "5"
run: |
uv run make ci-test
poetry run make ci-test
- if: ${{ always() }}
uses: codecov/codecov-action@v5
with:
@ -156,8 +156,8 @@ jobs:
uses: helm/kind-action@v1.12.0
- name: run integration
run: |
uv run coverage run manage.py test tests/integration
uv run coverage xml
poetry run coverage run manage.py test tests/integration
poetry run coverage xml
- if: ${{ always() }}
uses: codecov/codecov-action@v5
with:
@ -214,8 +214,8 @@ jobs:
npm run build
- name: run e2e
run: |
uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage xml
poetry run coverage run manage.py test ${{ matrix.job.glob }}
poetry run coverage xml
- if: ${{ always() }}
uses: codecov/codecov-action@v5
with:

View File

@ -29,7 +29,7 @@ jobs:
- name: Generate API
run: make gen-client-go
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout 5000s --verbose
@ -82,7 +82,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
uses: docker/setup-qemu-action@v3.4.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables

View File

@ -2,7 +2,7 @@ name: authentik-gen-update-webauthn-mds
on:
workflow_dispatch:
schedule:
- cron: "30 1 1,15 * *"
- cron: '30 1 1,15 * *'
env:
POSTGRES_DB: authentik
@ -24,7 +24,7 @@ jobs:
token: ${{ steps.generate_token.outputs.token }}
- name: Setup authentik env
uses: ./.github/actions/setup
- run: uv run ak update_webauthn_mds
- run: poetry run ak update_webauthn_mds
- uses: peter-evans/create-pull-request@v7
id: cpr
with:

View File

@ -21,8 +21,8 @@ jobs:
uses: ./.github/actions/setup
- name: generate docs
run: |
uv run make migrate
uv run ak build_source_docs
poetry run make migrate
poetry run ak build_source_docs
- name: Publish
uses: netlify/actions/cli@master
with:

View File

@ -42,7 +42,7 @@ jobs:
with:
go-version-file: "go.mod"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.6.0
uses: docker/setup-qemu-action@v3.4.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -186,7 +186,7 @@ jobs:
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
docker cp ${container}:web/ .
- name: Create a Sentry.io release
uses: getsentry/action-release@v3
uses: getsentry/action-release@v1
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

View File

@ -1,27 +0,0 @@
name: authentik-semgrep
on:
workflow_dispatch: {}
pull_request: {}
push:
branches:
- main
- master
paths:
- .github/workflows/semgrep.yml
schedule:
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
- cron: '12 15 * * *'
jobs:
semgrep:
name: semgrep/ci
runs-on: ubuntu-latest
permissions:
contents: read
env:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
container:
image: semgrep/semgrep
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@v4
- run: semgrep ci

View File

@ -19,27 +19,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: generate_token
if: ${{ github.event_name != 'pull_request' }}
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
if: ${{ github.event_name != 'pull_request' }}
with:
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/checkout@v4
if: ${{ github.event_name == 'pull_request' }}
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Generate API
run: make gen-client-ts
- name: run extract
run: |
uv run make i18n-extract
poetry run make i18n-extract
- name: run compile
run: |
uv run ak compilemessages
poetry run ak compilemessages
make web-check-compile
- name: Create Pull Request
if: ${{ github.event_name != 'pull_request' }}

22
.vscode/settings.json vendored
View File

@ -1,4 +1,26 @@
{
"cSpell.words": [
"akadmin",
"asgi",
"authentik",
"authn",
"entra",
"goauthentik",
"jwe",
"jwks",
"kubernetes",
"oidc",
"openid",
"passwordless",
"plex",
"saml",
"scim",
"slo",
"sso",
"totp",
"traefik",
"webauthn"
],
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
"yaml.customTags": [

46
.vscode/tasks.json vendored
View File

@ -3,13 +3,8 @@
"tasks": [
{
"label": "authentik/core: make",
"command": "uv",
"args": [
"run",
"make",
"lint-fix",
"lint"
],
"command": "poetry",
"args": ["run", "make", "lint-fix", "lint"],
"presentation": {
"panel": "new"
},
@ -17,12 +12,8 @@
},
{
"label": "authentik/core: run",
"command": "uv",
"args": [
"run",
"ak",
"server"
],
"command": "poetry",
"args": ["run", "ak", "server"],
"group": "build",
"presentation": {
"panel": "dedicated",
@ -32,17 +23,13 @@
{
"label": "authentik/web: make",
"command": "make",
"args": [
"web"
],
"args": ["web"],
"group": "build"
},
{
"label": "authentik/web: watch",
"command": "make",
"args": [
"web-watch"
],
"args": ["web-watch"],
"group": "build",
"presentation": {
"panel": "dedicated",
@ -52,26 +39,19 @@
{
"label": "authentik: install",
"command": "make",
"args": [
"install",
"-j4"
],
"args": ["install", "-j4"],
"group": "build"
},
{
"label": "authentik/website: make",
"command": "make",
"args": [
"website"
],
"args": ["website"],
"group": "build"
},
{
"label": "authentik/website: watch",
"command": "make",
"args": [
"website-watch"
],
"args": ["website-watch"],
"group": "build",
"presentation": {
"panel": "dedicated",
@ -80,12 +60,8 @@
},
{
"label": "authentik/api: generate",
"command": "uv",
"args": [
"run",
"make",
"gen"
],
"command": "poetry",
"args": ["run", "make", "gen"],
"group": "build"
}
]

View File

@ -10,7 +10,7 @@ schemas/ @goauthentik/backend
scripts/ @goauthentik/backend
tests/ @goauthentik/backend
pyproject.toml @goauthentik/backend
uv.lock @goauthentik/backend
poetry.lock @goauthentik/backend
go.mod @goauthentik/backend
go.sum @goauthentik/backend
# Infrastructure

View File

@ -5,7 +5,7 @@
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socioeconomic status,
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

View File

@ -43,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
ARG TARGETOS
ARG TARGETARCH
@ -76,7 +76,7 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
go build -o /go/authentik ./cmd/server
# Stage 4: MaxMind GeoIP
@ -93,59 +93,53 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
mkdir -p /usr/share/GeoIP && \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.6.11 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_NATIVE_TLS=1 \
UV_PYTHON_DOWNLOADS=0
WORKDIR /ak-root/
COPY --from=uv /uv /uvx /bin/
# Stage 7: Python dependencies
FROM python-base AS python-deps
# Stage 5: Python dependencies
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-deps
ARG TARGETARCH
ARG TARGETVARIANT
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
WORKDIR /ak-root/poetry
ENV PATH="/root/.cargo/bin:$PATH"
ENV VENV_PATH="/ak-root/venv" \
POETRY_VIRTUALENVS_CREATE=false \
PATH="/ak-root/venv/bin:$PATH"
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
apt-get update && \
# Required for installing pip packages
apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev libkrb5-dev
RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
--mount=type=cache,target=/root/.cache/pip \
--mount=type=cache,target=/root/.cache/pypoetry \
pip install --no-cache cffi && \
apt-get update && \
apt-get install -y --no-install-recommends \
# Build essentials
build-essential pkg-config libffi-dev git \
# cryptography
curl \
# libxml
libxslt-dev zlib1g-dev \
# postgresql
libpq-dev \
# python-kadmin-rs
clang libkrb5-dev sccache \
# xmlsec
libltdl-dev && \
curl https://sh.rustup.rs -sSf | sh -s -- -y
build-essential libffi-dev \
# Required for cryptography
curl pkg-config \
# Required for lxml
libxslt-dev zlib1g-dev \
# Required for xmlsec
libltdl-dev \
# Required for kadmin
sccache clang && \
curl https://sh.rustup.rs -sSf | sh -s -- -y && \
. "$HOME/.cargo/env" && \
python -m venv /ak-root/venv/ && \
bash -c "source ${VENV_PATH}/bin/activate && \
pip3 install --upgrade pip poetry && \
poetry config --local installer.no-binary cryptography,xmlsec,lxml,python-kadmin-rs && \
poetry install --only=main --no-ansi --no-interaction --no-root && \
pip uninstall cryptography -y && \
poetry install --only=main --no-ansi --no-interaction --no-root"
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec"
RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
--mount=type=bind,target=uv.lock,src=uv.lock \
--mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
# Stage 8: Run
FROM python-base AS final-image
# Stage 6: Run
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS final-image
ARG VERSION
ARG GIT_BUILD_HASH
@ -177,7 +171,7 @@ RUN apt-get update && \
COPY ./authentik/ /authentik
COPY ./pyproject.toml /
COPY ./uv.lock /
COPY ./poetry.lock /
COPY ./schemas /schemas
COPY ./locale /locale
COPY ./tests /tests
@ -186,7 +180,7 @@ COPY ./blueprints /blueprints
COPY ./lifecycle/ /lifecycle
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
COPY --from=go-builder /go/authentik /bin/authentik
COPY --from=python-deps /ak-root/.venv /ak-root/.venv
COPY --from=python-deps /ak-root/venv /ak-root/venv
COPY --from=web-builder /work/web/dist/ /web/dist/
COPY --from=web-builder /work/web/authentik/ /web/authentik/
COPY --from=website-builder /work/website/build/ /website/help/
@ -197,6 +191,9 @@ USER 1000
ENV TMPDIR=/dev/shm/ \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
VENV_PATH="/ak-root/venv" \
POETRY_VIRTUALENVS_CREATE=false \
GOFIPS=1
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]

View File

@ -4,17 +4,34 @@
PWD = $(shell pwd)
UID = $(shell id -u)
GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.generate_semver)
NPM_VERSION = $(shell python -m scripts.npm_version)
PY_SOURCES = authentik tests scripts lifecycle .github
GO_SOURCES = cmd internal
WEB_SOURCES = web/src web/packages
DOCKER_IMAGE ?= "authentik:test"
GEN_API_TS = "gen-ts-api"
GEN_API_PY = "gen-py-api"
GEN_API_GO = "gen-go-api"
pg_user := $(shell uv run python -m authentik.lib.config postgresql.user 2>/dev/null)
pg_host := $(shell uv run python -m authentik.lib.config postgresql.host 2>/dev/null)
pg_name := $(shell uv run python -m authentik.lib.config postgresql.name 2>/dev/null)
pg_user := $(shell python -m authentik.lib.config postgresql.user 2>/dev/null)
pg_host := $(shell python -m authentik.lib.config postgresql.host 2>/dev/null)
pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null)
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
-I .github/codespell-words.txt \
-S 'web/src/locales/**' \
-S 'website/docs/developer-docs/api/reference/**' \
-S '**/node_modules/**' \
-S '**/dist/**' \
$(PY_SOURCES) \
$(GO_SOURCES) \
$(WEB_SOURCES) \
website/src \
website/blog \
website/docs \
website/integrations \
website/src
all: lint-fix lint test gen web ## Lint, build, and test everything
@ -32,37 +49,34 @@ go-test:
go test -timeout 0 -v -race -cover ./...
test: ## Run the server tests and produce a coverage report (locally)
uv run coverage run manage.py test --keepdb authentik
uv run coverage html
uv run coverage report
coverage run manage.py test --keepdb authentik
coverage html
coverage report
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
uv run black $(PY_SOURCES)
uv run ruff check --fix $(PY_SOURCES)
black $(PY_SOURCES)
ruff check --fix $(PY_SOURCES)
lint-codespell: ## Reports spelling errors.
uv run codespell -w
codespell -w $(CODESPELL_ARGS)
lint: ## Lint the python and golang sources
uv run bandit -c pyproject.toml -r $(PY_SOURCES)
bandit -r $(PY_SOURCES) -x web/node_modules -x tests/wdio/node_modules -x website/node_modules
golangci-lint run -v
core-install:
uv sync --frozen
poetry install
migrate: ## Run the Authentik Django server's migrations
uv run python -m lifecycle.migrate
python -m lifecycle.migrate
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
run: ## Run the main authentik server process
uv run ak server
core-i18n-extract:
uv run ak makemessages \
ak makemessages \
--add-location file \
--no-obsolete \
--ignore web \
@ -93,11 +107,11 @@ gen-build: ## Extract the schema from the database
AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
uv run ak make_blueprint_schema > blueprints/schema.json
ak make_blueprint_schema > blueprints/schema.json
AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
uv run ak spectacular --file schema.yml
ak spectacular --file schema.yml
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
@ -148,7 +162,7 @@ gen-client-py: gen-clean-py ## Build and install the authentik API for Python
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:v7.4.0 generate \
-i /local/schema.yml \
-g python \
-o /local/${GEN_API_PY} \
@ -176,7 +190,7 @@ gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
gen-dev-config: ## Generate a local development config file
uv run scripts/generate_config.py
python -m scripts.generate_config
gen: gen-build gen-client-ts
@ -257,21 +271,21 @@ ci--meta-debug:
node --version
ci-black: ci--meta-debug
uv run black --check $(PY_SOURCES)
black --check $(PY_SOURCES)
ci-ruff: ci--meta-debug
uv run ruff check $(PY_SOURCES)
ruff check $(PY_SOURCES)
ci-codespell: ci--meta-debug
uv run codespell -s
codespell $(CODESPELL_ARGS) -s
ci-bandit: ci--meta-debug
uv run bandit -r $(PY_SOURCES)
bandit -r $(PY_SOURCES)
ci-pending-migrations: ci--meta-debug
uv run ak makemigrations --check
ak makemigrations --check
ci-test: ci--meta-debug
uv run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
uv run coverage report
uv run coverage xml
coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
coverage report
coverage xml

View File

@ -2,7 +2,7 @@ authentik takes security very seriously. We follow the rules of [responsible di
## Independent audits and pentests
We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specific audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).
We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specfic audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).
## What authentik classifies as a CVE

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2025.2.3"
__version__ = "2025.2.0"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -59,7 +59,7 @@ class SystemInfoSerializer(PassiveSerializer):
if not isinstance(value, str):
continue
actual_value = value
if raw_session is not None and raw_session in actual_value:
if raw_session in actual_value:
actual_value = actual_value.replace(
raw_session, SafeExceptionReporterFilter.cleansed_substitute
)

View File

@ -49,8 +49,6 @@ class BrandSerializer(ModelSerializer):
"branding_title",
"branding_logo",
"branding_favicon",
"branding_custom_css",
"branding_default_flow_background",
"flow_authentication",
"flow_invalidation",
"flow_recovery",
@ -88,7 +86,6 @@ class CurrentBrandSerializer(PassiveSerializer):
branding_title = CharField()
branding_logo = CharField(source="branding_logo_url")
branding_favicon = CharField(source="branding_favicon_url")
branding_custom_css = CharField()
ui_footer_links = ListField(
child=FooterLinkSerializer(),
read_only=True,
@ -128,7 +125,6 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
"branding_title",
"branding_logo",
"branding_favicon",
"branding_default_flow_background",
"flow_authentication",
"flow_invalidation",
"flow_recovery",

View File

@ -1,35 +0,0 @@
# Generated by Django 5.0.12 on 2025-02-22 01:51
from pathlib import Path
from django.db import migrations, models
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Brand = apps.get_model("authentik_brands", "brand")
db_alias = schema_editor.connection.alias
path = Path("/web/dist/custom.css")
if not path.exists():
return
css = path.read_text()
Brand.objects.using(db_alias).update(branding_custom_css=css)
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0007_brand_default_application"),
]
operations = [
migrations.AddField(
model_name="brand",
name="branding_custom_css",
field=models.TextField(blank=True, default=""),
),
migrations.RunPython(migrate_custom_css),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-19 22:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_brands", "0008_brand_branding_custom_css"),
]
operations = [
migrations.AddField(
model_name="brand",
name="branding_default_flow_background",
field=models.TextField(default="/static/dist/assets/images/flow_background.jpg"),
),
]

View File

@ -33,10 +33,6 @@ class Brand(SerializerModel):
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
branding_custom_css = models.TextField(default="", blank=True)
branding_default_flow_background = models.TextField(
default="/static/dist/assets/images/flow_background.jpg"
)
flow_authentication = models.ForeignKey(
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
@ -88,12 +84,6 @@ class Brand(SerializerModel):
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
return self.branding_favicon
def branding_default_flow_background_url(self) -> str:
"""Get branding_default_flow_background with the correct prefix"""
if self.branding_default_flow_background.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_default_flow_background
return self.branding_default_flow_background
@property
def serializer(self) -> Serializer:
from authentik.brands.api import BrandSerializer

View File

@ -24,7 +24,6 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": brand.domain,
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
@ -44,7 +43,6 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "custom",
"branding_custom_css": "",
"matched_domain": "bar.baz",
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
@ -61,7 +59,6 @@ class TestBrands(APITestCase):
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": "fallback",
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
@ -124,27 +121,3 @@ class TestBrands(APITestCase):
"subject": None,
},
)
def test_branding_url(self):
"""Test branding attributes return correct values"""
brand = create_test_brand()
brand.branding_default_flow_background = "https://goauthentik.io/img/icon.png"
brand.branding_favicon = "https://goauthentik.io/img/icon.png"
brand.branding_logo = "https://goauthentik.io/img/icon.png"
brand.save()
self.assertEqual(
brand.branding_default_flow_background_url(), "https://goauthentik.io/img/icon.png"
)
self.assertJSONEqual(
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
{
"branding_logo": "https://goauthentik.io/img/icon.png",
"branding_favicon": "https://goauthentik.io/img/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": brand.domain,
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
"default_locale": "",
},
)

View File

@ -46,7 +46,7 @@ LOGGER = get_logger()
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
"""Cache key where application list for user is saved"""
key = f"{CACHE_PREFIX}app_access/{user_pk}"
key = f"{CACHE_PREFIX}/app_access/{user_pk}"
if page_number:
key += f"/{page_number}"
return key

View File

@ -5,7 +5,6 @@ from collections.abc import Iterable
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
@ -155,17 +154,6 @@ class SourceViewSet(
matching_sources.append(source_settings.validated_data)
return Response(matching_sources)
def destroy(self, request: Request, *args, **kwargs):
"""Prevent deletion of built-in sources"""
instance: Source = self.get_object()
if instance.managed == Source.MANAGED_INBUILT:
raise ValidationError(
{"detail": "Built-in sources cannot be deleted"}, code="protected"
)
return super().destroy(request, *args, **kwargs)
class UserSourceConnectionSerializer(SourceSerializer):
"""User source connection"""

View File

@ -1,14 +1,13 @@
"""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.base import SessionBase
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.db.models.functions import ExtractHour
from django.db.transaction import atomic
from django.db.utils import IntegrityError
@ -92,7 +91,6 @@ 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):
@ -375,7 +373,7 @@ class UsersFilter(FilterSet):
method="filter_attributes",
)
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
uuid = UUIDFilter(field_name="uuid")
path = CharFilter(field_name="path")
@ -393,11 +391,6 @@ class UsersFilter(FilterSet):
queryset=Group.objects.all().order_by("name"),
)
def filter_is_superuser(self, queryset, name, value):
if value:
return queryset.filter(ak_groups__is_superuser=True).distinct()
return queryset.exclude(ak_groups__is_superuser=True).distinct()
def filter_attributes(self, queryset, name, value):
"""Filter attributes by query args"""
try:
@ -776,8 +769,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not instance.is_active:
sessions = AuthenticatedSession.objects.filter(user=instance)
session_ids = sessions.values_list("session_key", flat=True)
for session in session_ids:
SessionStore(session).delete()
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
sessions.delete()
LOGGER.debug("Deleted user's sessions", user=instance.username)
return response

View File

@ -32,5 +32,5 @@ class AuthentikCoreConfig(ManagedAppConfig):
"name": "authentik Built-in",
"slug": "authentik-built-in",
},
managed=Source.MANAGED_INBUILT,
managed="goauthentik.io/sources/inbuilt",
)

View File

@ -678,8 +678,6 @@ class SourceGroupMatchingModes(models.TextChoices):
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
MANAGED_INBUILT = "goauthentik.io/sources/inbuilt"
name = models.TextField(help_text=_("Source's display Name."))
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
@ -761,17 +759,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
@property
def component(self) -> str:
"""Return component used to edit this object"""
if self.managed == self.MANAGED_INBUILT:
return ""
raise NotImplementedError
@property
def property_mapping_type(self) -> "type[PropertyMapping]":
"""Return property mapping type used by this object"""
if self.managed == self.MANAGED_INBUILT:
from authentik.core.models import PropertyMapping
return PropertyMapping
raise NotImplementedError
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
@ -786,14 +778,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a user to build final properties upon."""
if self.managed == self.MANAGED_INBUILT:
return {}
raise NotImplementedError
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
"""Get base properties for a group to build final properties upon."""
if self.managed == self.MANAGED_INBUILT:
return {}
raise NotImplementedError
def __str__(self):

View File

@ -1,10 +1,7 @@
"""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.base import SessionBase
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
@ -28,7 +25,6 @@ password_changed = Signal()
login_failed = Signal()
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
@receiver(post_save, sender=Application)
@ -64,7 +60,8 @@ 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"""
SessionStore(instance.session_key).delete()
cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)
@receiver(pre_save)

View File

@ -36,7 +36,6 @@ from authentik.flows.planner import (
)
from authentik.flows.stage import StageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
from authentik.lib.utils.urls import is_url_absolute
from authentik.lib.views import bad_request_message
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.utils import delete_none_values
@ -49,7 +48,6 @@ LOGGER = get_logger()
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
SESSION_KEY_SOURCE_FLOW_CONTEXT = "authentik/flows/source_flow_context"
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
@ -210,8 +208,6 @@ class SourceFlowManager:
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if not is_url_absolute(final_redirect):
final_redirect = "authentik_core:if-user"
flow_context.update(
{
# Since we authenticate the user by their token, they have no backend set
@ -265,7 +261,6 @@ class SourceFlowManager:
plan.append_stage(stage)
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
plan.append_stage(stage)
plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {}))
return plan.to_redirect(self.request, flow)
def handle_auth(

View File

@ -11,7 +11,6 @@
build: "{{ build }}",
api: {
base: "{{ base_url }}",
relBase: "{{ base_url_rel }}",
},
};
window.addEventListener("DOMContentLoaded", function () {

View File

@ -16,7 +16,7 @@
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<style>{{ brand.branding_custom_css }}</style>
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %}

View File

@ -4,7 +4,7 @@
{% load i18n %}
{% block head_before %}
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %}
@ -13,7 +13,7 @@
{% block head %}
<style>
:root {
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}");
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);

View File

@ -1,19 +0,0 @@
from django.apps import apps
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
class TestSourceAPI(APITestCase):
def setUp(self) -> None:
self.user = create_test_admin_user()
self.client.force_login(self.user)
def test_builtin_source_used_by(self):
"""Test Providers's types endpoint"""
apps.get_app_config("authentik_core").source_inbuilt()
response = self.client.get(
reverse("authentik_api:source-used-by", kwargs={"slug": "authentik-built-in"}),
)
self.assertEqual(response.status_code, 200)

View File

@ -1,7 +1,6 @@
"""Test Users API"""
from datetime import datetime
from json import loads
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
@ -16,12 +15,7 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.core.tests.utils import (
create_test_admin_user,
create_test_brand,
create_test_flow,
create_test_user,
)
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.flows.models import FlowDesignation
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.email.models import EmailStage
@ -32,7 +26,7 @@ class TestUsersAPI(APITestCase):
def setUp(self) -> None:
self.admin = create_test_admin_user()
self.user = create_test_user()
self.user = User.objects.create(username="test-user")
def test_filter_type(self):
"""Test API filtering by type"""
@ -47,35 +41,6 @@ class TestUsersAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
def test_filter_is_superuser(self):
"""Test API filtering by superuser status"""
User.objects.all().delete()
admin = create_test_admin_user()
self.client.force_login(admin)
# Test superuser
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": True,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1)
self.assertEqual(body["results"][0]["username"], admin.username)
# Test non-superuser
user = create_test_user()
response = self.client.get(
reverse("authentik_api:user-list"),
data={
"is_superuser": False,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content)
self.assertEqual(len(body["results"]), 1, body)
self.assertEqual(body["results"][0]["username"], user.username)
def test_list_with_groups(self):
"""Test listing with groups"""
self.client.force_login(self.admin)
@ -134,8 +99,6 @@ class TestUsersAPI(APITestCase):
def test_recovery_email_no_flow(self):
"""Test user recovery link (no recovery flow set)"""
self.client.force_login(self.admin)
self.user.email = ""
self.user.save()
response = self.client.post(
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
)

View File

@ -55,7 +55,7 @@ class RedirectToAppLaunch(View):
)
except FlowNonApplicableException:
raise Http404 from None
plan.append_stage(in_memory_stage(RedirectToAppStage))
plan.insert_stage(in_memory_stage(RedirectToAppStage))
return plan.to_redirect(request, flow)

View File

@ -53,7 +53,6 @@ class InterfaceView(TemplateView):
kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
kwargs["base_url_rel"] = CONFIG.get("web.path", "/")
return super().get_context_data(**kwargs)

View File

@ -37,7 +37,6 @@ class GoogleWorkspaceProviderSerializer(EnterpriseRequiredMixin, ProviderSeriali
"user_delete_action",
"group_delete_action",
"default_group_email_domain",
"dry_run",
]
extra_kwargs = {}

View File

@ -8,10 +8,9 @@ from httplib2 import HttpLib2Error, HttpLib2ErrorWithResponse
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
from authentik.lib.sync.outgoing import HTTP_CONFLICT
from authentik.lib.sync.outgoing.base import SAFE_METHODS, BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.exceptions import (
BadRequestSyncException,
DryRunRejected,
NotFoundSyncException,
ObjectExistsSyncException,
StopSync,
@ -44,8 +43,6 @@ class GoogleWorkspaceSyncClient[TModel: Model, TConnection: Model, TSchema: dict
self.domains.append(domain_name)
def _request(self, request: HttpRequest):
if self.provider.dry_run and request.method.upper() not in SAFE_METHODS:
raise DryRunRejected(request.uri, request.method, request.body)
try:
response = request.execute()
except GoogleAuthError as exc:

View File

@ -1,24 +0,0 @@
# Generated by Django 5.0.12 on 2025-02-24 19:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_google_workspace",
"0003_googleworkspaceprovidergroup_attributes_and_more",
),
]
operations = [
migrations.AddField(
model_name="googleworkspaceprovider",
name="dry_run",
field=models.BooleanField(
default=False,
help_text="When enabled, provider will not modify or create objects in the remote system.",
),
),
]

View File

@ -36,7 +36,6 @@ class MicrosoftEntraProviderSerializer(EnterpriseRequiredMixin, ProviderSerializ
"filter_group",
"user_delete_action",
"group_delete_action",
"dry_run",
]
extra_kwargs = {}

View File

@ -3,7 +3,6 @@ from collections.abc import Coroutine
from dataclasses import asdict
from typing import Any
import httpx
from azure.core.exceptions import (
ClientAuthenticationError,
ServiceRequestError,
@ -13,7 +12,6 @@ from azure.identity.aio import ClientSecretCredential
from django.db.models import Model
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from kiota_abstractions.api_error import APIError
from kiota_abstractions.request_information import RequestInformation
from kiota_authentication_azure.azure_identity_authentication_provider import (
AzureIdentityAuthenticationProvider,
)
@ -23,15 +21,13 @@ from msgraph.generated.models.o_data_errors.o_data_error import ODataError
from msgraph.graph_request_adapter import GraphRequestAdapter, options
from msgraph.graph_service_client import GraphServiceClient
from msgraph_core import GraphClientFactory
from opentelemetry import trace
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
from authentik.events.utils import sanitize_item
from authentik.lib.sync.outgoing import HTTP_CONFLICT
from authentik.lib.sync.outgoing.base import SAFE_METHODS, BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.exceptions import (
BadRequestSyncException,
DryRunRejected,
NotFoundSyncException,
ObjectExistsSyncException,
StopSync,
@ -39,24 +35,20 @@ from authentik.lib.sync.outgoing.exceptions import (
)
class AuthentikRequestAdapter(GraphRequestAdapter):
def __init__(self, auth_provider, provider: MicrosoftEntraProvider, client=None):
super().__init__(auth_provider, client)
self._provider = provider
def get_request_adapter(
credentials: ClientSecretCredential, scopes: list[str] | None = None
) -> GraphRequestAdapter:
if scopes:
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials, scopes=scopes)
else:
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials)
async def get_http_response_message(
self,
request_info: RequestInformation,
parent_span: trace.Span,
claims: str = "",
) -> httpx.Response:
if self._provider.dry_run and request_info.http_method.value.upper() not in SAFE_METHODS:
raise DryRunRejected(
url=request_info.url,
method=request_info.http_method.value,
body=request_info.content.decode("utf-8"),
)
return await super().get_http_response_message(request_info, parent_span, claims=claims)
return GraphRequestAdapter(
auth_provider=auth_provider,
client=GraphClientFactory.create_with_default_middleware(
options=options, client=KiotaClientFactory.get_default_client()
),
)
class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict](
@ -71,27 +63,9 @@ class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict]
self.credentials = provider.microsoft_credentials()
self.__prefetch_domains()
def get_request_adapter(
self, credentials: ClientSecretCredential, scopes: list[str] | None = None
) -> AuthentikRequestAdapter:
if scopes:
auth_provider = AzureIdentityAuthenticationProvider(
credentials=credentials, scopes=scopes
)
else:
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials)
return AuthentikRequestAdapter(
auth_provider=auth_provider,
provider=self.provider,
client=GraphClientFactory.create_with_default_middleware(
options=options, client=KiotaClientFactory.get_default_client()
),
)
@property
def client(self):
return GraphServiceClient(request_adapter=self.get_request_adapter(**self.credentials))
return GraphServiceClient(request_adapter=get_request_adapter(**self.credentials))
def _request[T](self, request: Coroutine[Any, Any, T]) -> T:
try:

View File

@ -1,24 +0,0 @@
# Generated by Django 5.0.12 on 2025-02-24 19:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_microsoft_entra",
"0002_microsoftentraprovidergroup_attributes_and_more",
),
]
operations = [
migrations.AddField(
model_name="microsoftentraprovider",
name="dry_run",
field=models.BooleanField(
default=False,
help_text="When enabled, provider will not modify or create objects in the remote system.",
),
),
]

View File

@ -32,6 +32,7 @@ class MicrosoftEntraUserTests(APITestCase):
@apply_blueprint("system/providers-microsoft-entra.yaml")
def setUp(self) -> None:
# Delete all users and groups as the mocked HTTP responses only return one ID
# which will cause errors with multiple users
Tenant.objects.update(avatars="none")
@ -96,38 +97,6 @@ class MicrosoftEntraUserTests(APITestCase):
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_create.assert_called_once()
def test_user_create_dry_run(self):
"""Test user creation (dry run)"""
self.provider.dry_run = True
self.provider.save()
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNone(microsoft_user)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
def test_user_not_created(self):
"""Test without property mappings, no group is created"""
self.provider.property_mappings.clear()

View File

@ -11,14 +11,13 @@ from guardian.shortcuts import get_anonymous_user
from authentik.core.models import Source, User
from authentik.core.sources.flow_manager import (
SESSION_KEY_OVERRIDE_FLOW_TOKEN,
SESSION_KEY_SOURCE_FLOW_CONTEXT,
SESSION_KEY_SOURCE_FLOW_STAGES,
)
from authentik.core.types import UILoginButton
from authentik.enterprise.stages.source.models import SourceStage
from authentik.flows.challenge import Challenge, ChallengeResponse
from authentik.flows.models import FlowToken, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_IS_REDIRECTED, PLAN_CONTEXT_IS_RESTORED
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED
from authentik.flows.stage import ChallengeStageView, StageView
from authentik.lib.utils.time import timedelta_from_string
@ -54,9 +53,6 @@ class SourceStageView(ChallengeStageView):
resume_token = self.create_flow_token()
self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token
self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)]
self.request.session[SESSION_KEY_SOURCE_FLOW_CONTEXT] = {
PLAN_CONTEXT_IS_REDIRECTED: self.executor.flow,
}
return self.login_button.challenge
def create_flow_token(self) -> FlowToken:
@ -93,9 +89,9 @@ class SourceStageFinal(StageView):
This stage uses the override flow token to resume execution of the initial flow the
source stage is bound to."""
def dispatch(self, *args, **kwargs):
def dispatch(self):
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self.logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
plan = token.plan
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
response = plan.to_redirect(self.request, token.flow)

View File

@ -4,8 +4,7 @@ from django.urls import reverse
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.enterprise.stages.source.models import SourceStage
from authentik.enterprise.stages.source.stage import SourceStageFinal
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken, in_memory_stage
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
@ -88,7 +87,6 @@ class TestSourceStage(FlowTestCase):
self.assertIsNotNone(flow_token)
session = self.client.session
plan: FlowPlan = session[SESSION_KEY_PLAN]
plan.insert_stage(in_memory_stage(SourceStageFinal), index=0)
plan.context[PLAN_CONTEXT_IS_RESTORED] = flow_token
session[SESSION_KEY_PLAN] = plan
session.save()
@ -98,6 +96,4 @@ class TestSourceStage(FlowTestCase):
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), follow=True
)
self.assertEqual(response.status_code, 200)
self.assertStageRedirects(
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
)
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))

View File

@ -50,8 +50,7 @@ class NotificationTransportSerializer(ModelSerializer):
"mode",
"mode_verbose",
"webhook_url",
"webhook_mapping_body",
"webhook_mapping_headers",
"webhook_mapping",
"send_once",
]

View File

@ -1,43 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-20 19:54
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0008_event_authentik_e_expires_8c73a8_idx_and_more"),
]
operations = [
migrations.RenameField(
model_name="notificationtransport",
old_name="webhook_mapping",
new_name="webhook_mapping_body",
),
migrations.AlterField(
model_name="notificationtransport",
name="webhook_mapping_body",
field=models.ForeignKey(
default=None,
help_text="Customize the body of the request. Mapping should return data that is JSON-serializable.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="authentik_events.notificationwebhookmapping",
),
),
migrations.AddField(
model_name="notificationtransport",
name="webhook_mapping_headers",
field=models.ForeignKey(
default=None,
help_text="Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="authentik_events.notificationwebhookmapping",
),
),
]

View File

@ -336,27 +336,8 @@ class NotificationTransport(SerializerModel):
mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL)
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
webhook_mapping_body = models.ForeignKey(
"NotificationWebhookMapping",
on_delete=models.SET_DEFAULT,
null=True,
default=None,
related_name="+",
help_text=_(
"Customize the body of the request. "
"Mapping should return data that is JSON-serializable."
),
)
webhook_mapping_headers = models.ForeignKey(
"NotificationWebhookMapping",
on_delete=models.SET_DEFAULT,
null=True,
default=None,
related_name="+",
help_text=_(
"Configure additional headers to be sent. "
"Mapping should return a dictionary of key-value pairs"
),
webhook_mapping = models.ForeignKey(
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
)
send_once = models.BooleanField(
default=False,
@ -379,8 +360,8 @@ class NotificationTransport(SerializerModel):
def send_local(self, notification: "Notification") -> list[str]:
"""Local notification delivery"""
if self.webhook_mapping_body:
self.webhook_mapping_body.evaluate(
if self.webhook_mapping:
self.webhook_mapping.evaluate(
user=notification.user,
request=None,
notification=notification,
@ -399,18 +380,9 @@ class NotificationTransport(SerializerModel):
if notification.event and notification.event.user:
default_body["event_user_email"] = notification.event.user.get("email", None)
default_body["event_user_username"] = notification.event.user.get("username", None)
headers = {}
if self.webhook_mapping_body:
if self.webhook_mapping:
default_body = sanitize_item(
self.webhook_mapping_body.evaluate(
user=notification.user,
request=None,
notification=notification,
)
)
if self.webhook_mapping_headers:
headers = sanitize_item(
self.webhook_mapping_headers.evaluate(
self.webhook_mapping.evaluate(
user=notification.user,
request=None,
notification=notification,
@ -420,7 +392,6 @@ class NotificationTransport(SerializerModel):
response = get_http_session().post(
self.webhook_url,
json=default_body,
headers=headers,
)
response.raise_for_status()
except RequestException as exc:

View File

@ -120,7 +120,7 @@ class TestEventsNotifications(APITestCase):
)
transport = NotificationTransport.objects.create(
name=generate_id(), webhook_mapping_body=mapping, mode=TransportMode.LOCAL
name=generate_id(), webhook_mapping=mapping, mode=TransportMode.LOCAL
)
NotificationRule.objects.filter(name__startswith="default").delete()
trigger = NotificationRule.objects.create(name=generate_id(), group=self.group)

View File

@ -60,25 +60,20 @@ class TestEventTransports(TestCase):
def test_transport_webhook_mapping(self):
"""Test webhook transport with custom mapping"""
mapping_body = NotificationWebhookMapping.objects.create(
mapping = NotificationWebhookMapping.objects.create(
name=generate_id(), expression="return request.user"
)
mapping_headers = NotificationWebhookMapping.objects.create(
name=generate_id(), expression="""return {"foo": "bar"}"""
)
transport: NotificationTransport = NotificationTransport.objects.create(
name=generate_id(),
mode=TransportMode.WEBHOOK,
webhook_url="http://localhost:1234/test",
webhook_mapping_body=mapping_body,
webhook_mapping_headers=mapping_headers,
webhook_mapping=mapping,
)
with Mocker() as mocker:
mocker.post("http://localhost:1234/test")
transport.send(self.notification)
self.assertEqual(mocker.call_count, 1)
self.assertEqual(mocker.request_history[0].method, "POST")
self.assertEqual(mocker.request_history[0].headers["foo"], "bar")
self.assertJSONEqual(
mocker.request_history[0].body.decode(),
{"email": self.user.email, "pk": self.user.pk, "username": self.user.username},

View File

@ -6,7 +6,6 @@ from typing import TYPE_CHECKING
from uuid import uuid4
from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer
@ -179,12 +178,11 @@ class Flow(SerializerModel, PolicyBindingModel):
help_text=_("Required level of authentication and authorization to access a flow."),
)
def background_url(self, request: HttpRequest | None = None) -> str:
@property
def background_url(self) -> str:
"""Get the URL to the background image. If the name is /static or starts with http
it is returned as-is"""
if not self.background:
if request:
return request.brand.branding_default_flow_background_url()
return (
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg"
)

View File

@ -76,10 +76,10 @@ class FlowPlan:
self.bindings.append(binding)
self.markers.append(marker or StageMarker())
def insert_stage(self, stage: Stage, marker: StageMarker | None = None, index=1):
def insert_stage(self, stage: Stage, marker: StageMarker | None = None):
"""Insert stage into plan, as immediate next stage"""
self.bindings.insert(index, FlowStageBinding(stage=stage, order=0))
self.markers.insert(index, marker or StageMarker())
self.bindings.insert(1, FlowStageBinding(stage=stage, order=0))
self.markers.insert(1, marker or StageMarker())
def redirect(self, destination: str):
"""Insert a redirect stage as next stage"""

View File

@ -184,7 +184,7 @@ class ChallengeStageView(StageView):
flow_info = ContextualFlowInfo(
data={
"title": self.format_title(),
"background": self.executor.flow.background_url(self.request),
"background": self.executor.flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"layout": self.executor.flow.layout,
}

View File

@ -27,6 +27,7 @@ class FlowTestCase(APITestCase):
self.assertIsNotNone(raw_response["component"])
if flow:
self.assertIn("flow_info", raw_response)
self.assertEqual(raw_response["flow_info"]["background"], flow.background_url)
self.assertEqual(
raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel")
)

View File

@ -1,11 +1,9 @@
"""API flow tests"""
from json import loads
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.api.stages import StageSerializer, StageViewSet
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage
from authentik.lib.generators import generate_id
@ -79,22 +77,6 @@ class TestFlowsAPI(APITestCase):
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(response.content, {"diagram": DIAGRAM_EXPECTED})
def test_api_background(self):
"""Test custom background"""
user = create_test_admin_user()
self.client.force_login(user)
flow = create_test_flow()
response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug}))
body = loads(response.content.decode())
self.assertEqual(body["background"], "/static/dist/assets/images/flow_background.jpg")
flow.background = "https://goauthentik.io/img/icon.png"
flow.save()
response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug}))
body = loads(response.content.decode())
self.assertEqual(body["background"], "https://goauthentik.io/img/icon.png")
def test_api_diagram_no_stages(self):
"""Test flow diagram with no stages."""
user = create_test_admin_user()

View File

@ -49,7 +49,7 @@ class TestFlowInspector(APITestCase):
"captcha_stage": None,
"component": "ak-stage-identification",
"flow_info": {
"background": "/static/dist/assets/images/flow_background.jpg",
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": flow.title,
"layout": "stacked",

View File

@ -69,7 +69,6 @@ SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre"
SESSION_KEY_GET = "authentik/flows/get"
SESSION_KEY_POST = "authentik/flows/post"
SESSION_KEY_HISTORY = "authentik/flows/history"
SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started"
QS_KEY_TOKEN = "flow_token" # nosec
QS_QUERY = "query"
@ -454,7 +453,6 @@ class FlowExecutorView(APIView):
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN,
SESSION_KEY_GET,
SESSION_KEY_AUTH_STARTED,
# We might need the initial POST payloads for later requests
# SESSION_KEY_POST,
# We don't delete the history on purpose, as a user might

View File

@ -6,22 +6,14 @@ from django.shortcuts import get_object_or_404
from ua_parser.user_agent_parser import Parse
from authentik.core.views.interface import InterfaceView
from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED
from authentik.flows.models import Flow
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["flow"] = flow
if (
not self.request.user.is_authenticated
and flow.designation == FlowDesignation.AUTHENTICATION
):
self.request.session[SESSION_KEY_AUTH_STARTED] = True
self.request.session.save()
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)

View File

@ -282,14 +282,16 @@ 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, UNSET)
value = self.get(path, default)
if value is UNSET:
return default
try:
return int(value)
except (ValueError, TypeError) as exc:
if value is None or (isinstance(value, str) and value.lower() == "null"):
return None
return default
if value is UNSET:
return default
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
return default
@ -370,9 +372,9 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
"sslcert": config.get("postgresql.sslcert"),
"sslkey": config.get("postgresql.sslkey"),
},
"CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0),
"CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False),
"DISABLE_SERVER_SIDE_CURSORS": config.get_bool(
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
"postgresql.disable_server_side_cursors", False
),
"TEST": {
@ -381,8 +383,8 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
}
}
conn_max_age = config.get_optional_int("postgresql.conn_max_age", UNSET)
disable_server_side_cursors = config.get_bool("postgresql.disable_server_side_cursors", UNSET)
conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET)
disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET)
if config.get_bool("postgresql.use_pgpool", False):
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
if disable_server_side_cursors is not UNSET:

View File

@ -1,20 +1,5 @@
# authentik configuration
#
# https://docs.goauthentik.io/docs/install-config/configuration/
#
# To override the settings in this file, run the following command from the repository root:
#
# ```shell
# make gen-dev-config
# ```
#
# You may edit the generated file to override the configuration below.
#
# When making modifying the default configuration file,
# ensure that the corresponding documentation is updated to match.
#
# @see {@link ../../website/docs/install-config/configuration/configuration.mdx Configuration documentation} for more information.
# update website/docs/install-config/configuration/configuration.mdx
# This is the default configuration file
postgresql:
host: localhost
name: authentik
@ -60,8 +45,6 @@ redis:
# url: ""
# transport_options: ""
http_timeout: 30
cache:
# url: ""
timeout: 300

View File

@ -18,15 +18,6 @@ class SerializerModel(models.Model):
@property
def serializer(self) -> type[BaseSerializer]:
"""Get serializer for this model"""
# Special handling for built-in source
if (
hasattr(self, "managed")
and hasattr(self, "MANAGED_INBUILT")
and self.managed == self.MANAGED_INBUILT
):
from authentik.core.api.sources import SourceSerializer
return SourceSerializer
raise NotImplementedError

View File

@ -33,7 +33,6 @@ class SyncObjectSerializer(PassiveSerializer):
)
)
sync_object_id = CharField()
override_dry_run = BooleanField(default=False)
class SyncObjectResultSerializer(PassiveSerializer):
@ -99,7 +98,6 @@ class OutgoingSyncProviderStatusMixin:
page=1,
provider_pk=provider.pk,
pk=params.validated_data["sync_object_id"],
override_dry_run=params.validated_data["override_dry_run"],
).get()
return Response(SyncObjectResultSerializer(instance={"messages": res}).data)

View File

@ -28,14 +28,6 @@ class Direction(StrEnum):
remove = "remove"
SAFE_METHODS = [
"GET",
"HEAD",
"OPTIONS",
"TRACE",
]
class BaseOutgoingSyncClient[
TModel: "Model", TConnection: "Model", TSchema: dict, TProvider: "OutgoingSyncProvider"
]:

View File

@ -21,22 +21,6 @@ class BadRequestSyncException(BaseSyncException):
"""Exception when invalid data was sent to the remote system"""
class DryRunRejected(BaseSyncException):
"""When dry_run is enabled and a provider dropped a mutating request"""
def __init__(self, url: str, method: str, body: dict):
super().__init__()
self.url = url
self.method = method
self.body = body
def __repr__(self):
return self.__str__()
def __str__(self):
return f"Dry-run rejected request: {self.method} {self.url}"
class StopSync(BaseSyncException):
"""Exception raised when a configuration error should stop the sync process"""

View File

@ -1,9 +1,8 @@
from typing import Any, Self
import pglock
from django.db import connection, models
from django.db import connection
from django.db.models import Model, QuerySet, TextChoices
from django.utils.translation import gettext_lazy as _
from authentik.core.models import Group, User
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
@ -19,14 +18,6 @@ class OutgoingSyncDeleteAction(TextChoices):
class OutgoingSyncProvider(Model):
"""Base abstract models for providers implementing outgoing sync"""
dry_run = models.BooleanField(
default=False,
help_text=_(
"When enabled, provider will not modify or create objects in the remote system."
),
)
class Meta:
abstract = True
@ -41,7 +32,7 @@ class OutgoingSyncProvider(Model):
@property
def sync_lock(self) -> pglock.advisory:
"""Postgres lock for syncing to prevent multiple parallel syncs happening"""
"""Postgres lock for syncing SCIM to prevent multiple parallel syncs happening"""
return pglock.advisory(
lock_id=f"goauthentik.io/{connection.schema_name}/providers/outgoing-sync/{str(self.pk)}",
timeout=0,

View File

@ -20,7 +20,6 @@ from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT
from authentik.lib.sync.outgoing.base import Direction
from authentik.lib.sync.outgoing.exceptions import (
BadRequestSyncException,
DryRunRejected,
StopSync,
TransientSyncException,
)
@ -106,9 +105,7 @@ class SyncTasks:
return
task.set_status(TaskStatus.SUCCESSFUL, *messages)
def sync_objects(
self, object_type: str, page: int, provider_pk: int, override_dry_run=False, **filter
):
def sync_objects(self, object_type: str, page: int, provider_pk: int, **filter):
_object_type = path_to_class(object_type)
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
@ -119,10 +116,6 @@ class SyncTasks:
provider = self._provider_model.objects.filter(pk=provider_pk).first()
if not provider:
return messages
# Override dry run mode if requested, however don't save the provider
# so that scheduled sync tasks still run in dry_run mode
if override_dry_run:
provider.dry_run = False
try:
client = provider.client_for_model(_object_type)
except TransientSyncException:
@ -139,22 +132,6 @@ class SyncTasks:
except SkipObjectException:
self.logger.debug("skipping object due to SkipObject", obj=obj)
continue
except DryRunRejected as exc:
messages.append(
asdict(
LogEvent(
_("Dropping mutating request due to dry run"),
log_level="info",
logger=f"{provider._meta.verbose_name}@{object_type}",
attributes={
"obj": sanitize_item(obj),
"method": exc.method,
"url": exc.url,
"body": exc.body,
},
)
)
)
except BadRequestSyncException as exc:
self.logger.warning("failed to sync object", exc=exc, obj=obj)
messages.append(
@ -254,10 +231,8 @@ class SyncTasks:
raise Retry() from exc
except SkipObjectException:
continue
except DryRunRejected as exc:
self.logger.info("Rejected dry-run event", exc=exc)
except StopSync as exc:
self.logger.warning("Stopping sync", exc=exc, provider_pk=provider.pk)
self.logger.warning(exc, provider_pk=provider.pk)
def sync_signal_m2m(self, group_pk: str, action: str, pk_set: list[int]):
self.logger = get_logger().bind(
@ -288,7 +263,5 @@ class SyncTasks:
raise Retry() from exc
except SkipObjectException:
continue
except DryRunRejected as exc:
self.logger.info("Rejected dry-run event", exc=exc)
except StopSync as exc:
self.logger.warning("Stopping sync", exc=exc, provider_pk=provider.pk)
self.logger.warning(exc, provider_pk=provider.pk)

View File

@ -158,18 +158,6 @@ class TestConfig(TestCase):
test_obj = Test()
dumps(test_obj, indent=4, cls=AttrEncoder)
def test_get_optional_int(self):
config = ConfigLoader()
self.assertEqual(config.get_optional_int("foo", 21), 21)
self.assertEqual(config.get_optional_int("foo"), None)
config.set("foo", "21")
self.assertEqual(config.get_optional_int("foo"), 21)
self.assertEqual(config.get_optional_int("foo", 0), 21)
self.assertEqual(config.get_optional_int("foo", "null"), 21)
config.set("foo", "null")
self.assertEqual(config.get_optional_int("foo"), None)
self.assertEqual(config.get_optional_int("foo", 21), None)
@mock.patch.dict(environ, check_deprecations_env_vars)
def test_check_deprecations(self):
"""Test config key re-write for deprecated env vars"""
@ -233,16 +221,6 @@ class TestConfig(TestCase):
},
)
def test_db_conn_max_age(self):
"""Test DB conn_max_age Config"""
config = ConfigLoader()
config.set("postgresql.conn_max_age", "null")
conf = django_db_config(config)
self.assertEqual(
conf["default"]["CONN_MAX_AGE"],
None,
)
def test_db_read_replicas(self):
"""Test read replicas"""
config = ConfigLoader()

View File

@ -16,40 +16,7 @@ def authentik_user_agent() -> str:
return f"authentik@{get_full_version()}"
class TimeoutSession(Session):
"""Always set a default HTTP request timeout"""
def __init__(self, default_timeout=None):
super().__init__()
self.timeout = default_timeout
def send(
self,
request,
*,
stream=...,
verify=...,
proxies=...,
cert=...,
timeout=...,
allow_redirects=...,
**kwargs,
):
if not timeout and self.timeout:
timeout = self.timeout
return super().send(
request,
stream=stream,
verify=verify,
proxies=proxies,
cert=cert,
timeout=timeout,
allow_redirects=allow_redirects,
**kwargs,
)
class DebugSession(TimeoutSession):
class DebugSession(Session):
"""requests session which logs http requests and responses"""
def send(self, req: PreparedRequest, *args, **kwargs):
@ -75,9 +42,8 @@ class DebugSession(TimeoutSession):
def get_http_session() -> Session:
"""Get a requests session with common headers"""
session = TimeoutSession()
session = Session()
if CONFIG.get_bool("debug") or CONFIG.get("log_level") == "trace":
session = DebugSession()
session.headers["User-Agent"] = authentik_user_agent()
session.timeout = CONFIG.get_optional_int("http_timeout")
return session

View File

@ -13,7 +13,6 @@ from paramiko.ssh_exception import SSHException
from structlog.stdlib import get_logger
from yaml import safe_dump
from authentik import __version__
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
@ -185,7 +184,7 @@ class DockerController(BaseController):
try:
self.client.images.pull(image)
except DockerException: # pragma: no cover
image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}"
image = f"ghcr.io/goauthentik/{self.outpost.type}:latest"
self.client.images.pull(image)
return image

View File

@ -1,6 +1,5 @@
"""Base Kubernetes Reconciler"""
import re
from dataclasses import asdict
from json import dumps
from typing import TYPE_CHECKING, Generic, TypeVar
@ -68,8 +67,7 @@ class KubernetesObjectReconciler(Generic[T]):
@property
def name(self) -> str:
"""Get the name of the object this reconciler manages"""
base_name = (
return (
self.controller.outpost.config.object_naming_template
% {
"name": slugify(self.controller.outpost.name),
@ -77,16 +75,6 @@ class KubernetesObjectReconciler(Generic[T]):
}
).lower()
formatted = slugify(base_name)
formatted = re.sub(r"[^a-z0-9-]", "-", formatted)
formatted = re.sub(r"-+", "-", formatted)
formatted = formatted[:63]
if not formatted:
formatted = f"outpost-{self.controller.outpost.uuid.hex}"[:63]
return formatted
def get_patched_reference_object(self) -> T:
"""Get patched reference object"""
reference = self.get_reference_object()
@ -124,6 +112,7 @@ class KubernetesObjectReconciler(Generic[T]):
try:
current = self.retrieve()
except (OpenApiException, HTTPError) as exc:
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
self.logger.debug("Failed to get current, triggering recreate")
raise NeedsRecreate from exc
@ -167,6 +156,7 @@ class KubernetesObjectReconciler(Generic[T]):
self.delete(current)
self.logger.debug("Removing")
except (OpenApiException, HTTPError) as exc:
if isinstance(exc, ApiException) and exc.status == HttpResponseNotFound.status_code:
self.logger.debug("Failed to get current, assuming non-existent")
return

View File

@ -61,14 +61,9 @@ class KubernetesController(BaseController):
client: KubernetesClient
connection: KubernetesServiceConnection
def __init__(
self,
outpost: Outpost,
connection: KubernetesServiceConnection,
client: KubernetesClient | None = None,
) -> None:
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection) -> None:
super().__init__(outpost, connection)
self.client = client if client else KubernetesClient(connection)
self.client = KubernetesClient(connection)
self.reconcilers = {
SecretReconciler.reconciler_name(): SecretReconciler,
DeploymentReconciler.reconciler_name(): DeploymentReconciler,

View File

@ -1,44 +0,0 @@
"""Kubernetes controller tests"""
from django.test import TestCase
from authentik.blueprints.tests import reconcile_app
from authentik.lib.generators import generate_id
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
from authentik.outposts.controllers.kubernetes import KubernetesController
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
class KubernetesControllerTests(TestCase):
"""Kubernetes controller tests"""
@reconcile_app("authentik_outposts")
def setUp(self) -> None:
self.outpost = Outpost.objects.create(
name="test",
type=OutpostType.PROXY,
)
self.integration = KubernetesServiceConnection(name="test")
def test_gen_name(self):
"""Ensure the generated name is valid"""
controller = KubernetesController(
Outpost.objects.filter(managed=MANAGED_OUTPOST).first(),
self.integration,
# Pass something not-none as client so we don't
# attempt to connect to K8s as that's not needed
client=self,
)
rec = DeploymentReconciler(controller)
self.assertEqual(rec.name, "ak-outpost-authentik-embedded-outpost")
controller.outpost.name = generate_id()
self.assertLess(len(rec.name), 64)
# Test custom naming template
_cfg = controller.outpost.config
_cfg.object_naming_template = ""
controller.outpost.config = _cfg
self.assertEqual(rec.name, f"outpost-{controller.outpost.uuid.hex}")
self.assertLess(len(rec.name), 64)

View File

@ -35,4 +35,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
label = "authentik_policies"
verbose_name = "authentik Policies"
default = True
mountpoint = "policy/"

View File

@ -1,89 +0,0 @@
{% extends 'login/base_full.html' %}
{% load static %}
{% load i18n %}
{% block head %}
{{ block.super }}
<script>
let redirecting = false;
const checkAuth = async () => {
if (redirecting) return true;
const url = "{{ check_auth_url }}";
console.debug("authentik/policies/buffer: Checking authentication...");
try {
const result = await fetch(url, {
method: "HEAD",
});
if (result.status >= 400) {
return false
}
console.debug("authentik/policies/buffer: Continuing");
redirecting = true;
if ("{{ auth_req_method }}" === "post") {
document.querySelector("form").submit();
} else {
window.location.assign("{{ continue_url|escapejs }}");
}
} catch {
return false;
}
};
let timeout = 100;
let offset = 20;
let attempt = 0;
const main = async () => {
attempt += 1;
await checkAuth();
console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`);
setTimeout(main, timeout);
timeout += (offset * attempt);
if (timeout >= 2000) {
timeout = 2000;
}
}
document.addEventListener("visibilitychange", async () => {
if (document.hidden) return;
console.debug("authentik/policies/buffer: Checking authentication on tab activate...");
await checkAuth();
});
main();
</script>
{% endblock %}
{% block title %}
{% trans 'Waiting for authentication...' %} - {{ brand.branding_title }}
{% endblock %}
{% block card_title %}
{% trans 'Waiting for authentication...' %}
{% endblock %}
{% block card %}
<form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}">
{% if auth_req_method == "post" %}
{% for key, value in auth_req_body.items %}
<input type="hidden" name="{{ key }}" value="{{ value }}" />
{% endfor %}
{% endif %}
<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<div class="pf-c-empty-state__icon">
<span class="pf-c-spinner pf-m-xl" role="progressbar">
<span class="pf-c-spinner__clipper"></span>
<span class="pf-c-spinner__lead-ball"></span>
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
<h1 class="pf-c-title pf-m-lg">
{% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %}
</h1>
</div>
</div>
<div class="pf-c-form__group pf-m-action">
<a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block">
{% trans "Authenticate in this tab" %}
</a>
</div>
</form>
{% endblock %}

View File

@ -1,121 +0,0 @@
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.http import HttpResponse
from django.test import RequestFactory, TestCase
from django.urls import reverse
from authentik.core.models import Application, Provider
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.flows.models import FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response
from authentik.policies.views import (
QS_BUFFER_ID,
SESSION_KEY_BUFFER,
BufferedPolicyAccessView,
BufferView,
PolicyAccessView,
)
class TestPolicyViews(TestCase):
"""Test PolicyAccessView"""
def setUp(self):
super().setUp()
self.factory = RequestFactory()
self.user = create_test_user()
def test_pav(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
class TestView(PolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = self.user
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.content, b"foo")
def test_pav_buffer(self):
"""Test simple policy access view"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer")))
def test_pav_buffer_skip(self):
"""Test simple policy access view (skip buffer)"""
provider = Provider.objects.create(
name=generate_id(),
)
app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
class TestView(BufferedPolicyAccessView):
def resolve_provider_application(self):
self.provider = provider
self.application = app
def get(self, *args, **kwargs):
return HttpResponse("foo")
req = self.factory.get("/?skip_buffer=true")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk)
req.session.save()
res = TestView.as_view()(req)
self.assertEqual(res.status_code, 302)
self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication")))
def test_buffer(self):
"""Test buffer view"""
uid = generate_id()
req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}")
req.user = AnonymousUser()
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(req)
ts = generate_id()
req.session[SESSION_KEY_BUFFER % uid] = {
"method": "get",
"body": {},
"url": f"/{ts}",
}
req.session.save()
res = BufferView.as_view()(req)
self.assertEqual(res.status_code, 200)
self.assertIn(ts, res.render().content.decode())

View File

@ -1,14 +1,7 @@
"""API URLs"""
from django.urls import path
from authentik.policies.api.bindings import PolicyBindingViewSet
from authentik.policies.api.policies import PolicyViewSet
from authentik.policies.views import BufferView
urlpatterns = [
path("buffer", BufferView.as_view(), name="buffer"),
]
api_urlpatterns = [
("policies/all", PolicyViewSet),

View File

@ -1,37 +1,23 @@
"""authentik access helper classes"""
from typing import Any
from uuid import uuid4
from django.contrib import messages
from django.contrib.auth.mixins import AccessMixin
from django.contrib.auth.views import redirect_to_login
from django.http import HttpRequest, HttpResponse, QueryDict
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlencode
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from django.views.generic.base import TemplateView, View
from django.views.generic.base import View
from structlog.stdlib import get_logger
from authentik.core.models import Application, Provider, User
from authentik.flows.models import Flow, FlowDesignation
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import (
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_AUTH_STARTED,
SESSION_KEY_PLAN,
SESSION_KEY_POST,
)
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST
from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.denied import AccessDeniedResponse
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger()
QS_BUFFER_ID = "af_bf_id"
QS_SKIP_BUFFER = "skip_buffer"
SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s"
class RequestValidationError(SentryIgnoredException):
@ -139,65 +125,3 @@ class PolicyAccessView(AccessMixin, View):
for message in result.messages:
messages.error(self.request, _(message))
return result
def url_with_qs(url: str, **kwargs):
"""Update/set querystring of `url` with the parameters in `kwargs`. Original query string
parameters are retained"""
if "?" not in url:
return url + f"?{urlencode(kwargs)}"
url, _, qs = url.partition("?")
qs = QueryDict(qs, mutable=True)
qs.update(kwargs)
return url + f"?{urlencode(qs.items())}"
class BufferView(TemplateView):
"""Buffer view"""
template_name = "policies/buffer.html"
def get_context_data(self, **kwargs):
buf_id = self.request.GET.get(QS_BUFFER_ID)
buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id)
kwargs["auth_req_method"] = buffer["method"]
kwargs["auth_req_body"] = buffer["body"]
kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True})
kwargs["check_auth_url"] = reverse("authentik_api:user-me")
kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id})
return super().get_context_data(**kwargs)
class BufferedPolicyAccessView(PolicyAccessView):
"""PolicyAccessView which buffers access requests in case the user is not logged in"""
def handle_no_permission(self):
plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN)
authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED)
if plan:
flow = Flow.objects.filter(pk=plan.flow_pk).first()
if not flow or flow.designation != FlowDesignation.AUTHENTICATION:
LOGGER.debug("Not buffering request, no flow or flow not for authentication")
return super().handle_no_permission()
if not plan and authenticating is None:
LOGGER.debug("Not buffering request, no flow plan active")
return super().handle_no_permission()
if self.request.GET.get(QS_SKIP_BUFFER):
LOGGER.debug("Not buffering request, explicit skip")
return super().handle_no_permission()
buffer_id = str(uuid4())
LOGGER.debug("Buffering access request", bf_id=buffer_id)
self.request.session[SESSION_KEY_BUFFER % buffer_id] = {
"body": self.request.POST,
"url": self.request.build_absolute_uri(self.request.get_full_path()),
"method": self.request.method.lower(),
}
return redirect(
url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id})
)
def dispatch(self, request, *args, **kwargs):
response = super().dispatch(request, *args, **kwargs)
if QS_BUFFER_ID in self.request.GET:
self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None)
return response

View File

@ -9,12 +9,7 @@ from hashlib import sha256
from typing import Any
from urllib.parse import urlparse, urlunparse
from cryptography.hazmat.primitives.asymmetric.ec import (
SECP256R1,
SECP384R1,
SECP521R1,
EllipticCurvePrivateKey,
)
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from dacite import Config
@ -119,22 +114,6 @@ class JWTAlgorithms(models.TextChoices):
HS256 = "HS256", _("HS256 (Symmetric Encryption)")
RS256 = "RS256", _("RS256 (Asymmetric Encryption)")
ES256 = "ES256", _("ES256 (Asymmetric Encryption)")
ES384 = "ES384", _("ES384 (Asymmetric Encryption)")
ES512 = "ES512", _("ES512 (Asymmetric Encryption)")
@classmethod
def from_private_key(cls, private_key: PrivateKeyTypes | None) -> str:
if isinstance(private_key, RSAPrivateKey):
return cls.RS256
if isinstance(private_key, EllipticCurvePrivateKey):
curve = private_key.curve
if isinstance(curve, SECP256R1):
return cls.ES256
if isinstance(curve, SECP384R1):
return cls.ES384
if isinstance(curve, SECP521R1):
return cls.ES512
raise ValueError(f"Invalid private key type: {type(private_key)}")
class ScopeMapping(PropertyMapping):
@ -284,7 +263,11 @@ class OAuth2Provider(WebfingerProvider, Provider):
return self.client_secret, JWTAlgorithms.HS256
key: CertificateKeyPair = self.signing_key
private_key = key.private_key
return private_key, JWTAlgorithms.from_private_key(private_key)
if isinstance(private_key, RSAPrivateKey):
return private_key, JWTAlgorithms.RS256
if isinstance(private_key, EllipticCurvePrivateKey):
return private_key, JWTAlgorithms.ES256
raise ValueError(f"Invalid private key type: {type(private_key)}")
def get_issuer(self, request: HttpRequest) -> str | None:
"""Get issuer, based on request"""

View File

@ -30,7 +30,7 @@ from authentik.flows.stage import StageView
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError
from authentik.policies.views import PolicyAccessView, RequestValidationError
from authentik.providers.oauth2.constants import (
PKCE_METHOD_PLAIN,
PKCE_METHOD_S256,
@ -254,10 +254,10 @@ class OAuthAuthorizationParams:
raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type, self.state)
if SCOPE_OFFLINE_ACCESS in self.scope:
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
# Don't explicitly request consent with offline_access, as the spec allows for
# "other conditions for processing the request permitting offline access to the
# requested resources are in place"
# which we interpret as "the admin picks an authorization flow with or without consent"
if PROMPT_CONSENT not in self.prompt:
# Instead of ignoring the `offline_access` scope when `prompt`
# isn't set to `consent`, we set override it ourselves
self.prompt.add(PROMPT_CONSENT)
if self.response_type not in [
ResponseTypes.CODE,
ResponseTypes.CODE_TOKEN,
@ -328,7 +328,7 @@ class OAuthAuthorizationParams:
return code
class AuthorizationFlowInitView(BufferedPolicyAccessView):
class AuthorizationFlowInitView(PolicyAccessView):
"""OAuth2 Flow initializer, checks access to application and starts flow"""
params: OAuthAuthorizationParams

View File

@ -71,7 +71,7 @@ class CodeValidatorView(PolicyAccessView):
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
plan.append_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
return plan.to_redirect(self.request, self.token.provider.authorization_flow)

View File

@ -34,5 +34,5 @@ class EndSessionView(PolicyAccessView):
PLAN_CONTEXT_APPLICATION: self.application,
},
)
plan.append_stage(in_memory_stage(SessionEndStage))
plan.insert_stage(in_memory_stage(SessionEndStage))
return plan.to_redirect(self.request, self.flow)

View File

@ -75,7 +75,10 @@ class JWKSView(View):
key_data = {}
if use == "sig":
key_data["alg"] = JWTAlgorithms.from_private_key(private_key)
if isinstance(private_key, RSAPrivateKey):
key_data["alg"] = JWTAlgorithms.RS256
elif isinstance(private_key, EllipticCurvePrivateKey):
key_data["alg"] = JWTAlgorithms.ES256
elif use == "enc":
key_data["alg"] = "RSA-OAEP-256"
key_data["enc"] = "A256CBC-HS512"

View File

@ -36,17 +36,17 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]):
def reconciler_name() -> str:
return "ingress"
def _check_annotations(self, current: V1Ingress, reference: V1Ingress):
def _check_annotations(self, reference: V1Ingress):
"""Check that all annotations *we* set are correct"""
for key, value in reference.metadata.annotations.items():
if key not in current.metadata.annotations:
for key, value in self.get_ingress_annotations().items():
if key not in reference.metadata.annotations:
raise NeedsUpdate()
if current.metadata.annotations[key] != value:
if reference.metadata.annotations[key] != value:
raise NeedsUpdate()
def reconcile(self, current: V1Ingress, reference: V1Ingress):
super().reconcile(current, reference)
self._check_annotations(current, reference)
self._check_annotations(reference)
# Create a list of all expected host and tls hosts
expected_hosts = []
expected_hosts_tls = []

View File

@ -1,9 +1,9 @@
"""RAC app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikProviderRAC(ManagedAppConfig):
class AuthentikProviderRAC(AppConfig):
"""authentik rac app config"""
name = "authentik.providers.rac"

View File

@ -4,7 +4,8 @@ from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.contrib.auth.signals import user_logged_out
from django.core.cache import cache
from django.db.models.signals import post_delete, post_save, pre_delete
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.http import HttpRequest
@ -45,8 +46,12 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
)
@receiver([post_save, post_delete], sender=Endpoint)
def post_save_post_delete_endpoint(**_):
"""Clear user's endpoint cache upon endpoint creation or deletion"""
@receiver(post_save, sender=Endpoint)
def post_save_endpoint(sender: type[Model], instance, created: bool, **_):
"""Clear user's endpoint cache upon endpoint creation"""
if not created: # pragma: no cover
return
# Delete user endpoint cache
keys = cache.keys(user_endpoint_cache_key("*"))
cache.delete_many(keys)

View File

@ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine
from authentik.policies.views import BufferedPolicyAccessView
from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
class RACStartView(BufferedPolicyAccessView):
class RACStartView(PolicyAccessView):
"""Start a RAC connection by checking access and creating a connection token"""
endpoint: Endpoint
@ -46,7 +46,7 @@ class RACStartView(BufferedPolicyAccessView):
)
except FlowNonApplicableException:
raise Http404 from None
plan.append_stage(
plan.insert_stage(
in_memory_stage(
RACFinalStage,
application=self.application,

View File

@ -180,7 +180,6 @@ class SAMLProviderSerializer(ProviderSerializer):
"session_valid_not_on_or_after",
"property_mappings",
"name_id_mapping",
"authn_context_class_ref_mapping",
"digest_algorithm",
"signature_algorithm",
"signing_kp",

View File

@ -1,28 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-18 17:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0016_samlprovider_encryption_kp_and_more"),
]
operations = [
migrations.AddField(
model_name="samlprovider",
name="authn_context_class_ref_mapping",
field=models.ForeignKey(
blank=True,
default=None,
help_text="Configure how the AuthnContextClassRef value will be created. When left empty, the AuthnContextClassRef will be set based on which authentication methods the user used to authenticate.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="+",
to="authentik_providers_saml.samlpropertymapping",
verbose_name="AuthnContextClassRef Property Mapping",
),
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 5.0.13 on 2025-03-31 13:50
import authentik.lib.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0017_samlprovider_authn_context_class_ref_mapping"),
]
operations = [
migrations.AlterField(
model_name="samlprovider",
name="acs_url",
field=models.TextField(
validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))],
verbose_name="ACS URL",
),
),
]

View File

@ -10,7 +10,6 @@ from structlog.stdlib import get_logger
from authentik.core.api.object_types import CreatableType
from authentik.core.models import PropertyMapping, Provider
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import DomainlessURLValidator
from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import (
DSA_SHA1,
@ -41,9 +40,7 @@ class SAMLBindings(models.TextChoices):
class SAMLProvider(Provider):
"""SAML 2.0 Endpoint for applications which support SAML."""
acs_url = models.TextField(
validators=[DomainlessURLValidator(schemes=("http", "https"))], verbose_name=_("ACS URL")
)
acs_url = models.URLField(verbose_name=_("ACS URL"))
audience = models.TextField(
default="",
blank=True,
@ -74,20 +71,6 @@ class SAMLProvider(Provider):
"the NameIDPolicy of the incoming request will be considered"
),
)
authn_context_class_ref_mapping = models.ForeignKey(
"SAMLPropertyMapping",
default=None,
blank=True,
null=True,
on_delete=models.SET_DEFAULT,
verbose_name=_("AuthnContextClassRef Property Mapping"),
related_name="+",
help_text=_(
"Configure how the AuthnContextClassRef value will be created. When left empty, "
"the AuthnContextClassRef will be set based on which authentication methods the user "
"used to authenticate."
),
)
assertion_valid_not_before = models.TextField(
default="minutes=-5",
@ -187,6 +170,7 @@ class SAMLProvider(Provider):
def launch_url(self) -> str | None:
"""Use IDP-Initiated SAML flow as launch URL"""
try:
return reverse(
"authentik_providers_saml:sso-init",
kwargs={"application_slug": self.application.slug},

View File

@ -1,6 +1,5 @@
"""SAML Assertion generator"""
from datetime import datetime
from hashlib import sha256
from types import GeneratorType
@ -53,7 +52,6 @@ class AssertionProcessor:
_assertion_id: str
_response_id: str
_auth_instant: str
_valid_not_before: str
_session_not_on_or_after: str
_valid_not_on_or_after: str
@ -67,11 +65,6 @@ class AssertionProcessor:
self._assertion_id = get_random_id()
self._response_id = get_random_id()
_login_event = get_login_event(self.http_request)
_login_time = datetime.now()
if _login_event:
_login_time = _login_event.created
self._auth_instant = get_time_string(_login_time)
self._valid_not_before = get_time_string(
timedelta_from_string(self.provider.assertion_valid_not_before)
)
@ -138,7 +131,7 @@ class AssertionProcessor:
def get_assertion_auth_n_statement(self) -> Element:
"""Generate AuthnStatement with AuthnContext and ContextClassRef Elements."""
auth_n_statement = Element(f"{{{NS_SAML_ASSERTION}}}AuthnStatement")
auth_n_statement.attrib["AuthnInstant"] = self._auth_instant
auth_n_statement.attrib["AuthnInstant"] = self._valid_not_before
auth_n_statement.attrib["SessionIndex"] = sha256(
self.http_request.session.session_key.encode("ascii")
).hexdigest()
@ -165,28 +158,6 @@ class AssertionProcessor:
auth_n_context_class_ref.text = (
"urn:oasis:names:tc:SAML:2.0:ac:classes:MobileOneFactorContract"
)
if self.provider.authn_context_class_ref_mapping:
try:
value = self.provider.authn_context_class_ref_mapping.evaluate(
user=self.http_request.user,
request=self.http_request,
provider=self.provider,
)
if value is not None:
auth_n_context_class_ref.text = str(value)
return auth_n_statement
except PropertyMappingExpressionException as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=(
"Failed to evaluate property-mapping: "
f"'{self.provider.authn_context_class_ref_mapping.name}'"
),
provider=self.provider,
mapping=self.provider.authn_context_class_ref_mapping,
).from_http(self.http_request)
LOGGER.warning("Failed to evaluate property mapping", exc=exc)
return auth_n_statement
return auth_n_statement
def get_assertion_conditions(self) -> Element:

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