Compare commits

...

239 Commits

Author SHA1 Message Date
a4f92f5e30 Prep for monorepo use.
web: Update config.

Flesh out build.

Fix issue surrounding build.

Fix paths.

Update workspaces.

Fix build steps.

Apply linter. Temporarily remove problem rules.

Add ignorefile. Prep for formatting.

Lint website.

Lint web, repo packages.

Refine Prettier usage. Fix imports.

Tidy build.

Move node ignore files.

Remove unused.

Update job. Fix lint step.

Build before compiling.

Use root for paths.

Fix issues surrounding import references, types, package names.

Fix build paths.

Tidy.

Enforce prefix.

Apply prefixes to imports.

Enable linter, compiler, etc.

Fix references. Update names.

Mark optional.

Revise mounts. Fix build order.

Update package.json.

Ignore all docusaurus.

Fix paths, types.

Clean up build steps, names.

Fix paths.

website: Fix nested paragraphs build warning.

web: Enforce module resolution.

Use consistent LTS version.

Track Node version.

Use default resolution.

Test main entrypoint.

Fix Node v20 compatibility.

Add task names.

WIP: Fix styles.
2025-04-17 02:46:10 +02:00
c15c0cbe86 website: integrations: apache guacamole: Fix deprecated start-of-doc … (#14114)
website: integrations: apache guacamole: Fix deprecated start-of-doc values
2025-04-16 15:45:44 -05:00
c6fe0c1d85 website integrations: actual budget: remove old header and support_level (#14112) 2025-04-16 15:45:20 -05:00
07f0666a6f website/integrations: general cleanup and updates (#12716)
* squash commits for future merge conflict resolution, if any

* adventurelog cleanup + lint

* lint (again)

* Update website/integrations/services/adventurelog/index.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/actual-budget/index.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/apache-guacamole/index.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/gatus/index.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/bookstack/index.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/freshrss/index.mdx

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/budibase/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/cloudflare-access/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/dokuwiki/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/frappe/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/espocrm/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/fortimanager/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/fortigate-admin/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* Update website/integrations/services/firezone/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Dominic R <dominic@sdko.org>

* fix

Signed-off-by: Dominic R <dominic@sdko.org>

* wip: migr actual budget integration to new codeblock

* Replaced multilinecodeblocks with docusaurus style codeblocks

* Fixed linting and removed kbd and em tags from codeblock

---------

Signed-off-by: Dominic R <dominic@sdko.org>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
2025-04-16 15:16:01 -05:00
51609d696d policies/geoip: fix result when only dynamic results are used (#14107) 2025-04-16 15:50:26 +00:00
c0d08df161 core: bump opentelemetry-api from 1.32.0 to v1.32.1 (#14102) 2025-04-16 15:50:10 +00:00
643a97f0a5 core: bump rsa from 4.9 to v4.9.1 (#14103) 2025-04-16 09:51:53 -04:00
155a31fd70 sources/oauth: introduce authorization code auth method (#14034)
Co-authored-by: Rsgm <rsgm123@gmail.com>
2025-04-16 13:00:08 +00:00
c6f9d5df7b core, web: update translations (#14096)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-16 13:16:06 +02:00
ea85331a7e web/api: Fix Hoisted exports across entrypoints. Update Axios. (#14089)
* web/api: Fix issue where hoisted exports across entrypoints do not
order.

* web/api: Override OpenAPI transitive dep.
2025-04-15 20:09:41 +02:00
4f4c5253dd translate: Updates for file web/xliff/en.xlf in fr (#14091)
Translate web/xliff/en.xlf in fr

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 17:11:40 +00:00
83b2fc36df translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#14090)
Translate locale/en/LC_MESSAGES/django.po in fr

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 17:08:09 +00:00
d99d2b8bdc translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#14087)
Translate django.po in zh-Hans

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 18:40:19 +02:00
9b96d04b3a translate: Updates for file web/xliff/en.xlf in zh-Hans (#14086)
Translate web/xliff/en.xlf in zh-Hans

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 18:40:02 +02:00
ca5b99eb16 translate: Updates for file web/xliff/en.xlf in zh_CN (#14084)
Translate web/xliff/en.xlf in zh_CN

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 18:39:49 +02:00
4c1676e97c translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#14083)
Translate locale/en/LC_MESSAGES/django.po in zh_CN

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-15 18:39:36 +02:00
81855cf2fe core: bump google-auth from 2.38.0 to v2.39.0 (#14076) 2025-04-15 08:32:54 -04:00
bd904027be core: bump sentry-sdk from 2.25.1 to v2.26.1 (#14079) 2025-04-15 08:32:14 -04:00
0ffc97db15 core: bump prompt-toolkit from 3.0.50 to v3.0.51 (#14078) 2025-04-15 08:31:41 -04:00
2c515b1e17 core: bump boto3 from 1.37.33 to v1.37.34 (#14074) 2025-04-15 08:31:12 -04:00
f8900fbaf3 core: bump msgraph-sdk from 1.27.0 to v1.28.0 (#14077) 2025-04-15 08:30:44 -04:00
0f4a98d9c6 website/docs: fix minor typo in working_with_policies.md (#14071)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-04-15 11:40:23 +00:00
8853f25b45 core, web: update translations (#14064)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-15 13:26:02 +02:00
1c40f7b95a stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#14065)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-04-15 13:25:13 +02:00
9b5d6ec1af core: bump goauthentik.io/api/v3 from 3.2025024.4 to 3.2025024.6 (#14069)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 13:24:28 +02:00
36d29a9ae1 Small fix for Actual-Budget wiki guide (#14066)
Remove ending slash from redirect uri

Signed-off-by: James Armstrong <32995055+jmarmstrong1207@users.noreply.github.com>
2025-04-15 09:43:59 +01:00
0606b1aba4 root: support db pool (#13534) 2025-04-14 16:05:31 +00:00
03d5dad867 rbac: add InitialPermissions (#13795)
* add `InitialPermissions` model to RBAC

This is a powerful construct between Permission and Role to set initial
permissions for newly created objects.

* use safer `request.user`

* fixup! use safer `request.user`

* force all self-defined serializers to descend from our custom one

See https://github.com/goauthentik/authentik/pull/10139

* reorganize initial permission assignment

* fixup! reorganize initial permission assignment
2025-04-14 17:55:49 +02:00
38a9e46af3 web: bump API Client version (#14058)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-04-14 17:24:47 +02:00
5eb848e376 core: Bump django from 5.0.14 to 5.1.8 (#14059)
Bump django from 5.0.14 to 5.1.8
2025-04-14 14:54:58 +00:00
61a293daad core: bump django-rest-framework from 3.14.0 to 3.16.0 (#14057)
upgrade `django-rest-framework` to `3.16.0`

The reverted commit is purely an optimization which unfortunately breaks authentik, specifically Blueprints. It adds `getattr(serializer.instance, field)` to a validator. If `field` is a `RelatedObject`, that invocation queries the database.

When authentik creates objects using Blueprints, it doesn't place related objects into the database before the validator tries to get them from there, so with the reverted commit, it produces `RelatedObjectDoesNotExist`.

Perhaps a long-term solution is to revise how Blueprints work, or perhaps it is to change upstream. But in the meantime, Django 5.0 support ended and upgrading to Django 5.1 requires an upgrade of `django-rest-framework` to `3.16.0`, hence this workaround.

See
- https://github.com/encode/django-rest-framework/pull/9154
- https://github.com/encode/django-rest-framework/issues/9358
- https://github.com/encode/django-rest-framework/pull/9482
- https://github.com/encode/django-rest-framework/pull/9483
2025-04-14 16:24:11 +02:00
edf3300944 policies/reputation: limit reputation score (#14008)
* add limits to reputation score

* limit reputation score limits

Upper to non-negative, Lower to non-positive

* simplify tests

* "fix" bandit false-positives

* move magic numbers to constants

Is it too much to ask for a world in which I can just import these
straight from Python?
2025-04-14 14:18:59 +00:00
5d9c40eac8 ci: fix api-py-publish by disabling poetry cache (#14010) 2025-04-14 16:18:31 +02:00
6ebfbcb66e core: bump goauthentik/fips-python from 3.12.9-slim-bookworm-fips to 3.12.10-slim-bookworm-fips (#14044) 2025-04-14 08:15:20 -06:00
bf0235c113 ci: add NPM packages publish (#13974)
Co-authored-by: Teffen Ellis <teffen@nirri.us>
2025-04-14 08:14:17 -06:00
895cd23b57 root: add packages/ to codeowners (#13975) 2025-04-14 08:05:09 -06:00
c908d9e95e providers/oauth2, rac: make sure tokens are revoked after session deletion (#14011) 2025-04-14 15:48:39 +02:00
a07fd8d54b core: bump multidict from 6.4.2 to v6.4.3 (#14051) 2025-04-14 13:26:50 +00:00
39a46a6dc4 core: bump uvicorn from 0.34.0 to v0.34.1 (#14056) 2025-04-14 13:26:10 +00:00
ad71960d77 core: bump typing-extensions from 4.13.1 to v4.13.2 (#14055) 2025-04-14 13:04:16 +00:00
2a384511f5 core: bump ruff from 0.11.4 to v0.11.5 (#14053) 2025-04-14 13:03:52 +00:00
4dcc104947 core: bump boto3 from 1.37.31 to v1.37.33 (#14045) 2025-04-14 09:02:05 -04:00
71fe526e47 core: bump opentelemetry-api from 1.31.1 to v1.32.0 (#14052) 2025-04-14 09:01:39 -04:00
03e3f516ac core: bump httpcore from 1.0.7 to v1.0.8 (#14050) 2025-04-14 13:00:58 +00:00
3b59333246 core: bump google-api-python-client from 2.166.0 to v2.167.0 (#14048) 2025-04-14 13:00:20 +00:00
4e800c14cb core: bump googleapis-common-protos from 1.69.2 to v1.70.0 (#14049) 2025-04-14 12:59:54 +00:00
789b29a3e7 core: bump debugpy from 1.8.13 to v1.8.14 (#14047) 2025-04-14 12:59:21 +00:00
857b6e63a0 root: prevent docker-compose up when secret key is missing (#14043) 2025-04-14 12:56:41 +00:00
edc937dd78 core: bump goauthentik.io/api/v3 from 3.2025024.2 to 3.2025024.4 (#14042)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 14:49:32 +02:00
d98b6f29d4 core: bump github.com/sethvargo/go-envconfig from 1.1.1 to 1.2.0 (#14041)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 14:49:17 +02:00
53ba2a0ca8 core, web: update translations (#14037)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-14 14:40:38 +02:00
ae364292e6 website: Port WWW theme to docs site. Prep for package. (#13962)
Update sidebar.css

Signed-off-by: Teffen Ellis <592134+GirlBossRush@users.noreply.github.com>

website/docs: Update paths.

website/docs: Use package theme.
2025-04-12 01:31:57 +02:00
f15bc2df97 translate: Updates for file locale/en/LC_MESSAGES/django.po in nl [Manual Sync] (#14026)
Translate django.po in nl [Manual Sync]

80% of minimum 75% translated source file: 'django.po'
on 'nl'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 15:55:33 +00:00
b27d49e55f translate: Updates for file web/xliff/en.xlf in fi [Manual Sync] (#14012)
Translate web/xliff/en.xlf in fi [Manual Sync]

95% of minimum 75% translated source file: 'web/xliff/en.xlf'
on 'fi'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:38:37 +02:00
e0d2beb225 translate: Updates for file locale/en/LC_MESSAGES/django.po in de [Manual Sync] (#14020)
Translate django.po in de [Manual Sync]

96% of minimum 75% translated source file: 'django.po'
on 'de'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:38:11 +02:00
2313b4755b translate: Updates for file locale/en/LC_MESSAGES/django.po in pt_BR [Manual Sync] (#14027)
Translate django.po in pt_BR [Manual Sync]

75% of minimum 75% translated source file: 'django.po'
on 'pt_BR'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:37:55 +02:00
1cffadecb0 translate: Updates for file locale/en/LC_MESSAGES/django.po in it [Manual Sync] (#14024)
Translate django.po in it [Manual Sync]

99% of minimum 75% translated source file: 'django.po'
on 'it'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:37:40 +02:00
5e163d6da1 translate: Updates for file locale/en/LC_MESSAGES/django.po in pl [Manual Sync] (#14025)
Translate django.po in pl [Manual Sync]

82% of minimum 75% translated source file: 'django.po'
on 'pl'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:37:23 +02:00
0626e18674 translate: Updates for file locale/en/LC_MESSAGES/django.po in fi [Manual Sync] (#14023)
Translate django.po in fi [Manual Sync]

94% of minimum 75% translated source file: 'django.po'
on 'fi'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:37:04 +02:00
e986a62a12 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_TW [Manual Sync] (#14031)
Translate django.po in zh_TW [Manual Sync]

79% of minimum 75% translated source file: 'django.po'
on 'zh_TW'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:36:48 +02:00
e25afcb84a translate: Updates for file web/xliff/en.xlf in pl [Manual Sync] (#14015)
Translate web/xliff/en.xlf in pl [Manual Sync]

85% of minimum 75% translated source file: 'web/xliff/en.xlf'
on 'pl'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:36:33 +02:00
bb95613104 translate: Updates for file web/xliff/en.xlf in zh_CN [Manual Sync] (#14017)
Translate web/xliff/en.xlf in zh_CN [Manual Sync]

99% of minimum 75% translated source file: 'web/xliff/en.xlf'
on 'zh_CN'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:36:17 +02:00
89dfac2f57 translate: Updates for file web/xliff/en.xlf in it [Manual Sync] (#14016)
Translate web/xliff/en.xlf in it [Manual Sync]

99% of minimum 75% translated source file: 'web/xliff/en.xlf'
on 'it'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:36:14 +02:00
31462b55e6 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN [Manual Sync] (#14028)
Translate django.po in zh_CN [Manual Sync]

99% of minimum 75% translated source file: 'django.po'
on 'zh_CN'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:36:08 +02:00
60337c1cf0 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans [Manual Sync] (#14029)
Translate django.po in zh-Hans [Manual Sync]

99% of minimum 75% translated source file: 'django.po'
on 'zh-Hans'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 17:36:03 +02:00
343d3bb1fb translate: Updates for file locale/en/LC_MESSAGES/django.po in ru [Manual Sync] (#14032)
Translate django.po in ru [Manual Sync]

91% of minimum 75% translated source file: 'django.po'
on 'ru'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 15:09:01 +00:00
11fe86c4f6 translate: Updates for file locale/en/LC_MESSAGES/django.po in fr [Manual Sync] (#14022)
Translate django.po in fr [Manual Sync]

99% of minimum 75% translated source file: 'django.po'
on 'fr'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 15:07:16 +00:00
963ce085e4 translate: Updates for file locale/en/LC_MESSAGES/django.po in es [Manual Sync] (#14019)
Translate django.po in es [Manual Sync]

95% of minimum 75% translated source file: 'django.po'
on 'es'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 15:06:47 +00:00
3642b89ab0 translate: Updates for file web/xliff/en.xlf in zh-Hans [Manual Sync] (#14021)
Translate en.xlf in zh-Hans [Manual Sync]

99% of minimum 75% translated source file: 'en.xlf'
on 'zh-Hans'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 15:06:07 +00:00
8cfb371ed3 translate: Updates for file web/xliff/en.xlf in ru [Manual Sync] (#14013)
Translate web/xliff/en.xlf in ru [Manual Sync]

90% of minimum 75% translated source file: 'web/xliff/en.xlf'
on 'ru'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 15:05:28 +00:00
6e74edb9f2 web: bump API Client version (#13972)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-04-11 14:32:58 +02:00
397905f8f0 translate: Updates for file web/xliff/en.xlf in fr [Manual Sync] (#13979)
* Translate web/xliff/en.xlf in fr [Manual Sync]

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

* Removing web/xliff/en.xlf in fr

99% of minimum 100% translated source file: 'web/xliff/en.xlf'
on 'fr'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 14:22:54 +02:00
7fd35b1dfc sources/ldap: add source connections (#13796) 2025-04-11 12:07:18 +00:00
9ba03f5439 core: bump urllib3 from 2.3.0 to 2.4.0 (#14006)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-04-11 12:00:56 +00:00
1139d6d27c translate: Updates for file web/xliff/en.xlf in zh-Hans [Manual Sync] (#13985)
* Translate en.xlf in zh-Hans [Manual Sync]

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

* Removing web/xliff/en.xlf in zh-Hans

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

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 11:58:56 +00:00
077fd966c2 translate: Updates for file locale/en/LC_MESSAGES/django.po in ru [Manual Sync] (#13992)
Translate django.po in ru [Manual Sync]

91% of minimum 60% translated source file: 'django.po'
on 'ru'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:46:11 +02:00
bd41822a57 translate: Updates for file locale/en/LC_MESSAGES/django.po in de [Manual Sync] (#13986)
Translate django.po in de [Manual Sync]

96% of minimum 60% translated source file: 'django.po'
on 'de'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:45:49 +02:00
dfd3d76434 translate: Updates for file locale/en/LC_MESSAGES/django.po in nl [Manual Sync] (#13991)
Translate django.po in nl [Manual Sync]

81% of minimum 60% translated source file: 'django.po'
on 'nl'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:45:24 +02:00
397e98906d translate: Updates for file locale/en/LC_MESSAGES/django.po in es [Manual Sync] (#13987)
Translate django.po in es [Manual Sync]

95% of minimum 60% translated source file: 'django.po'
on 'es'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:45:12 +02:00
65d8da8c64 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_TW [Manual Sync] (#13994)
Translate django.po in zh_TW [Manual Sync]

79% of minimum 60% translated source file: 'django.po'
on 'zh_TW'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:44:55 +02:00
5b435297c5 translate: Updates for file locale/en/LC_MESSAGES/django.po in fi [Manual Sync] (#13988)
Translate django.po in fi [Manual Sync]

94% of minimum 60% translated source file: 'django.po'
on 'fi'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:44:39 +02:00
f792fd42f6 translate: Updates for file web/xliff/en.xlf in fi [Manual Sync] (#13978)
Translate web/xliff/en.xlf in fi [Manual Sync]

95% of minimum 60% translated source file: 'web/xliff/en.xlf'
on 'fi'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:44:32 +02:00
70c0fdd5fa translate: Updates for file locale/en/LC_MESSAGES/django.po in pl [Manual Sync] (#13989)
Translate django.po in pl [Manual Sync]

82% of minimum 60% translated source file: 'django.po'
on 'pl'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:44:24 +02:00
9b636eba01 translate: Updates for file locale/en/LC_MESSAGES/django.po in pt_BR [Manual Sync] (#13990)
Translate django.po in pt_BR [Manual Sync]

75% of minimum 60% translated source file: 'django.po'
on 'pt_BR'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:44:19 +02:00
a982224502 translate: Updates for file locale/en/LC_MESSAGES/django.po in tr [Manual Sync] (#13995)
Translate django.po in tr [Manual Sync]

91% of minimum 60% translated source file: 'django.po'
on 'tr'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:44:13 +02:00
6a16cccb40 translate: Updates for file locale/en/LC_MESSAGES/django.po in ko [Manual Sync] (#13993)
Translate django.po in ko [Manual Sync]

68% of minimum 60% translated source file: 'django.po'
on 'ko'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-11 13:43:49 +02:00
6dac91e2b4 core: bump github.com/getsentry/sentry-go from 0.31.1 to 0.32.0 (#14004)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 13:43:29 +02:00
3e2d0532d1 core: bump goauthentik.io/api/v3 from 3.2025024.1 to 3.2025024.2 (#14005)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 13:42:03 +02:00
4e1300650b core, web: update translations (#13999)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-11 13:40:50 +02:00
06b3ed0c9c core: fix migrations (#14009) 2025-04-11 13:36:53 +02:00
395ad722b7 core: migrate all sessions to the database (#9736)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-04-11 09:10:55 +02:00
9917d81246 website/integrations: add openproject (#13838)
* Added scope mapping section

* Updated formatting

* Bolded UI elements

* Update website/integrations/services/openproject/index.md

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/integrations/services/openproject/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/integrations/services/openproject/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/integrations/services/openproject/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Indented code block

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-04-10 15:52:00 -05:00
2a87687d34 website/integrations: add wazuh (#13776)
* Document explaining integration between authentik and knocknoc

* Clarified Knocknoc config

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Fixed typos

* Document mostly complete. Work to be done on Wazuh config section

* Completed the Wazuh config section

* Changed URL in Wazuh config

* typo

* Removed knocknoc doc

* Update website/integrations/services/wazuh/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/integrations/services/wazuh/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/integrations/services/wazuh/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/integrations/services/wazuh/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/integrations/services/wazuh/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Multiline codeblocks and moved SAML metadata note to beginning of section.

* Update sidebarsIntegrations.js to remove knoknok

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/integrations/services/wazuh/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/integrations/services/wazuh/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Changed group creation section to cut down on repetition of the word "click"

* Update website/integrations/services/wazuh/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Fixed mistake in the config.yml section and applied various suggestions from Dominic

* Fixed multilinecodeblocks and commands formatting

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Changed multiline codeblocks due to formatting issues.

* Clarified what run_as parameter does

* Update website/integrations/services/wazuh/index.mdx

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/integrations/services/wazuh/index.mdx

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/integrations/services/wazuh/index.mdx

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/integrations/services/wazuh/index.mdx

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/integrations/services/wazuh/index.mdx

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/integrations/services/wazuh/index.mdx

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Fixed codeblock indenting and prettier issue

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Dominic R <dominic@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-04-10 15:43:02 -05:00
a726c2260a translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans [Manual Sync] (#13996)
Translate django.po in zh-Hans [Manual Sync]

99% of minimum 60% translated source file: 'django.po'
on 'zh-Hans'.

Sync of partially translated files: 
untranslated content is included with an empty translation 
or source language content depending on file format

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-10 15:38:46 +00:00
44e0bfd4ef website: dev docs: fix upper-case authentik (#13961)
Signed-off-by: Dominic R <dominic@sdko.org>
2025-04-10 16:06:17 +01:00
8d0b362c9c web: elements: Table: Fix table selection clearing behavior (#13959)
web: elements: Table: Fix table selection clearing and modal closing behavior

Closes https://github.com/goauthentik/authentik/issues/13831
2025-04-10 17:03:02 +02:00
e5e53f034e core: bump multidict from 6.2.0 to v6.4.2 (#13971) 2025-04-10 10:24:19 -04:00
71b87127d1 core: bump msgraph-sdk from 1.26.0 to v1.27.0 (#13970) 2025-04-10 10:23:42 -04:00
d5d67fe22d core: bump boto3 from 1.37.30 to v1.37.31 (#13968) 2025-04-10 10:23:18 -04:00
5d2685341d sources/ldap: lookup group memberships from user attribute (#12661)
* sources/ldap: add support for group lookups from user

* sources/ldap: implement working membership lookups

* sources/ldap: add schema changes

* sources/ldap: add group membership toggle ui element

* sources/ldap: lint changed files

* website/docs: add note about lookups to AD docs

* Update website/docs/users-sources/sources/directory-sync/active-directory/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Amélie Krejčí <amelie@krejci.vip>

* website/docs: simplify wording of attribute documentation

Follows suggestions from @jorhett

* sources/ldap: add missing spaces in docstrings

Follows suggestions from @jorhett

* Add a test for memberof attribute

* sources/ldap: implement test

* format

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

* re-migrate

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

* revert website changes in favor of #13966

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* update frontend help text

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Amélie Krejčí <amelie@krejci.vip>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Shawn Weeks <sweeks@weeksconsulting.us>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jo Rhett <geek@jorhett.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-04-10 14:37:38 +02:00
f1ac4ff9c9 translate: Updates for file web/xliff/en.xlf in it (#13956)
Translate web/xliff/en.xlf in it

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-10 13:40:33 +02:00
79f4c66286 translate: Updates for file locale/en/LC_MESSAGES/django.po in it (#13957)
Translate locale/en/LC_MESSAGES/django.po in it

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-10 13:40:12 +02:00
1f82094c0b core: bump astral-sh/uv from 0.6.13 to 0.6.14 (#13964)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-10 13:36:27 +02:00
35440acba3 core: bump django from 5.0.13 to 5.0.14 (#13954)
bump django to 5.0.14
2025-04-10 12:55:27 +02:00
eca9901704 website/docs: Remove enterprise badge from Fleet integration (#13963)
website/docs: Remove enterprise badge.
2025-04-10 08:29:39 +01:00
6ddd5a3d5f website/integrations: add Homarr (#13818)
* Adding Homarr integration details

* Fixing typo in homarr doc

* Lint fix

* Update website/integrations/services/homarr/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Nate Fonseka <882236+nfons@users.noreply.github.com>

* Update website/integrations/services/homarr/index.md

Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Nate Fonseka <882236+nfons@users.noreply.github.com>

* Update website/integrations/services/homarr/index.md

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

---------

Signed-off-by: Nate Fonseka <882236+nfons@users.noreply.github.com>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: nfonseka <nfonseka@rxsense.com>
Co-authored-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-04-10 08:15:15 +01:00
5664e62eca website/integration: update harbor integration document (#13816)
* Updates harbor integration doc to new template and fixes reported issue

* typo

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
2025-04-09 18:23:52 -05:00
1403f17d62 Fix redirect URL in wordpress integration guide (#13955)
Update wordpress integration guide to fix redirect URL

Signed-off-by: virtualboys <emanmomot@gmail.com>
2025-04-09 17:10:33 -05:00
1ac8989e81 lifecycle/aws: bump aws-cdk-lib (#13953) 2025-04-09 18:24:45 +00:00
b0a1db77e3 core: bump protobuf from 5.29.4 to v6.30.2 (#13950) 2025-04-09 18:01:53 +00:00
46da4cb59e core: bump pyasn1-modules from 0.4.1 to v0.4.2 (#13951) 2025-04-09 18:01:16 +00:00
154df5cdf7 core: bump microsoft-kiota-authentication-azure from 1.9.2 to v1.9.3 (#13948) 2025-04-09 18:00:55 +00:00
5b889456f6 core: bump microsoft-kiota-http from 1.9.2 to v1.9.3 (#13949) 2025-04-09 18:00:20 +00:00
3eaed82c48 core: bump trio-websocket from 0.11.1 to v0.12.2 (#13934) 2025-04-09 17:49:55 +00:00
feaf9d8bc9 core: bump msgraph-core from 1.3.1 to v1.3.3 (#13900) 2025-04-09 17:48:08 +00:00
2899668ae2 core: bump jsii from 1.109.0 to v1.111.0 (#13886) 2025-04-09 17:46:29 +00:00
4c25e1bb24 core: bump setuptools from 72.1.0 to v78.1.0 (#13928) 2025-04-09 19:29:15 +02:00
464ff3f5b1 core: bump kombu from 5.3.7 to v5.5.2 (#13888) 2025-04-09 19:27:57 +02:00
22eb5f56f1 core: bump msgpack from 1.0.8 to v1.1.0 (#13899) 2025-04-09 19:27:27 +02:00
7e48e87f49 core: bump msgraph-sdk from 1.24.0 to v1.26.0 (#13901) 2025-04-09 19:26:52 +02:00
8ce12f7850 core: bump proto-plus from 1.24.0 to v1.26.1 (#13910) 2025-04-09 19:26:37 +02:00
2514baabeb core: bump protobuf from 5.27.2 to v5.29.4 (#13911) 2025-04-09 19:26:16 +02:00
945930a507 core: bump pydantic from 2.10.6 to v2.11.3 (#13914) 2025-04-09 19:25:51 +02:00
537a80ad97 core: bump rich from 13.7.1 to v14.0.0 (#13922) 2025-04-09 19:25:26 +02:00
5c993e23fe core: bump twisted from 24.7.0 to v24.11.0 (#13936) 2025-04-09 19:25:02 +02:00
eb2db18494 core: bump watchfiles from 0.22.0 to v1.0.5 (#13941) 2025-04-09 19:24:40 +02:00
12a46a8426 core: bump typing-extensions from 4.12.2 to v4.13.1 (#13937) 2025-04-09 19:24:25 +02:00
4a1213310a core: bump multidict from 6.0.5 to v6.2.0 (#13902) 2025-04-09 19:24:11 +02:00
84c2097148 core: bump sentry-sdk from 2.22.0 to v2.25.1 (#13927) 2025-04-09 19:23:56 +02:00
c05dedc573 core: bump rpds-py from 0.19.1 to v0.24.0 (#13923) 2025-04-09 19:23:38 +02:00
18c197e75b core: bump propcache from 0.2.0 to v0.3.1 (#13909) 2025-04-09 19:23:29 +02:00
0c26a0bce2 core: bump pbr from 6.0.0 to v6.1.1 (#13905) 2025-04-09 19:23:17 +02:00
5fd6a4cead core: bump golang.org/x/sync from 0.12.0 to 0.13.0 (#13787)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 17:22:55 +00:00
51fb1bd8e7 core: bump ua-parser-builtins from 0.18.0 to v0.18.0.post1 (#13939) 2025-04-09 19:21:54 +02:00
4a30f87a42 core: bump msal from 1.30.0 to v1.32.0 (#13897) 2025-04-09 19:21:42 +02:00
8e6b6ede30 core: bump msal-extensions from 1.2.0 to v1.3.1 (#13898) 2025-04-09 19:21:33 +02:00
af30c2a68e core: bump prometheus-client from 0.20.0 to v0.21.1 (#13907) 2025-04-09 19:21:23 +02:00
9b65627a3e core: bump jsonschema-specifications from 2023.12.1 to v2024.10.1 (#13887) 2025-04-09 19:21:13 +02:00
4bad91c901 core: bump markupsafe from 2.1.5 to v3.0.2 (#13890) 2025-04-09 19:21:01 +02:00
f3c479d077 core: bump stevedore from 5.2.0 to v5.4.1 (#13932) 2025-04-09 19:20:47 +02:00
b024df9903 core: bump zope-interface from 6.4.post2 to v7.2 (#13946) 2025-04-09 19:20:30 +02:00
f6a6458088 core: bump websockets from 12.0 to v15.0.1 (#13942) 2025-04-09 19:18:14 +02:00
f0dc0e8900 core: bump platformdirs from 4.2.2 to v4.3.7 (#13906) 2025-04-09 19:17:21 +02:00
79e89b0376 core: bump selenium from 4.29.0 to v4.31.0 (#13926) 2025-04-09 19:17:11 +02:00
4cc7d91379 core: bump six from 1.16.0 to v1.17.0 (#13929) 2025-04-09 19:17:00 +02:00
245909e31a core: bump hpack from 4.0.0 to v4.1.0 (#13878) 2025-04-09 19:16:51 +02:00
997a1ddb3d core: bump iniconfig from 2.0.0 to v2.1.0 (#13885) 2025-04-09 19:16:34 +02:00
42335a60bf core: bump hyperframe from 6.0.1 to v6.1.0 (#13882) 2025-04-09 19:16:20 +02:00
fc539332e1 core: bump uvloop from 0.19.0 to v0.21.0 (#13940) 2025-04-09 19:16:05 +02:00
d9efb02078 core: bump httpx from 0.27.0 to v0.28.1 (#13880) 2025-04-09 19:15:53 +02:00
6212250e19 core: bump importlib-resources from 6.4.0 to v6.5.2 (#13884) 2025-04-09 19:15:40 +02:00
c18beefc8f core: bump zipp from 3.20.2 to v3.21.0 (#13945) 2025-04-09 19:15:30 +02:00
f23da6e402 core: bump trio from 0.26.0 to v0.29.0 (#13933) 2025-04-09 19:14:56 +02:00
e934b246c8 core: bump deprecated from 1.2.14 to v1.2.18 (#13866) 2025-04-09 19:14:29 +02:00
ead684a410 core: bump wrapt from 1.16.0 to v1.17.2 (#13943) 2025-04-09 19:14:04 +02:00
d782aadab7 core: bump h2 from 4.1.0 to v4.2.0 (#13877) 2025-04-09 19:13:50 +02:00
4ac6f83aea core: bump yarl from 1.17.2 to v1.19.0 (#13944) 2025-04-09 19:13:38 +02:00
6281d36a69 core: bump django-storages from 1.14.5 to v1.14.6 (#13869) 2025-04-09 17:10:57 +00:00
8129ad4ec0 core: bump github.com/coreos/go-oidc/v3 from 3.13.0 to 3.14.1 (#13772)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 19:01:47 +02:00
24eea415b2 core: bump golang.org/x/oauth2 from 0.28.0 to 0.29.0 (#13788)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 19:00:38 +02:00
a615ce8e95 web: bump API Client version (#13798)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-04-09 18:59:59 +02:00
5b275cf7fb core: bump goauthentik.io/api/v3 from 3.2025023.4 to 3.2025024.1 (#13833)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 18:59:39 +02:00
d6e91c119f core, web: update translations (#13832)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-09 18:59:17 +02:00
7841e47e74 core: bump celery from 5.4.0 to v5.5.1 (#13858) 2025-04-09 18:58:06 +02:00
ad2a4bea3e core: bump ruff from 0.9.10 to v0.11.4 (#13924) 2025-04-09 16:53:44 +00:00
a554c085c1 core: bump tzdata from 2024.1 to v2025.2 (#13938) 2025-04-09 16:20:39 +00:00
ff0d978754 core: bump std-uritemplate from 2.0.2 to v2.0.3 (#13931) 2025-04-09 16:19:54 +00:00
de48e62819 core: bump twilio from 9.5.0 to v9.5.2 (#13935) 2025-04-09 16:19:18 +00:00
e50e995d2f core: bump sqlparse from 0.5.1 to v0.5.3 (#13930) 2025-04-09 16:18:57 +00:00
3bf4156cb3 core: bump referencing from 0.35.1 to v0.36.2 (#13921) 2025-04-09 16:17:31 +00:00
89990facf5 core: bump pytest-django from 4.10.0 to v4.11.1 (#13917) 2025-04-09 16:17:26 +00:00
48545950ed core: bump redis from 5.0.7 to v5.2.1 (#13920) 2025-04-09 16:17:12 +00:00
0544aa5fae core: bump pyparsing from 3.1.2 to v3.2.3 (#13916) 2025-04-09 16:16:48 +00:00
5d69455b87 core: bump pytz from 2024.1 to v2025.2 (#13919) 2025-04-09 16:16:24 +00:00
3d291cf4da core: bump python-dotenv from 1.0.1 to v1.1.0 (#13918) 2025-04-09 16:15:36 +00:00
44d7c42dc7 core: bump pyasn1 from 0.6.0 to v0.6.1 (#13912) 2025-04-09 16:15:22 +00:00
4ea4e925e3 core: bump pygments from 2.18.0 to v2.19.1 (#13915) 2025-04-09 16:14:57 +00:00
169172c85f core: bump prompt-toolkit from 3.0.47 to v3.0.50 (#13908) 2025-04-09 16:14:52 +00:00
adea637fa4 core: bump pyasn1-modules from 0.4.0 to v0.4.1 (#13913) 2025-04-09 16:14:38 +00:00
0231277d9c core: bump opentelemetry-api from 1.31.0 to v1.31.1 (#13903) 2025-04-09 16:13:12 +00:00
45643ed1f6 core: bump microsoft-kiota-serialization-form from 1.9.2 to v1.9.3 (#13893) 2025-04-09 16:12:37 +00:00
3823d56dbd core: bump orjson from 3.10.6 to v3.10.16 (#13904) 2025-04-09 16:12:22 +00:00
43cfd59ac0 core: bump microsoft-kiota-serialization-text from 1.9.2 to v1.9.3 (#13896) 2025-04-09 16:11:24 +00:00
c8555bbf59 core: bump microsoft-kiota-serialization-json from 1.9.2 to v1.9.3 (#13894) 2025-04-09 16:11:07 +00:00
a4251a3410 core: bump microsoft-kiota-serialization-multipart from 1.9.2 to v1.9.3 (#13895) 2025-04-09 16:10:33 +00:00
50985f9b0b core: bump lxml from 5.3.1 to v5.3.2 (#13889) 2025-04-09 16:08:51 +00:00
9ec24528d4 core: bump maxminddb from 2.6.2 to v2.6.3 (#13891) 2025-04-09 16:08:41 +00:00
5eac38c0cc core: bump humanize from 4.10.0 to v4.12.2 (#13881) 2025-04-09 16:08:36 +00:00
010df0c31c core: bump httpcore from 1.0.5 to v1.0.7 (#13879) 2025-04-09 16:08:18 +00:00
7ba858eff3 core: bump django-pgactivity from 1.4.1 to v1.7.1 (#13868) 2025-04-09 16:08:00 +00:00
817d2d5ff8 core: bump googleapis-common-protos from 1.63.2 to v1.69.2 (#13876) 2025-04-09 16:07:37 +00:00
70e34e03b4 core: bump dnspython from 2.6.1 to v2.7.0 (#13870) 2025-04-09 16:07:18 +00:00
d61f9f6d57 core: bump google-api-core from 2.19.1 to v2.24.2 (#13873) 2025-04-09 16:07:03 +00:00
bdf81706b8 core: bump duo-client from 5.4.0 to v5.5.0 (#13871) 2025-04-09 16:06:47 +00:00
7b56602fc9 core: bump idna from 3.7 to v3.10 (#13883) 2025-04-09 16:06:32 +00:00
7c6e25a996 core: bump google-api-python-client from 2.164.0 to v2.166.0 (#13874) 2025-04-09 16:06:15 +00:00
0eeaeaf1ff core: bump google-auth from 2.32.0 to v2.38.0 (#13875) 2025-04-09 16:06:03 +00:00
9ce4337b11 core: bump coverage from 7.6.12 to v7.8.0 (#13864) 2025-04-09 16:05:37 +00:00
c6a3c7371c core: bump frozenlist from 1.4.1 to v1.5.0 (#13872) 2025-04-09 16:05:21 +00:00
42a7cf10f2 core: bump decorator from 5.1.1 to v5.2.1 (#13865) 2025-04-09 16:04:55 +00:00
bb4f7b1193 core: bump click from 8.1.7 to v8.1.8 (#13863) 2025-04-09 16:04:14 +00:00
3eecfb835b core: bump charset-normalizer from 3.3.2 to v3.4.1 (#13862) 2025-04-09 16:02:41 +00:00
92ab856bd3 core: bump channels from 4.2.0 to v4.2.2 (#13861) 2025-04-09 16:01:28 +00:00
178549a756 core: bump cffi from 1.16.0 to v1.17.1 (#13860) 2025-04-09 15:59:19 +00:00
67d178aa11 core: bump certifi from 2024.7.4 to v2025.1.31 (#13859) 2025-04-09 15:56:20 +00:00
ef53abace9 core: bump cattrs from 24.1.2 to v24.1.3 (#13857) 2025-04-09 15:46:55 +00:00
5effb3a0f6 core: bump cachetools from 5.4.0 to v5.5.2 (#13856) 2025-04-09 15:43:53 +00:00
3a37916a8f core: bump boto3 from 1.34.150 to v1.37.30 (#13854) 2025-04-09 15:36:10 +00:00
428d5ac9cf core: bump attrs from 23.2.0 to 25.3.0 (#13846) 2025-04-09 17:33:00 +02:00
7b4037fdda core: bump anyio from 4.4.0 to 4.9.0 (#13845) 2025-04-09 17:32:28 +02:00
2c7bbcc27b core: bump billiard from 4.2.0 to v4.2.1 (#13853) 2025-04-09 15:19:45 +00:00
19fb24de99 core: bump azure-core from 1.30.2 to 1.33.0 (#13850) 2025-04-09 15:18:54 +00:00
2709702896 core: bump bcrypt from 4.2.0 to v4.3.0 (#13852) 2025-04-09 15:18:39 +00:00
7d0d5a7dc2 core: bump azure-identity from 1.17.1 to 1.21.0 (#13851) 2025-04-09 15:17:46 +00:00
6a04a2ca69 core: bump automat from 22.10.0 to 24.8.1 (#13848) 2025-04-09 15:15:01 +00:00
ea561c9da6 core: bump amqp from 5.2.0 to 5.3.1 (#13844) 2025-04-09 15:08:00 +00:00
9b9c55f17c core: bump aiosignal from 1.3.1 to 1.3.2 (#13843) 2025-04-09 15:07:22 +00:00
bd5e78bd44 core: bump aiohttp-retry from 2.8.3 to 2.9.1 (#13842) 2025-04-09 15:07:10 +00:00
ab98028022 core: bump aiohttp from 3.10.11 to 3.11.16 (#13841) 2025-04-09 15:03:36 +00:00
813ff64ba1 core: bump autobahn from 23.6.2 to 24.4.2 (#13847) 2025-04-09 15:02:57 +00:00
c99e742214 core: bump aiohappyeyeballs from 2.3.5 to 2.6.1 (#13840) 2025-04-09 17:00:49 +02:00
dac6ad3cd6 core: bump github.com/prometheus/client_golang from 1.21.1 to 1.22.0 (#13834)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 15:48:58 +02:00
e4d2a53ccc release: 2025.2.4 (#13830)
* release: 2025.2.4

* bump version in uv.lock
2025-04-08 19:16:00 +00:00
3b6775fd9c website/integrations: rename Hoarder to Karakeep (#13789)
* Hoarder renamed to karakeep

The Hoarder app has been renamed to Karakeep recently. https://www.reddit.com/r/selfhosted/comments/1js667o/hoarder_is_rebranding_to_karakeep/

Signed-off-by: petrsimunek <49954958+petrsimunek@users.noreply.github.com>

* folder hoarder renamed to karakeep

* sidebar changed from hoarder to karakeep

---------

Signed-off-by: petrsimunek <49954958+petrsimunek@users.noreply.github.com>
2025-04-08 13:09:11 -05:00
5882e0b2cb website/docs: Add release notes for 2025.2.4 (#13829) 2025-04-08 18:08:03 +00:00
65f0b471d8 website/docs: Add release notes for 2024.12.5 (#13828)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-04-08 17:56:15 +00:00
7d054db1a5 Revert "core: fix non-exploitable open redirect (#13696)" (#13824) 2025-04-08 17:10:12 +00:00
cb75ba2e5e translate: Updates for file web/xliff/en.xlf in fr (#13822)
Translate web/xliff/en.xlf in fr

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-08 16:56:13 +00:00
36cecc1391 translate: Updates for file web/xliff/en.xlf in zh_CN (#13820)
Translate web/xliff/en.xlf in zh_CN

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-08 18:42:16 +02:00
81b91d8777 translate: Updates for file web/xliff/en.xlf in zh-Hans (#13821)
Translate web/xliff/en.xlf in zh-Hans

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2025-04-08 18:41:58 +02:00
41dc23b3c2 core: users API: add date_joined (#13817) 2025-04-08 13:26:11 +00:00
370eff1494 website/integrations: fix ArgoCD redirect paths (#13804) 2025-04-08 15:24:46 +02:00
0ff8def03b core, web: update translations (#13806)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-08 15:23:29 +02:00
b01cafd9fe core: bump goauthentik.io/api/v3 from 3.2025023.2 to 3.2025023.4 (#13811)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 15:19:30 +02:00
90aa8abb80 core: bump astral-sh/uv from 0.6.12 to 0.6.13 (#13812)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 15:19:04 +02:00
fd21aae4f9 website/docs: Correcting IDP URLs on Mailcow integration page (#13803)
Updating URLs to correct IDP URLs

Signed-off-by: Adam Berry <adam.berry@racklab.io>
2025-04-08 08:35:55 +01:00
360223a2ff web: Flesh out configs. (#13801) 2025-04-08 01:21:05 +02:00
0e83de2697 web: Tidy temporal utilities. (#13755) 2025-04-07 18:37:03 +00:00
a23bac9d9b website/integrations: nextcloud: add warning about admin lockout (#13782)
* docs: add a danger warning in nextcloud integration

If a user follwoing the guide for OpenID integration. They can lock out their Admin users, if used the customer profile scope and select the **use unique user ID** option. 
So a danger box was added to let people know that can happen and why

Signed-off-by: Unfaehig <38919962+Unfaehig@users.noreply.github.com>

* docs: website/integrations/services/nextcloud/index.mdx

Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Unfaehig <38919962+Unfaehig@users.noreply.github.com>

* chore: nextcloud integration fix formating

---------

Signed-off-by: Unfaehig <38919962+Unfaehig@users.noreply.github.com>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-04-07 13:13:41 -05:00
505 changed files with 69279 additions and 41490 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2025.2.3
current_version = 2025.2.4
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*))?

View File

@ -10,6 +10,9 @@ insert_final_newline = true
[*.html]
indent_size = 2
[schemas/*.json]
indent_size = 2
[*.{yaml,yml}]
indent_size = 2

View File

@ -28,9 +28,9 @@ runs:
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: web/package.json
node-version-file: package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
cache-dependency-path: package-lock.json
- name: Setup go
uses: actions/setup-go@v5
with:
@ -44,7 +44,7 @@ runs:
run: |
export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/docker-compose.yml up -d
cd web && npm ci
npm ci
- name: Generate config
shell: uv run python {0}
run: |

View File

@ -1,5 +1,5 @@
# Re-usable workflow for a single-architecture build
name: Single-arch Container build
name: "Single-arch Container build"
on:
workflow_call:
@ -42,7 +42,7 @@ jobs:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3.6.0
- uses: docker/setup-buildx-action@v3
- name: prepare variables
- name: Prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
@ -64,12 +64,12 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: make empty clients
- name: Make empty clients
if: ${{ inputs.release }}
run: |
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: generate ts client
- name: Generate TypeScript API Client
if: ${{ !inputs.release }}
run: make gen-client-ts
- name: Build Docker Image

View File

@ -1,5 +1,5 @@
# Re-usable workflow for a multi-architecture build
name: Multi-arch container build
name: "Multi-arch container build"
on:
workflow_call:
@ -49,7 +49,7 @@ jobs:
shouldPush: ${{ steps.ev.outputs.shouldPush }}
steps:
- uses: actions/checkout@v4
- name: prepare variables
- name: Prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
@ -69,7 +69,7 @@ jobs:
tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
steps:
- uses: actions/checkout@v4
- name: prepare variables
- name: Prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:

View File

@ -1,4 +1,5 @@
name: authentik-api-py-publish
name: "Python API Publish"
on:
push:
branches: [main]
@ -7,6 +8,7 @@ on:
workflow_dispatch:
jobs:
build:
name: "Build and Publish"
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
permissions:
@ -30,8 +32,7 @@ jobs:
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"
cache: "poetry"
- name: Generate API Client
- name: Generate Python API Client
run: make gen-client-py
- name: Publish package
working-directory: gen-py-api/

View File

@ -1,4 +1,4 @@
name: authentik-api-ts-publish
name: "TypeScript API Publish"
on:
push:
branches: [main]
@ -7,6 +7,7 @@ on:
workflow_dispatch:
jobs:
build:
name: "Build and Publish"
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:
@ -20,9 +21,9 @@ jobs:
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
node-version-file: package.json
registry-url: "https://registry.npmjs.org"
- name: Generate API Client
- name: Generate TypeScript API Client
run: make gen-client-ts
- name: Publish package
working-directory: gen-ts-api/

View File

@ -1,4 +1,4 @@
name: authentik-ci-aws-cfn
name: "authentik CI AWS CloudFormation"
on:
push:
@ -18,6 +18,7 @@ env:
jobs:
check-changes-applied:
name: "Check changes applied"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -36,6 +37,7 @@ jobs:
uv run make aws-cfn
git diff --exit-code
ci-aws-cfn-mark:
name: "CI AWS CloudFormation Mark"
if: always()
needs:
- check-changes-applied

View File

@ -1,5 +1,5 @@
---
name: authentik-ci-main-daily
name: "authentik CI Main Daily"
on:
workflow_dispatch:
@ -9,6 +9,7 @@ on:
jobs:
test-container:
name: "Test Container ${{ matrix.version }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false

View File

@ -1,5 +1,5 @@
---
name: authentik-ci-main
name: "authentik CI Main"
on:
push:
@ -19,6 +19,7 @@ env:
jobs:
lint:
name: "Lint"
strategy:
fail-fast: false
matrix:
@ -33,9 +34,10 @@ jobs:
- uses: actions/checkout@v4
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run job
- name: Run job ${{ matrix.job }}
run: uv run make ci-${{ matrix.job }}
test-migrations:
name: "Test Migrations"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -44,6 +46,7 @@ jobs:
- name: run migrations
run: uv run python -m lifecycle.migrate
test-make-seed:
name: "Test Make Seed"
runs-on: ubuntu-latest
steps:
- id: seed
@ -52,7 +55,7 @@ jobs:
outputs:
seed: ${{ steps.seed.outputs.seed }}
test-migrations-from-stable:
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
name: "Test Migrations From Stable - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5"
runs-on: ubuntu-latest
timeout-minutes: 20
needs: test-make-seed
@ -67,7 +70,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: checkout stable
- name: Checkout Stable
run: |
# Copy current, latest config to local
# Temporarly comment the .github backup while migrating to uv
@ -84,9 +87,9 @@ jobs:
with:
postgresql_version: ${{ matrix.psql }}
continue-on-error: true
- name: run migrations to stable
- name: Run migrations to stable
run: poetry run python -m lifecycle.migrate
- name: checkout current code
- name: Checkout current code
run: |
set -x
git fetch
@ -97,10 +100,10 @@ jobs:
uses: ./.github/actions/setup
with:
postgresql_version: ${{ matrix.psql }}
- name: migrate to latest
- name: Migrate to latest
run: |
uv run python -m lifecycle.migrate
- name: run tests
- name: Run tests
env:
# Test in the main database that we just migrated from the previous stable version
AUTHENTIK_POSTGRESQL__TEST__NAME: authentik
@ -110,7 +113,7 @@ jobs:
run: |
uv run make ci-test
test-unittest:
name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
name: "Unit tests - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5"
runs-on: ubuntu-latest
timeout-minutes: 20
needs: test-make-seed
@ -146,6 +149,7 @@ jobs:
file: unittest.xml
token: ${{ secrets.CODECOV_TOKEN }}
test-integration:
name: "Integration tests"
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@ -154,7 +158,7 @@ jobs:
uses: ./.github/actions/setup
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.12.0
- name: run integration
- name: Run integration
run: |
uv run coverage run manage.py test tests/integration
uv run coverage xml
@ -170,49 +174,50 @@ jobs:
file: unittest.xml
token: ${{ secrets.CODECOV_TOKEN }}
test-e2e:
name: test-e2e (${{ matrix.job.name }})
name: "Test E2E (${{ matrix.job.name }})"
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
job:
- name: proxy
- name: Proxy Provider
glob: tests/e2e/test_provider_proxy*
- name: oauth
- name: OAuth2 Provider
glob: tests/e2e/test_provider_oauth2* tests/e2e/test_source_oauth*
- name: oauth-oidc
- name: OIDC Provider
glob: tests/e2e/test_provider_oidc*
- name: saml
- name: SAML Provider
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
- name: ldap
- name: LDAP Provider
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
- name: radius
- name: RADIUS Provider
glob: tests/e2e/test_provider_radius*
- name: scim
- name: SCIM Source
glob: tests/e2e/test_source_scim*
- name: flows
- name: Flows
glob: tests/e2e/test_flows*
steps:
- uses: actions/checkout@v4
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Setup e2e env (chrome, etc)
- name: Setup E2E env (chrome, etc)
run: |
docker compose -f tests/e2e/docker-compose.yml up -d --quiet-pull
- id: cache-web
uses: actions/cache@v4
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/src/**') }}
- name: prepare web ui
key: ${{ runner.os }}-web-${{ hashFiles('./package-lock.json', 'web/src/**') }}
- name: Prepare Web UI
if: steps.cache-web.outputs.cache-hit != 'true'
working-directory: web
run: |
npm ci
make -C .. gen-client-ts
npm run build
- name: run e2e
make gen-client-ts
npm run build -w @goauthentik/web
npm run typecheck
- name: Run E2E tests
run: |
uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage xml
@ -228,6 +233,7 @@ jobs:
file: unittest.xml
token: ${{ secrets.CODECOV_TOKEN }}
ci-core-mark:
name: "CI Core Mark"
if: always()
needs:
- lint
@ -242,6 +248,7 @@ jobs:
with:
jobs: ${{ toJSON(needs) }}
build:
name: "Build"
permissions:
# Needed to upload container images to ghcr.io
packages: write
@ -255,6 +262,7 @@ jobs:
image_name: ghcr.io/goauthentik/dev-server
release: false
pr-comment:
name: "PR Comment"
needs:
- build
runs-on: ubuntu-latest
@ -267,7 +275,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: prepare variables
- name: Prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:

View File

@ -1,5 +1,5 @@
---
name: authentik-ci-outpost
name: "authentik CI Outpost"
on:
push:
@ -14,6 +14,7 @@ on:
jobs:
lint-golint:
name: "Lint Go"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -26,7 +27,7 @@ jobs:
mkdir -p web/dist
mkdir -p website/help
touch web/dist/test website/help/test
- name: Generate API
- name: Generate Go API Client
run: make gen-client-go
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
@ -35,6 +36,7 @@ jobs:
args: --timeout 5000s --verbose
skip-cache: true
test-unittest:
name: "Unit Test Go"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -43,12 +45,13 @@ jobs:
go-version-file: "go.mod"
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Generate API
- name: Generate Go API Client
run: make gen-client-go
- name: Go unittests
run: |
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./...
ci-outpost-mark:
name: "CI Outpost Mark"
if: always()
needs:
- lint-golint
@ -59,6 +62,7 @@ jobs:
with:
jobs: ${{ toJSON(needs) }}
build-container:
name: "Build Container"
timeout-minutes: 120
needs:
- ci-outpost-mark
@ -85,7 +89,7 @@ jobs:
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
- name: Prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
@ -99,7 +103,7 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate API
- name: Generate Go API Client
run: make gen-client-go
- name: Build Docker Image
id: push
@ -122,6 +126,7 @@ jobs:
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
build-binary:
name: "Build Binary"
timeout-minutes: 120
needs:
- ci-outpost-mark
@ -140,21 +145,22 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: npm ci
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Generate API
- name: Generate Go API Client
run: make gen-client-go
- name: Build web
working-directory: web/
run: |
npm ci
npm run build-proxy
npm run build-proxy -w @goauthentik/web
- name: Build outpost
run: |
set -x

View File

@ -1,4 +1,4 @@
name: authentik-ci-web
name: CI Web UI
on:
push:
@ -13,54 +13,50 @@ on:
jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command:
- lint
- lint:lockfile
- tsc
- prettier-check
project:
- web
include:
- command: tsc
project: web
- command: lit-analyse
project: web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
cache-dependency-path: ${{ matrix.project }}/package-lock.json
- working-directory: ${{ matrix.project }}/
run: |
npm ci
- name: Generate API
run: make gen-client-ts
- name: Lint
working-directory: ${{ matrix.project }}/
run: npm run ${{ matrix.command }}
build:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
node-version-file: package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: npm ci
- name: Generate API
- name: Generate TypeScript API
run: make gen-client-ts
- name: Build
run: |
npm run build -w @goauthentik/web
- name: Type check
run: |
npm run typecheck
- name: Lint
run: |
npm run lint -w @goauthentik/web
npm run lint:lockfile -w @goauthentik/web
npm run lit-analyse -w @goauthentik/web
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: npm ci
- name: Generate TypeScript API
run: make gen-client-ts
- name: build
working-directory: web/
run: npm run build
run: |
npm run build -w @goauthentik/web
npm run typecheck
ci-web-mark:
name: CI Web Mark
if: always()
needs:
- build
@ -71,6 +67,7 @@ jobs:
with:
jobs: ${{ toJSON(needs) }}
test:
name: Test
needs:
- ci-web-mark
runs-on: ubuntu-latest
@ -78,13 +75,12 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
node-version-file: package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: npm ci
- name: Generate API
- name: Generate TypeScript API
run: make gen-client-ts
- name: test
working-directory: web/
run: npm run test || exit 0
- name: Test Web UI
run: npm run test -w @goauthentik/web || exit 0

View File

@ -1,4 +1,4 @@
name: authentik-ci-website
name: CI Docs Website
on:
push:
@ -13,55 +13,59 @@ on:
jobs:
lint:
name: "Lint"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command:
- lint:lockfile
- prettier-check
steps:
- uses: actions/checkout@v4
- working-directory: website/
run: npm ci
- name: Lint
working-directory: website/
run: npm run ${{ matrix.command }}
- uses: actions/setup-node@v4
with:
node-version-file: package.json
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: |
npm ci
- name: Generate TypeScript API
run: make gen-client-ts
- name: Lint Docs
run: |
npm run lint:prettier:check
npm run lint:lockfile -w @goauthentik/docs
test:
name: "Test Docs"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
node-version-file: package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
- name: test
working-directory: website/
run: npm test
cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: |
npm ci
- name: Generate TypeScript API
run: make gen-client-ts
- name: Test Docs
run: |
npm run test -w @goauthentik/docs
build:
name: "Build Docs"
runs-on: ubuntu-latest
name: ${{ matrix.job }}
strategy:
fail-fast: false
matrix:
job:
- build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
node-version-file: package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/
cache-dependency-path: package-lock.json
- name: Install Node.js dependencies
run: npm ci
- name: build
working-directory: website/
run: npm run ${{ matrix.job }}
- name: Build
run: |
npm run build -w @goauthentik/docs
ci-website-mark:
name: "CI Website Mark"
if: always()
needs:
- lint

View File

@ -10,7 +10,7 @@ on:
jobs:
analyze:
name: Analyze
name: "Analyze"
runs-on: ubuntu-latest
permissions:
actions: read

View File

@ -1,4 +1,4 @@
name: authentik-gen-update-webauthn-mds
name: "authentik CI Update WebAuthn MDS"
on:
workflow_dispatch:
schedule:
@ -11,6 +11,7 @@ env:
jobs:
build:
name: "Update WebAuthn MDS"
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:

View File

@ -1,6 +1,6 @@
---
# See https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries
name: Cleanup cache after PR is closed
name: "Post-PR Closed Cache Cleanup"
on:
pull_request:
types:
@ -12,6 +12,7 @@ permissions:
jobs:
cleanup:
name: "Cleanup Cache"
runs-on: ubuntu-latest
steps:
- name: Check out code

View File

@ -1,4 +1,4 @@
name: ghcr-retention
name: "authentik GHCR Retention Policy"
on:
# schedule:
@ -8,7 +8,7 @@ on:
jobs:
clean-ghcr:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
name: Delete old unused container images
name: "Delete old unused container images"
runs-on: ubuntu-latest
steps:
- id: generate_token

View File

@ -1,5 +1,5 @@
---
name: authentik-compress-images
name: "authentik CI Image Compression"
on:
push:
@ -20,7 +20,7 @@ on:
jobs:
compress:
name: compress
name: "Compress Docker images"
runs-on: ubuntu-latest
# Don't run on forks. Token will not be available. Will run on main and open a PR anyway
if: |

View File

@ -0,0 +1,45 @@
name: authentik-packages-npm-publish
on:
push:
branches: [main]
paths:
- packages/docusaurus-config
- packages/eslint-config
- packages/prettier-config
- packages/tsconfig
workflow_dispatch:
jobs:
publish:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package:
- docusaurus-config
- eslint-config
- prettier-config
- tsconfig
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: actions/setup-node@v4
with:
node-version-file: packages/${{ matrix.package }}/package.json
registry-url: "https://registry.npmjs.org"
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c
with:
files: |
packages/${{ matrix.package }}/package.json
- name: Publish package
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: packages/${{ matrix.package}}
run: |
npm ci
npm run build
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

View File

@ -1,4 +1,4 @@
name: authentik-publish-source-docs
name: "authentik Publish Source Docs"
on:
push:
@ -12,6 +12,7 @@ env:
jobs:
publish-source-docs:
name: "Publish"
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
timeout-minutes: 120
@ -19,11 +20,11 @@ jobs:
- uses: actions/checkout@v4
- name: Setup authentik env
uses: ./.github/actions/setup
- name: generate docs
- name: Generate docs
run: |
uv run make migrate
uv run ak build_source_docs
- name: Publish
- name: Deploy to Netlify
uses: netlify/actions/cli@master
with:
args: deploy --dir=source_docs --prod

View File

@ -1,4 +1,4 @@
name: authentik-on-release-next-branch
name: "authentik on Release Next Branch"
on:
schedule:
@ -11,6 +11,7 @@ permissions:
jobs:
update-next:
name: "Update Next Branch"
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
environment: internal-production

View File

@ -1,5 +1,5 @@
---
name: authentik-on-release
name: "Release publish"
on:
release:
@ -7,6 +7,7 @@ on:
jobs:
build-server:
name: "Build server"
uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit
permissions:
@ -21,6 +22,7 @@ jobs:
registry_dockerhub: true
registry_ghcr: true
build-outpost:
name: "Build outpost"
runs-on: ubuntu-latest
permissions:
# Needed to upload container images to ghcr.io
@ -45,14 +47,14 @@ jobs:
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
- name: Prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }}
- name: make empty clients
- name: Make empty clients
run: |
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
@ -85,6 +87,7 @@ jobs:
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
build-outpost-binary:
name: "Build outpost binary"
timeout-minutes: 120
runs-on: ubuntu-latest
permissions:
@ -106,14 +109,13 @@ jobs:
go-version-file: "go.mod"
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
node-version-file: package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
cache-dependency-path: package-lock.json
- name: Build web
working-directory: web/
run: |
npm ci
npm run build-proxy
npm run build-proxy -w @goauthentik/web
- name: Build outpost
run: |
set -x
@ -129,6 +131,7 @@ jobs:
asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
tag: ${{ github.ref }}
upload-aws-cfn-template:
name: "Upload AWS CloudFormation template"
permissions:
# Needed for AWS login
id-token: write
@ -150,6 +153,7 @@ jobs:
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
test-release:
name: "Test release"
needs:
- build-server
- build-outpost
@ -166,6 +170,7 @@ jobs:
docker compose start postgresql redis
docker compose run -u root server test-all
sentry-release:
name: "Sentry release"
needs:
- build-server
- build-outpost
@ -173,7 +178,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: prepare variables
- name: Prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:

View File

@ -1,5 +1,5 @@
---
name: authentik-on-tag
name: "authentik on Tag Release"
on:
push:
@ -8,7 +8,7 @@ on:
jobs:
build:
name: Create Release from Tag
name: "Create Release from Tag"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@ -20,7 +20,7 @@ jobs:
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: prepare variables
- name: Prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:

View File

@ -1,13 +1,15 @@
name: "authentik-repo-mirror"
name: "authentik Repository Mirror"
on: [push, delete]
jobs:
to_internal:
name: "Mirror to internal repository"
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
name: "Checkout repository"
with:
fetch-depth: 0
- if: ${{ env.MIRROR_KEY != '' }}

View File

@ -1,4 +1,4 @@
name: "authentik-repo-stale"
name: "authentik Repository Stale Issues"
on:
schedule:
@ -11,6 +11,7 @@ permissions:
jobs:
stale:
name: "Stale Issues"
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:

View File

@ -1,4 +1,4 @@
name: authentik-semgrep
name: "authentik CI Semgrep"
on:
workflow_dispatch: {}
pull_request: {}
@ -13,7 +13,7 @@ on:
- cron: '12 15 * * *'
jobs:
semgrep:
name: semgrep/ci
name: "semgrep/ci"
runs-on: ubuntu-latest
permissions:
contents: read

View File

@ -1,4 +1,4 @@
name: authentik-translation-advice
name: "authentik Translations Advice"
on:
pull_request:
@ -16,6 +16,7 @@ permissions:
jobs:
post-comment:
name: "Post Comment"
runs-on: ubuntu-latest
steps:
- name: Find Comment

View File

@ -1,5 +1,5 @@
---
name: authentik-translate-extract-compile
name: "authentik Extract & Compile Translations"
on:
schedule:
- cron: "0 0 * * *" # every day at midnight
@ -16,6 +16,7 @@ env:
jobs:
compile:
name: "Compile Translations"
runs-on: ubuntu-latest
steps:
- id: generate_token
@ -32,15 +33,20 @@ jobs:
if: ${{ github.event_name == 'pull_request' }}
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Generate API
- name: Generate TypeScript API
run: make gen-client-ts
- name: run extract
- name: Extract Translations
run: |
uv run make i18n-extract
- name: run compile
- name: Build Docs Site
run: npm run build-bundled -w @goauthentik/docs
- name: Build Web UI
run: npm run build -w @goauthentik/web
- name: Type check
run: npm run typecheck
- name: Compile Messages
run: |
uv run ak compilemessages
make web-check-compile
- name: Create Pull Request
if: ${{ github.event_name != 'pull_request' }}
uses: peter-evans/create-pull-request@v7

View File

@ -1,6 +1,6 @@
# Rename transifex pull requests to have a correct naming
# Also enables auto squash-merge
name: authentik-translation-transifex-rename
name: "authentik Translations Transifex PR Rename"
on:
pull_request:
@ -12,6 +12,7 @@ permissions:
jobs:
rename_pr:
name: "Rename PR"
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
steps:

28
.gitignore vendored
View File

@ -11,6 +11,10 @@ local_settings.py
db.sqlite3
media
# Node
node_modules
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
# in your Git repository. Update and uncomment the following line accordingly.
# <django-project-name>/staticfiles/
@ -33,6 +37,7 @@ eggs/
lib64/
parts/
dist/
out/
sdist/
var/
wheels/
@ -212,3 +217,26 @@ source_docs/
### Docker ###
docker-compose.override.yml
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
node_modules/
tsconfig.tsbuildinfo
# Wireit's cache
.wireit
custom-elements.json
### Development ###
.drafts

52
.prettierignore Normal file
View File

@ -0,0 +1,52 @@
# Prettier Ignorefile
## Static Files
**/LICENSE
authentik/stages/**/*
authentik/sources/**/*
schemas/**/*
blueprints/**/*
## Build asset directories
coverage
dist
out
.docusaurus
.wireit
website/docs/developer-docs/api/**/*
## Environment
*.env
## Secrets
*.secrets
## Yarn
.yarn/**/*
## Node
node_modules
coverage
## Configs
*.log
*.yaml
*.yml
# Templates
# TODO: Rename affected files to *.template.* or similar.
authentik/**/*.html
*.html
*.mdx
*.md
## Import order matters
web/src/poly.ts
web/src/locale-codes.ts
web/src/locales/
# Storybook
storybook-static/
.storybook/css-import-maps*

View File

@ -17,6 +17,6 @@
"ms-python.vscode-pylance",
"redhat.vscode-yaml",
"Tobermory.es6-string-html",
"unifiedjs.vscode-mdx",
"unifiedjs.vscode-mdx"
]
}

72
.vscode/settings.json vendored
View File

@ -16,7 +16,7 @@
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index",
"typescript.tsdk": "./web/node_modules/typescript/lib",
"typescript.tsdk": "./node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true,
"yaml.schemas": {
"./blueprints/schema.json": "blueprints/**/*.yaml"
@ -30,7 +30,71 @@
}
],
"go.testFlags": ["-count=1"],
"github-actions.workflows.pinned.workflows": [
".github/workflows/ci-main.yml"
]
"github-actions.workflows.pinned.workflows": [".github/workflows/ci-main.yml"],
"eslint.useFlatConfig": true,
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
"*.mjs": "*.d.mts",
"*.cjs": "*.d.cts",
"package.json": "package-lock.json, yarn.lock, .yarnrc, .yarnrc.yml, .yarn, .nvmrc, .node-version",
"tsconfig.json": "tsconfig.*.json, jsconfig.json",
"Dockerfile": "*.Dockerfile"
},
"search.exclude": {
"**/node_modules": true,
"**/*.code-search": true,
"**/dist": true,
"**/out": true,
"**/package-lock.json": true
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[markdown]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[shellscript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[django-html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.removeUnusedImports": "explicit"
},
// We use Prettier for formatting, but specifying these settings
// will ensure that VS Code's IntelliSense doesn't autocomplete unformatted code.
"javascript.format.semicolons": "insert",
"typescript.format.semicolons": "insert",
"javascript.preferences.quoteStyle": "double",
"typescript.preferences.quoteStyle": "double",
"github.copilot.enable": {
"*": true,
"plaintext": true,
"markdown": true,
"scminput": false,
"csv": false,
"json": true,
"yaml": true
}
}

40
.vscode/tasks.json vendored
View File

@ -4,12 +4,7 @@
{
"label": "authentik/core: make",
"command": "uv",
"args": [
"run",
"make",
"lint-fix",
"lint"
],
"args": ["run", "make", "lint-fix", "lint"],
"presentation": {
"panel": "new"
},
@ -18,11 +13,7 @@
{
"label": "authentik/core: run",
"command": "uv",
"args": [
"run",
"ak",
"server"
],
"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",
@ -81,11 +61,7 @@
{
"label": "authentik/api: generate",
"command": "uv",
"args": [
"run",
"make",
"gen"
],
"args": ["run", "make", "gen"],
"group": "build"
}
]

View File

@ -23,6 +23,8 @@ docker-compose.yml @goauthentik/infrastructure
Makefile @goauthentik/infrastructure
.editorconfig @goauthentik/infrastructure
CODEOWNERS @goauthentik/infrastructure
# Web packages
packages/ @goauthentik/frontend
# Web
web/ @goauthentik/frontend
tests/wdio/ @goauthentik/frontend

View File

@ -1,48 +1,31 @@
# syntax=docker/dockerfile:1
# Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
# Stage 1 Web UI and Documentation build
ENV NODE_ENV=production
WORKDIR /work/website
RUN --mount=type=bind,target=/work/website/package.json,src=./website/package.json \
--mount=type=bind,target=/work/website/package-lock.json,src=./website/package-lock.json \
--mount=type=cache,id=npm-website,sharing=shared,target=/root/.npm \
npm ci --include=dev
COPY ./website /work/website/
COPY ./blueprints /work/blueprints/
COPY ./schema.yml /work/
COPY ./SECURITY.md /work/
RUN npm run build-bundled
# Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS web-builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
ENV NODE_ENV=production
WORKDIR /work/web
WORKDIR /work
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
npm ci --include=dev
COPY ./package.json ./package.json
COPY ./package-lock.json ./package-lock.json
COPY ./packages ./packages
COPY ./web ./web
COPY ./website ./website
COPY ./package.json /work
COPY ./web /work/web/
COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
COPY ./gen-ts-api ./gen-ts-api
COPY ./blueprints ./blueprints
COPY ./schema.yml ./schema.yml
COPY ./SECURITY.md ./SECURITY.md
RUN npm run build
RUN --mount=type=cache,target=/root/.npm npm ci --include=dev
RUN npm run build-bundled -w @goauthentik/docs
RUN npm run build -w @goauthentik/web
# Stage 2: Build go proxy
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
ARG TARGETOS
@ -79,7 +62,8 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
go build -o /go/authentik ./cmd/server
# Stage 4: MaxMind GeoIP
# Stage 3: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
@ -93,10 +77,11 @@ 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.12 AS uv
# Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
# Stage 4: Download uv
FROM ghcr.io/astral-sh/uv:0.6.14 AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.10-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
@ -109,7 +94,7 @@ WORKDIR /ak-root/
COPY --from=uv /uv /uvx /bin/
# Stage 7: Python dependencies
# Stage 6: Python dependencies
FROM python-base AS python-deps
ARG TARGETARCH
@ -144,7 +129,7 @@ RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
--mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
# Stage 8: Run
# Stage 7: Run
FROM python-base AS final-image
ARG VERSION
@ -189,7 +174,7 @@ COPY --from=go-builder /go/authentik /bin/authentik
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/
COPY --from=web-builder /work/website/build/ /website/help/
COPY --from=geoip /usr/share/GeoIP /geoip
USER 1000

107
Makefile
View File

@ -36,6 +36,13 @@ test: ## Run the server tests and produce a coverage report (locally)
uv run coverage html
uv run coverage report
node-check-compile: ## Check and compile the TypeScript source code
npm run typecheck
node-lint-fix: ## Lint and automatically fix errors in the javascript source code
lint-codespell
npm run lint:fix
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)
@ -47,9 +54,6 @@ lint: ## Lint the python and golang sources
uv run bandit -c pyproject.toml -r $(PY_SOURCES)
golangci-lint run -v
core-install:
uv sync --frozen
migrate: ## Run the Authentik Django server's migrations
uv run python -m lifecycle.migrate
@ -72,7 +76,9 @@ core-i18n-extract:
--ignore website \
-l en
install: web-install website-install core-install ## Install all requires dependencies for `web`, `website` and `core`
install: ## Install all requires dependencies for `web`, `website` and `core`
npm ci
uv sync --frozen
dev-drop-db:
dropdb -U ${pg_user} -h ${pg_host} ${pg_name}
@ -94,6 +100,7 @@ gen-build: ## Extract the schema from the database
AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
uv run ak make_blueprint_schema > blueprints/schema.json
AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
@ -101,19 +108,24 @@ gen-build: ## Extract the schema from the database
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
npx prettier --write changelog.md
gen-diff: ## (Release) generate the changelog diff between the current schema and the last tag
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > old_schema.yml
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-diff:2.1.0-beta.8 \
--markdown /local/diff.md \
/local/old_schema.yml /local/schema.yml
rm old_schema.yml
sed -i 's/{/&#123;/g' diff.md
sed -i 's/}/&#125;/g' diff.md
npx prettier --write diff.md
gen-clean-ts: ## Remove generated API client for Typescript
@ -133,46 +145,57 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \
-g typescript-fetch \
-o /local/${GEN_API_TS} \
-c /local/scripts/api-ts-config.yaml \
--input-spec /local/schema.yml \
--generator-name typescript-fetch \
--output /local/${GEN_API_TS} \
--config /local/scripts/api-ts-config.yaml \
--additional-properties=npmVersion=${NPM_VERSION} \
--git-repo-id authentik \
--git-user-id goauthentik
mkdir -p web/node_modules/@goauthentik/api
cd ./${GEN_API_TS} && npm i
\cp -rf ./${GEN_API_TS}/* web/node_modules/@goauthentik/api
npm install
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 \
-i /local/schema.yml \
-g python \
-o /local/${GEN_API_PY} \
-c /local/scripts/api-py-config.yaml \
--input-spec /local/schema.yml \
--generator-name python \
--output /local/${GEN_API_PY} \
--config /local/scripts/api-py-config.yaml \
--additional-properties=packageVersion=${NPM_VERSION} \
--git-repo-id authentik \
--git-user-id goauthentik
pip install ./${GEN_API_PY}
gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
mkdir -p ./${GEN_API_GO} ./${GEN_API_GO}/templates
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./${GEN_API_GO}/config.yaml
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./${GEN_API_GO}/templates/README.mustache
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O ./${GEN_API_GO}/templates/go.mod.mustache
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml \
-O ./${GEN_API_GO}/config.yaml
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache \
-O ./${GEN_API_GO}/templates/README.mustache
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache \
-O ./${GEN_API_GO}/templates/go.mod.mustache
cp schema.yml ./${GEN_API_GO}/
docker run \
--rm -v ${PWD}/${GEN_API_GO}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
-i /local/schema.yml \
-g go \
-o /local/ \
-c /local/config.yaml
--input-spec /local/schema.yml \
--generator-name go \
--output /local/ \
--config /local/config.yaml
go mod edit -replace goauthentik.io/api/v3=./${GEN_API_GO}
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
gen-dev-config: ## Generate a local development config file
@ -184,56 +207,38 @@ gen: gen-build gen-client-ts
## Web
#########################
web-build: web-install ## Build the Authentik UI
cd web && npm run build
web: web-lint-fix web-lint web-check-compile ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
web-install: ## Install the necessary libraries to build the Authentik UI
cd web && npm ci
web: web-lint-fix web-lint node-check-compile ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
web-test: ## Run tests for the Authentik UI
cd web && npm run test
npm run test -w @goauthentik/web
web-watch: ## Build and watch the Authentik UI for changes, updating automatically
rm -rf web/dist/
mkdir web/dist/
touch web/dist/.gitkeep
cd web && npm run watch
npm run watch -w @goauthentik/web
web-storybook-watch: ## Build and run the storybook documentation server
cd web && npm run storybook
npm run storybook -w @goauthentik/web
web-lint-fix:
cd web && npm run prettier
npm run prettier -w @goauthentik/web
web-lint:
cd web && npm run lint
cd web && npm run lit-analyse
web-check-compile:
cd web && npm run tsc
npm run lint -w @goauthentik/web
npm run lit-analyse -w @goauthentik/web
web-i18n-extract:
cd web && npm run extract-locales
npm run extract-locales -w @goauthentik/web
#########################
## Website
#########################
website: website-lint-fix website-build ## Automatically fix formatting issues in the Authentik website/docs source code, lint the code, and compile it
website-install:
cd website && npm ci
website-lint-fix: lint-codespell
cd website && npm run prettier
website: node-lint-fix website-build ## Automatically fix formatting issues in the Authentik website/docs source code, lint the code, and compile it
website-build:
cd website && npm run build
npm run build -w @goauthentik/docs
website-watch: ## Build and watch the documentation website, updating automatically
cd website && npm run watch
npm run watch -w @goauthentik/docs
#########################
## Docker

View File

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

View File

@ -7,7 +7,7 @@ from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, DateTimeField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer
from rest_framework.serializers import ListSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.blueprints.models import BlueprintInstance
@ -15,7 +15,7 @@ from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
from authentik.rbac.decorators import permission_required

View File

@ -36,6 +36,7 @@ from authentik.core.models import (
GroupSourceConnection,
PropertyMapping,
Provider,
Session,
Source,
User,
UserSourceConnection,
@ -108,6 +109,7 @@ def excluded_models() -> list[type[Model]]:
Policy,
PolicyBindingModel,
# Classes that have other dependencies
Session,
AuthenticatedSession,
# Classes which are only internally managed
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin

View File

@ -5,6 +5,7 @@ from typing import TypedDict
from rest_framework import mixins
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request
from rest_framework.serializers import CharField, DateTimeField, IPAddressField
from rest_framework.viewsets import GenericViewSet
from ua_parser import user_agent_parser
@ -54,6 +55,11 @@ class UserAgentDict(TypedDict):
class AuthenticatedSessionSerializer(ModelSerializer):
"""AuthenticatedSession Serializer"""
expires = DateTimeField(source="session.expires", read_only=True)
last_ip = IPAddressField(source="session.last_ip", read_only=True)
last_user_agent = CharField(source="session.last_user_agent", read_only=True)
last_used = DateTimeField(source="session.last_used", read_only=True)
current = SerializerMethodField()
user_agent = SerializerMethodField()
geo_ip = SerializerMethodField()
@ -62,19 +68,19 @@ class AuthenticatedSessionSerializer(ModelSerializer):
def get_current(self, instance: AuthenticatedSession) -> bool:
"""Check if session is currently active session"""
request: Request = self.context["request"]
return request._request.session.session_key == instance.session_key
return request._request.session.session_key == instance.session.session_key
def get_user_agent(self, instance: AuthenticatedSession) -> UserAgentDict:
"""Get parsed user agent"""
return user_agent_parser.Parse(instance.last_user_agent)
return user_agent_parser.Parse(instance.session.last_user_agent)
def get_geo_ip(self, instance: AuthenticatedSession) -> GeoIPDict | None: # pragma: no cover
"""Get GeoIP Data"""
return GEOIP_CONTEXT_PROCESSOR.city_dict(instance.last_ip)
return GEOIP_CONTEXT_PROCESSOR.city_dict(instance.session.last_ip)
def get_asn(self, instance: AuthenticatedSession) -> ASNDict | None: # pragma: no cover
"""Get ASN Data"""
return ASN_CONTEXT_PROCESSOR.asn_dict(instance.last_ip)
return ASN_CONTEXT_PROCESSOR.asn_dict(instance.session.last_ip)
class Meta:
model = AuthenticatedSession
@ -90,6 +96,7 @@ class AuthenticatedSessionSerializer(ModelSerializer):
"last_used",
"expires",
]
extra_args = {"uuid": {"read_only": True}}
class AuthenticatedSessionViewSet(
@ -101,9 +108,10 @@ class AuthenticatedSessionViewSet(
):
"""AuthenticatedSession Viewset"""
queryset = AuthenticatedSession.objects.all()
lookup_field = "uuid"
queryset = AuthenticatedSession.objects.select_related("session").all()
serializer_class = AuthenticatedSessionSerializer
search_fields = ["user__username", "last_ip", "last_user_agent"]
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
search_fields = ["user__username", "session__last_ip", "session__last_user_agent"]
filterset_fields = ["user__username", "session__last_ip", "session__last_user_agent"]
ordering = ["user__username"]
owner_field = "user"

View File

@ -1,14 +1,11 @@
"""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.db.models.functions import ExtractHour
from django.db.transaction import atomic
from django.db.utils import IntegrityError
@ -72,8 +69,8 @@ from authentik.core.middleware import (
from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_EXPIRING,
USER_PATH_SERVICE_ACCOUNT,
AuthenticatedSession,
Group,
Session,
Token,
TokenIntents,
User,
@ -92,7 +89,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):
@ -228,6 +224,7 @@ class UserSerializer(ModelSerializer):
"name",
"is_active",
"last_login",
"date_joined",
"is_superuser",
"groups",
"groups_obj",
@ -242,6 +239,7 @@ class UserSerializer(ModelSerializer):
]
extra_kwargs = {
"name": {"allow_blank": True},
"date_joined": {"read_only": True},
"password_change_date": {"read_only": True},
}
@ -774,10 +772,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
response = super().partial_update(request, *args, **kwargs)
instance: User = self.get_object()
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()
sessions.delete()
Session.objects.filter(authenticatedsession__user=instance).delete()
LOGGER.debug("Deleted user's sessions", user=instance.username)
return response

View File

@ -20,6 +20,8 @@ from rest_framework.serializers import (
raise_errors_on_nested_writes,
)
from authentik.rbac.permissions import assign_initial_permissions
def is_dict(value: Any):
"""Ensure a value is a dictionary, useful for JSONFields"""
@ -29,6 +31,14 @@ def is_dict(value: Any):
class ModelSerializer(BaseModelSerializer):
def create(self, validated_data):
instance = super().create(validated_data)
request = self.context.get("request")
if request and hasattr(request, "user") and not request.user.is_anonymous:
assign_initial_permissions(request.user, instance)
return instance
def update(self, instance: Model, validated_data):
raise_errors_on_nested_writes("update", self, validated_data)

View File

@ -24,6 +24,15 @@ class InbuiltBackend(ModelBackend):
self.set_method("password", request)
return user
async def aauthenticate(
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
) -> User | None:
user = await super().aauthenticate(request, username=username, password=password, **kwargs)
if not user:
return None
self.set_method("password", request)
return user
def set_method(self, method: str, request: HttpRequest | None, **kwargs):
"""Set method data on current flow, if possbiel"""
if not request:

View File

@ -0,0 +1,15 @@
"""Change user type"""
from importlib import import_module
from django.conf import settings
from authentik.tenants.management import TenantCommand
class Command(TenantCommand):
"""Delete all sessions"""
def handle_per_tenant(self, **options):
engine = import_module(settings.SESSION_ENGINE)
engine.SessionStore.clear_expired()

View File

@ -2,9 +2,14 @@
from collections.abc import Callable
from contextvars import ContextVar
from functools import partial
from uuid import uuid4
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
from django.utils.deprecation import MiddlewareMixin
from django.utils.functional import SimpleLazyObject
from django.utils.translation import override
from sentry_sdk.api import set_tag
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
@ -20,6 +25,40 @@ CTX_HOST = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + "host", default=None)
CTX_AUTH_VIA = ContextVar[str | None](STRUCTLOG_KEY_PREFIX + KEY_AUTH_VIA, default=None)
def get_user(request):
if not hasattr(request, "_cached_user"):
user = None
if (authenticated_session := request.session.get("authenticatedsession", None)) is not None:
user = authenticated_session.user
request._cached_user = user or AnonymousUser()
return request._cached_user
async def aget_user(request):
if not hasattr(request, "_cached_user"):
user = None
if (
authenticated_session := await request.session.aget("authenticatedsession", None)
) is not None:
user = authenticated_session.user
request._cached_user = user or AnonymousUser()
return request._cached_user
class AuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
if not hasattr(request, "session"):
raise ImproperlyConfigured(
"The Django authentication middleware requires session "
"middleware to be installed. Edit your MIDDLEWARE setting to "
"insert "
"'authentik.root.middleware.SessionMiddleware' before "
"'authentik.core.middleware.AuthenticationMiddleware'."
)
request.user = SimpleLazyObject(lambda: get_user(request))
request.auser = partial(aget_user, request)
class ImpersonateMiddleware:
"""Middleware to impersonate users"""

View File

@ -0,0 +1,238 @@
# Generated by Django 5.0.11 on 2025-01-27 12:58
import uuid
import pickle # nosec
from django.core import signing
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.utils.timezone import now, timedelta
from authentik.lib.migrations import progress_bar
from authentik.root.middleware import ClientIPMiddleware
SESSION_CACHE_ALIAS = "default"
class PickleSerializer:
"""
Simple wrapper around pickle to be used in signing.dumps()/loads() and
cache backends.
"""
def __init__(self, protocol=None):
self.protocol = pickle.HIGHEST_PROTOCOL if protocol is None else protocol
def dumps(self, obj):
"""Pickle data to be stored in redis"""
return pickle.dumps(obj, self.protocol)
def loads(self, data):
"""Unpickle data to be loaded from redis"""
return pickle.loads(data) # nosec
def _migrate_session(
apps,
db_alias,
session_key,
session_data,
expires,
):
Session = apps.get_model("authentik_core", "Session")
OldAuthenticatedSession = apps.get_model("authentik_core", "OldAuthenticatedSession")
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
old_auth_session = (
OldAuthenticatedSession.objects.using(db_alias).filter(session_key=session_key).first()
)
args = {
"session_key": session_key,
"expires": expires,
"last_ip": ClientIPMiddleware.default_ip,
"last_user_agent": "",
"session_data": {},
}
for k, v in session_data.items():
if k == "authentik/stages/user_login/last_ip":
args["last_ip"] = v
elif k in ["last_user_agent", "last_used"]:
args[k] = v
elif args in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY]:
pass
else:
args["session_data"][k] = v
if old_auth_session:
args["last_user_agent"] = old_auth_session.last_user_agent
args["last_used"] = old_auth_session.last_used
args["session_data"] = pickle.dumps(args["session_data"])
session = Session.objects.using(db_alias).create(**args)
if old_auth_session:
AuthenticatedSession.objects.using(db_alias).create(
session=session,
user=old_auth_session.user,
)
def migrate_redis_sessions(apps, schema_editor):
from django.core.cache import caches
db_alias = schema_editor.connection.alias
cache = caches[SESSION_CACHE_ALIAS]
# Not a redis cache, skipping
if not hasattr(cache, "keys"):
return
print("\nMigrating Redis sessions to database, this might take a couple of minutes...")
for key, session_data in progress_bar(cache.get_many(cache.keys(f"{KEY_PREFIX}*")).items()):
_migrate_session(
apps=apps,
db_alias=db_alias,
session_key=key.removeprefix(KEY_PREFIX),
session_data=session_data,
expires=now() + timedelta(seconds=cache.ttl(key)),
)
def migrate_database_sessions(apps, schema_editor):
DjangoSession = apps.get_model("sessions", "Session")
db_alias = schema_editor.connection.alias
print("\nMigration database sessions, this might take a couple of minutes...")
for django_session in progress_bar(DjangoSession.objects.using(db_alias).all()):
session_data = signing.loads(
django_session.session_data,
salt="django.contrib.sessions.SessionStore",
serializer=PickleSerializer,
)
_migrate_session(
apps=apps,
db_alias=db_alias,
session_key=django_session.session_key,
session_data=session_data,
expires=django_session.expire_date,
)
class Migration(migrations.Migration):
dependencies = [
("sessions", "0001_initial"),
("authentik_core", "0045_rename_new_identifier_usersourceconnection_identifier_and_more"),
("authentik_providers_oauth2", "0027_accesstoken_authentik_p_expires_9f24a5_idx_and_more"),
("authentik_providers_rac", "0006_connectiontoken_authentik_p_expires_91f148_idx_and_more"),
]
operations = [
# Rename AuthenticatedSession to OldAuthenticatedSession
migrations.RenameModel(
old_name="AuthenticatedSession",
new_name="OldAuthenticatedSession",
),
migrations.RenameIndex(
model_name="oldauthenticatedsession",
new_name="authentik_c_expires_cf4f72_idx",
old_name="authentik_c_expires_08251d_idx",
),
migrations.RenameIndex(
model_name="oldauthenticatedsession",
new_name="authentik_c_expirin_c1f17f_idx",
old_name="authentik_c_expirin_9cd839_idx",
),
migrations.RenameIndex(
model_name="oldauthenticatedsession",
new_name="authentik_c_expirin_e04f5d_idx",
old_name="authentik_c_expirin_195a84_idx",
),
migrations.RenameIndex(
model_name="oldauthenticatedsession",
new_name="authentik_c_session_a44819_idx",
old_name="authentik_c_session_d0f005_idx",
),
migrations.RunSQL(
sql="ALTER INDEX authentik_core_authenticatedsession_user_id_5055b6cf RENAME TO authentik_core_oldauthenticatedsession_user_id_5055b6cf",
reverse_sql="ALTER INDEX authentik_core_oldauthenticatedsession_user_id_5055b6cf RENAME TO authentik_core_authenticatedsession_user_id_5055b6cf",
),
# Create new Session and AuthenticatedSession models
migrations.CreateModel(
name="Session",
fields=[
(
"session_key",
models.CharField(
max_length=40, primary_key=True, serialize=False, verbose_name="session key"
),
),
("expires", models.DateTimeField(default=None, null=True)),
("expiring", models.BooleanField(default=True)),
("session_data", models.BinaryField(verbose_name="session data")),
("last_ip", models.GenericIPAddressField()),
("last_user_agent", models.TextField(blank=True)),
("last_used", models.DateTimeField(auto_now=True)),
],
options={
"default_permissions": [],
"verbose_name": "Session",
"verbose_name_plural": "Sessions",
},
),
migrations.AddIndex(
model_name="session",
index=models.Index(fields=["expires"], name="authentik_c_expires_d2f607_idx"),
),
migrations.AddIndex(
model_name="session",
index=models.Index(fields=["expiring"], name="authentik_c_expirin_7c2cfb_idx"),
),
migrations.AddIndex(
model_name="session",
index=models.Index(
fields=["expiring", "expires"], name="authentik_c_expirin_1ab2e4_idx"
),
),
migrations.AddIndex(
model_name="session",
index=models.Index(
fields=["expires", "session_key"], name="authentik_c_expires_c49143_idx"
),
),
migrations.CreateModel(
name="AuthenticatedSession",
fields=[
(
"session",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
serialize=False,
to="authentik_core.session",
),
),
("uuid", models.UUIDField(default=uuid.uuid4, unique=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"verbose_name": "Authenticated Session",
"verbose_name_plural": "Authenticated Sessions",
},
),
migrations.RunPython(
code=migrate_redis_sessions,
reverse_code=migrations.RunPython.noop,
),
migrations.RunPython(
code=migrate_database_sessions,
reverse_code=migrations.RunPython.noop,
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.11 on 2025-01-27 13:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0046_session_and_more"),
("authentik_providers_rac", "0007_migrate_session"),
("authentik_providers_oauth2", "0028_migrate_session"),
]
operations = [
migrations.DeleteModel(
name="OldAuthenticatedSession",
),
]

View File

@ -1,6 +1,7 @@
"""authentik core models"""
from datetime import datetime
from enum import StrEnum
from hashlib import sha256
from typing import Any, Optional, Self
from uuid import uuid4
@ -9,6 +10,7 @@ from deepmerge import always_merger
from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.contrib.sessions.base_session import AbstractBaseSession
from django.db import models
from django.db.models import Q, QuerySet, options
from django.db.models.constants import LOOKUP_SEP
@ -646,19 +648,30 @@ class SourceUserMatchingModes(models.TextChoices):
"""Different modes a source can handle new/returning users"""
IDENTIFIER = "identifier", _("Use the source-specific identifier")
EMAIL_LINK = "email_link", _(
"Link to a user with identical email address. Can have security implications "
"when a source doesn't validate email addresses."
EMAIL_LINK = (
"email_link",
_(
"Link to a user with identical email address. Can have security implications "
"when a source doesn't validate email addresses."
),
)
EMAIL_DENY = "email_deny", _(
"Use the user's email address, but deny enrollment when the email address already exists."
EMAIL_DENY = (
"email_deny",
_(
"Use the user's email address, but deny enrollment when the email address already "
"exists."
),
)
USERNAME_LINK = "username_link", _(
"Link to a user with identical username. Can have security implications "
"when a username is used with another source."
USERNAME_LINK = (
"username_link",
_(
"Link to a user with identical username. Can have security implications "
"when a username is used with another source."
),
)
USERNAME_DENY = "username_deny", _(
"Use the user's username, but deny enrollment when the username already exists."
USERNAME_DENY = (
"username_deny",
_("Use the user's username, but deny enrollment when the username already exists."),
)
@ -666,12 +679,16 @@ class SourceGroupMatchingModes(models.TextChoices):
"""Different modes a source can handle new/returning groups"""
IDENTIFIER = "identifier", _("Use the source-specific identifier")
NAME_LINK = "name_link", _(
"Link to a group with identical name. Can have security implications "
"when a group name is used with another source."
NAME_LINK = (
"name_link",
_(
"Link to a group with identical name. Can have security implications "
"when a group name is used with another source."
),
)
NAME_DENY = "name_deny", _(
"Use the group name, but deny enrollment when the name already exists."
NAME_DENY = (
"name_deny",
_("Use the group name, but deny enrollment when the name already exists."),
)
@ -730,8 +747,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
choices=SourceGroupMatchingModes.choices,
default=SourceGroupMatchingModes.IDENTIFIER,
help_text=_(
"How the source determines if an existing group should be used or "
"a new group created."
"How the source determines if an existing group should be used or a new group created."
),
)
@ -1012,45 +1028,75 @@ class PropertyMapping(SerializerModel, ManagedModel):
verbose_name_plural = _("Property Mappings")
class AuthenticatedSession(ExpiringModel):
"""Additional session class for authenticated users. Augments the standard django session
to achieve the following:
- Make it queryable by user
- Have a direct connection to user objects
- Allow users to view their own sessions and terminate them
- Save structured and well-defined information.
"""
class Session(ExpiringModel, AbstractBaseSession):
"""User session with extra fields for fast access"""
uuid = models.UUIDField(default=uuid4, primary_key=True)
# Remove upstream field because we're using our own ExpiringModel
expire_date = None
session_data = models.BinaryField(_("session data"))
session_key = models.CharField(max_length=40)
user = models.ForeignKey(User, on_delete=models.CASCADE)
last_ip = models.TextField()
# Keep in sync with Session.Keys
last_ip = models.GenericIPAddressField()
last_user_agent = models.TextField(blank=True)
last_used = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _("Session")
verbose_name_plural = _("Sessions")
indexes = ExpiringModel.Meta.indexes + [
models.Index(fields=["expires", "session_key"]),
]
default_permissions = []
def __str__(self):
return self.session_key
class Keys(StrEnum):
"""
Keys to be set with the session interface for the fields above to be updated.
If a field is added here that needs to be initialized when the session is initialized,
it must also be reflected in authentik.root.middleware.SessionMiddleware.process_request
and in authentik.core.sessions.SessionStore.__init__
"""
LAST_IP = "last_ip"
LAST_USER_AGENT = "last_user_agent"
LAST_USED = "last_used"
@classmethod
def get_session_store_class(cls):
from authentik.core.sessions import SessionStore
return SessionStore
def get_decoded(self):
raise NotImplementedError
class AuthenticatedSession(SerializerModel):
session = models.OneToOneField(Session, on_delete=models.CASCADE, primary_key=True)
# We use the session as primary key, but we need the API to be able to reference
# this object uniquely without exposing the session key
uuid = models.UUIDField(default=uuid4, unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE)
class Meta:
verbose_name = _("Authenticated Session")
verbose_name_plural = _("Authenticated Sessions")
indexes = ExpiringModel.Meta.indexes + [
models.Index(fields=["session_key"]),
]
def __str__(self) -> str:
return f"Authenticated Session {self.session_key[:10]}"
return f"Authenticated Session {str(self.pk)[:10]}"
@staticmethod
def from_request(request: HttpRequest, user: User) -> Optional["AuthenticatedSession"]:
"""Create a new session from a http request"""
from authentik.root.middleware import ClientIPMiddleware
if not hasattr(request, "session") or not request.session.session_key:
if not hasattr(request, "session") or not request.session.exists(
request.session.session_key
):
return None
return AuthenticatedSession(
session_key=request.session.session_key,
session=Session.objects.filter(session_key=request.session.session_key).first(),
user=user,
last_ip=ClientIPMiddleware.get_client_ip(request),
last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
expires=request.session.get_expiry_date(),
)

168
authentik/core/sessions.py Normal file
View File

@ -0,0 +1,168 @@
"""authentik sessions engine"""
import pickle # nosec
from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_KEY
from django.contrib.sessions.backends.db import SessionStore as SessionBase
from django.core.exceptions import SuspiciousOperation
from django.utils import timezone
from django.utils.functional import cached_property
from structlog.stdlib import get_logger
from authentik.root.middleware import ClientIPMiddleware
LOGGER = get_logger()
class SessionStore(SessionBase):
def __init__(self, session_key=None, last_ip=None, last_user_agent=""):
super().__init__(session_key)
self._create_kwargs = {
"last_ip": last_ip or ClientIPMiddleware.default_ip,
"last_user_agent": last_user_agent,
}
@classmethod
def get_model_class(cls):
from authentik.core.models import Session
return Session
@cached_property
def model_fields(self):
return [k.value for k in self.model.Keys]
def _get_session_from_db(self):
try:
return (
self.model.objects.select_related(
"authenticatedsession",
"authenticatedsession__user",
)
.prefetch_related(
"authenticatedsession__user__groups",
"authenticatedsession__user__user_permissions",
)
.get(
session_key=self.session_key,
expires__gt=timezone.now(),
)
)
except (self.model.DoesNotExist, SuspiciousOperation) as exc:
if isinstance(exc, SuspiciousOperation):
LOGGER.warning(str(exc))
self._session_key = None
async def _aget_session_from_db(self):
try:
return (
await self.model.objects.select_related(
"authenticatedsession",
"authenticatedsession__user",
)
.prefetch_related(
"authenticatedsession__user__groups",
"authenticatedsession__user__user_permissions",
)
.aget(
session_key=self.session_key,
expires__gt=timezone.now(),
)
)
except (self.model.DoesNotExist, SuspiciousOperation) as exc:
if isinstance(exc, SuspiciousOperation):
LOGGER.warning(str(exc))
self._session_key = None
def encode(self, session_dict):
return pickle.dumps(session_dict, protocol=pickle.HIGHEST_PROTOCOL)
def decode(self, session_data):
try:
return pickle.loads(session_data) # nosec
except pickle.PickleError:
# ValueError, unpickling exceptions. If any of these happen, just return an empty
# dictionary (an empty session)
pass
return {}
def load(self):
s = self._get_session_from_db()
if s:
return {
"authenticatedsession": getattr(s, "authenticatedsession", None),
**{k: getattr(s, k) for k in self.model_fields},
**self.decode(s.session_data),
}
else:
return {}
async def aload(self):
s = await self._aget_session_from_db()
if s:
return {
"authenticatedsession": getattr(s, "authenticatedsession", None),
**{k: getattr(s, k) for k in self.model_fields},
**self.decode(s.session_data),
}
else:
return {}
def create_model_instance(self, data):
args = {
"session_key": self._get_or_create_session_key(),
"expires": self.get_expiry_date(),
"session_data": {},
**self._create_kwargs,
}
for k, v in data.items():
# Don't save:
# - unused auth data
# - related models
if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
pass
elif k in self.model_fields:
args[k] = v
else:
args["session_data"][k] = v
args["session_data"] = self.encode(args["session_data"])
return self.model(**args)
async def acreate_model_instance(self, data):
args = {
"session_key": await self._aget_or_create_session_key(),
"expires": await self.aget_expiry_date(),
"session_data": {},
**self._create_kwargs,
}
for k, v in data.items():
# Don't save:
# - unused auth data
# - related models
if k in [SESSION_KEY, BACKEND_SESSION_KEY, HASH_SESSION_KEY, "authenticatedsession"]:
pass
elif k in self.model_fields:
args[k] = v
else:
args["session_data"][k] = v
args["session_data"] = self.encode(args["session_data"])
return self.model(**args)
@classmethod
def clear_expired(cls):
cls.get_model_class().objects.filter(expires__lt=timezone.now()).delete()
@classmethod
async def aclear_expired(cls):
await cls.get_model_class().objects.filter(expires__lt=timezone.now()).adelete()
def cycle_key(self):
data = self._session
key = self.session_key
self.create()
self._session_cache = data
if key:
self.delete(key)
if (authenticated_session := data.get("authenticatedsession")) is not None:
authenticated_session.session_id = self.session_key
authenticated_session.save(force_insert=True)

View File

@ -1,14 +1,10 @@
"""authentik core signals"""
from importlib import import_module
from django.conf import settings
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.base import SessionBase
from django.contrib.auth.signals import user_logged_in
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete, pre_save
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from django.http.request import HttpRequest
from structlog.stdlib import get_logger
@ -18,6 +14,7 @@ from authentik.core.models import (
AuthenticatedSession,
BackchannelProvider,
ExpiringModel,
Session,
User,
default_token_duration,
)
@ -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)
@ -53,18 +49,10 @@ def user_logged_in_session(sender, request: HttpRequest, user: User, **_):
session.save()
@receiver(user_logged_out)
def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
"""Delete AuthenticatedSession if it exists"""
if not request.session or not request.session.session_key:
return
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()
@receiver(pre_delete, sender=AuthenticatedSession)
@receiver(post_delete, sender=AuthenticatedSession)
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
"""Delete session when authenticated session is deleted"""
SessionStore(instance.session_key).delete()
Session.objects.filter(session_key=instance.pk).delete()
@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
@ -210,8 +209,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

View File

@ -2,22 +2,16 @@
from datetime import datetime, timedelta
from django.conf import ImproperlyConfigured
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.contrib.sessions.backends.db import SessionStore as DBSessionStore
from django.core.cache import cache
from django.utils.timezone import now
from structlog.stdlib import get_logger
from authentik.core.models import (
USER_ATTRIBUTE_EXPIRES,
USER_ATTRIBUTE_GENERATED,
AuthenticatedSession,
ExpiringModel,
User,
)
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@ -38,40 +32,6 @@ def clean_expired_models(self: SystemTask):
obj.expire_action()
LOGGER.debug("Expired models", model=cls, amount=amount)
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
# Special case
amount = 0
for session in AuthenticatedSession.objects.all():
match CONFIG.get("session_storage", "cache"):
case "cache":
cache_key = f"{KEY_PREFIX}{session.session_key}"
value = None
try:
value = cache.get(cache_key)
except Exception as exc:
LOGGER.debug("Failed to get session from cache", exc=exc)
if not value:
session.delete()
amount += 1
case "db":
if not (
DBSessionStore.get_model_class()
.objects.filter(session_key=session.session_key, expire_date__gt=now())
.exists()
):
session.delete()
amount += 1
case _:
# Should never happen, as we check for other values in authentik/root/settings.py
raise ImproperlyConfigured(
"Invalid session_storage setting, allowed values are db and cache"
)
if CONFIG.get("session_storage", "cache") == "db":
DBSessionStore.clear_expired()
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
self.set_status(TaskStatus.SUCCESSFUL, *messages)

View File

@ -2,20 +2,22 @@
{% get_current_language as LANGUAGE_CODE %}
<script>
window.authentik = {
locale: "{{ LANGUAGE_CODE }}",
config: JSON.parse('{{ config_json|escapejs }}'),
brand: JSON.parse('{{ brand_json|escapejs }}'),
versionFamily: "{{ version_family }}",
versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}",
api: {
base: "{{ base_url }}",
relBase: "{{ base_url_rel }}",
},
};
window.authentik = {
locale: "{{ LANGUAGE_CODE }}",
config: JSON.parse("{{ config_json|escapejs }}" || "{}"),
brand: JSON.parse("{{ brand_json|escapejs }}" || "{}"),
versionFamily: "{{ version_family }}",
versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}",
api: {
base: "{{ base_url }}",
relBase: "{{ base_url_rel }}",
},
};
{% if messages %}
window.addEventListener("DOMContentLoaded", function () {
{% for message in messages %}
{% for message in messages %}
window.dispatchEvent(
new CustomEvent("ak-message", {
bubbles: true,
@ -26,6 +28,7 @@
},
}),
);
{% endfor %}
{% endfor %}
});
{% endif %}
</script>

View File

@ -2,31 +2,79 @@
{% load i18n %}
{% load authentik_core %}
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
<meta name="darkreader-lock">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<style>{{ brand.branding_custom_css }}</style>
<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 %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />
</head>
<body>
{% block body %}
{% endblock %}
{% block scripts %}
{% endblock %}
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
{% comment %}
Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we
default to a dark theme based on preferred colour-scheme
{% endcomment %}
<meta name="darkreader-lock" />
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}" />
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}" />
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}" />
<style data-test-id="color-scheme">
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark light;
}
}
@media (prefers-color-scheme: light) {
:root {
color-scheme: light dark;
}
}
</style>
<style data-test-id="custom-branding-css">
{{ brand.branding_custom_css }}
</style>
<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 %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />
</head>
<body>
{% block body %}{% endblock %}
{% block scripts %}{% endblock %}
<noscript>
<style>
body {
font-family: var(--ak-font-family-base), sans-serif;
}
</style>
<h1>
JavaScript is required to use
{% trans title|default:brand.branding_title %}
</h1>
<p>
Please enable JavaScript in your browser settings and reload the page. If you are using a
browser extension that blocks JavaScript, please disable it for this site.
</p>
</noscript>
</body>
</html>

View File

@ -4,14 +4,16 @@
{% block head %}
<script src="{% versioned_script 'dist/admin/AdminInterface-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
{% include "base/header_js.html" %}
{% endblock %}
{% block body %}
<ak-message-container></ak-message-container>
<ak-interface-admin>
<ak-loading></ak-loading>
<ak-loading></ak-loading>
</ak-interface-admin>
{% endblock %}

View File

@ -13,9 +13,14 @@
{% block card %}
<form method="POST" class="pf-c-form">
<p>{% trans message %}</p>
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">
{% trans 'Go home' %}
</a>
<p>{% trans message %}</p>
<a
id="ak-back-home"
href="{% url 'authentik_core:root-redirect' %}"
class="pf-c-button pf-m-primary"
>
{% trans 'Go home' %}
</a>
</form>
{% endblock %}

View File

@ -4,14 +4,17 @@
{% block head %}
<script src="{% versioned_script 'dist/user/UserInterface-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)" />
{% include "base/header_js.html" %}
{% endblock %}
{% block body %}
<ak-message-container></ak-message-container>
<ak-interface-user>
<ak-loading></ak-loading>
<ak-loading></ak-loading>
</ak-interface-user>
{% endblock %}

View File

@ -5,78 +5,82 @@
{% block head_before %}
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
<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)">
<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" %}
{% endblock %}
{% block head %}
<style>
:root {
<style data-test-id="base-full-root-styles">
:root {
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
--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);
--pf-c-background-image--BackgroundImage--sm-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--lg: var(--ak-flow-background);
}
/* Form with user */
.form-control-static {
}
/* Form with user */
.form-control-static {
margin-top: var(--pf-global--spacer--sm);
display: flex;
align-items: center;
justify-content: space-between;
}
.form-control-static .avatar {
}
.form-control-static .avatar {
display: flex;
align-items: center;
}
.form-control-static img {
}
.form-control-static img {
margin-right: var(--pf-global--spacer--xs);
}
.form-control-static a {
}
.form-control-static a {
padding-top: var(--pf-global--spacer--xs);
padding-bottom: var(--pf-global--spacer--xs);
line-height: var(--pf-global--spacer--xl);
}
}
</style>
{% endblock %}
{% block body %}
<div class="pf-c-background-image">
</div>
<div class="pf-c-background-image"></div>
<ak-message-container></ak-message-container>
<div class="pf-c-login stacked">
<div class="ak-login-container">
<main class="pf-c-login__main">
<div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
</div>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% block card_title %}
{% endblock %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% block card %}
{% endblock %}
</div>
</main>
<footer class="pf-c-login__footer">
<ul class="pf-c-list pf-m-inline">
{% for link in footer_links %}
<li>
<a href="{{ link.href }}">{{ link.name }}</a>
</li>
{% endfor %}
<li>
<span>
{% trans 'Powered by authentik' %}
</span>
</li>
</ul>
</footer>
</div>
<div class="ak-login-container">
<main class="pf-c-login__main">
<div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
</div>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% block card_title %}
{% endblock %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% block card %}
{% endblock %}
</div>
</main>
<footer class="pf-c-login__footer">
<ul class="pf-c-list pf-m-inline">
{% for link in footer_links %}
<li>
<a href="{{ link.href }}">{{ link.name }}</a>
</li>
{% endfor %}
<li>
<span>
{% trans 'Powered by authentik' %}
</span>
</li>
</ul>
</footer>
</div>
</div>
{% endblock %}

View File

@ -1,9 +1,17 @@
"""Test API Utils"""
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import (
HyperlinkedModelSerializer,
)
from rest_framework.serializers import (
ModelSerializer as BaseModelSerializer,
)
from rest_framework.test import APITestCase
from authentik.core.api.utils import ModelSerializer as CustomModelSerializer
from authentik.core.api.utils import is_dict
from authentik.lib.utils.reflection import all_subclasses
class TestAPIUtils(APITestCase):
@ -14,3 +22,14 @@ class TestAPIUtils(APITestCase):
self.assertIsNone(is_dict({}))
with self.assertRaises(ValidationError):
is_dict("foo")
def test_all_serializers_descend_from_custom(self):
"""Test that every serializer we define descends from our own ModelSerializer"""
# Weirdly, there's only one serializer in `rest_framework` which descends from
# ModelSerializer: HyperlinkedModelSerializer
expected = {CustomModelSerializer, HyperlinkedModelSerializer}
actual = set(all_subclasses(BaseModelSerializer)) - set(
all_subclasses(CustomModelSerializer)
)
self.assertEqual(expected, actual)

View File

@ -5,7 +5,7 @@ from json import loads
from django.urls.base import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.models import AuthenticatedSession, Session, User
from authentik.core.tests.utils import create_test_admin_user
@ -30,3 +30,18 @@ class TestAuthenticatedSessionsAPI(APITestCase):
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["pagination"]["count"], 1)
def test_delete(self):
"""Test deletion"""
self.client.force_login(self.user)
self.assertEqual(AuthenticatedSession.objects.all().count(), 1)
self.assertEqual(Session.objects.all().count(), 1)
response = self.client.delete(
reverse(
"authentik_api:authenticatedsession-detail",
kwargs={"uuid": AuthenticatedSession.objects.first().uuid},
)
)
self.assertEqual(response.status_code, 204)
self.assertEqual(AuthenticatedSession.objects.all().count(), 0)
self.assertEqual(Session.objects.all().count(), 0)

View File

@ -3,8 +3,6 @@
from datetime import datetime
from json import loads
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.urls.base import reverse
from rest_framework.test import APITestCase
@ -12,6 +10,7 @@ from authentik.brands.models import Brand
from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_EXPIRING,
AuthenticatedSession,
Session,
Token,
User,
UserTypes,
@ -381,12 +380,15 @@ class TestUsersAPI(APITestCase):
"""Ensure sessions are deleted when a user is deactivated"""
user = create_test_admin_user()
session_id = generate_id()
AuthenticatedSession.objects.create(
user=user,
session = Session.objects.create(
session_key=session_id,
last_ip="",
last_ip="255.255.255.255",
last_user_agent="",
)
AuthenticatedSession.objects.create(
session=session,
user=user,
)
cache.set(KEY_PREFIX + session_id, "foo")
self.client.force_login(self.admin)
response = self.client.patch(
@ -397,5 +399,7 @@ class TestUsersAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
self.assertIsNone(cache.get(KEY_PREFIX + session_id))
self.assertFalse(AuthenticatedSession.objects.filter(session_key=session_id).exists())
self.assertFalse(Session.objects.filter(session_key=session_id).exists())
self.assertFalse(
AuthenticatedSession.objects.filter(session__session_key=session_id).exists()
)

View File

@ -1,7 +1,5 @@
"""authentik URL Configuration"""
from channels.auth import AuthMiddleware
from channels.sessions import CookieMiddleware
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.urls import path
@ -29,7 +27,7 @@ from authentik.core.views.interface import (
RootRedirectView,
)
from authentik.flows.views.interface import FlowInterfaceView
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.asgi_middleware import AuthMiddlewareStack
from authentik.root.messages.consumer import MessageConsumer
from authentik.root.middleware import ChannelsLoggingMiddleware
@ -99,9 +97,7 @@ api_urlpatterns = [
websocket_urlpatterns = [
path(
"ws/client/",
ChannelsLoggingMiddleware(
CookieMiddleware(SessionMiddleware(AuthMiddleware(MessageConsumer.as_asgi())))
),
ChannelsLoggingMiddleware(AuthMiddlewareStack(MessageConsumer.as_asgi())),
),
]

View File

@ -102,7 +102,7 @@ def ssf_user_session_delete_session_revoked(sender, instance: AuthenticatedSessi
"format": "complex",
"session": {
"format": "opaque",
"id": sha256(instance.session_key.encode("ascii")).hexdigest(),
"id": sha256(instance.session.session_key.encode("ascii")).hexdigest(),
},
"user": {
"format": "email",

View File

@ -4,10 +4,9 @@ from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.fields import CharField, ChoiceField, ListField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from structlog.stdlib import get_logger
from authentik.core.api.utils import PassiveSerializer
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.enterprise.providers.ssf.models import (
DeliveryMethods,
EventTypes,

View File

@ -2,11 +2,11 @@
from rest_framework import mixins
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
AuthenticatorEndpointGDTCStage,

View File

@ -59,7 +59,7 @@ def get_login_event(request_or_session: HttpRequest | AuthenticatedSession | Non
session = request_or_session.session
if isinstance(request_or_session, AuthenticatedSession):
SessionStore = _session_engine.SessionStore
session = SessionStore(request_or_session.session_key)
session = SessionStore(request_or_session.session.session_key)
return session.get(SESSION_LOGIN_EVENT, None)

View File

@ -2,53 +2,52 @@
{% load i18n %}
{% load authentik_core %}
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
<meta name="sentry-trace" content="{{ sentry_trace }}" />
{% include "base/header_js.html" %}
<style>
html,
body {
height: 100%;
}
body {
background-image: url("{{ flow.background_url }}");
background-repeat: no-repeat;
background-size: cover;
}
.card {
padding: 3rem;
}
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}" />
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}" />
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}" />
<meta name="sentry-trace" content="{{ sentry_trace }}" />
{% include "base/header_js.html" %}
<style>
html,
body {
height: 100%;
}
body {
background-image: url("{{ flow.background_url }}");
background-repeat: no-repeat;
background-size: cover;
}
.card {
padding: 3rem;
}
.form-signin {
max-width: 330px;
padding: 1rem;
}
.form-signin {
max-width: 330px;
padding: 1rem;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.brand-icon {
max-width: 100%;
}
</style>
</head>
<body class="d-flex align-items-center py-4 bg-body-tertiary">
<div class="card m-auto">
<main class="form-signin w-100 m-auto" id="flow-sfe-container">
</main>
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
</div>
<script src="{% static 'dist/sfe/index.js' %}"></script>
</body>
.form-signin .form-floating:focus-within {
z-index: 2;
}
.brand-icon {
max-width: 100%;
}
</style>
</head>
<body class="d-flex align-items-center py-4 bg-body-tertiary">
<div class="card m-auto">
<main class="form-signin w-100 m-auto" id="flow-sfe-container"></main>
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
</div>
<script src="{% static 'dist/sfe/index.js' %}"></script>
</body>
</html>

View File

@ -1,34 +1,40 @@
{% extends "base/skeleton.html" %}
{% load static %}
{% load authentik_core %}
{% block head_before %}
{{ block.super }}
<link rel="prefetch" href="{{ flow.background_url }}" />
{% if flow.compatibility_mode and not inspector %}
<script>ShadyDOM = { force: !navigator.webdriver };</script>
{% endif %}
{% include "base/header_js.html" %}
<script>
window.authentik.flow = {
"layout": "{{ flow.layout }}",
};
ShadyDOM = { force: !navigator.webdriver };
</script>
{% endif %}
{% include "base/header_js.html" %}
<script>
window.authentik.flow = {
layout: "{{ flow.layout }}",
};
</script>
{% endblock %}
{% block head %}
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
<style>
:root {
<style data-test-id="flow-root-styles">
:root {
--ak-flow-background: url("{{ flow.background_url }}");
}
}
</style>
{% endblock %}
{% block body %}
<ak-message-container></ak-message-container>
<ak-flow-executor flowSlug="{{ flow.slug }}">
<ak-loading></ak-loading>
<ak-loading></ak-loading>
</ak-flow-executor>
{% endblock %}

View File

@ -356,6 +356,14 @@ def redis_url(db: int) -> str:
def django_db_config(config: ConfigLoader | None = None) -> dict:
if not config:
config = CONFIG
pool_options = False
use_pool = config.get_bool("postgresql.use_pool", False)
if use_pool:
pool_options = config.get_dict_from_b64_json("postgresql.pool_options", True)
if not pool_options:
pool_options = True
db = {
"default": {
"ENGINE": "authentik.root.db",
@ -369,6 +377,7 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
"sslrootcert": config.get("postgresql.sslrootcert"),
"sslcert": config.get("postgresql.sslcert"),
"sslkey": config.get("postgresql.sslkey"),
"pool": pool_options,
},
"CONN_MAX_AGE": config.get_optional_int("postgresql.conn_max_age", 0),
"CONN_HEALTH_CHECKS": config.get_bool("postgresql.conn_health_checks", False),

View File

@ -21,6 +21,7 @@ postgresql:
user: authentik
port: 5432
password: "env://POSTGRES_PASSWORD"
use_pool: False
test:
name: test_authentik
default_schema: public

View File

@ -18,7 +18,7 @@ from sentry_sdk import start_span
from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, User
from authentik.core.models import User
from authentik.events.models import Event
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.utils.http import get_http_session
@ -203,9 +203,7 @@ class BaseEvaluator:
provider = OAuth2Provider.objects.get(name=provider)
session = None
if hasattr(request, "session") and request.session.session_key:
session = AuthenticatedSession.objects.filter(
session_key=request.session.session_key
).first()
session = request.session["authenticatedsession"]
access_token = AccessToken(
provider=provider,
user=user,

View File

@ -217,6 +217,7 @@ class TestConfig(TestCase):
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": False,
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
@ -267,6 +268,7 @@ class TestConfig(TestCase):
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": False,
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
@ -285,6 +287,7 @@ class TestConfig(TestCase):
"HOST": "bar",
"NAME": "foo",
"OPTIONS": {
"pool": False,
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
@ -333,6 +336,7 @@ class TestConfig(TestCase):
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": False,
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
@ -351,6 +355,7 @@ class TestConfig(TestCase):
"HOST": "bar",
"NAME": "foo",
"OPTIONS": {
"pool": False,
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
@ -394,6 +399,7 @@ class TestConfig(TestCase):
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": False,
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
@ -412,6 +418,7 @@ class TestConfig(TestCase):
"HOST": "bar",
"NAME": "foo",
"OPTIONS": {
"pool": False,
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
@ -451,6 +458,7 @@ class TestConfig(TestCase):
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": False,
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
@ -469,6 +477,7 @@ class TestConfig(TestCase):
"HOST": "bar",
"NAME": "foo",
"OPTIONS": {
"pool": False,
"sslcert": "bar",
"sslkey": "foo",
"sslmode": "foo",
@ -484,3 +493,87 @@ class TestConfig(TestCase):
},
},
)
def test_db_pool(self):
"""Test DB Config with pool"""
config = ConfigLoader()
config.set("postgresql.host", "foo")
config.set("postgresql.name", "foo")
config.set("postgresql.user", "foo")
config.set("postgresql.password", "foo")
config.set("postgresql.port", "foo")
config.set("postgresql.test.name", "foo")
config.set("postgresql.use_pool", True)
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"ENGINE": "authentik.root.db",
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": True,
"sslcert": None,
"sslkey": None,
"sslmode": None,
"sslrootcert": None,
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
}
},
)
def test_db_pool_options(self):
"""Test DB Config with pool"""
config = ConfigLoader()
config.set("postgresql.host", "foo")
config.set("postgresql.name", "foo")
config.set("postgresql.user", "foo")
config.set("postgresql.password", "foo")
config.set("postgresql.port", "foo")
config.set("postgresql.test.name", "foo")
config.set("postgresql.use_pool", True)
config.set(
"postgresql.pool_options",
base64.b64encode(
dumps(
{
"max_size": 15,
}
).encode()
).decode(),
)
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"ENGINE": "authentik.root.db",
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"pool": {
"max_size": 15,
},
"sslcert": None,
"sslkey": None,
"sslmode": None,
"sslrootcert": None,
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
}
},
)

View File

@ -66,7 +66,9 @@ class GeoIPPolicy(Policy):
if not static_results and not dynamic_results:
return PolicyResult(True)
passing = any(r.passing for r in static_results) and all(r.passing for r in dynamic_results)
static_passing = any(r.passing for r in static_results) if static_results else True
dynamic_passing = all(r.passing for r in dynamic_results)
passing = static_passing and dynamic_passing
messages = chain(
*[r.messages for r in static_results], *[r.messages for r in dynamic_results]
)
@ -113,13 +115,19 @@ class GeoIPPolicy(Policy):
to previous authentication requests"""
# Get previous login event and GeoIP data
previous_logins = Event.objects.filter(
action=EventAction.LOGIN, user__pk=request.user.pk, context__geo__isnull=False
action=EventAction.LOGIN,
user__pk=request.user.pk, # context__geo__isnull=False
).order_by("-created")[: self.history_login_count]
_now = now()
geoip_data: GeoIPDict | None = request.context.get("geoip")
if not geoip_data:
return PolicyResult(False)
if not previous_logins.exists():
return PolicyResult(True)
result = False
for previous_login in previous_logins:
if "geo" not in previous_login.context:
continue
previous_login_geoip: GeoIPDict = previous_login.context["geo"]
# Figure out distance
@ -142,7 +150,8 @@ class GeoIPPolicy(Policy):
(MAX_DISTANCE_HOUR_KM * rel_time_hours) + self.distance_tolerance_km
):
return PolicyResult(False, _("Distance is further than possible."))
return PolicyResult(True)
result = True
return PolicyResult(result)
class Meta(Policy.PolicyMeta):
verbose_name = _("GeoIP Policy")

View File

@ -163,7 +163,7 @@ class TestGeoIPPolicy(TestCase):
result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing)
def test_history_impossible_travel(self):
def test_history_impossible_travel_failing(self):
"""Test history checks"""
Event.objects.create(
action=EventAction.LOGIN,
@ -181,6 +181,24 @@ class TestGeoIPPolicy(TestCase):
result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing)
def test_history_impossible_travel_passing(self):
"""Test history checks"""
Event.objects.create(
action=EventAction.LOGIN,
user=get_user(self.user),
context={
# Random location in Canada
"geo": {"lat": 55.868351, "long": -104.441011},
},
)
# Same location
self.request.context["geoip"] = {"lat": 55.868351, "long": -104.441011}
policy = GeoIPPolicy.objects.create(check_impossible_travel=True)
result: PolicyResult = policy.passes(self.request)
self.assertTrue(result.passing)
def test_history_no_geoip(self):
"""Test history checks (previous login with no geoip data)"""
Event.objects.create(
@ -195,3 +213,18 @@ class TestGeoIPPolicy(TestCase):
result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing)
def test_impossible_travel_no_geoip(self):
"""Test impossible travel checks (previous login with no geoip data)"""
Event.objects.create(
action=EventAction.LOGIN,
user=get_user(self.user),
context={},
)
# Random location in Poland
self.request.context["geoip"] = {"lat": 50.950613, "long": 20.363679}
policy = GeoIPPolicy.objects.create(check_impossible_travel=True)
result: PolicyResult = policy.passes(self.request)
self.assertFalse(result.passing)

View File

@ -2,7 +2,6 @@
from django.contrib.auth.signals import user_logged_in
from django.db import transaction
from django.db.models import F
from django.dispatch import receiver
from django.http import HttpRequest
from structlog.stdlib import get_logger
@ -13,20 +12,29 @@ from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
from authentik.policies.reputation.models import Reputation, reputation_expiry
from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.identification.signals import identification_failed
from authentik.tenants.utils import get_current_tenant
LOGGER = get_logger()
def clamp(value, min, max):
return sorted([min, value, max])[1]
def update_score(request: HttpRequest, identifier: str, amount: int):
"""Update score for IP and User"""
remote_ip = ClientIPMiddleware.get_client_ip(request)
tenant = get_current_tenant()
new_score = clamp(amount, tenant.reputation_lower_limit, tenant.reputation_upper_limit)
with transaction.atomic():
reputation, created = Reputation.objects.select_for_update().get_or_create(
ip=remote_ip,
identifier=identifier,
defaults={
"score": amount,
"score": clamp(
amount, tenant.reputation_lower_limit, tenant.reputation_upper_limit
),
"ip_geo_data": GEOIP_CONTEXT_PROCESSOR.city_dict(remote_ip) or {},
"ip_asn_data": ASN_CONTEXT_PROCESSOR.asn_dict(remote_ip) or {},
"expires": reputation_expiry(),
@ -34,9 +42,15 @@ def update_score(request: HttpRequest, identifier: str, amount: int):
)
if not created:
reputation.score = F("score") + amount
new_score = clamp(
reputation.score + amount,
tenant.reputation_lower_limit,
tenant.reputation_upper_limit,
)
reputation.score = new_score
reputation.save()
LOGGER.info("Updated score", amount=amount, for_user=identifier, for_ip=remote_ip)
LOGGER.info("Updated score", amount=new_score, for_user=identifier, for_ip=remote_ip)
@receiver(login_failed)

View File

@ -6,9 +6,11 @@ from authentik.core.models import User
from authentik.lib.generators import generate_id
from authentik.policies.reputation.api import ReputationPolicySerializer
from authentik.policies.reputation.models import Reputation, ReputationPolicy
from authentik.policies.reputation.signals import update_score
from authentik.policies.types import PolicyRequest
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import authenticate
from authentik.tenants.models import DEFAULT_REPUTATION_LOWER_LIMIT, DEFAULT_REPUTATION_UPPER_LIMIT
class TestReputationPolicy(TestCase):
@ -17,36 +19,48 @@ class TestReputationPolicy(TestCase):
def setUp(self):
self.request_factory = RequestFactory()
self.request = self.request_factory.get("/")
self.test_ip = "127.0.0.1"
self.test_username = "test"
self.ip = "127.0.0.1"
self.username = "username"
self.password = generate_id()
# We need a user for the one-to-one in userreputation
self.user = User.objects.create(username=self.test_username)
self.user = User.objects.create(username=self.username)
self.user.set_password(self.password)
self.backends = [BACKEND_INBUILT]
def test_ip_reputation(self):
"""test IP reputation"""
# Trigger negative reputation
authenticate(
self.request, self.backends, username=self.test_username, password=self.test_username
)
self.assertEqual(Reputation.objects.get(ip=self.test_ip).score, -1)
authenticate(self.request, self.backends, username=self.username, password=self.username)
self.assertEqual(Reputation.objects.get(ip=self.ip).score, -1)
def test_user_reputation(self):
"""test User reputation"""
# Trigger negative reputation
authenticate(
self.request, self.backends, username=self.test_username, password=self.test_username
)
self.assertEqual(Reputation.objects.get(identifier=self.test_username).score, -1)
authenticate(self.request, self.backends, username=self.username, password=self.username)
self.assertEqual(Reputation.objects.get(identifier=self.username).score, -1)
def test_update_reputation(self):
"""test reputation update"""
Reputation.objects.create(identifier=self.test_username, ip=self.test_ip, score=43)
Reputation.objects.create(identifier=self.username, ip=self.ip, score=4)
# Trigger negative reputation
authenticate(
self.request, self.backends, username=self.test_username, password=self.test_username
authenticate(self.request, self.backends, username=self.username, password=self.username)
self.assertEqual(Reputation.objects.get(identifier=self.username).score, 3)
def test_reputation_lower_limit(self):
"""test reputation lower limit"""
Reputation.objects.create(identifier=self.username, ip=self.ip)
update_score(self.request, identifier=self.username, amount=-1000)
self.assertEqual(
Reputation.objects.get(identifier=self.username).score, DEFAULT_REPUTATION_LOWER_LIMIT
)
def test_reputation_upper_limit(self):
"""test reputation upper limit"""
Reputation.objects.create(identifier=self.username, ip=self.ip)
update_score(self.request, identifier=self.username, amount=1000)
self.assertEqual(
Reputation.objects.get(identifier=self.username).score, DEFAULT_REPUTATION_UPPER_LIMIT
)
self.assertEqual(Reputation.objects.get(identifier=self.test_username).score, 42)
def test_policy(self):
"""Test Policy"""

View File

@ -126,7 +126,7 @@ class IDToken:
id_token.iat = int(now.timestamp())
id_token.auth_time = int(token.auth_time.timestamp())
if token.session:
id_token.sid = hash_session_key(token.session.session_key)
id_token.sid = hash_session_key(token.session.session.session_key)
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
auth_event = get_login_event(token.session)

View File

@ -0,0 +1,116 @@
# Generated by Django 5.0.11 on 2025-01-27 13:00
from django.db import migrations
import django.db.models.deletion
from django.db import migrations, models
from functools import partial
def migrate_sessions(apps, schema_editor, model):
Model = apps.get_model("authentik_providers_oauth2", model)
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
db_alias = schema_editor.connection.alias
for obj in Model.objects.using(db_alias).all():
if not obj.old_session:
continue
obj.session = (
AuthenticatedSession.objects.using(db_alias)
.filter(session__session_key=obj.old_session.session_key)
.first()
)
if obj.session:
obj.save()
else:
obj.delete()
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_oauth2", "0027_accesstoken_authentik_p_expires_9f24a5_idx_and_more"),
("authentik_core", "0046_session_and_more"),
]
operations = [
migrations.RenameField(
model_name="accesstoken",
old_name="session",
new_name="old_session",
),
migrations.RenameField(
model_name="authorizationcode",
old_name="session",
new_name="old_session",
),
migrations.RenameField(
model_name="devicetoken",
old_name="session",
new_name="old_session",
),
migrations.RenameField(
model_name="refreshtoken",
old_name="session",
new_name="old_session",
),
migrations.AddField(
model_name="accesstoken",
name="session",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="authentik_core.authenticatedsession",
),
),
migrations.AddField(
model_name="authorizationcode",
name="session",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="authentik_core.authenticatedsession",
),
),
migrations.AddField(
model_name="devicetoken",
name="session",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_core.authenticatedsession",
),
),
migrations.AddField(
model_name="refreshtoken",
name="session",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_core.authenticatedsession",
),
),
migrations.RunPython(code=partial(migrate_sessions, model="AccessToken")),
migrations.RunPython(code=partial(migrate_sessions, model="AuthorizationCode")),
migrations.RunPython(code=partial(migrate_sessions, model="DeviceToken")),
migrations.RunPython(code=partial(migrate_sessions, model="RefreshToken")),
migrations.RemoveField(
model_name="accesstoken",
name="old_session",
),
migrations.RemoveField(
model_name="authorizationcode",
name="old_session",
),
migrations.RemoveField(
model_name="devicetoken",
name="old_session",
),
migrations.RemoveField(
model_name="refreshtoken",
name="old_session",
),
]

View File

@ -1,18 +1,30 @@
from django.contrib.auth.signals import user_logged_out
from django.db.models.signals import post_save
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.http import HttpRequest
from authentik.core.models import User
from authentik.core.models import AuthenticatedSession, User
from authentik.providers.oauth2.models import AccessToken, DeviceToken, RefreshToken
@receiver(user_logged_out)
def user_logged_out_oauth_access_token(sender, request: HttpRequest, user: User, **_):
"""Revoke access tokens upon user logout"""
def user_logged_out_oauth_tokens_removal(sender, request: HttpRequest, user: User, **_):
"""Revoke tokens upon user logout"""
if not request.session or not request.session.session_key:
return
AccessToken.objects.filter(user=user, session__session_key=request.session.session_key).delete()
AccessToken.objects.filter(
user=user,
session__session__session_key=request.session.session_key,
).delete()
@receiver(pre_delete, sender=AuthenticatedSession)
def user_session_deleted_oauth_tokens_removal(sender, instance: AuthenticatedSession, **_):
"""Revoke tokens upon user logout"""
AccessToken.objects.filter(
user=instance.user,
session__session__session_key=instance.session.session_key,
).delete()
@receiver(post_save, sender=User)
@ -20,6 +32,6 @@ def user_deactivated(sender, instance: User, **_):
"""Remove user tokens when deactivated"""
if instance.is_active:
return
AccessToken.objects.filter(session__user=instance).delete()
RefreshToken.objects.filter(session__user=instance).delete()
DeviceToken.objects.filter(session__user=instance).delete()
AccessToken.objects.filter(user=instance).delete()
RefreshToken.objects.filter(user=instance).delete()
DeviceToken.objects.filter(user=instance).delete()

View File

@ -7,12 +7,13 @@ from dataclasses import asdict
from django.urls import reverse
from django.utils import timezone
from authentik.core.models import Application
from authentik.core.models import Application, AuthenticatedSession, Session
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import (
AccessToken,
ClientTypes,
DeviceToken,
IDToken,
OAuth2Provider,
RedirectURI,
@ -20,6 +21,7 @@ from authentik.providers.oauth2.models import (
RefreshToken,
)
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.root.middleware import ClientIPMiddleware
class TesOAuth2Revoke(OAuthTestCase):
@ -135,3 +137,86 @@ class TesOAuth2Revoke(OAuthTestCase):
},
)
self.assertEqual(res.status_code, 200)
def test_revoke_logout(self):
"""Test revoke on logout"""
self.client.force_login(self.user)
AccessToken.objects.create(
provider=self.provider,
user=self.user,
session=self.client.session["authenticatedsession"],
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
),
)
self.client.logout()
self.assertEqual(AccessToken.objects.all().count(), 0)
def test_revoke_session_delete(self):
"""Test revoke on logout"""
session = AuthenticatedSession.objects.create(
session=Session.objects.create(
session_key=generate_id(),
last_ip=ClientIPMiddleware.default_ip,
),
user=self.user,
)
AccessToken.objects.create(
provider=self.provider,
user=self.user,
session=session,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
),
)
session.delete()
self.assertEqual(AccessToken.objects.all().count(), 0)
def test_revoke_user_deactivated(self):
"""Test revoke on logout"""
AccessToken.objects.create(
provider=self.provider,
user=self.user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
),
)
RefreshToken.objects.create(
provider=self.provider,
user=self.user,
token=generate_id(),
auth_time=timezone.now(),
_scope="openid user profile",
_id_token=json.dumps(
asdict(
IDToken("foo", "bar"),
)
),
)
DeviceToken.objects.create(
provider=self.provider,
user=self.user,
_scope="openid user profile",
)
self.user.is_active = False
self.user.save()
self.assertEqual(AccessToken.objects.all().count(), 0)
self.assertEqual(RefreshToken.objects.all().count(), 0)
self.assertEqual(DeviceToken.objects.all().count(), 0)

View File

@ -15,7 +15,7 @@ from django.utils import timezone
from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
from authentik.core.models import Application, AuthenticatedSession
from authentik.core.models import Application
from authentik.events.models import Event, EventAction
from authentik.events.signals import get_login_event
from authentik.flows.challenge import (
@ -316,9 +316,7 @@ class OAuthAuthorizationParams:
expires=now + timedelta_from_string(self.provider.access_code_validity),
scope=self.scope,
nonce=self.nonce,
session=AuthenticatedSession.objects.filter(
session_key=request.session.session_key
).first(),
session=request.session["authenticatedsession"],
)
if self.code_challenge and self.code_challenge_method:
@ -615,9 +613,7 @@ class OAuthFulfillmentStage(StageView):
expires=access_token_expiry,
provider=self.provider,
auth_time=auth_event.created if auth_event else now,
session=AuthenticatedSession.objects.filter(
session_key=self.request.session.session_key
).first(),
session=self.request.session["authenticatedsession"],
)
id_token = IDToken.new(self.provider, token, self.request)

View File

@ -20,4 +20,4 @@ def logout_proxy_revoke_direct(sender: type[User], request: HttpRequest, **_):
@receiver(pre_delete, sender=AuthenticatedSession)
def logout_proxy_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
"""Catch logout by expiring sessions being deleted"""
proxy_on_logout.delay(instance.session_key)
proxy_on_logout.delay(instance.session.session_key)

View File

@ -0,0 +1,60 @@
# Generated by Django 5.0.11 on 2025-01-27 12:59
from django.db import migrations
import django.db.models.deletion
from django.db import migrations, models
def migrate_sessions(apps, schema_editor):
ConnectionToken = apps.get_model("authentik_providers_rac", "ConnectionToken")
AuthenticatedSession = apps.get_model("authentik_core", "AuthenticatedSession")
db_alias = schema_editor.connection.alias
for token in ConnectionToken.objects.using(db_alias).all():
token.session = (
AuthenticatedSession.objects.using(db_alias)
.filter(session_key=token.old_session.session_key)
.first()
)
if token.session:
token.save()
else:
token.delete()
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_rac", "0006_connectiontoken_authentik_p_expires_91f148_idx_and_more"),
("authentik_core", "0046_session_and_more"),
]
operations = [
migrations.RenameField(
model_name="connectiontoken",
old_name="session",
new_name="old_session",
),
migrations.AddField(
model_name="connectiontoken",
name="session",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="authentik_core.authenticatedsession",
),
),
migrations.RunPython(code=migrate_sessions),
migrations.AlterField(
model_name="connectiontoken",
name="session",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_core.authenticatedsession",
),
),
migrations.RemoveField(
model_name="connectiontoken",
name="old_session",
),
]

View File

@ -8,7 +8,7 @@ from django.db.models.signals import post_delete, post_save, pre_delete
from django.dispatch import receiver
from django.http import HttpRequest
from authentik.core.models import User
from authentik.core.models import AuthenticatedSession, User
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
from authentik.providers.rac.consumer_client import (
RAC_CLIENT_GROUP_SESSION,
@ -32,6 +32,18 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
)
@receiver(pre_delete, sender=AuthenticatedSession)
def user_session_deleted(sender, instance: AuthenticatedSession, **_):
layer = get_channel_layer()
async_to_sync(layer.group_send)(
RAC_CLIENT_GROUP_SESSION
% {
"session": instance.session.session_key,
},
{"type": "event.disconnect", "reason": "session_logout"},
)
@receiver(pre_delete, sender=ConnectionToken)
def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **_):
"""Disconnect session when connection token is deleted"""

View File

@ -4,10 +4,13 @@
{% block head %}
<script src="{% versioned_script 'dist/rac/index-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon_url }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon_url }}">
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
<link rel="icon" href="{{ tenant.branding_favicon_url }}" />
<link rel="shortcut icon" href="{{ tenant.branding_favicon_url }}" />
{% include "base/header_js.html" %}
{% endblock %}

View File

@ -2,7 +2,7 @@
from django.test import TransactionTestCase
from authentik.core.models import Application, AuthenticatedSession
from authentik.core.models import Application, AuthenticatedSession, Session
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
from authentik.providers.rac.models import (
@ -36,13 +36,15 @@ class TestModels(TransactionTestCase):
def test_settings_merge(self):
"""Test settings merge"""
session = Session.objects.create(
session_key=generate_id(),
last_ip="255.255.255.255",
)
auth_session = AuthenticatedSession.objects.create(session=session, user=self.user)
token = ConnectionToken.objects.create(
provider=self.provider,
endpoint=self.endpoint,
session=AuthenticatedSession.objects.create(
user=self.user,
session_key=generate_id(),
),
session=auth_session,
)
path = f"/tmp/connection/{token.token}" # nosec
self.assertEqual(

View File

@ -1,7 +1,5 @@
"""rac urls"""
from channels.auth import AuthMiddleware
from channels.sessions import CookieMiddleware
from django.urls import path
from authentik.outposts.channels import TokenOutpostMiddleware
@ -12,7 +10,7 @@ from authentik.providers.rac.api.providers import RACProviderViewSet
from authentik.providers.rac.consumer_client import RACClientConsumer
from authentik.providers.rac.consumer_outpost import RACOutpostConsumer
from authentik.providers.rac.views import RACInterface, RACStartView
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.asgi_middleware import AuthMiddlewareStack
from authentik.root.middleware import ChannelsLoggingMiddleware
urlpatterns = [
@ -31,9 +29,7 @@ urlpatterns = [
websocket_urlpatterns = [
path(
"ws/rac/<str:token>/",
ChannelsLoggingMiddleware(
CookieMiddleware(SessionMiddleware(AuthMiddleware(RACClientConsumer.as_asgi())))
),
ChannelsLoggingMiddleware(AuthMiddlewareStack(RACClientConsumer.as_asgi())),
),
path(
"ws/outpost_rac/<str:channel>/",

View File

@ -8,7 +8,7 @@ from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext as _
from authentik.core.models import Application, AuthenticatedSession
from authentik.core.models import Application
from authentik.core.views.interface import InterfaceView
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import RedirectChallenge
@ -113,9 +113,7 @@ class RACFinalStage(RedirectStage):
provider=self.provider,
endpoint=self.endpoint,
settings=self.executor.plan.context.get("connection_settings", {}),
session=AuthenticatedSession.objects.filter(
session_key=self.request.session.session_key
).first(),
session=self.request.session["authenticatedsession"],
expires=now() + timedelta_from_string(self.provider.connection_expiry),
expiring=True,
)

View File

@ -0,0 +1,41 @@
"""RBAC Initial Permissions"""
from rest_framework.serializers import ListSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.rbac.api.rbac import PermissionSerializer
from authentik.rbac.models import InitialPermissions
class InitialPermissionsSerializer(ModelSerializer):
"""InitialPermissions serializer"""
permissions_obj = ListSerializer(
child=PermissionSerializer(),
read_only=True,
source="permissions",
required=False,
)
class Meta:
model = InitialPermissions
fields = [
"pk",
"name",
"mode",
"role",
"permissions",
"permissions_obj",
]
class InitialPermissionsViewSet(UsedByMixin, ModelViewSet):
"""InitialPermissions viewset"""
queryset = InitialPermissions.objects.all()
serializer_class = InitialPermissionsSerializer
search_fields = ["name"]
ordering = ["name"]
filterset_fields = ["name"]

View File

@ -0,0 +1,39 @@
# Generated by Django 5.0.13 on 2025-04-07 13:05
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
("authentik_rbac", "0004_alter_systempermission_options"),
]
operations = [
migrations.CreateModel(
name="InitialPermissions",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("name", models.TextField(max_length=150, unique=True)),
("mode", models.CharField(choices=[("user", "User"), ("role", "Role")])),
("permissions", models.ManyToManyField(blank=True, to="auth.permission")),
(
"role",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_rbac.role"
),
),
],
options={
"verbose_name": "Initial Permissions",
"verbose_name_plural": "Initial Permissions",
},
),
]

View File

@ -3,6 +3,7 @@
from uuid import uuid4
from django.contrib.auth.management import _get_all_permissions
from django.contrib.auth.models import Permission
from django.db import models
from django.db.transaction import atomic
from django.utils.translation import gettext_lazy as _
@ -75,6 +76,35 @@ class Role(SerializerModel):
]
class InitialPermissionsMode(models.TextChoices):
"""Determines which entity the initial permissions are assigned to."""
USER = "user", _("User")
ROLE = "role", _("Role")
class InitialPermissions(SerializerModel):
"""Assigns permissions for newly created objects."""
name = models.TextField(max_length=150, unique=True)
mode = models.CharField(choices=InitialPermissionsMode.choices)
role = models.ForeignKey(Role, on_delete=models.CASCADE)
permissions = models.ManyToManyField(Permission, blank=True)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.rbac.api.initial_permissions import InitialPermissionsSerializer
return InitialPermissionsSerializer
def __str__(self) -> str:
return f"Initial Permissions for Role #{self.role_id}, applying to #{self.mode}"
class Meta:
verbose_name = _("Initial Permissions")
verbose_name_plural = _("Initial Permissions")
class SystemPermission(models.Model):
"""System-wide permissions that are not related to any direct
database model"""

View File

@ -1,9 +1,13 @@
"""RBAC Permissions"""
from django.contrib.contenttypes.models import ContentType
from django.db.models import Model
from guardian.shortcuts import assign_perm
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from rest_framework.request import Request
from authentik.rbac.models import InitialPermissions, InitialPermissionsMode
class ObjectPermissions(DjangoObjectPermissions):
"""RBAC Permissions"""
@ -51,3 +55,20 @@ def HasPermission(*perm: str) -> type[BasePermission]:
return bool(request.user and request.user.has_perms(perm))
return checker
# TODO: add `user: User` type annotation without circular dependencies.
# The author of this function isn't proficient/patient enough to do it.
def assign_initial_permissions(user, instance: Model):
# Performance here should not be an issue, but if needed, there are many optimization routes
initial_permissions_list = InitialPermissions.objects.filter(role__group__in=user.groups.all())
for initial_permissions in initial_permissions_list:
for permission in initial_permissions.permissions.all():
if permission.content_type != ContentType.objects.get_for_model(instance):
continue
assign_to = (
user
if initial_permissions.mode == InitialPermissionsMode.USER
else initial_permissions.role.group
)
assign_perm(permission, assign_to, instance)

View File

@ -0,0 +1,116 @@
"""Test InitialPermissions"""
from django.contrib.auth.models import Permission
from guardian.shortcuts import assign_perm
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group
from authentik.core.tests.utils import create_test_user
from authentik.lib.generators import generate_id
from authentik.rbac.models import InitialPermissions, InitialPermissionsMode, Role
from authentik.stages.dummy.models import DummyStage
class TestInitialPermissions(APITestCase):
"""Test InitialPermissions"""
def setUp(self) -> None:
self.user = create_test_user()
self.same_role_user = create_test_user()
self.different_role_user = create_test_user()
self.role = Role.objects.create(name=generate_id())
self.different_role = Role.objects.create(name=generate_id())
self.group = Group.objects.create(name=generate_id())
self.different_group = Group.objects.create(name=generate_id())
self.group.roles.add(self.role)
self.group.users.add(self.user, self.same_role_user)
self.different_group.roles.add(self.different_role)
self.different_group.users.add(self.different_role_user)
self.ip = InitialPermissions.objects.create(
name=generate_id(), mode=InitialPermissionsMode.USER, role=self.role
)
self.view_role = Permission.objects.filter(codename="view_role").first()
self.ip.permissions.add(self.view_role)
assign_perm("authentik_rbac.add_role", self.user)
self.client.force_login(self.user)
def test_different_role(self):
"""InitialPermissions for different role does nothing"""
self.ip.role = self.different_role
self.ip.save()
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertFalse(self.user.has_perm("authentik_rbac.view_role", role))
def test_different_model(self):
"""InitialPermissions for different model does nothing"""
assign_perm("authentik_stages_dummy.add_dummystage", self.user)
self.client.post(
reverse("authentik_api:stages-dummy-list"), {"name": "test-stage", "throw-error": False}
)
role = Role.objects.filter(name="test-role").first()
self.assertFalse(self.user.has_perm("authentik_rbac.view_role", role))
stage = DummyStage.objects.filter(name="test-stage").first()
self.assertFalse(self.user.has_perm("authentik_stages_dummy.view_dummystage", stage))
def test_mode_user(self):
"""InitialPermissions adds user permission in user mode"""
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertFalse(self.same_role_user.has_perm("authentik_rbac.view_role", role))
def test_mode_role(self):
"""InitialPermissions adds role permission in role mode"""
self.ip.mode = InitialPermissionsMode.ROLE
self.ip.save()
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.same_role_user.has_perm("authentik_rbac.view_role", role))
def test_many_permissions(self):
"""InitialPermissions can add multiple permissions"""
change_role = Permission.objects.filter(codename="change_role").first()
self.ip.permissions.add(change_role)
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.user.has_perm("authentik_rbac.change_role", role))
def test_permissions_separated_by_role(self):
"""When the triggering user is part of two different roles with InitialPermissions in role
mode, it only adds permissions to the relevant role."""
self.ip.mode = InitialPermissionsMode.ROLE
self.ip.save()
different_ip = InitialPermissions.objects.create(
name=generate_id(), mode=InitialPermissionsMode.ROLE, role=self.different_role
)
change_role = Permission.objects.filter(codename="change_role").first()
different_ip.permissions.add(change_role)
self.different_group.users.add(self.user)
self.client.post(reverse("authentik_api:roles-list"), {"name": "test-role"})
role = Role.objects.filter(name="test-role").first()
self.assertTrue(self.user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.same_role_user.has_perm("authentik_rbac.view_role", role))
self.assertFalse(self.different_role_user.has_perm("authentik_rbac.view_role", role))
self.assertTrue(self.user.has_perm("authentik_rbac.change_role", role))
self.assertFalse(self.same_role_user.has_perm("authentik_rbac.change_role", role))
self.assertTrue(self.different_role_user.has_perm("authentik_rbac.change_role", role))

View File

@ -1,5 +1,6 @@
"""RBAC API urls"""
from authentik.rbac.api.initial_permissions import InitialPermissionsViewSet
from authentik.rbac.api.rbac import RBACPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_roles import RoleAssignedPermissionViewSet
from authentik.rbac.api.rbac_assigned_by_users import UserAssignedPermissionViewSet
@ -21,5 +22,6 @@ api_urlpatterns = [
("rbac/permissions/users", UserPermissionViewSet, "permissions-users"),
("rbac/permissions/roles", RolePermissionViewSet, "permissions-roles"),
("rbac/permissions", RBACPermissionViewSet),
("rbac/roles", RoleViewSet),
("rbac/roles", RoleViewSet, "roles"),
("rbac/initial_permissions", InitialPermissionsViewSet, "initial-permissions"),
]

View File

@ -50,7 +50,7 @@ class TestRecovery(TestCase):
)
token = Token.objects.get(intent=TokenIntents.INTENT_RECOVERY, user=self.user)
self.client.get(reverse("authentik_recovery:use-token", kwargs={"key": token.key}))
self.assertEqual(int(self.client.session["_auth_user_id"]), token.user.pk)
self.assertEqual(self.client.session["authenticatedsession"].user.pk, token.user.pk)
def test_recovery_view_invalid(self):
"""Test recovery view with invalid token"""

View File

@ -1,8 +1,12 @@
"""ASGI middleware"""
from channels.auth import UserLazyObject
from channels.db import database_sync_to_async
from channels.middleware import BaseMiddleware
from channels.sessions import CookieMiddleware
from channels.sessions import InstanceSessionWrapper as UpstreamInstanceSessionWrapper
from channels.sessions import SessionMiddleware as UpstreamSessionMiddleware
from django.contrib.auth.models import AnonymousUser
from authentik.root.middleware import SessionMiddleware as HTTPSessionMiddleware
@ -33,3 +37,48 @@ class SessionMiddleware(UpstreamSessionMiddleware):
await wrapper.resolve_session()
return await self.inner(wrapper.scope, receive, wrapper.send)
@database_sync_to_async
def get_user(scope):
"""
Return the user model instance associated with the given scope.
If no user is retrieved, return an instance of `AnonymousUser`.
"""
if "session" not in scope:
raise ValueError(
"Cannot find session in scope. You should wrap your consumer in SessionMiddleware."
)
user = None
if (authenticated_session := scope["session"].get("authenticated_session", None)) is not None:
user = authenticated_session.user
return user or AnonymousUser()
class AuthMiddleware(BaseMiddleware):
def populate_scope(self, scope):
# Make sure we have a session
if "session" not in scope:
raise ValueError(
"AuthMiddleware cannot find session in scope. SessionMiddleware must be above it."
)
# Add it to the scope if it's not there already
if "user" not in scope:
scope["user"] = UserLazyObject()
async def resolve_scope(self, scope):
scope["user"]._wrapped = await get_user(scope)
async def __call__(self, scope, receive, send):
scope = dict(scope)
# Scope injection/mutation per this middleware's needs.
self.populate_scope(scope)
# Grab the finalized/resolved scope
await self.resolve_scope(scope)
return await super().__call__(scope, receive, send)
# Handy shortcut for applying all three layers at once
def AuthMiddlewareStack(inner):
return CookieMiddleware(SessionMiddleware(AuthMiddleware(inner)))

View File

@ -49,7 +49,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
return False
@staticmethod
def decode_session_key(key: str) -> str:
def decode_session_key(key: str | None) -> str | None:
"""Decode raw session cookie, and parse JWT"""
# We need to support the standard django format of just a session key
# for testing setups, where the session is directly set
@ -64,7 +64,11 @@ class SessionMiddleware(UpstreamSessionMiddleware):
def process_request(self, request: HttpRequest):
raw_session = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
session_key = SessionMiddleware.decode_session_key(raw_session)
request.session = self.SessionStore(session_key)
request.session = self.SessionStore(
session_key,
last_ip=ClientIPMiddleware.get_client_ip(request),
last_user_agent=request.META.get("HTTP_USER_AGENT", ""),
)
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
"""

View File

@ -1,23 +0,0 @@
"""
Module for abstract serializer/unserializer base classes.
"""
import pickle # nosec
class PickleSerializer:
"""
Simple wrapper around pickle to be used in signing.dumps()/loads() and
cache backends.
"""
def __init__(self, protocol=None):
self.protocol = pickle.HIGHEST_PROTOCOL if protocol is None else protocol
def dumps(self, obj):
"""Pickle data to be stored in redis"""
return pickle.dumps(obj, self.protocol)
def loads(self, data):
"""Unpickle data to be loaded from redis"""
return pickle.loads(data) # nosec

View File

@ -7,7 +7,6 @@ from pathlib import Path
import orjson
from celery.schedules import crontab
from django.conf import ImproperlyConfigured
from sentry_sdk import set_tag
from xmlsec import enable_debug_trace
@ -43,7 +42,6 @@ SESSION_COOKIE_DOMAIN = CONFIG.get("cookie_domain", None)
APPEND_SLASH = False
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
BACKEND_INBUILT,
BACKEND_APP_PASSWORD,
BACKEND_LDAP,
@ -229,17 +227,7 @@ CACHES = {
DJANGO_REDIS_SCAN_ITERSIZE = 1000
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
match CONFIG.get("session_storage", "cache"):
case "cache":
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
case "db":
SESSION_ENGINE = "django.contrib.sessions.backends.db"
case _:
raise ImproperlyConfigured(
"Invalid session_storage setting, allowed values are db and cache"
)
SESSION_SERIALIZER = "authentik.root.sessions.pickle.PickleSerializer"
SESSION_CACHE_ALIAS = "default"
SESSION_ENGINE = "authentik.core.sessions"
# Configured via custom SessionMiddleware
# SESSION_COOKIE_SAMESITE = "None"
# SESSION_COOKIE_SECURE = True
@ -256,7 +244,7 @@ MIDDLEWARE = [
"django_prometheus.middleware.PrometheusBeforeMiddleware",
"authentik.root.middleware.ClientIPMiddleware",
"authentik.stages.user_login.middleware.BoundSessionMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"authentik.core.middleware.AuthenticationMiddleware",
"authentik.core.middleware.RequestIDMiddleware",
"authentik.brands.middleware.BrandMiddleware",
"authentik.events.middleware.AuditMiddleware",

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