Compare commits

...

40 Commits

Author SHA1 Message Date
9deed34479 web: Fix issue stemming from locale initialization triggering UI repeat reloads. 2025-04-23 14:29:20 +02:00
2033d52dc2 core, web: update translations (#14187)
Co-authored-by: melizeche <484773+melizeche@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-04-23 10:57:09 +00:00
be00f47ddc core: bump goauthentik.io/api/v3 from 3.2025024.8 to 3.2025024.9 (#14189)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-23 12:44:09 +02:00
2cc5f4b273 website/docs: update user object doc (#14132)
* Updated formatting, changed examples, added headers, updated django doc link to stable

* Prettier fix

* Update website/docs/users-sources/user/user_ref.mdx

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Dewi Roberts <dewi@goauthentik.io>

* Update website/docs/users-sources/user/user_ref.mdx

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

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-04-23 08:26:10 +01:00
4e8f3407a4 website/docs: dev-docs: style guide: no longer using italic for vars (#14185)
We no longer use italic for variables

Signed-off-by: Dominic R <dominic@sdko.org>
2025-04-22 17:30:46 -05:00
7f861cc2a1 website/docs: dev docs: style guide: update style conventions for urls (#14184)
* website/docs: dev docs: style guide: update style conventions for urls

Updates URL styling conventions to use angle bracket surrounded values instead of <em>s and <kbd>s

Part of https://www.notion.so/authentiksecurity/Check-ins-17caee05b24e80a0aec6c7d508406435?pvs=4#1ddaee05b24e80138155e120174c3502

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

* yep

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

---------

Signed-off-by: Dominic R <dominic@sdko.org>
2025-04-22 17:30:02 -05:00
7bf58d0ba2 website/integrations: paperless: use <slug>. instead of hardcoded slug value (#14183)
Closes https://github.com/goauthentik/authentik/issues/13778

Signed-off-by: Dominic R <dominic@sdko.org>
2025-04-22 16:55:53 -05:00
fffcb00f39 website/docs: updates style guide code block section (#14088)
* Removed multiline code block section. Added docusaurus style codeblock section. Fixed some capitalisation.

* Update website/docs/developer-docs/docs/style-guide.mdx

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

* Added highlighting info and fixed formatting.

* Typo and prettier.

---------

Signed-off-by: Dewi Roberts <dewi@goauthentik.io>
Co-authored-by: Dominic R <dominic@sdko.org>
2025-04-22 12:10:06 -05:00
77ee868573 website: components: delete multilinecodeblock src (#14094)
* Delete website/src/components/MultilineCodeblock/index.tsx

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

* Delete website/src/components/MultilineCodeblock/styles.css

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

---------

Signed-off-by: Dominic R <dominic@sdko.org>
2025-04-22 17:41:41 +01:00
6aaec08496 Revert "policies: buffered policy access view for concurrent authorization attempts when unauthenticated (#13629)" (#14180) 2025-04-22 15:45:45 +00:00
cc15584650 core: bump uvicorn from 0.34.1 to v0.34.2 (#14175) 2025-04-22 17:14:01 +02:00
e55e446b89 website/integrations: add xcreds (#14163)
* Created guide and modified sidebar

* make

* Typo

* Clarified wording

* Added words

* Fixed login to vs log into

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2025-04-22 14:34:56 +00:00
76088e48b5 core, web: update translations (#14179)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-22 13:56:32 +00:00
4165a0a6b2 web: update default flow background (#14115)
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2025-04-22 15:02:10 +02:00
647fefe5ce web: bump API Client version (#14176)
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-22 15:01:42 +02:00
723dccdae3 enterprise/policies: Add Password Uniqueness History Policy (#13453)
Co-authored-by: David Gunter <david@davidgunter.ca>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2025-04-22 14:39:07 +02:00
c82f747e5e web/xliff: fix duplicated translations (#14164) 2025-04-22 12:13:34 +00:00
43406e2464 website/docs: fix postgres pool recommended settings (#14149) 2025-04-22 12:01:36 +00:00
a0ff0bef85 core: bump astral-sh/uv from 0.6.14 to 0.6.16 (#14161)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-22 13:57:29 +02:00
bedf548a5f web: fix scrollbar styling (#12600)
* Fix `.pf-c-card__body` scrollbars

* Fallback scrollbar styling for browsers that don't support `::-webkit-scrollbar`

---------

Co-authored-by: Marcelo Elizeche Landó <marcelo@goauthentik.io>
2025-04-22 04:47:02 +02:00
976e81c1dd website: integrations: gravity: fix issuer URL (#14155)
Signed-off-by: Dominic R <dominic@sdko.org>
2025-04-21 20:53:25 +02:00
ad733033d7 web: Packagify live reload plugin. (#14134)
* web: Packagify live reload plugin.

* web: Use shared formatter.

* web: Format.

* web: Use project mode typecheck.

* web: Fix type errors.
2025-04-21 19:07:45 +02:00
ba686f6a93 web: bump API Client version (#14062)
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-18 17:17:30 +02:00
dc50be1e13 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#14146)
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-18 17:17:03 +02:00
205686d252 translate: Updates for file web/xliff/en.xlf in zh_CN (#14145)
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-18 17:16:48 +02:00
6d589013e6 core: bump goauthentik.io/api/v3 from 3.2025024.7 to 3.2025024.8 (#14143)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-18 17:16:34 +02:00
2d6433ca9a translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#14144)
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-18 17:16:23 +02:00
b5f07acb26 translate: Updates for file web/xliff/en.xlf in zh-Hans (#14139)
* Translate web/xliff/en.xlf in zh-Hans

100% translated source file: 'web/xliff/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'.

* 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-18 17:16:10 +02:00
ea8702077c core, web: update translations (#14142)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2025-04-18 11:18:32 +02:00
6593357115 core: bump yarl from 1.19.0 to v1.20.0 (#14128) 2025-04-17 13:44:53 -04:00
6daed865c1 core: bump kombu from 5.5.2 to v5.5.3 (#14127) 2025-04-17 13:44:35 -04:00
c48a21707a core: bump boto3 from 1.37.34 to v1.37.35 (#14126) 2025-04-17 13:44:06 -04:00
e857770c0a core: bump automat from 24.8.1 to v25.4.16 (#14125) 2025-04-17 13:43:47 -04:00
add74c8799 website: bump http-proxy-middleware from 2.0.7 to 2.0.9 in /website (#14111)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.7 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:19:25 +02:00
12d854035d website: bump estree-util-value-to-estree from 3.1.1 to 3.3.3 in /website (#13808)
website: bump estree-util-value-to-estree in /website

Bumps [estree-util-value-to-estree](https://github.com/remcohaszing/estree-util-value-to-estree) from 3.1.1 to 3.3.3.
- [Release notes](https://github.com/remcohaszing/estree-util-value-to-estree/releases)
- [Commits](https://github.com/remcohaszing/estree-util-value-to-estree/compare/v3.1.1...v3.3.3)

---
updated-dependencies:
- dependency-name: estree-util-value-to-estree
  dependency-version: 3.3.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:02:02 +02:00
57dd4ae91d website: bump wireit from 0.14.11 to 0.14.12 in /website (#14003)
Bumps [wireit](https://github.com/google/wireit) from 0.14.11 to 0.14.12.
- [Changelog](https://github.com/google/wireit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/wireit/compare/v0.14.11...v0.14.12)

---
updated-dependencies:
- dependency-name: wireit
  dependency-version: 0.14.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:01:46 +02:00
37fbc98177 website: bump the build group in /website with 9 updates (#13748)
Bumps the build group in /website with 9 updates:

| Package | From | To |
| --- | --- | --- |
| [@rspack/binding-darwin-arm64](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.3.0` | `1.3.1` |
| [@rspack/binding-linux-arm64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.3.0` | `1.3.1` |
| [@rspack/binding-linux-x64-gnu](https://github.com/web-infra-dev/rspack/tree/HEAD/packages/rspack) | `1.3.0` | `1.3.1` |
| [@swc/core-darwin-arm64](https://github.com/swc-project/swc) | `1.11.13` | `1.11.16` |
| [@swc/core-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.11.13` | `1.11.16` |
| [@swc/core-linux-x64-gnu](https://github.com/swc-project/swc) | `1.11.13` | `1.11.16` |
| [@swc/html-darwin-arm64](https://github.com/swc-project/swc) | `1.11.13` | `1.11.16` |
| [@swc/html-linux-arm64-gnu](https://github.com/swc-project/swc) | `1.11.13` | `1.11.16` |
| [@swc/html-linux-x64-gnu](https://github.com/swc-project/swc) | `1.11.13` | `1.11.16` |


Updates `@rspack/binding-darwin-arm64` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.3.1/packages/rspack)

Updates `@rspack/binding-linux-arm64-gnu` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.3.1/packages/rspack)

Updates `@rspack/binding-linux-x64-gnu` from 1.3.0 to 1.3.1
- [Release notes](https://github.com/web-infra-dev/rspack/releases)
- [Commits](https://github.com/web-infra-dev/rspack/commits/v1.3.1/packages/rspack)

Updates `@swc/core-darwin-arm64` from 1.11.13 to 1.11.16
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.11.13...v1.11.16)

Updates `@swc/core-linux-arm64-gnu` from 1.11.13 to 1.11.16
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.11.13...v1.11.16)

Updates `@swc/core-linux-x64-gnu` from 1.11.13 to 1.11.16
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.11.13...v1.11.16)

Updates `@swc/html-darwin-arm64` from 1.11.13 to 1.11.16
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.11.13...v1.11.16)

Updates `@swc/html-linux-arm64-gnu` from 1.11.13 to 1.11.16
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.11.13...v1.11.16)

Updates `@swc/html-linux-x64-gnu` from 1.11.13 to 1.11.16
- [Release notes](https://github.com/swc-project/swc/releases)
- [Changelog](https://github.com/swc-project/swc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/swc-project/swc/compare/v1.11.13...v1.11.16)

---
updated-dependencies:
- dependency-name: "@rspack/binding-darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-arm64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@rspack/binding-linux-x64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-arm64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/core-linux-x64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-arm64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
- dependency-name: "@swc/html-linux-x64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: build
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:00:44 +02:00
14f216eb40 core: bump github.com/go-ldap/ldap/v3 from 3.4.10 to 3.4.11 (#14068)
Bumps [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap) from 3.4.10 to 3.4.11.
- [Release notes](https://github.com/go-ldap/ldap/releases)
- [Commits](https://github.com/go-ldap/ldap/compare/v3.4.10...v3.4.11)

---
updated-dependencies:
- dependency-name: github.com/go-ldap/ldap/v3
  dependency-version: 3.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-17 19:00:21 +02:00
1209dd022e translate: Updates for file web/xliff/en.xlf in fr (#14124)
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-17 10:58:55 +00:00
c96f13ac66 translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#14123)
* Translate locale/en/LC_MESSAGES/django.po in fr

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

* 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-17 10:57:24 +00:00
115 changed files with 3494 additions and 1822 deletions

View File

@ -94,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Download uv # Stage 5: Download uv
FROM ghcr.io/astral-sh/uv:0.6.14 AS uv FROM ghcr.io/astral-sh/uv:0.6.16 AS uv
# Stage 6: Base python image # Stage 6: Base python image
FROM ghcr.io/goauthentik/fips-python:3.12.10-slim-bookworm-fips AS python-base FROM ghcr.io/goauthentik/fips-python:3.12.10-slim-bookworm-fips AS python-base

View File

@ -13,7 +13,10 @@ from authentik.core.models import (
TokenIntents, TokenIntents,
User, User,
) )
from authentik.core.tasks import clean_expired_models, clean_temporary_users from authentik.core.tasks import (
clean_expired_models,
clean_temporary_users,
)
from authentik.core.tests.utils import create_test_admin_user from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id

View File

@ -0,0 +1,27 @@
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.policies.unique_password.models import UniquePasswordPolicy
from authentik.policies.api.policies import PolicySerializer
class UniquePasswordPolicySerializer(EnterpriseRequiredMixin, PolicySerializer):
"""Password Uniqueness Policy Serializer"""
class Meta:
model = UniquePasswordPolicy
fields = PolicySerializer.Meta.fields + [
"password_field",
"num_historical_passwords",
]
class UniquePasswordPolicyViewSet(UsedByMixin, ModelViewSet):
"""Password Uniqueness Policy Viewset"""
queryset = UniquePasswordPolicy.objects.all()
serializer_class = UniquePasswordPolicySerializer
filterset_fields = "__all__"
ordering = ["name"]
search_fields = ["name"]

View File

@ -0,0 +1,10 @@
"""authentik Unique Password policy app config"""
from authentik.enterprise.apps import EnterpriseConfig
class AuthentikEnterprisePoliciesUniquePasswordConfig(EnterpriseConfig):
name = "authentik.enterprise.policies.unique_password"
label = "authentik_policies_unique_password"
verbose_name = "authentik Enterprise.Policies.Unique Password"
default = True

View File

@ -0,0 +1,81 @@
# Generated by Django 5.0.13 on 2025-03-26 23:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_policies", "0011_policybinding_failure_result_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="UniquePasswordPolicy",
fields=[
(
"policy_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_policies.policy",
),
),
(
"password_field",
models.TextField(
default="password",
help_text="Field key to check, field keys defined in Prompt stages are available.",
),
),
(
"num_historical_passwords",
models.PositiveIntegerField(
default=1, help_text="Number of passwords to check against."
),
),
],
options={
"verbose_name": "Password Uniqueness Policy",
"verbose_name_plural": "Password Uniqueness Policies",
"indexes": [
models.Index(fields=["policy_ptr_id"], name="authentik_p_policy__f559aa_idx")
],
},
bases=("authentik_policies.policy",),
),
migrations.CreateModel(
name="UserPasswordHistory",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("old_password", models.CharField(max_length=128)),
("created_at", models.DateTimeField(auto_now_add=True)),
("hibp_prefix_sha1", models.CharField(max_length=5)),
("hibp_pw_hash", models.TextField()),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="old_passwords",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "User Password History",
},
),
]

View File

@ -0,0 +1,151 @@
from hashlib import sha1
from django.contrib.auth.hashers import identify_hasher, make_password
from django.db import models
from django.utils.translation import gettext as _
from rest_framework.serializers import BaseSerializer
from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.policies.models import Policy
from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
LOGGER = get_logger()
class UniquePasswordPolicy(Policy):
"""This policy prevents users from reusing old passwords."""
password_field = models.TextField(
default="password",
help_text=_("Field key to check, field keys defined in Prompt stages are available."),
)
# Limit on the number of previous passwords the policy evaluates
# Also controls number of old passwords the system stores.
num_historical_passwords = models.PositiveIntegerField(
default=1,
help_text=_("Number of passwords to check against."),
)
@property
def serializer(self) -> type[BaseSerializer]:
from authentik.enterprise.policies.unique_password.api import UniquePasswordPolicySerializer
return UniquePasswordPolicySerializer
@property
def component(self) -> str:
return "ak-policy-password-uniqueness-form"
def passes(self, request: PolicyRequest) -> PolicyResult:
from authentik.enterprise.policies.unique_password.models import UserPasswordHistory
password = request.context.get(PLAN_CONTEXT_PROMPT, {}).get(
self.password_field, request.context.get(self.password_field)
)
if not password:
LOGGER.warning(
"Password field not found in request when checking UniquePasswordPolicy",
field=self.password_field,
fields=request.context.keys(),
)
return PolicyResult(False, _("Password not set in context"))
password = str(password)
if not self.num_historical_passwords:
# Policy not configured to check against any passwords
return PolicyResult(True)
num_to_check = self.num_historical_passwords
password_history = UserPasswordHistory.objects.filter(user=request.user).order_by(
"-created_at"
)[:num_to_check]
if not password_history:
return PolicyResult(True)
for record in password_history:
if not record.old_password:
continue
if self._passwords_match(new_password=password, old_password=record.old_password):
# Return on first match. Authentik does not consider timing attacks
# on old passwords to be an attack surface.
return PolicyResult(
False,
_("This password has been used previously. Please choose a different one."),
)
return PolicyResult(True)
def _passwords_match(self, *, new_password: str, old_password: str) -> bool:
try:
hasher = identify_hasher(old_password)
except ValueError:
LOGGER.warning(
"Skipping password; could not load hash algorithm",
)
return False
return hasher.verify(new_password, old_password)
@classmethod
def is_in_use(cls):
"""Check if any UniquePasswordPolicy is in use, either through policy bindings
or direct attachment to a PromptStage.
Returns:
bool: True if any policy is in use, False otherwise
"""
from authentik.policies.models import PolicyBinding
# Check if any policy is in use through bindings
if PolicyBinding.in_use.for_policy(cls).exists():
return True
# Check if any policy is attached to a PromptStage
if cls.objects.filter(promptstage__isnull=False).exists():
return True
return False
class Meta(Policy.PolicyMeta):
verbose_name = _("Password Uniqueness Policy")
verbose_name_plural = _("Password Uniqueness Policies")
class UserPasswordHistory(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="old_passwords")
# Mimic's column type of AbstractBaseUser.password
old_password = models.CharField(max_length=128)
created_at = models.DateTimeField(auto_now_add=True)
hibp_prefix_sha1 = models.CharField(max_length=5)
hibp_pw_hash = models.TextField()
class Meta:
verbose_name = _("User Password History")
def __str__(self) -> str:
timestamp = f"{self.created_at:%Y/%m/%d %X}" if self.created_at else "N/A"
return f"Previous Password (user: {self.user_id}, recorded: {timestamp})"
@classmethod
def create_for_user(cls, user: User, password: str):
# To check users' passwords against Have I been Pwned, we need the first 5 chars
# of the password hashed with SHA1 without a salt...
pw_hash_sha1 = sha1(password.encode("utf-8")).hexdigest() # nosec
# ...however that'll give us a list of hashes from HIBP, and to compare that we still
# need a full unsalted SHA1 of the password. We don't want to save that directly in
# the database, so we hash that SHA1 again with a modern hashing alg,
# and then when we check users' passwords against HIBP we can use `check_password`
# which will take care of this.
hibp_hash_hash = make_password(pw_hash_sha1)
return cls.objects.create(
user=user,
old_password=password,
hibp_prefix_sha1=pw_hash_sha1[:5],
hibp_pw_hash=hibp_hash_hash,
)

View File

@ -0,0 +1,20 @@
"""Unique Password Policy settings"""
from celery.schedules import crontab
from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = {
"policies_unique_password_trim_history": {
"task": "authentik.enterprise.policies.unique_password.tasks.trim_password_histories",
"schedule": crontab(minute=fqdn_rand("policies_unique_password_trim"), hour="*/12"),
"options": {"queue": "authentik_scheduled"},
},
"policies_unique_password_check_purge": {
"task": (
"authentik.enterprise.policies.unique_password.tasks.check_and_purge_password_history"
),
"schedule": crontab(minute=fqdn_rand("policies_unique_password_purge"), hour="*/24"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -0,0 +1,23 @@
"""authentik policy signals"""
from django.dispatch import receiver
from authentik.core.models import User
from authentik.core.signals import password_changed
from authentik.enterprise.policies.unique_password.models import (
UniquePasswordPolicy,
UserPasswordHistory,
)
@receiver(password_changed)
def copy_password_to_password_history(sender, user: User, *args, **kwargs):
"""Preserve the user's old password if UniquePasswordPolicy is enabled anywhere"""
# Check if any UniquePasswordPolicy is in use
unique_pwd_policy_in_use = UniquePasswordPolicy.is_in_use()
if unique_pwd_policy_in_use:
"""NOTE: Because we run this in a signal after saving the user,
we are not atomically guaranteed to save password history.
"""
UserPasswordHistory.create_for_user(user, user.password)

View File

@ -0,0 +1,66 @@
from django.db.models.aggregates import Count
from structlog import get_logger
from authentik.enterprise.policies.unique_password.models import (
UniquePasswordPolicy,
UserPasswordHistory,
)
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task(bind=True, base=SystemTask)
@prefill_task
def check_and_purge_password_history(self: SystemTask):
"""Check if any UniquePasswordPolicy exists, and if not, purge the password history table.
This is run on a schedule instead of being triggered by policy binding deletion.
"""
if not UniquePasswordPolicy.objects.exists():
UserPasswordHistory.objects.all().delete()
LOGGER.debug("Purged UserPasswordHistory table as no policies are in use")
self.set_status(TaskStatus.SUCCESSFUL, "Successfully purged UserPasswordHistory")
return
self.set_status(
TaskStatus.SUCCESSFUL, "Not purging password histories, a unique password policy exists"
)
@CELERY_APP.task(bind=True, base=SystemTask)
def trim_password_histories(self: SystemTask):
"""Removes rows from UserPasswordHistory older than
the `n` most recent entries.
The `n` is defined by the largest configured value for all bound
UniquePasswordPolicy policies.
"""
# No policy, we'll let the cleanup above do its thing
if not UniquePasswordPolicy.objects.exists():
return
num_rows_to_preserve = 0
for policy in UniquePasswordPolicy.objects.all():
num_rows_to_preserve = max(num_rows_to_preserve, policy.num_historical_passwords)
all_pks_to_keep = []
# Get all users who have password history entries
users_with_history = (
UserPasswordHistory.objects.values("user")
.annotate(count=Count("user"))
.filter(count__gt=0)
.values_list("user", flat=True)
)
for user_pk in users_with_history:
entries = UserPasswordHistory.objects.filter(user__pk=user_pk)
pks_to_keep = entries.order_by("-created_at")[:num_rows_to_preserve].values_list(
"pk", flat=True
)
all_pks_to_keep.extend(pks_to_keep)
num_deleted, _ = UserPasswordHistory.objects.exclude(pk__in=all_pks_to_keep).delete()
LOGGER.debug("Deleted stale password history records", count=num_deleted)
self.set_status(TaskStatus.SUCCESSFUL, f"Delete {num_deleted} stale password history records")

View File

@ -0,0 +1,108 @@
"""Unique Password Policy flow tests"""
from django.contrib.auth.hashers import make_password
from django.urls.base import reverse
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.enterprise.policies.unique_password.models import (
UniquePasswordPolicy,
UserPasswordHistory,
)
from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.tests import FlowTestCase
from authentik.lib.generators import generate_id
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
class TestUniquePasswordPolicyFlow(FlowTestCase):
"""Test Unique Password Policy in a flow"""
REUSED_PASSWORD = "hunter1" # nosec B105
def setUp(self) -> None:
self.user = create_test_user()
self.flow = create_test_flow(FlowDesignation.AUTHENTICATION)
password_prompt = Prompt.objects.create(
name=generate_id(),
field_key="password",
label="PASSWORD_LABEL",
type=FieldTypes.PASSWORD,
required=True,
placeholder="PASSWORD_PLACEHOLDER",
)
self.policy = UniquePasswordPolicy.objects.create(
name="password_must_unique",
password_field=password_prompt.field_key,
num_historical_passwords=1,
)
stage = PromptStage.objects.create(name="prompt-stage")
stage.validation_policies.set([self.policy])
stage.fields.set(
[
password_prompt,
]
)
FlowStageBinding.objects.create(target=self.flow, stage=stage, order=2)
# Seed the user's password history
UserPasswordHistory.create_for_user(self.user, make_password(self.REUSED_PASSWORD))
def test_prompt_data(self):
"""Test policy attached to a prompt stage"""
# Test the policy directly
from authentik.policies.types import PolicyRequest
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
# Create a policy request with the reused password
request = PolicyRequest(user=self.user)
request.context[PLAN_CONTEXT_PROMPT] = {"password": self.REUSED_PASSWORD}
# Test the policy directly
result = self.policy.passes(request)
# Verify that the policy fails (returns False) with the expected error message
self.assertFalse(result.passing, "Policy should fail for reused password")
self.assertEqual(
result.messages[0],
"This password has been used previously. Please choose a different one.",
"Incorrect error message",
)
# API-based testing approach:
self.client.force_login(self.user)
# Send a POST request to the flow executor with the reused password
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
{"password": self.REUSED_PASSWORD},
)
self.assertStageResponse(
response,
self.flow,
component="ak-stage-prompt",
fields=[
{
"choices": None,
"field_key": "password",
"label": "PASSWORD_LABEL",
"order": 0,
"placeholder": "PASSWORD_PLACEHOLDER",
"initial_value": "",
"required": True,
"type": "password",
"sub_text": "",
}
],
response_errors={
"non_field_errors": [
{
"code": "invalid",
"string": "This password has been used previously. "
"Please choose a different one.",
}
]
},
)

View File

@ -0,0 +1,77 @@
"""Unique Password Policy tests"""
from django.contrib.auth.hashers import make_password
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.core.models import User
from authentik.enterprise.policies.unique_password.models import (
UniquePasswordPolicy,
UserPasswordHistory,
)
from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
class TestUniquePasswordPolicy(TestCase):
"""Test Password Uniqueness Policy"""
def setUp(self) -> None:
self.policy = UniquePasswordPolicy.objects.create(
name="test_unique_password", num_historical_passwords=1
)
self.user = User.objects.create(username="test-user")
def test_invalid(self):
"""Test without password present in request"""
request = PolicyRequest(get_anonymous_user())
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
self.assertEqual(result.messages[0], "Password not set in context")
def test_passes_no_previous_passwords(self):
request = PolicyRequest(get_anonymous_user())
request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter2"}}
result: PolicyResult = self.policy.passes(request)
self.assertTrue(result.passing)
def test_passes_passwords_are_different(self):
# Seed database with an old password
UserPasswordHistory.create_for_user(self.user, make_password("hunter1"))
request = PolicyRequest(self.user)
request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter2"}}
result: PolicyResult = self.policy.passes(request)
self.assertTrue(result.passing)
def test_passes_multiple_old_passwords(self):
# Seed with multiple old passwords
UserPasswordHistory.objects.bulk_create(
[
UserPasswordHistory(user=self.user, old_password=make_password("hunter1")),
UserPasswordHistory(user=self.user, old_password=make_password("hunter2")),
]
)
request = PolicyRequest(self.user)
request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter3"}}
result: PolicyResult = self.policy.passes(request)
self.assertTrue(result.passing)
def test_fails_password_matches_old_password(self):
# Seed database with an old password
UserPasswordHistory.create_for_user(self.user, make_password("hunter1"))
request = PolicyRequest(self.user)
request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter1"}}
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)
def test_fails_if_identical_password_with_different_hash_algos(self):
UserPasswordHistory.create_for_user(
self.user, make_password("hunter2", "somesalt", "scrypt")
)
request = PolicyRequest(self.user)
request.context = {PLAN_CONTEXT_PROMPT: {"password": "hunter2"}}
result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing)

View File

@ -0,0 +1,90 @@
from django.urls import reverse
from authentik.core.models import Group, Source, User
from authentik.core.tests.utils import create_test_flow, create_test_user
from authentik.enterprise.policies.unique_password.models import (
UniquePasswordPolicy,
UserPasswordHistory,
)
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_key
from authentik.policies.models import PolicyBinding, PolicyBindingModel
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.stages.user_write.models import UserWriteStage
class TestUserWriteStage(FlowTestCase):
"""Write tests"""
def setUp(self):
super().setUp()
self.flow = create_test_flow()
self.group = Group.objects.create(name="test-group")
self.other_group = Group.objects.create(name="other-group")
self.stage: UserWriteStage = UserWriteStage.objects.create(
name="write", create_users_as_inactive=True, create_users_group=self.group
)
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
self.source = Source.objects.create(name="fake_source")
def test_save_password_history_if_policy_binding_enforced(self):
"""Test user's new password is recorded when ANY enabled UniquePasswordPolicy exists"""
unique_password_policy = UniquePasswordPolicy.objects.create(num_historical_passwords=5)
pbm = PolicyBindingModel.objects.create()
PolicyBinding.objects.create(
target=pbm, policy=unique_password_policy, order=0, enabled=True
)
test_user = create_test_user()
# Store original password for verification
original_password = test_user.password
# We're changing our own password
self.client.force_login(test_user)
new_password = generate_key()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
plan.context[PLAN_CONTEXT_PENDING_USER] = test_user
plan.context[PLAN_CONTEXT_PROMPT] = {
"username": test_user.username,
"password": new_password,
}
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
# Password history should be recorded
user_password_history_qs = UserPasswordHistory.objects.filter(user=test_user)
self.assertTrue(user_password_history_qs.exists(), "Password history should be recorded")
self.assertEqual(len(user_password_history_qs), 1, "expected 1 recorded password")
# Create a password history entry manually to simulate the signal behavior
# This is what would happen if the signal worked correctly
UserPasswordHistory.objects.create(user=test_user, old_password=original_password)
user_password_history_qs = UserPasswordHistory.objects.filter(user=test_user)
self.assertTrue(user_password_history_qs.exists(), "Password history should be recorded")
self.assertEqual(len(user_password_history_qs), 2, "expected 2 recorded password")
# Execute the flow by sending a POST request to the flow executor endpoint
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
)
# Verify that the request was successful
self.assertEqual(response.status_code, 200)
user_qs = User.objects.filter(username=plan.context[PLAN_CONTEXT_PROMPT]["username"])
self.assertTrue(user_qs.exists())
# Verify the password history entry exists
user_password_history_qs = UserPasswordHistory.objects.filter(user=test_user)
self.assertTrue(user_password_history_qs.exists(), "Password history should be recorded")
self.assertEqual(len(user_password_history_qs), 3, "expected 3 recorded password")
# Verify that one of the entries contains the original password
self.assertTrue(
any(entry.old_password == original_password for entry in user_password_history_qs),
"original password should be in password history table",
)

View File

@ -0,0 +1,178 @@
from datetime import datetime, timedelta
from django.test import TestCase
from authentik.core.tests.utils import create_test_user
from authentik.enterprise.policies.unique_password.models import (
UniquePasswordPolicy,
UserPasswordHistory,
)
from authentik.enterprise.policies.unique_password.tasks import (
check_and_purge_password_history,
trim_password_histories,
)
from authentik.policies.models import PolicyBinding, PolicyBindingModel
class TestUniquePasswordPolicyModel(TestCase):
"""Test the UniquePasswordPolicy model methods"""
def test_is_in_use_with_binding(self):
"""Test is_in_use returns True when a policy binding exists"""
# Create a UniquePasswordPolicy and a PolicyBinding for it
policy = UniquePasswordPolicy.objects.create(num_historical_passwords=5)
pbm = PolicyBindingModel.objects.create()
PolicyBinding.objects.create(target=pbm, policy=policy, order=0, enabled=True)
# Verify is_in_use returns True
self.assertTrue(UniquePasswordPolicy.is_in_use())
def test_is_in_use_with_promptstage(self):
"""Test is_in_use returns True when attached to a PromptStage"""
from authentik.stages.prompt.models import PromptStage
# Create a UniquePasswordPolicy and attach it to a PromptStage
policy = UniquePasswordPolicy.objects.create(num_historical_passwords=5)
prompt_stage = PromptStage.objects.create(
name="Test Prompt Stage",
)
# Use the set() method for many-to-many relationships
prompt_stage.validation_policies.set([policy])
# Verify is_in_use returns True
self.assertTrue(UniquePasswordPolicy.is_in_use())
class TestTrimAllPasswordHistories(TestCase):
"""Test the task that trims password history for all users"""
def setUp(self):
self.user1 = create_test_user("test-user1")
self.user2 = create_test_user("test-user2")
self.pbm = PolicyBindingModel.objects.create()
# Create a policy with a limit of 1 password
self.policy = UniquePasswordPolicy.objects.create(num_historical_passwords=1)
PolicyBinding.objects.create(
target=self.pbm,
policy=self.policy,
enabled=True,
order=0,
)
class TestCheckAndPurgePasswordHistory(TestCase):
"""Test the scheduled task that checks if any policy is in use and purges if not"""
def setUp(self):
self.user = create_test_user("test-user")
self.pbm = PolicyBindingModel.objects.create()
def test_purge_when_no_policy_in_use(self):
"""Test that the task purges the table when no policy is in use"""
# Create some password history entries
UserPasswordHistory.create_for_user(self.user, "hunter2")
# Verify we have entries
self.assertTrue(UserPasswordHistory.objects.exists())
# Run the task - should purge since no policy is in use
check_and_purge_password_history()
# Verify the table is empty
self.assertFalse(UserPasswordHistory.objects.exists())
def test_no_purge_when_policy_in_use(self):
"""Test that the task doesn't purge when a policy is in use"""
# Create a policy and binding
policy = UniquePasswordPolicy.objects.create(num_historical_passwords=5)
PolicyBinding.objects.create(
target=self.pbm,
policy=policy,
enabled=True,
order=0,
)
# Create some password history entries
UserPasswordHistory.create_for_user(self.user, "hunter2")
# Verify we have entries
self.assertTrue(UserPasswordHistory.objects.exists())
# Run the task - should NOT purge since a policy is in use
check_and_purge_password_history()
# Verify the entries still exist
self.assertTrue(UserPasswordHistory.objects.exists())
class TestTrimPasswordHistory(TestCase):
"""Test password history cleanup task"""
def setUp(self):
self.user = create_test_user("test-user")
self.pbm = PolicyBindingModel.objects.create()
def test_trim_password_history_ok(self):
"""Test passwords over the define limit are deleted"""
_now = datetime.now()
UserPasswordHistory.objects.bulk_create(
[
UserPasswordHistory(
user=self.user,
old_password="hunter1", # nosec B106
created_at=_now - timedelta(days=3),
),
UserPasswordHistory(
user=self.user,
old_password="hunter2", # nosec B106
created_at=_now - timedelta(days=2),
),
UserPasswordHistory(
user=self.user,
old_password="hunter3", # nosec B106
created_at=_now,
),
]
)
policy = UniquePasswordPolicy.objects.create(num_historical_passwords=1)
PolicyBinding.objects.create(
target=self.pbm,
policy=policy,
enabled=True,
order=0,
)
trim_password_histories.delay()
user_pwd_history_qs = UserPasswordHistory.objects.filter(user=self.user)
self.assertEqual(len(user_pwd_history_qs), 1)
def test_trim_password_history_policy_diabled_no_op(self):
"""Test no passwords removed if policy binding is disabled"""
# Insert a record to ensure it's not deleted after executing task
UserPasswordHistory.create_for_user(self.user, "hunter2")
policy = UniquePasswordPolicy.objects.create(num_historical_passwords=1)
PolicyBinding.objects.create(
target=self.pbm,
policy=policy,
enabled=False,
order=0,
)
trim_password_histories.delay()
self.assertTrue(UserPasswordHistory.objects.filter(user=self.user).exists())
def test_trim_password_history_fewer_records_than_maximum_is_no_op(self):
"""Test no passwords deleted if fewer passwords exist than limit"""
UserPasswordHistory.create_for_user(self.user, "hunter2")
policy = UniquePasswordPolicy.objects.create(num_historical_passwords=2)
PolicyBinding.objects.create(
target=self.pbm,
policy=policy,
enabled=True,
order=0,
)
trim_password_histories.delay()
self.assertTrue(UserPasswordHistory.objects.filter(user=self.user).exists())

View File

@ -0,0 +1,7 @@
"""API URLs"""
from authentik.enterprise.policies.unique_password.api import UniquePasswordPolicyViewSet
api_urlpatterns = [
("policies/unique_password", UniquePasswordPolicyViewSet),
]

View File

@ -14,6 +14,7 @@ CELERY_BEAT_SCHEDULE = {
TENANT_APPS = [ TENANT_APPS = [
"authentik.enterprise.audit", "authentik.enterprise.audit",
"authentik.enterprise.policies.unique_password",
"authentik.enterprise.providers.google_workspace", "authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra", "authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.ssf", "authentik.enterprise.providers.ssf",

View File

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

View File

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

View File

@ -1,4 +1,8 @@
"""authentik policies app config""" """Authentik policies app config
Every system policy should be its own Django app under the `policies` app.
For example: The 'dummy' policy is available at `authentik.policies.dummy`.
"""
from prometheus_client import Gauge, Histogram from prometheus_client import Gauge, Histogram
@ -35,4 +39,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig):
label = "authentik_policies" label = "authentik_policies"
verbose_name = "authentik Policies" verbose_name = "authentik Policies"
default = True default = True
mountpoint = "policy/"

View File

@ -52,6 +52,13 @@ class PolicyBindingModel(models.Model):
return ["policy", "user", "group"] return ["policy", "user", "group"]
class BoundPolicyQuerySet(models.QuerySet):
"""QuerySet for filtering enabled bindings for a Policy type"""
def for_policy(self, policy: "Policy"):
return self.filter(policy__in=policy._default_manager.all()).filter(enabled=True)
class PolicyBinding(SerializerModel): class PolicyBinding(SerializerModel):
"""Relationship between a Policy and a PolicyBindingModel.""" """Relationship between a Policy and a PolicyBindingModel."""
@ -148,6 +155,9 @@ class PolicyBinding(SerializerModel):
return f"Binding - #{self.order} to {suffix}" return f"Binding - #{self.order} to {suffix}"
return "" return ""
objects = models.Manager()
in_use = BoundPolicyQuerySet.as_manager()
class Meta: class Meta:
verbose_name = _("Policy Binding") verbose_name = _("Policy Binding")
verbose_name_plural = _("Policy Bindings") verbose_name_plural = _("Policy Bindings")

View File

@ -2,4 +2,6 @@
from authentik.policies.password.api import PasswordPolicyViewSet from authentik.policies.password.api import PasswordPolicyViewSet
api_urlpatterns = [("policies/password", PasswordPolicyViewSet)] api_urlpatterns = [
("policies/password", PasswordPolicyViewSet),
]

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ from authentik.flows.stage import StageView
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest from authentik.policies.types import PolicyRequest
from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError from authentik.policies.views import PolicyAccessView, RequestValidationError
from authentik.providers.oauth2.constants import ( from authentik.providers.oauth2.constants import (
PKCE_METHOD_PLAIN, PKCE_METHOD_PLAIN,
PKCE_METHOD_S256, PKCE_METHOD_S256,
@ -326,7 +326,7 @@ class OAuthAuthorizationParams:
return code return code
class AuthorizationFlowInitView(BufferedPolicyAccessView): class AuthorizationFlowInitView(PolicyAccessView):
"""OAuth2 Flow initializer, checks access to application and starts flow""" """OAuth2 Flow initializer, checks access to application and starts flow"""
params: OAuthAuthorizationParams params: OAuthAuthorizationParams

View File

@ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage from authentik.flows.stage import RedirectStage
from authentik.lib.utils.time import timedelta_from_string from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.views import BufferedPolicyAccessView from authentik.policies.views import PolicyAccessView
from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider
class RACStartView(BufferedPolicyAccessView): class RACStartView(PolicyAccessView):
"""Start a RAC connection by checking access and creating a connection token""" """Start a RAC connection by checking access and creating a connection token"""
endpoint: Endpoint endpoint: Endpoint

View File

@ -15,7 +15,7 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_POST from authentik.flows.views.executor import SESSION_KEY_POST
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.policies.views import BufferedPolicyAccessView from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLBindings, SAMLProvider from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
@ -35,7 +35,7 @@ from authentik.stages.consent.stage import (
LOGGER = get_logger() LOGGER = get_logger()
class SAMLSSOView(BufferedPolicyAccessView): class SAMLSSOView(PolicyAccessView):
"""SAML SSO Base View, which plans a flow and injects our final stage. """SAML SSO Base View, which plans a flow and injects our final stage.
Calls get/post handler.""" Calls get/post handler."""
@ -83,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView):
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse: def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""GET and POST use the same handler, but we can't """GET and POST use the same handler, but we can't
override .dispatch easily because BufferedPolicyAccessView's dispatch""" override .dispatch easily because PolicyAccessView's dispatch"""
return self.get(request, application_slug) return self.get(request, application_slug)

View File

@ -171,7 +171,8 @@ def username_field_validator_factory() -> Callable[[PromptChallengeResponse, str
def password_single_validator_factory() -> Callable[[PromptChallengeResponse, str], Any]: def password_single_validator_factory() -> Callable[[PromptChallengeResponse, str], Any]:
"""Return a `clean_` method for `field`. Clean method checks if username is taken already.""" """Return a `clean_` method for `field`. Clean method checks if the password meets configured
PasswordPolicy."""
def password_single_clean(self: PromptChallengeResponse, value: str) -> Any: def password_single_clean(self: PromptChallengeResponse, value: str) -> Any:
"""Send password validation signals for e.g. LDAP Source""" """Send password validation signals for e.g. LDAP Source"""

View File

@ -4,7 +4,13 @@ from unittest.mock import patch
from django.urls import reverse from django.urls import reverse
from authentik.core.models import USER_ATTRIBUTE_SOURCES, Group, Source, User, UserSourceConnection from authentik.core.models import (
USER_ATTRIBUTE_SOURCES,
Group,
Source,
User,
UserSourceConnection,
)
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction

View File

@ -3641,6 +3641,46 @@
} }
} }
}, },
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_policies_unique_password.uniquepasswordpolicy"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_policies_unique_password.uniquepasswordpolicy_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_policies_unique_password.uniquepasswordpolicy"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_policies_unique_password.uniquepasswordpolicy"
}
}
},
{ {
"type": "object", "type": "object",
"required": [ "required": [
@ -4822,6 +4862,7 @@
"authentik.core", "authentik.core",
"authentik.enterprise", "authentik.enterprise",
"authentik.enterprise.audit", "authentik.enterprise.audit",
"authentik.enterprise.policies.unique_password",
"authentik.enterprise.providers.google_workspace", "authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.microsoft_entra", "authentik.enterprise.providers.microsoft_entra",
"authentik.enterprise.providers.ssf", "authentik.enterprise.providers.ssf",
@ -4929,6 +4970,7 @@
"authentik_core.applicationentitlement", "authentik_core.applicationentitlement",
"authentik_core.token", "authentik_core.token",
"authentik_enterprise.license", "authentik_enterprise.license",
"authentik_policies_unique_password.uniquepasswordpolicy",
"authentik_providers_google_workspace.googleworkspaceprovider", "authentik_providers_google_workspace.googleworkspaceprovider",
"authentik_providers_google_workspace.googleworkspaceprovidermapping", "authentik_providers_google_workspace.googleworkspaceprovidermapping",
"authentik_providers_microsoft_entra.microsoftentraprovider", "authentik_providers_microsoft_entra.microsoftentraprovider",
@ -7084,6 +7126,14 @@
"authentik_policies_reputation.delete_reputationpolicy", "authentik_policies_reputation.delete_reputationpolicy",
"authentik_policies_reputation.view_reputation", "authentik_policies_reputation.view_reputation",
"authentik_policies_reputation.view_reputationpolicy", "authentik_policies_reputation.view_reputationpolicy",
"authentik_policies_unique_password.add_uniquepasswordpolicy",
"authentik_policies_unique_password.add_userpasswordhistory",
"authentik_policies_unique_password.change_uniquepasswordpolicy",
"authentik_policies_unique_password.change_userpasswordhistory",
"authentik_policies_unique_password.delete_uniquepasswordpolicy",
"authentik_policies_unique_password.delete_userpasswordhistory",
"authentik_policies_unique_password.view_uniquepasswordpolicy",
"authentik_policies_unique_password.view_userpasswordhistory",
"authentik_providers_google_workspace.add_googleworkspaceprovider", "authentik_providers_google_workspace.add_googleworkspaceprovider",
"authentik_providers_google_workspace.add_googleworkspaceprovidergroup", "authentik_providers_google_workspace.add_googleworkspaceprovidergroup",
"authentik_providers_google_workspace.add_googleworkspaceprovidermapping", "authentik_providers_google_workspace.add_googleworkspaceprovidermapping",
@ -13784,6 +13834,14 @@
"authentik_policies_reputation.delete_reputationpolicy", "authentik_policies_reputation.delete_reputationpolicy",
"authentik_policies_reputation.view_reputation", "authentik_policies_reputation.view_reputation",
"authentik_policies_reputation.view_reputationpolicy", "authentik_policies_reputation.view_reputationpolicy",
"authentik_policies_unique_password.add_uniquepasswordpolicy",
"authentik_policies_unique_password.add_userpasswordhistory",
"authentik_policies_unique_password.change_uniquepasswordpolicy",
"authentik_policies_unique_password.change_userpasswordhistory",
"authentik_policies_unique_password.delete_uniquepasswordpolicy",
"authentik_policies_unique_password.delete_userpasswordhistory",
"authentik_policies_unique_password.view_uniquepasswordpolicy",
"authentik_policies_unique_password.view_userpasswordhistory",
"authentik_providers_google_workspace.add_googleworkspaceprovider", "authentik_providers_google_workspace.add_googleworkspaceprovider",
"authentik_providers_google_workspace.add_googleworkspaceprovidergroup", "authentik_providers_google_workspace.add_googleworkspaceprovidergroup",
"authentik_providers_google_workspace.add_googleworkspaceprovidermapping", "authentik_providers_google_workspace.add_googleworkspaceprovidermapping",
@ -14468,6 +14526,61 @@
} }
} }
}, },
"model_authentik_policies_unique_password.uniquepasswordpolicy": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"execution_logging": {
"type": "boolean",
"title": "Execution logging",
"description": "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged."
},
"password_field": {
"type": "string",
"minLength": 1,
"title": "Password field",
"description": "Field key to check, field keys defined in Prompt stages are available."
},
"num_historical_passwords": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"title": "Num historical passwords",
"description": "Number of passwords to check against."
}
},
"required": []
},
"model_authentik_policies_unique_password.uniquepasswordpolicy_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_uniquepasswordpolicy",
"change_uniquepasswordpolicy",
"delete_uniquepasswordpolicy",
"view_uniquepasswordpolicy"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_providers_google_workspace.googleworkspaceprovider": { "model_authentik_providers_google_workspace.googleworkspaceprovider": {
"type": "object", "type": "object",
"properties": { "properties": {

6
go.mod
View File

@ -7,7 +7,7 @@ require (
github.com/coreos/go-oidc/v3 v3.14.1 github.com/coreos/go-oidc/v3 v3.14.1
github.com/getsentry/sentry-go v0.32.0 github.com/getsentry/sentry-go v0.32.0
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.10 github.com/go-ldap/ldap/v3 v3.4.11
github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/runtime v0.28.0
github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
@ -27,7 +27,7 @@ require (
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2025024.7 goauthentik.io/api/v3 v3.2025024.9
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.29.0 golang.org/x/oauth2 v0.29.0
golang.org/x/sync v0.13.0 golang.org/x/sync v0.13.0
@ -43,7 +43,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect github.com/go-http-utils/fresh v0.0.0-20161124030543-7231e26a4b27 // indirect
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect github.com/go-jose/go-jose/v4 v4.0.5 // indirect

82
go.sum
View File

@ -71,8 +71,8 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY=
github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY=
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@ -86,8 +86,8 @@ github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a h1:v6zMvHuY9
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw= github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a/go.mod h1:I79BieaU4fxrw4LMXby6q5OS9XnoR9UIKLOzDFjUmuw=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@ -148,7 +148,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@ -172,16 +171,13 @@ github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyE
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -266,15 +262,10 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wwt/guac v1.3.2 h1:sH6OFGa/1tBs7ieWBVlZe7t6F5JAOWBry/tqQL/Vup4= github.com/wwt/guac v1.3.2 h1:sH6OFGa/1tBs7ieWBVlZe7t6F5JAOWBry/tqQL/Vup4=
@ -282,7 +273,6 @@ github.com/wwt/guac v1.3.2/go.mod h1:eKm+NrnK7A88l4UBEcYNpZQGMpZRryYKoz4D/0/n1C0
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -300,20 +290,14 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2025024.7 h1:OOBuyLzv+l5rtvrOYzoDs6Hy9cIfkE5sewRqR5ThSRc= goauthentik.io/api/v3 v3.2025024.9 h1:i3tbkyotE32ZpJ729BsPWTuLQUdtZ54Li4aP1amZzsM=
goauthentik.io/api/v3 v3.2025024.7/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2025024.9/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -348,11 +332,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -379,17 +358,8 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -406,12 +376,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -440,40 +404,14 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -519,10 +457,6 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-17 00:09+0000\n" "POT-Creation-Date: 2025-04-23 09:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -451,6 +451,36 @@ msgstr ""
msgid "License Usage Records" msgid "License Usage Records"
msgstr "" msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Number of passwords to check against."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "This password has been used previously. Please choose a different one."
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policy"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "Password Uniqueness Policies"
msgstr ""
#: authentik/enterprise/policies/unique_password/models.py
msgid "User Password History"
msgstr ""
#: authentik/enterprise/policy.py #: authentik/enterprise/policy.py
msgid "Enterprise required to access this feature." msgid "Enterprise required to access this feature."
msgstr "" msgstr ""
@ -1175,10 +1205,6 @@ msgstr ""
msgid "Clear Policy's cache metrics" msgid "Clear Policy's cache metrics"
msgstr "" msgstr ""
#: authentik/policies/password/models.py
msgid "Field key to check, field keys defined in Prompt stages are available."
msgstr ""
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "How many times the password hash is allowed to be on haveibeenpwned" msgid "How many times the password hash is allowed to be on haveibeenpwned"
msgstr "" msgstr ""
@ -1188,10 +1214,6 @@ msgid ""
"If the zxcvbn score is equal or less than this value, the policy will fail." "If the zxcvbn score is equal or less than this value, the policy will fail."
msgstr "" msgstr ""
#: authentik/policies/password/models.py
msgid "Password not set in context"
msgstr ""
#: authentik/policies/password/models.py #: authentik/policies/password/models.py
msgid "Invalid password." msgid "Invalid password."
msgstr "" msgstr ""
@ -1233,20 +1255,6 @@ msgstr ""
msgid "Reputation Scores" msgid "Reputation Scores"
msgstr "" msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Waiting for authentication..."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid ""
"You're already authenticating in another tab. This page will refresh once "
"authentication is completed."
msgstr ""
#: authentik/policies/templates/policies/buffer.html
msgid "Authenticate in this tab"
msgstr ""
#: authentik/policies/templates/policies/denied.html #: authentik/policies/templates/policies/denied.html
msgid "Permission denied" msgid "Permission denied"
msgstr "" msgstr ""
@ -3144,6 +3152,12 @@ msgid ""
"info is entered." "info is entered."
msgstr "" msgstr ""
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users "
"to skip straight to entering their password."
msgstr ""
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "" msgstr ""

Binary file not shown.

View File

@ -19,7 +19,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-15 00:11+0000\n" "POT-Creation-Date: 2025-04-17 00:09+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: Marc Schmitt, 2025\n" "Last-Translator: Marc Schmitt, 2025\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -2508,6 +2508,14 @@ msgstr "Le mot de passe ne correspond pas à la complexité d'Active Directory."
msgid "No token received." msgid "No token received."
msgstr "Pas de jeton reçu." msgstr "Pas de jeton reçu."
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr "Authentification HTTP Basic"
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr "Inclure le client ID et secret comme paramètres de la requête"
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "URL du jeton de requête" msgstr "URL du jeton de requête"
@ -2549,6 +2557,14 @@ msgstr ""
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "Portées additionnelles" msgstr "Portées additionnelles"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr ""
"Comment effectuer l'authentification lors d'une demande de jeton pour le "
"flux authorization_code"
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "Source OAuth" msgstr "Source OAuth"

View File

@ -15,7 +15,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-15 00:11+0000\n" "POT-Creation-Date: 2025-04-18 00:09+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -2286,6 +2286,14 @@ msgstr "密码与 Active Directory 复杂度不匹配。"
msgid "No token received." msgid "No token received."
msgstr "未收到令牌。" msgstr "未收到令牌。"
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr "HTTP 基本身份验证"
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr "包括客户端 ID 和密钥作为请求参数"
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "请求令牌 URL" msgstr "请求令牌 URL"
@ -2324,6 +2332,12 @@ msgstr "authentik 用来获取用户信息的 URL。"
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "额外的作用域" msgstr "额外的作用域"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr "在 authorization_code 令牌请求流程期间,如何执行身份验证"
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "OAuth 源" msgstr "OAuth 源"
@ -3194,6 +3208,12 @@ msgid ""
"info is entered." "info is entered."
msgstr "启用时,即使输入错误的用户信息,此阶段也会成功并继续。" msgstr "启用时,即使输入错误的用户信息,此阶段也会成功并继续。"
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr "向用户显示“在此设备上记住我”开关,允许相同用户直接跳过输入密码。"
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "可选注册流程,链接在页面底部。" msgstr "可选注册流程,链接在页面底部。"

Binary file not shown.

View File

@ -14,7 +14,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-04-15 00:11+0000\n" "POT-Creation-Date: 2025-04-18 00:09+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n" "PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2025\n" "Last-Translator: deluxghost, 2025\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -2285,6 +2285,14 @@ msgstr "密码与 Active Directory 复杂度不匹配。"
msgid "No token received." msgid "No token received."
msgstr "未收到令牌。" msgstr "未收到令牌。"
#: authentik/sources/oauth/models.py
msgid "HTTP Basic Authentication"
msgstr "HTTP 基本身份验证"
#: authentik/sources/oauth/models.py
msgid "Include the client ID and secret as request parameters"
msgstr "包括客户端 ID 和密钥作为请求参数"
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "Request Token URL" msgid "Request Token URL"
msgstr "请求令牌 URL" msgstr "请求令牌 URL"
@ -2323,6 +2331,12 @@ msgstr "authentik 用来获取用户信息的 URL。"
msgid "Additional Scopes" msgid "Additional Scopes"
msgstr "额外的作用域" msgstr "额外的作用域"
#: authentik/sources/oauth/models.py
msgid ""
"How to perform authentication during an authorization_code token request "
"flow"
msgstr "在 authorization_code 令牌请求流程期间,如何执行身份验证"
#: authentik/sources/oauth/models.py #: authentik/sources/oauth/models.py
msgid "OAuth Source" msgid "OAuth Source"
msgstr "OAuth 源" msgstr "OAuth 源"
@ -3193,6 +3207,12 @@ msgid ""
"info is entered." "info is entered."
msgstr "启用时,即使输入错误的用户信息,此阶段也会成功并继续。" msgstr "启用时,即使输入错误的用户信息,此阶段也会成功并继续。"
#: authentik/stages/identification/models.py
msgid ""
"Show the user the 'Remember me on this device' toggle, allowing repeat users"
" to skip straight to entering their password."
msgstr "向用户显示“在此设备上记住我”开关,允许相同用户直接跳过输入密码。"
#: authentik/stages/identification/models.py #: authentik/stages/identification/models.py
msgid "Optional enrollment flow, which is linked at the bottom of the page." msgid "Optional enrollment flow, which is linked at the bottom of the page."
msgstr "可选注册流程,链接在页面底部。" msgstr "可选注册流程,链接在页面底部。"

Binary file not shown.

View File

@ -14721,6 +14721,302 @@ paths:
schema: schema:
$ref: '#/components/schemas/GenericError' $ref: '#/components/schemas/GenericError'
description: '' description: ''
/policies/unique_password/:
get:
operationId: policies_unique_password_list
description: Password Uniqueness Policy Viewset
parameters:
- in: query
name: created
schema:
type: string
format: date-time
- in: query
name: execution_logging
schema:
type: boolean
- in: query
name: last_updated
schema:
type: string
format: date-time
- in: query
name: name
schema:
type: string
- in: query
name: num_historical_passwords
schema:
type: integer
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- in: query
name: password_field
schema:
type: string
- in: query
name: policy_uuid
schema:
type: string
format: uuid
- name: search
required: false
in: query
description: A search term.
schema:
type: string
tags:
- policies
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedUniquePasswordPolicyList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: policies_unique_password_create
description: Password Uniqueness Policy Viewset
tags:
- policies
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UniquePasswordPolicyRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/UniquePasswordPolicy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/policies/unique_password/{policy_uuid}/:
get:
operationId: policies_unique_password_retrieve
description: Password Uniqueness Policy Viewset
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Password Uniqueness Policy.
required: true
tags:
- policies
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/UniquePasswordPolicy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: policies_unique_password_update
description: Password Uniqueness Policy Viewset
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Password Uniqueness Policy.
required: true
tags:
- policies
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UniquePasswordPolicyRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/UniquePasswordPolicy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: policies_unique_password_partial_update
description: Password Uniqueness Policy Viewset
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Password Uniqueness Policy.
required: true
tags:
- policies
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedUniquePasswordPolicyRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/UniquePasswordPolicy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: policies_unique_password_destroy
description: Password Uniqueness Policy Viewset
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Password Uniqueness Policy.
required: true
tags:
- policies
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/policies/unique_password/{policy_uuid}/used_by/:
get:
operationId: policies_unique_password_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: policy_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Password Uniqueness Policy.
required: true
tags:
- policies
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/propertymappings/all/: /propertymappings/all/:
get: get:
operationId: propertymappings_all_list operationId: propertymappings_all_list
@ -24616,6 +24912,7 @@ paths:
- authentik_policies_geoip.geoippolicy - authentik_policies_geoip.geoippolicy
- authentik_policies_password.passwordpolicy - authentik_policies_password.passwordpolicy
- authentik_policies_reputation.reputationpolicy - authentik_policies_reputation.reputationpolicy
- authentik_policies_unique_password.uniquepasswordpolicy
- authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovider
- authentik_providers_google_workspace.googleworkspaceprovidermapping - authentik_providers_google_workspace.googleworkspaceprovidermapping
- authentik_providers_ldap.ldapprovider - authentik_providers_ldap.ldapprovider
@ -24863,6 +25160,7 @@ paths:
- authentik_policies_geoip.geoippolicy - authentik_policies_geoip.geoippolicy
- authentik_policies_password.passwordpolicy - authentik_policies_password.passwordpolicy
- authentik_policies_reputation.reputationpolicy - authentik_policies_reputation.reputationpolicy
- authentik_policies_unique_password.uniquepasswordpolicy
- authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovider
- authentik_providers_google_workspace.googleworkspaceprovidermapping - authentik_providers_google_workspace.googleworkspaceprovidermapping
- authentik_providers_ldap.ldapprovider - authentik_providers_ldap.ldapprovider
@ -40643,6 +40941,7 @@ components:
- authentik.core - authentik.core
- authentik.enterprise - authentik.enterprise
- authentik.enterprise.audit - authentik.enterprise.audit
- authentik.enterprise.policies.unique_password
- authentik.enterprise.providers.google_workspace - authentik.enterprise.providers.google_workspace
- authentik.enterprise.providers.microsoft_entra - authentik.enterprise.providers.microsoft_entra
- authentik.enterprise.providers.ssf - authentik.enterprise.providers.ssf
@ -48062,6 +48361,7 @@ components:
- authentik_core.applicationentitlement - authentik_core.applicationentitlement
- authentik_core.token - authentik_core.token
- authentik_enterprise.license - authentik_enterprise.license
- authentik_policies_unique_password.uniquepasswordpolicy
- authentik_providers_google_workspace.googleworkspaceprovider - authentik_providers_google_workspace.googleworkspaceprovider
- authentik_providers_google_workspace.googleworkspaceprovidermapping - authentik_providers_google_workspace.googleworkspaceprovidermapping
- authentik_providers_microsoft_entra.microsoftentraprovider - authentik_providers_microsoft_entra.microsoftentraprovider
@ -50616,6 +50916,18 @@ components:
required: required:
- pagination - pagination
- results - results
PaginatedUniquePasswordPolicyList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/UniquePasswordPolicy'
required:
- pagination
- results
PaginatedUserAssignedObjectPermissionList: PaginatedUserAssignedObjectPermissionList:
type: object type: object
properties: properties:
@ -54225,6 +54537,27 @@ components:
nullable: true nullable: true
expiring: expiring:
type: boolean type: boolean
PatchedUniquePasswordPolicyRequest:
type: object
description: Password Uniqueness Policy Serializer
properties:
name:
type: string
minLength: 1
execution_logging:
type: boolean
description: When this option is enabled, all executions of this policy
will be logged. By default, only execution errors are logged.
password_field:
type: string
minLength: 1
description: Field key to check, field keys defined in Prompt stages are
available.
num_historical_passwords:
type: integer
maximum: 2147483647
minimum: 0
description: Number of passwords to check against.
PatchedUserDeleteStageRequest: PatchedUserDeleteStageRequest:
type: object type: object
description: UserDeleteStage Serializer description: UserDeleteStage Serializer
@ -59221,6 +59554,81 @@ components:
- light - light
- dark - dark
type: string type: string
UniquePasswordPolicy:
type: object
description: Password Uniqueness Policy Serializer
properties:
pk:
type: string
format: uuid
readOnly: true
title: Policy uuid
name:
type: string
execution_logging:
type: boolean
description: When this option is enabled, all executions of this policy
will be logged. By default, only execution errors are logged.
component:
type: string
description: Get object component so that we know how to edit the object
readOnly: true
verbose_name:
type: string
description: Return object's verbose_name
readOnly: true
verbose_name_plural:
type: string
description: Return object's plural verbose_name
readOnly: true
meta_model_name:
type: string
description: Return internal model name
readOnly: true
bound_to:
type: integer
description: Return objects policy is bound to
readOnly: true
password_field:
type: string
description: Field key to check, field keys defined in Prompt stages are
available.
num_historical_passwords:
type: integer
maximum: 2147483647
minimum: 0
description: Number of passwords to check against.
required:
- bound_to
- component
- meta_model_name
- name
- pk
- verbose_name
- verbose_name_plural
UniquePasswordPolicyRequest:
type: object
description: Password Uniqueness Policy Serializer
properties:
name:
type: string
minLength: 1
execution_logging:
type: boolean
description: When this option is enabled, all executions of this policy
will be logged. By default, only execution errors are logged.
password_field:
type: string
minLength: 1
description: Field key to check, field keys defined in Prompt stages are
available.
num_historical_passwords:
type: integer
maximum: 2147483647
minimum: 0
description: Number of passwords to check against.
required:
- name
UsedBy: UsedBy:
type: object type: object
description: A list of all objects referencing the queried object description: A list of all objects referencing the queried object

View File

@ -410,77 +410,3 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text, self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
"Permission denied", "Permission denied",
) )
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint("default/flow-default-provider-authorization-implicit-consent.yaml")
@apply_blueprint("system/providers-oauth2.yaml")
@reconcile_app("authentik_crypto")
def test_authorization_consent_implied_parallel(self):
"""test OpenID Provider flow (default authorization flow with implied consent)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider = OAuth2Provider.objects.create(
name=generate_id(),
client_type=ClientTypes.CONFIDENTIAL,
client_id=self.client_id,
client_secret=self.client_secret,
signing_key=create_test_cert(),
redirect_uris=[
RedirectURI(
RedirectURIMatchingMode.STRICT, "http://localhost:3000/login/generic_oauth"
)
],
authorization_flow=authorization_flow,
)
provider.property_mappings.set(
ScopeMapping.objects.filter(
scope_name__in=[
SCOPE_OPENID,
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
SCOPE_OFFLINE_ACCESS,
]
)
)
Application.objects.create(
name=generate_id(),
slug=self.app_slug,
provider=provider,
)
self.driver.get(self.live_server_url)
login_window = self.driver.current_window_handle
self.driver.switch_to.new_window("tab")
grafana_window = self.driver.current_window_handle
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.switch_to.window(login_window)
self.login()
self.driver.switch_to.window(grafana_window)
self.wait_for_url("http://localhost:3000/?orgId=1")
self.driver.get("http://localhost:3000/profile")
self.assertEqual(
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
self.user.name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute("value"),
self.user.name,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=email]").get_attribute("value"),
self.user.email,
)
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "input[name=login]").get_attribute("value"),
self.user.email,
)

View File

@ -20,7 +20,7 @@ from tests.e2e.utils import SeleniumTestCase, retry
class TestProviderSAML(SeleniumTestCase): class TestProviderSAML(SeleniumTestCase):
"""test SAML Provider flow""" """test SAML Provider flow"""
def setup_client(self, provider: SAMLProvider, force_post: bool = False, **kwargs): def setup_client(self, provider: SAMLProvider, force_post: bool = False):
"""Setup client saml-sp container which we test SAML against""" """Setup client saml-sp container which we test SAML against"""
metadata_url = ( metadata_url = (
self.url( self.url(
@ -40,7 +40,6 @@ class TestProviderSAML(SeleniumTestCase):
"SP_ENTITY_ID": provider.issuer, "SP_ENTITY_ID": provider.issuer,
"SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", "SP_SSO_BINDING": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
"SP_METADATA_URL": metadata_url, "SP_METADATA_URL": metadata_url,
**kwargs,
}, },
) )
@ -112,74 +111,6 @@ class TestProviderSAML(SeleniumTestCase):
[self.user.email], [self.user.email],
) )
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_implicit_post(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
acs_url="http://localhost:9009/saml/acs",
audience="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
)
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML",
slug="authentik-saml",
provider=provider,
)
self.setup_client(provider, True)
self.driver.get("http://localhost:9009")
self.login()
self.wait_for_url("http://localhost:9009/")
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
[self.user.name],
)
self.assertEqual(
body["attr"][
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
[str(self.user.pk)],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
[self.user.email],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
[self.user.email],
)
@retry() @retry()
@apply_blueprint( @apply_blueprint(
"default/flow-default-authentication-flow.yaml", "default/flow-default-authentication-flow.yaml",
@ -519,81 +450,3 @@ class TestProviderSAML(SeleniumTestCase):
lambda driver: driver.current_url.startswith(should_url), lambda driver: driver.current_url.startswith(should_url),
f"URL {self.driver.current_url} doesn't match expected URL {should_url}", f"URL {self.driver.current_url} doesn't match expected URL {should_url}",
) )
@retry()
@apply_blueprint(
"default/flow-default-authentication-flow.yaml",
"default/flow-default-invalidation-flow.yaml",
)
@apply_blueprint(
"default/flow-default-provider-authorization-implicit-consent.yaml",
)
@apply_blueprint(
"system/providers-saml.yaml",
)
@reconcile_app("authentik_crypto")
def test_sp_initiated_implicit_post_buffer(self):
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
acs_url=f"http://{self.host}:9009/saml/acs",
audience="authentik-e2e",
issuer="authentik-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=create_test_cert(),
)
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
Application.objects.create(
name="SAML",
slug="authentik-saml",
provider=provider,
)
self.setup_client(provider, True, SP_ROOT_URL=f"http://{self.host}:9009")
self.driver.get(self.live_server_url)
login_window = self.driver.current_window_handle
self.driver.switch_to.new_window("tab")
client_window = self.driver.current_window_handle
# We need to access the SP on the same host as the IdP for SameSite cookies
self.driver.get(f"http://{self.host}:9009")
self.driver.switch_to.window(login_window)
self.login()
self.driver.switch_to.window(client_window)
self.wait_for_url(f"http://{self.host}:9009/")
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
[self.user.name],
)
self.assertEqual(
body["attr"][
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
[self.user.username],
)
self.assertEqual(
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
[str(self.user.pk)],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
[self.user.email],
)
self.assertEqual(
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
[self.user.email],
)

70
uv.lock generated
View File

@ -379,11 +379,11 @@ wheels = [
[[package]] [[package]]
name = "automat" name = "automat"
version = "24.8.1" version = "25.4.16"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8d/2d/ede4ad7fc34ab4482389fa3369d304f2fa22e50770af706678f6a332fa82/automat-24.8.1.tar.gz", hash = "sha256:b34227cf63f6325b8ad2399ede780675083e439b20c323d376373d8ee6306d88", size = 128679 } sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/af/cc/55a32a2c98022d88812b5986d2a92c4ff3ee087e83b712ebc703bba452bf/Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a", size = 42585 }, { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842 },
] ]
[[package]] [[package]]
@ -558,30 +558,30 @@ wheels = [
[[package]] [[package]]
name = "boto3" name = "boto3"
version = "1.37.34" version = "1.37.35"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "botocore" }, { name = "botocore" },
{ name = "jmespath" }, { name = "jmespath" },
{ name = "s3transfer" }, { name = "s3transfer" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/39/5d/6b1ca20ba4da350799509a69f2d295ae11d5ec08a98e82f74b5708a8180c/boto3-1.37.34.tar.gz", hash = "sha256:94ca07328474db3fa605eb99b011512caa73f7161740d365a1f00cfebfb6dd90", size = 111701 } sdist = { url = "https://files.pythonhosted.org/packages/48/5f/e356ecd2f236e6ddc7711eaf3f075c15b13e2d044cfdb47719d49c4ae7dd/boto3-1.37.35.tar.gz", hash = "sha256:751ed599c8fd9ca24896edcd6620e8a32b3db1b68efea3a90126312240e668a2", size = 111640 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/2e/ad43d1e87d46d11dcf4104f97b9a7f6beb38a52a0e752edfadf3eb8b6e38/boto3-1.37.34-py3-none-any.whl", hash = "sha256:586bfa72a00601c04067f9adcbb08ecaf63b05b7d731103f33cb2ce0d6950b1b", size = 139920 }, { url = "https://files.pythonhosted.org/packages/f6/e4/00958f65ac74ab0a76af33f16c8fdf5726a5c6f0d3c0d0c058ff0dd00fd7/boto3-1.37.35-py3-none-any.whl", hash = "sha256:5a90d674830adbaf86456d6b27a18f5f11378277da5286511fa860d2e7b14261", size = 139922 },
] ]
[[package]] [[package]]
name = "botocore" name = "botocore"
version = "1.37.34" version = "1.37.35"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jmespath" }, { name = "jmespath" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/ca/60/9ec251a0e2d3994f3eac8bd9741576757c3aad189abbdec8fab6011f5a1a/botocore-1.37.34.tar.gz", hash = "sha256:2909b6dbf9c90347c71a6fa0364acee522d6a7664f13d6f7996c9dd1b1f46fac", size = 13817141 } sdist = { url = "https://files.pythonhosted.org/packages/64/0b/d281d74d53f7d4733402aed7a536275084fa344a2672f7ea4dbc8ebe1f1b/botocore-1.37.35.tar.gz", hash = "sha256:197a9bf8251c45b9d882c405ec0d0ab40c10e2d2a55ee66960185daec4beb6ec", size = 13821053 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/51/19fff717cc5000708c4ce3d081bb0e63ca117c6823975b33101d52fdd9f5/botocore-1.37.34-py3-none-any.whl", hash = "sha256:bd9af0db1097befd2028ba8525e32cacc04f26ccb9dbd5d48d6ecd05bc16c27a", size = 13483679 }, { url = "https://files.pythonhosted.org/packages/22/00/bf9c894f5af8e35b06ecf757d4a95883408e71c48642dc7f8760580584fd/botocore-1.37.35-py3-none-any.whl", hash = "sha256:50839212e90650d0b0fa6b8f7514876bf802f6164f2775f3abcd4d53c98bb73c", size = 13485892 },
] ]
[[package]] [[package]]
@ -1726,16 +1726,16 @@ wheels = [
[[package]] [[package]]
name = "kombu" name = "kombu"
version = "5.5.2" version = "5.5.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "amqp" }, { name = "amqp" },
{ name = "tzdata" }, { name = "tzdata" },
{ name = "vine" }, { name = "vine" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c8/12/7a340f48920f30d6febb65d0c4aca70ed01b29e116131152977df78a9a39/kombu-5.5.2.tar.gz", hash = "sha256:2dd27ec84fd843a4e0a7187424313f87514b344812cb98c25daddafbb6a7ff0e", size = 461522 } sdist = { url = "https://files.pythonhosted.org/packages/60/0a/128b65651ed8120460fc5af754241ad595eac74993115ec0de4f2d7bc459/kombu-5.5.3.tar.gz", hash = "sha256:021a0e11fcfcd9b0260ef1fb64088c0e92beb976eb59c1dfca7ddd4ad4562ea2", size = 461784 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/af/ba/939f3db0fca87715c883e42cc93045347d61a9d519c270a38e54a06db6e1/kombu-5.5.2-py3-none-any.whl", hash = "sha256:40f3674ed19603b8a771b6c74de126dbf8879755a0337caac6602faa82d539cd", size = 209763 }, { url = "https://files.pythonhosted.org/packages/5d/35/1407fb0b2f5b07b50cbaf97fce09ad87d3bfefbf64f7171a8651cd8d2f68/kombu-5.5.3-py3-none-any.whl", hash = "sha256:5b0dbceb4edee50aa464f59469d34b97864be09111338cfb224a10b6a163909b", size = 209921 },
] ]
[[package]] [[package]]
@ -3185,15 +3185,15 @@ socks = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.34.1" version = "0.34.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "h11" }, { name = "h11" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 } sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 }, { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@ -3382,33 +3382,33 @@ wheels = [
[[package]] [[package]]
name = "yarl" name = "yarl"
version = "1.19.0" version = "1.20.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "multidict" }, { name = "multidict" },
{ name = "propcache" }, { name = "propcache" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/fc/4d/8a8f57caccce49573e567744926f88c6ab3ca0b47a257806d1cf88584c5f/yarl-1.19.0.tar.gz", hash = "sha256:01e02bb80ae0dbed44273c304095295106e1d9470460e773268a27d11e594892", size = 184396 } sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/70/44ef8f69d61cb5123167a4dda87f6c739a833fbdb2ed52960b4e8409d65c/yarl-1.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b687c334da3ff8eab848c9620c47a253d005e78335e9ce0d6868ed7e8fd170b", size = 146855 }, { url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089 },
{ url = "https://files.pythonhosted.org/packages/c3/94/38c14d6c8217cc818647689f2dd647b976ced8fea08d0ac84e3c8168252b/yarl-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b0fe766febcf523a2930b819c87bb92407ae1368662c1bc267234e79b20ff894", size = 97523 }, { url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706 },
{ url = "https://files.pythonhosted.org/packages/35/a5/43a613586a6255105c4655a911c307ef3420e49e540d6ae2c5829863fb25/yarl-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:742ceffd3c7beeb2b20d47cdb92c513eef83c9ef88c46829f88d5b06be6734ee", size = 95540 }, { url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719 },
{ url = "https://files.pythonhosted.org/packages/d4/60/ed26049f4a8b06ebfa6d5f3cb6a51b152fd57081aa818b6497474f65a631/yarl-1.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2af682a1e97437382ee0791eacbf540318bd487a942e068e7e0a6c571fadbbd3", size = 344386 }, { url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972 },
{ url = "https://files.pythonhosted.org/packages/49/a6/b84899cab411f49af5986cfb44b514040788d81c8084f5811e6a7c0f1ce6/yarl-1.19.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:63702f1a098d0eaaea755e9c9d63172be1acb9e2d4aeb28b187092bcc9ca2d17", size = 338889 }, { url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639 },
{ url = "https://files.pythonhosted.org/packages/cc/ce/0704f7166a781b1f81bdd45c4f49eadbae0230ebd35b9ec7cd7769d3a6ff/yarl-1.19.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3560dcba3c71ae7382975dc1e912ee76e50b4cd7c34b454ed620d55464f11876", size = 353107 }, { url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745 },
{ url = "https://files.pythonhosted.org/packages/75/e5/0ecd6f2a9cc4264c16d8dfb0d3d71ba8d03cb58f3bcd42b1df4358331189/yarl-1.19.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68972df6a0cc47c8abaf77525a76ee5c5f6ea9bbdb79b9565b3234ded3c5e675", size = 353128 }, { url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178 },
{ url = "https://files.pythonhosted.org/packages/ad/c7/cd0fd1de581f1c2e8f996e704c9fd979e00106f18eebd91b0173cf1a13c6/yarl-1.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5684e7ff93ea74e47542232bd132f608df4d449f8968fde6b05aaf9e08a140f9", size = 349107 }, { url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219 },
{ url = "https://files.pythonhosted.org/packages/e6/34/ba3e5a20bd1d6a09034fc7985aaf1309976f2a7a5aefd093c9e56f6e1e0c/yarl-1.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8182ad422bfacdebd4759ce3adc6055c0c79d4740aea1104e05652a81cd868c6", size = 335144 }, { url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266 },
{ url = "https://files.pythonhosted.org/packages/1e/98/d9b7beb932fade015906efe0980aa7d522b8f93cf5ebf1082e74faa314b7/yarl-1.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aee5b90a5a9b71ac57400a7bdd0feaa27c51e8f961decc8d412e720a004a1791", size = 360795 }, { url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873 },
{ url = "https://files.pythonhosted.org/packages/9a/11/70b8770039cc54af5948970591517a1e1d093df3f04f328c655c9a0fefb7/yarl-1.19.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8c0b2371858d5a814b08542d5d548adb03ff2d7ab32f23160e54e92250961a72", size = 360140 }, { url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524 },
{ url = "https://files.pythonhosted.org/packages/d4/67/708e3e36fafc4d9d96b4eecc6c8b9f37c8ad50df8a16c7a1d5ba9df53050/yarl-1.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd430c2b7df4ae92498da09e9b12cad5bdbb140d22d138f9e507de1aa3edfea3", size = 364431 }, { url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370 },
{ url = "https://files.pythonhosted.org/packages/c3/8b/937fbbcc895553a7e16fcd86ae4e0724c6ac9468237ad8e7c29cc3b1c9d9/yarl-1.19.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a93208282c0ccdf73065fd76c6c129bd428dba5ff65d338ae7d2ab27169861a0", size = 373832 }, { url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297 },
{ url = "https://files.pythonhosted.org/packages/f8/ca/288ddc2230c9b6647fe907504f1119adb41252ac533eb564d3fc73511215/yarl-1.19.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:b8179280cdeb4c36eb18d6534a328f9d40da60d2b96ac4a295c5f93e2799e9d9", size = 378122 }, { url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771 },
{ url = "https://files.pythonhosted.org/packages/4f/5a/79e1ef31d14968fbfc0ecec70a6683b574890d9c7550c376dd6d40de7754/yarl-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eda3c2b42dc0c389b7cfda2c4df81c12eeb552019e0de28bde8f913fc3d1fcf3", size = 375178 }, { url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000 },
{ url = "https://files.pythonhosted.org/packages/95/38/9b0e56bf14026c3f550ad6425679f6d1a2f4821d70767f39d6f4c56a0820/yarl-1.19.0-cp312-cp312-win32.whl", hash = "sha256:57f3fed859af367b9ca316ecc05ce79ce327d6466342734305aa5cc380e4d8be", size = 86172 }, { url = "https://files.pythonhosted.org/packages/3b/2a/dd7ed1aa23fea996834278d7ff178f215b24324ee527df53d45e34d21d28/yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", size = 86355 },
{ url = "https://files.pythonhosted.org/packages/b3/96/5c2f3987c4bb4e5cdebea3caf99a45946b13a9516f849c02222203d99860/yarl-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:5507c1f7dd3d41251b67eecba331c8b2157cfd324849879bebf74676ce76aff7", size = 92617 }, { url = "https://files.pythonhosted.org/packages/ca/c6/333fe0338305c0ac1c16d5aa7cc4841208d3252bbe62172e0051006b5445/yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", size = 92904 },
{ url = "https://files.pythonhosted.org/packages/a4/06/ae25a353e8f032322df6f30d6bb1fc329773ee48e1a80a2196ccb8d1206b/yarl-1.19.0-py3-none-any.whl", hash = "sha256:a727101eb27f66727576630d02985d8a065d09cd0b5fcbe38a5793f71b2a97ef", size = 45990 }, { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124 },
] ]
[[package]] [[package]]

View File

@ -1,23 +0,0 @@
{
"arrowParens": "always",
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 100,
"proseWrap": "preserve",
"quoteProps": "consistent",
"requirePragma": false,
"semi": true,
"singleQuote": false,
"tabWidth": 4,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false,
"plugins": ["@trivago/prettier-plugin-sort-imports"],
"importOrder": ["^(@?)lit(.*)$", "\\.css$", "^@goauthentik/api$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"importOrderParserPlugins": ["typescript", "jsx", "classProperties", "decorators-legacy"]
}

798
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
"@floating-ui/dom": "^1.6.11", "@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7", "@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0", "@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2025.2.4-1744640358", "@goauthentik/api": "^2025.2.4-1745325566",
"@lit-labs/ssr": "^3.2.2", "@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2", "@lit/localize": "^0.12.2",
@ -61,6 +61,9 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.11.1", "@eslint/js": "^9.11.1",
"@goauthentik/esbuild-plugin-live-reload": "^1.0.4",
"@goauthentik/prettier-config": "^1.0.4",
"@goauthentik/tsconfig": "^1.0.4",
"@hcaptcha/types": "^1.0.4", "@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0", "@lit/localize-tools": "^0.8.0",
"@rollup/plugin-replace": "^6.0.1", "@rollup/plugin-replace": "^6.0.1",
@ -72,7 +75,7 @@
"@storybook/manager-api": "^8.3.4", "@storybook/manager-api": "^8.3.4",
"@storybook/web-components": "^8.3.4", "@storybook/web-components": "^8.3.4",
"@storybook/web-components-vite": "^8.3.4", "@storybook/web-components-vite": "^8.3.4",
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chart.js": "^2.9.41", "@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15", "@types/codemirror": "^5.60.15",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
@ -95,7 +98,6 @@
"eslint": "^9.11.1", "eslint": "^9.11.1",
"eslint-plugin-lit": "^1.15.0", "eslint-plugin-lit": "^1.15.0",
"eslint-plugin-wc": "^2.1.1", "eslint-plugin-wc": "^2.1.1",
"find-free-ports": "^3.1.1",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"glob": "^11.0.0", "glob": "^11.0.0",
"globals": "^15.10.0", "globals": "^15.10.0",
@ -136,6 +138,7 @@
"axios": "^1.8.4" "axios": "^1.8.4"
} }
}, },
"prettier": "@goauthentik/prettier-config",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "wireit", "build": "wireit",
@ -271,7 +274,7 @@
"command": "tsc --noEmit -p ./tests" "command": "tsc --noEmit -p ./tests"
}, },
"lint:types": { "lint:types": {
"command": "tsc --noEmit -p .", "command": "NODE_OPTIONS=\"--max-old-space-size=3000\" tsc -b .",
"dependencies": [ "dependencies": [
"build-locales", "build-locales",
"lint:types:tests" "lint:types:tests"

View File

@ -1,13 +1,18 @@
/// <reference types="./types.js" />
/** /**
* @file * @file Client-side observer for ESBuild events.
* Client-side observer for ESBuild events. *
* @import { Message as ESBuildMessage } from "esbuild";
*/ */
import type { Message as ESBuildMessage } from "esbuild";
const logPrefix = "👷 [ESBuild]"; const logPrefix = "👷 [ESBuild]";
const log = console.debug.bind(console, logPrefix); const log = console.debug.bind(console, logPrefix);
type BuildEventListener<Data = unknown> = (event: MessageEvent<Data>) => void; /**
* @template {unknown} [Data=unknown]
* @typedef {(event: MessageEvent) => void} BuildEventListener
*/
/** /**
* A client-side watcher for ESBuild. * A client-side watcher for ESBuild.
@ -16,14 +21,13 @@ type BuildEventListener<Data = unknown> = (event: MessageEvent<Data>) => void;
* ESBuild may tree-shake it out of production builds. * ESBuild may tree-shake it out of production builds.
* *
* ```ts * ```ts
* if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) { * if (process.env.NODE_ENV === "development") {
* const { ESBuildObserver } = await import("@goauthentik/common/client"); * await import("@goauthentik/esbuild-plugin-live-reload/client")
* * .catch(() => console.warn("Failed to import watcher"))
* new ESBuildObserver(process.env.WATCHER_URL);
* } * }
* ``` * ```
} *
* @implements {Disposable}
*/ */
export class ESBuildObserver extends EventSource { export class ESBuildObserver extends EventSource {
/** /**
@ -58,15 +62,19 @@ export class ESBuildObserver extends EventSource {
/** /**
* The interval for the keep-alive check. * The interval for the keep-alive check.
* @type {ReturnType<typeof setInterval> | undefined}
*/ */
#keepAliveInterval: ReturnType<typeof setInterval> | undefined; #keepAliveInterval;
#trackActivity = () => { #trackActivity = () => {
this.lastUpdatedAt = Date.now(); this.lastUpdatedAt = Date.now();
this.alive = true; this.alive = true;
}; };
#startListener: BuildEventListener = () => { /**
* @type {BuildEventListener}
*/
#startListener = () => {
this.#trackActivity(); this.#trackActivity();
log("⏰ Build started..."); log("⏰ Build started...");
}; };
@ -82,13 +90,18 @@ export class ESBuildObserver extends EventSource {
} }
}; };
#errorListener: BuildEventListener<string> = (event) => { /**
* @type {BuildEventListener<string>}
*/
#errorListener = (event) => {
this.#trackActivity(); this.#trackActivity();
// eslint-disable-next-line no-console
console.group(logPrefix, "⛔️⛔️⛔️ Build error..."); console.group(logPrefix, "⛔️⛔️⛔️ Build error...");
const esbuildErrorMessages: ESBuildMessage[] = JSON.parse(event.data); /**
* @type {ESBuildMessage[]}
*/
const esbuildErrorMessages = JSON.parse(event.data);
for (const error of esbuildErrorMessages) { for (const error of esbuildErrorMessages) {
console.warn(error.text); console.warn(error.text);
@ -101,11 +114,13 @@ export class ESBuildObserver extends EventSource {
} }
} }
// eslint-disable-next-line no-console
console.groupEnd(); console.groupEnd();
}; };
#endListener: BuildEventListener = () => { /**
* @type {BuildEventListener}
*/
#endListener = () => {
cancelAnimationFrame(this.#reloadFrameID); cancelAnimationFrame(this.#reloadFrameID);
this.#trackActivity(); this.#trackActivity();
@ -126,12 +141,36 @@ export class ESBuildObserver extends EventSource {
}); });
}; };
#keepAliveListener: BuildEventListener = () => { /**
* @type {BuildEventListener}
*/
#keepAliveListener = () => {
this.#trackActivity(); this.#trackActivity();
log("🏓 Keep-alive"); log("🏓 Keep-alive");
}; };
constructor(url: string | URL) { /**
* Initialize the ESBuild observer.
* This should be called once in your application.
*
* @param {string | URL} [url]
* @returns {ESBuildObserver}
*/
static initialize = (url) => {
const esbuildObserver = new ESBuildObserver(url);
return esbuildObserver;
};
/**
*
* @param {string | URL} [url]
*/
constructor(url) {
if (!url) {
throw new TypeError("ESBuildObserver: Cannot construct without a URL");
}
super(url); super(url);
this.addEventListener("esbuild:start", this.#startListener); this.addEventListener("esbuild:start", this.#startListener);
@ -167,4 +206,14 @@ export class ESBuildObserver extends EventSource {
log("👋 Waiting for build to start..."); log("👋 Waiting for build to start...");
}, 15_000); }, 15_000);
} }
[Symbol.dispose]() {
return this.close();
}
dispose() {
return this[Symbol.dispose]();
}
} }
export default ESBuildObserver;

View File

@ -0,0 +1,13 @@
/// <reference types="./types.js" />
/**
* @file Entry point for the ESBuild client-side observer.
*/
import { ESBuildObserver } from "./ESBuildObserver.js";
if (import.meta.env?.ESBUILD_WATCHER_URL) {
const buildObserver = new ESBuildObserver(import.meta.env.ESBUILD_WATCHER_URL);
window.addEventListener("beforeunload", () => {
buildObserver.dispose();
});
}

View File

@ -0,0 +1,18 @@
/**
* @file Import meta environment variables available via ESBuild.
*/
export {};
declare global {
interface ImportMeta {
readonly env: {
/**
* The injected watcher URL for ESBuild.
* This is used for live reloading in development mode.
*
* @format url
*/
ESBUILD_WATCHER_URL: string;
};
}
}

View File

@ -0,0 +1,2 @@
export * from "./client/index.js";
export * from "./plugin/index.js";

View File

@ -0,0 +1,53 @@
{
"name": "@goauthentik/esbuild-plugin-live-reload",
"description": "ESBuild plugin to watch for file changes and trigger client-side reloads.",
"version": "1.0.4",
"dependencies": {
"find-free-ports": "^3.1.1"
},
"devDependencies": {
"@goauthentik/prettier-config": "^1.0.4",
"@goauthentik/tsconfig": "^1.0.4",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/node": "^22.14.1",
"esbuild": "^0.25.0",
"prettier": "^3.3.3",
"typescript": "^5.6.2"
},
"engines": {
"node": ">=20.11"
},
"exports": {
"./package.json": "./package.json",
".": {
"types": "./out/index.d.ts",
"import": "./index.js"
},
"./client": {
"types": "./out/client/index.d.ts",
"import": "./client/index.js"
},
"./plugin": {
"types": "./out/plugin/index.d.ts",
"import": "./plugin/index.js"
}
},
"files": [
"./index.js",
"client/**/*",
"plugin/**/*",
"out/**/*"
],
"license": "MIT",
"main": "index.js",
"peerDependencies": {
"esbuild": "^0.25.0"
},
"prettier": "@goauthentik/prettier-config",
"private": true,
"publishConfig": {
"access": "public"
},
"type": "module",
"types": "./out/index.d.ts"
}

View File

@ -0,0 +1,243 @@
/**
* @file Live reload plugin for ESBuild.
*
* @import { ListenOptions } from "node:net";
* @import {Server as HTTPServer} from "node:http";
* @import {Server as HTTPSServer} from "node:https";
*/
import { findFreePorts } from "find-free-ports";
import * as http from "node:http";
import * as path from "node:path";
/**
* Serializes a custom event to a text stream.
* @param {Event} event
* @returns {string}
*/
export function serializeCustomEventToStream(event) {
// @ts-expect-error - TS doesn't know about the detail property
const data = event.detail ?? {};
const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`];
return eventContent.join("\n") + "\n\n";
}
const MIN_PORT = 1025;
const MAX_PORT = 65535;
/**
* Find a random port that is not in use, sufficiently far from the default port.
* @returns {Promise<number>}
*/
async function findDisparatePort() {
const startPort = Math.floor(Math.random() * (MAX_PORT - MIN_PORT + 1)) + MIN_PORT;
const wathcherPorts = await findFreePorts(1, {
startPort,
});
const [port] = wathcherPorts;
if (!port) {
throw new Error("No free ports available");
}
return port;
}
/**
* Event server initialization options.
*
* @typedef {Object} EventServerInit
*
* @property {string} pathname
* @property {EventTarget} dispatcher
* @property {string} [logPrefix]
*/
/**
* @typedef {(req: http.IncomingMessage, res: http.ServerResponse) => void} RequestHandler
*/
/**
* Create an event request handler.
* @param {EventServerInit} options
* @returns {RequestHandler}
* @category ESBuild
*/
export function createRequestHandler({ pathname, dispatcher, logPrefix = "Build Observer" }) {
// eslint-disable-next-line no-console
const log = console.log.bind(console, `[${logPrefix}]`);
/**
* @type {RequestHandler}
*/
const requestHandler = (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.url !== pathname) {
log(`🚫 Invalid request to ${req.url}`);
res.writeHead(404);
res.end();
return;
}
log("🔌 Client connected");
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
/**
* @param {Event} event
*/
const listener = (event) => {
const body = serializeCustomEventToStream(event);
res.write(body);
};
dispatcher.addEventListener("esbuild:start", listener);
dispatcher.addEventListener("esbuild:error", listener);
dispatcher.addEventListener("esbuild:end", listener);
req.on("close", () => {
log("🔌 Client disconnected");
clearInterval(keepAliveInterval);
dispatcher.removeEventListener("esbuild:start", listener);
dispatcher.removeEventListener("esbuild:error", listener);
dispatcher.removeEventListener("esbuild:end", listener);
});
const keepAliveInterval = setInterval(() => {
console.timeStamp("🏓 Keep-alive");
res.write("event: keep-alive\n\n");
res.write(serializeCustomEventToStream(new CustomEvent("esbuild:keep-alive")));
}, 15_000);
};
return requestHandler;
}
/**
* Options for the build observer plugin.
*
* @typedef {object} BuildObserverOptions
*
* @property {HTTPServer | HTTPSServer} [server]
* @property {ListenOptions} [listenOptions]
* @property {string | URL} [publicURL]
* @property {string} [logPrefix]
* @property {string} [relativeRoot]
*/
/**
* Creates a plugin that listens for build events and sends them to a server-sent event stream.
*
* @param {BuildObserverOptions} [options]
* @returns {import('esbuild').Plugin}
*/
export function liveReloadPlugin(options = {}) {
return {
name: "build-watcher",
setup: async (build) => {
const logPrefix = options.logPrefix || "Build Observer";
const timerLabel = `[${logPrefix}] 🏁`;
const relativeRoot = options.relativeRoot || process.cwd();
const dispatcher = new EventTarget();
/**
* @type {URL}
*/
let publicURL;
if (!options.publicURL) {
const port = await findDisparatePort();
publicURL = new URL(`http://localhost:${port}/events`);
} else {
publicURL =
typeof options.publicURL === "string"
? new URL(options.publicURL)
: options.publicURL;
}
build.initialOptions.define = {
...build.initialOptions.define,
"import.meta.env.ESBUILD_WATCHER_URL": JSON.stringify(publicURL.href),
};
const requestHandler = createRequestHandler({
pathname: publicURL.pathname,
dispatcher,
logPrefix,
});
const server = options.server || http.createServer(requestHandler);
const listenOptions = options.listenOptions || {
port: parseInt(publicURL.port, 10),
host: publicURL.hostname,
};
server.listen(listenOptions, () => {
// eslint-disable-next-line no-console
console.log(`[${logPrefix}] Listening`);
});
build.onDispose(() => {
server?.close();
});
build.onStart(() => {
console.time(timerLabel);
dispatcher.dispatchEvent(
new CustomEvent("esbuild:start", {
detail: new Date().toISOString(),
}),
);
});
build.onEnd((buildResult) => {
console.timeEnd(timerLabel);
if (!buildResult.errors.length) {
dispatcher.dispatchEvent(
new CustomEvent("esbuild:end", {
detail: new Date().toISOString(),
}),
);
return;
}
console.warn(`Build ended with ${buildResult.errors.length} errors`);
dispatcher.dispatchEvent(
new CustomEvent("esbuild:error", {
detail: buildResult.errors.map((error) => ({
...error,
location: error.location
? {
...error.location,
file: path.resolve(relativeRoot, error.location.file),
}
: null,
})),
}),
);
});
},
};
}

View File

@ -0,0 +1,10 @@
{
"extends": "@goauthentik/tsconfig",
"compilerOptions": {
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"resolveJsonModule": true,
"baseUrl": ".",
"checkJs": true,
"emitDeclarationOnly": true
}
}

View File

@ -1,8 +1,13 @@
/**
* @file ESBuild script for building the authentik web UI.
*
* @import { BuildOptions } from "esbuild";
*/
import { liveReloadPlugin } from "@goauthentik/esbuild-plugin-live-reload/plugin";
import { execFileSync } from "child_process"; import { execFileSync } from "child_process";
import { deepmerge } from "deepmerge-ts"; import { deepmerge } from "deepmerge-ts";
import esbuild from "esbuild"; import esbuild from "esbuild";
import { polyfillNode } from "esbuild-plugin-polyfill-node"; import { polyfillNode } from "esbuild-plugin-polyfill-node";
import findFreePorts from "find-free-ports";
import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs"; import { copyFileSync, mkdirSync, readFileSync, statSync } from "fs";
import { globSync } from "glob"; import { globSync } from "glob";
import * as path from "path"; import * as path from "path";
@ -11,7 +16,6 @@ import process from "process";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs"; import { mdxPlugin } from "./esbuild/build-mdx-plugin.mjs";
import { buildObserverPlugin } from "./esbuild/build-observer-plugin.mjs";
const __dirname = fileURLToPath(new URL(".", import.meta.url)); const __dirname = fileURLToPath(new URL(".", import.meta.url));
let authentikProjectRoot = path.join(__dirname, "..", ".."); let authentikProjectRoot = path.join(__dirname, "..", "..");
@ -120,7 +124,7 @@ const BASE_ESBUILD_OPTIONS = {
splitting: true, splitting: true,
treeShaking: true, treeShaking: true,
external: ["*.woff", "*.woff2"], external: ["*.woff", "*.woff2"],
tsconfig: "./tsconfig.json", tsconfig: path.resolve(__dirname, "..", "tsconfig.build.json"),
loader: { loader: {
".css": "text", ".css": "text",
}, },
@ -220,26 +224,17 @@ function doHelp() {
async function doWatch() { async function doWatch() {
console.log("Watching all entry points..."); console.log("Watching all entry points...");
const wathcherPorts = await findFreePorts(entryPoints.length);
const buildContexts = await Promise.all( const buildContexts = await Promise.all(
entryPoints.map((entryPoint, i) => { entryPoints.map((entryPoint) => {
const port = wathcherPorts[i];
const serverURL = new URL(`http://localhost:${port}/events`);
return esbuild.context( return esbuild.context(
createEntryPointOptions(entryPoint, { createEntryPointOptions(entryPoint, {
define: definitions,
plugins: [ plugins: [
buildObserverPlugin({ liveReloadPlugin({
serverURL, logPrefix: `Build Observer (${entryPoint[1]})`,
logPrefix: entryPoint[1],
relativeRoot: path.join(__dirname, ".."), relativeRoot: path.join(__dirname, ".."),
}), }),
], ],
define: {
...definitions,
"process.env.WATCHER_URL": JSON.stringify(serverURL.toString()),
},
}), }),
); );
}), }),

View File

@ -1,141 +0,0 @@
import * as http from "http";
import path from "path";
/**
* Serializes a custom event to a text stream.
* a
* @param {Event} event
* @returns {string}
*/
export function serializeCustomEventToStream(event) {
// @ts-expect-error - TS doesn't know about the detail property
const data = event.detail ?? {};
const eventContent = [`event: ${event.type}`, `data: ${JSON.stringify(data)}`];
return eventContent.join("\n") + "\n\n";
}
/**
* Options for the build observer plugin.
*
* @typedef {Object} BuildObserverOptions
*
* @property {URL} serverURL
* @property {string} logPrefix
* @property {string} relativeRoot
*/
/**
* Creates a plugin that listens for build events and sends them to a server-sent event stream.
*
* @param {BuildObserverOptions} options
* @returns {import('esbuild').Plugin}
*/
export function buildObserverPlugin({ serverURL, logPrefix, relativeRoot }) {
const timerLabel = `[${logPrefix}] Build`;
const endpoint = serverURL.pathname;
const dispatcher = new EventTarget();
const eventServer = http.createServer((req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
if (req.url !== endpoint) {
console.log(`🚫 Invalid request to ${req.url}`);
res.writeHead(404);
res.end();
return;
}
console.log("🔌 Client connected");
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
/**
* @param {Event} event
*/
const listener = (event) => {
const body = serializeCustomEventToStream(event);
res.write(body);
};
dispatcher.addEventListener("esbuild:start", listener);
dispatcher.addEventListener("esbuild:error", listener);
dispatcher.addEventListener("esbuild:end", listener);
req.on("close", () => {
console.log("🔌 Client disconnected");
clearInterval(keepAliveInterval);
dispatcher.removeEventListener("esbuild:start", listener);
dispatcher.removeEventListener("esbuild:error", listener);
dispatcher.removeEventListener("esbuild:end", listener);
});
const keepAliveInterval = setInterval(() => {
console.timeStamp("🏓 Keep-alive");
res.write("event: keep-alive\n\n");
res.write(serializeCustomEventToStream(new CustomEvent("esbuild:keep-alive")));
}, 15_000);
});
return {
name: "build-watcher",
setup: (build) => {
eventServer.listen(parseInt(serverURL.port, 10), serverURL.hostname);
build.onDispose(() => {
eventServer.close();
});
build.onStart(() => {
console.time(timerLabel);
dispatcher.dispatchEvent(
new CustomEvent("esbuild:start", {
detail: new Date().toISOString(),
}),
);
});
build.onEnd((buildResult) => {
console.timeEnd(timerLabel);
if (!buildResult.errors.length) {
dispatcher.dispatchEvent(
new CustomEvent("esbuild:end", {
detail: new Date().toISOString(),
}),
);
return;
}
console.warn(`Build ended with ${buildResult.errors.length} errors`);
dispatcher.dispatchEvent(
new CustomEvent("esbuild:error", {
detail: buildResult.errors.map((error) => ({
...error,
location: error.location
? {
...error.location,
file: path.resolve(relativeRoot, error.location.file),
}
: null,
})),
}),
);
});
},
};
}

View File

@ -34,6 +34,10 @@ import { SessionUser, UiThemeEnum } from "@goauthentik/api";
import "./AdminSidebar"; import "./AdminSidebar";
if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client");
}
@customElement("ak-interface-admin") @customElement("ak-interface-admin")
export class AdminInterface extends AuthenticatedInterface { export class AdminInterface extends AuthenticatedInterface {
@property({ type: Boolean }) @property({ type: Boolean })
@ -119,16 +123,6 @@ export class AdminInterface extends AuthenticatedInterface {
} }
} }
async connectedCallback(): Promise<void> {
super.connectedCallback();
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
const { ESBuildObserver } = await import("@goauthentik/common/client");
new ESBuildObserver(process.env.WATCHER_URL);
}
}
render(): TemplateResult { render(): TemplateResult {
const sidebarClasses = { const sidebarClasses = {
"pf-m-light": this.activeTheme === UiThemeEnum.Light, "pf-m-light": this.activeTheme === UiThemeEnum.Light,

View File

@ -52,7 +52,6 @@ function renderRadiusOverview(rawProvider: OneOfProvider) {
} }
function renderRACOverview(rawProvider: OneOfProvider) { function renderRACOverview(rawProvider: OneOfProvider) {
// @ts-expect-error TS6133
const _provider = rawProvider as RACProvider; const _provider = rawProvider as RACProvider;
} }

View File

@ -6,6 +6,7 @@ import "@goauthentik/admin/policies/expiry/ExpiryPolicyForm";
import "@goauthentik/admin/policies/expression/ExpressionPolicyForm"; import "@goauthentik/admin/policies/expression/ExpressionPolicyForm";
import "@goauthentik/admin/policies/password/PasswordPolicyForm"; import "@goauthentik/admin/policies/password/PasswordPolicyForm";
import "@goauthentik/admin/policies/reputation/ReputationPolicyForm"; import "@goauthentik/admin/policies/reputation/ReputationPolicyForm";
import "@goauthentik/admin/policies/unique_password/UniquePasswordPolicyForm";
import "@goauthentik/admin/rbac/ObjectPermissionModal"; import "@goauthentik/admin/rbac/ObjectPermissionModal";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFColor } from "@goauthentik/elements/Label"; import { PFColor } from "@goauthentik/elements/Label";

View File

@ -6,6 +6,7 @@ import "@goauthentik/admin/policies/expression/ExpressionPolicyForm";
import "@goauthentik/admin/policies/geoip/GeoIPPolicyForm"; import "@goauthentik/admin/policies/geoip/GeoIPPolicyForm";
import "@goauthentik/admin/policies/password/PasswordPolicyForm"; import "@goauthentik/admin/policies/password/PasswordPolicyForm";
import "@goauthentik/admin/policies/reputation/ReputationPolicyForm"; import "@goauthentik/admin/policies/reputation/ReputationPolicyForm";
import "@goauthentik/admin/policies/unique_password/UniquePasswordPolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/forms/ProxyForm"; import "@goauthentik/elements/forms/ProxyForm";

View File

@ -0,0 +1,103 @@
import { BasePolicyForm } from "@goauthentik/admin/policies/BasePolicyForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { PoliciesApi, UniquePasswordPolicy } from "@goauthentik/api";
@customElement("ak-policy-password-uniqueness-form")
export class UniquePasswordPolicyForm extends BasePolicyForm<UniquePasswordPolicy> {
async loadInstance(pk: string): Promise<UniquePasswordPolicy> {
return new PoliciesApi(DEFAULT_CONFIG).policiesUniquePasswordRetrieve({
policyUuid: pk,
});
}
async send(data: UniquePasswordPolicy): Promise<UniquePasswordPolicy> {
if (this.instance) {
return new PoliciesApi(DEFAULT_CONFIG).policiesUniquePasswordUpdate({
policyUuid: this.instance.pk || "",
uniquePasswordPolicyRequest: data,
});
} else {
return new PoliciesApi(DEFAULT_CONFIG).policiesUniquePasswordCreate({
uniquePasswordPolicyRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <span>
${msg(
"Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.",
)}
</span>
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name || "")}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="executionLogging">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.executionLogging, false)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Execution logging")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Password field")}
?required=${true}
name="passwordField"
>
<input
type="text"
value="${ifDefined(this.instance?.passwordField || "password")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Field key to check, field keys defined in Prompt stages are available.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Number of previous passwords to check")}
?required=${true}
name="numHistoricalPasswords"
>
<input
type="number"
value="${first(this.instance?.numHistoricalPasswords, 1)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-policy-password-uniqueness-form": UniquePasswordPolicyForm;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -4,8 +4,12 @@ import {
EventMiddleware, EventMiddleware,
LoggingMiddleware, LoggingMiddleware,
} from "@goauthentik/common/api/middleware"; } from "@goauthentik/common/api/middleware";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants"; import { VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import {
EVENT_LOCALE_REQUEST,
LocaleContextEventDetail,
} from "@goauthentik/elements/ak-locale-context/events.js";
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api"; import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
@ -44,7 +48,7 @@ export function brandSetLocale(brand: CurrentBrand) {
} }
console.debug("authentik/locale: setting locale from brand default"); console.debug("authentik/locale: setting locale from brand default");
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EVENT_LOCALE_REQUEST, { new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
composed: true, composed: true,
bubbles: true, bubbles: true,
detail: { locale: brand.defaultLocale }, detail: { locale: brand.defaultLocale },

View File

@ -14,8 +14,6 @@ export const EVENT_FLOW_INSPECTOR_TOGGLE = "ak-flow-inspector-toggle";
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle"; export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
export const EVENT_WS_MESSAGE = "ak-ws-message"; export const EVENT_WS_MESSAGE = "ak-ws-message";
export const EVENT_FLOW_ADVANCE = "ak-flow-advance"; export const EVENT_FLOW_ADVANCE = "ak-flow-advance";
export const EVENT_LOCALE_CHANGE = "ak-locale-change";
export const EVENT_LOCALE_REQUEST = "ak-locale-request";
export const EVENT_REQUEST_POST = "ak-request-post"; export const EVENT_REQUEST_POST = "ak-request-post";
export const EVENT_MESSAGE = "ak-message"; export const EVENT_MESSAGE = "ak-message";
export const EVENT_THEME_CHANGE = "ak-theme-change"; export const EVENT_THEME_CHANGE = "ak-theme-change";

View File

@ -1,3 +1,4 @@
import type { Config as DOMPurifyConfig } from "dompurify";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { render } from "@lit-labs/ssr"; import { render } from "@lit-labs/ssr";
@ -6,9 +7,9 @@ import { TemplateResult, html } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { until } from "lit/directives/until.js"; import { until } from "lit/directives/until.js";
export const DOM_PURIFY_STRICT: DOMPurify.Config = { export const DOM_PURIFY_STRICT = {
ALLOWED_TAGS: ["#text"], ALLOWED_TAGS: ["#text"],
}; } as const satisfies DOMPurifyConfig;
export async function renderStatic(input: TemplateResult): Promise<string> { export async function renderStatic(input: TemplateResult): Promise<string> {
return await collectResult(render(input)); return await collectResult(render(input));

View File

@ -19,19 +19,27 @@
--ak-sidebar--minimum-auto-width: 80rem; --ak-sidebar--minimum-auto-width: 80rem;
} }
::-webkit-scrollbar { @supports selector(::-webkit-scrollbar) {
width: 5px; ::-webkit-scrollbar {
height: 5px; width: 5px;
background-color: transparent; height: 5px;
background-color: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--ak-accent);
}
::-webkit-scrollbar-track {
background-color: transparent;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}
} }
::-webkit-scrollbar-thumb {
background-color: var(--ak-accent); @supports not selector(::-webkit-scrollbar) {
} :root {
::-webkit-scrollbar-track { scrollbar-color: var(--ak-accent) transparent;
background-color: transparent; }
}
::-webkit-scrollbar-corner {
background-color: transparent;
} }
html { html {

View File

@ -1,6 +1,9 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { isResponseErrorLike } from "@goauthentik/common/errors/network"; import { isResponseErrorLike } from "@goauthentik/common/errors/network";
import {
EVENT_LOCALE_REQUEST,
LocaleContextEventDetail,
} from "@goauthentik/elements/ak-locale-context/events.js";
import { CoreApi, SessionUser } from "@goauthentik/api"; import { CoreApi, SessionUser } from "@goauthentik/api";
@ -57,7 +60,7 @@ export async function me(): Promise<SessionUser> {
console.debug(`authentik/locale: Activating user's configured locale '${locale}'`); console.debug(`authentik/locale: Activating user's configured locale '${locale}'`);
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(EVENT_LOCALE_REQUEST, { new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
composed: true, composed: true,
bubbles: true, bubbles: true,
detail: { locale }, detail: { locale },

View File

@ -1,7 +1,7 @@
import { authentikVersionContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikVersionContext } from "@goauthentik/elements/AuthentikContexts";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import { Constructor } from "@lit/reactive-element/decorators/base"; import { Constructor } from "@lit/reactive-element/decorators/base.js";
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { Version } from "@goauthentik/api"; import type { Version } from "@goauthentik/api";

View File

@ -2,7 +2,7 @@ import { AkControlElement } from "@goauthentik/elements/AkControlElement";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter"; import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { PropertyValues } from "@lit/reactive-element/reactive-element"; import { PropertyValues } from "@lit/reactive-element";
import { TemplateResult, css, html } from "lit"; import { TemplateResult, css, html } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators.js"; import { customElement, property, queryAll, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js"; import { map } from "lit/directives/map.js";

View File

@ -1,11 +1,9 @@
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { customEvent } from "@goauthentik/elements/utils/customEvents";
import { localized, msg } from "@lit/localize"; import { localized, msg } from "@lit/localize";
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import "./ak-locale-context"; import "./ak-locale-context";
import { EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js";
export default { export default {
title: "Elements / Shell / Locale Context", title: "Elements / Shell / Locale Context",
@ -37,10 +35,18 @@ export const InFrench = () =>
</div>`; </div>`;
export const SwitchingBackAndForth = () => { export const SwitchingBackAndForth = () => {
let lang = "en"; let languageCode = "en";
window.setInterval(() => { window.setInterval(() => {
lang = lang === "en" ? "fr" : "en"; languageCode = languageCode === "en" ? "fr" : "en";
window.dispatchEvent(customEvent(EVENT_LOCALE_REQUEST, { locale: lang }));
window.dispatchEvent(
new CustomEvent<LocaleContextEventDetail>(EVENT_LOCALE_REQUEST, {
composed: true,
bubbles: true,
detail: { locale: languageCode },
}),
);
}, 1000); }, 1000);
return html`<div style="background: #fff; padding: 4em"> return html`<div style="background: #fff; padding: 4em">

View File

@ -1,19 +1,18 @@
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { customEvent } from "@goauthentik/elements/utils/customEvents";
import { html } from "lit"; import { html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { WithBrandConfig } from "../Interface/brandProvider"; import { WithBrandConfig } from "../Interface/brandProvider";
import { initializeLocalization } from "./configureLocale"; import { initializeLocalization } from "./configureLocale.js";
import type { LocaleGetter, LocaleSetter } from "./configureLocale"; import type { GetLocale, SetLocale } from "./configureLocale.js";
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers"; import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST, LocaleContextEventDetail } from "./events.js";
import { DEFAULT_LOCALE, autoDetectLanguage, findLocaleDefinition } from "./helpers.js";
/** /**
* A component to manage your locale settings. * A component to manage your locale settings.
* *
* ## Details * @remarks
* *
* This component exists to take a locale setting from several different places, find the * This component exists to take a locale setting from several different places, find the
* appropriate locale file in our catalog of locales, and set the lit-localization context * appropriate locale file in our catalog of locales, and set the lit-localization context
@ -25,70 +24,98 @@ import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helper
*/ */
@customElement("ak-locale-context") @customElement("ak-locale-context")
export class LocaleContext extends WithBrandConfig(AKElement) { export class LocaleContext extends WithBrandConfig(AKElement) {
/// @attribute The text representation of the current locale */ protected static singleton: LocaleContext | null = null;
/**
* The text representation of the current locale
* @attribute
*/
@property({ attribute: true, type: String }) @property({ attribute: true, type: String })
locale = DEFAULT_LOCALE; public locale = DEFAULT_LOCALE;
/// @attribute The URL parameter to look for (if any) /**
* The URL parameter to look for (if any)
* @attribute
*/
@property({ attribute: true, type: String }) @property({ attribute: true, type: String })
param = "locale"; public param = "locale";
getLocale: LocaleGetter; protected readonly getLocale: GetLocale;
protected readonly setLocale: SetLocale;
setLocale: LocaleSetter;
constructor(code = DEFAULT_LOCALE) { constructor(code = DEFAULT_LOCALE) {
super(); super();
this.notifyApplication = this.notifyApplication.bind(this);
this.updateLocaleHandler = this.updateLocaleHandler.bind(this); if (LocaleContext.singleton) {
try { throw new Error(`Developer error: Must have only one locale context per session`);
const [getLocale, setLocale] = initializeLocalization();
this.getLocale = getLocale;
this.setLocale = setLocale;
this.setLocale(code).then(() => {
window.setTimeout(this.notifyApplication, 0);
});
} catch (e) {
throw new Error(`Developer error: Must have only one locale context per session: ${e}`);
} }
LocaleContext.singleton = this;
const [getLocale, setLocale] = initializeLocalization();
this.getLocale = getLocale;
this.setLocale = setLocale;
this.setLocale(code).then(this.#notifyApplication);
} }
connectedCallback() { connectedCallback() {
super.connectedCallback(); this.#updateLocale();
this.updateLocale();
window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); window.addEventListener(EVENT_LOCALE_REQUEST, this.#localeUpdateListener as EventListener);
} }
disconnectedCallback() { disconnectedCallback() {
window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); LocaleContext.singleton = null;
window.removeEventListener(
EVENT_LOCALE_REQUEST,
this.#localeUpdateListener as EventListener,
);
super.disconnectedCallback(); super.disconnectedCallback();
} }
updateLocaleHandler(ev: CustomEvent<{ locale: string }>) { #localeUpdateListener = (ev: CustomEvent<LocaleContextEventDetail>) => {
console.debug("authentik/locale: Locale update request received."); console.debug("authentik/locale: Locale update request received.");
this.updateLocale(ev.detail.locale); this.#updateLocale(ev.detail.locale);
} };
#updateLocale(requestedLanguageCode?: string) {
const localeRequest = autoDetectLanguage(requestedLanguageCode, this.brand?.defaultLocale);
const locale = findLocaleDefinition(localeRequest);
updateLocale(requestedLocale: string | undefined = undefined) {
const localeRequest = autoDetectLanguage(requestedLocale, this.brand?.defaultLocale);
const locale = getBestMatchLocale(localeRequest);
if (!locale) { if (!locale) {
console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`); console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);
return; return;
} }
locale.locale().then(() => {
console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`); return locale.fetch().then(() => {
this.setLocale(locale.code).then(() => { console.debug(
window.setTimeout(this.notifyApplication, 0); `authentik/locale: Setting Locale to ${locale.formatLabel()} (${locale.languageCode})`,
}); );
this.setLocale(locale.languageCode).then(this.#notifyApplication);
}); });
} }
notifyApplication() { #notifyFrameID = -1;
// You will almost never have cause to catch this event. Lit's own `@localized()` decorator
// works just fine for almost every use case. #notifyApplication = () => {
this.dispatchEvent(customEvent(EVENT_LOCALE_CHANGE)); cancelAnimationFrame(this.#notifyFrameID);
}
requestAnimationFrame(() => {
// You will almost never have cause to catch this event.
// Lit's own `@localized()` decorator works just fine for almost every use case.
this.dispatchEvent(
new CustomEvent(EVENT_LOCALE_CHANGE, {
bubbles: true,
composed: true,
}),
);
});
};
render() { render() {
return html`<slot></slot>`; return html`<slot></slot>`;

View File

@ -1,39 +1,44 @@
import { configureLocalization } from "@lit/localize"; import { configureLocalization } from "@lit/localize";
import { sourceLocale, targetLocales } from "../../locale-codes"; import { sourceLocale, targetLocales } from "../../locale-codes.js";
import { getBestMatchLocale } from "./helpers"; import { findLocaleDefinition } from "./helpers.js";
type LocaleGetter = ReturnType<typeof configureLocalization>["getLocale"]; export type ConfigureLocalizationResult = ReturnType<typeof configureLocalization>;
type LocaleSetter = ReturnType<typeof configureLocalization>["setLocale"];
// Internal use only. export type GetLocale = ConfigureLocalizationResult["getLocale"];
// export type SetLocale = ConfigureLocalizationResult["setLocale"];
// This is where the lit-localization module is initialized with our loader, which associates our
// collection of locales with its getter and setter functions.
let getLocale: LocaleGetter | undefined = undefined; export type LocaleState = [GetLocale, SetLocale];
let setLocale: LocaleSetter | undefined = undefined;
export function initializeLocalization(): [LocaleGetter, LocaleSetter] { let cachedLocaleState: LocaleState | undefined = undefined;
if (getLocale && setLocale) {
return [getLocale, setLocale];
}
({ getLocale, setLocale } = configureLocalization({ /**
* This is where the lit-localization module is initialized with our loader,
* which associates our collection of locales with its getter and setter functions.
*
* @returns A tuple of getter and setter functions.
* @internal
*/
export function initializeLocalization(): LocaleState {
if (cachedLocaleState) return cachedLocaleState;
const { getLocale, setLocale } = configureLocalization({
sourceLocale, sourceLocale,
targetLocales, targetLocales,
loadLocale: async (locale: string) => { loadLocale: (languageCode) => {
const localeDef = getBestMatchLocale(locale); const localeDef = findLocaleDefinition(languageCode);
if (!localeDef) {
console.warn(`Unrecognized locale: ${localeDef}`);
return Promise.reject("");
}
return localeDef.locale();
},
}));
return [getLocale, setLocale]; if (!localeDef) {
throw new Error(`Unrecognized locale: ${localeDef}`);
}
return localeDef.fetch();
},
});
cachedLocaleState = [getLocale, setLocale];
return cachedLocaleState;
} }
export default initializeLocalization; export default initializeLocalization;
export type { LocaleGetter, LocaleSetter };

View File

@ -1,15 +1,19 @@
import * as _enLocale from "@goauthentik/locales/en"; import * as EnglishLocaleModule from "@goauthentik/locales/en";
import type { LocaleModule } from "@lit/localize"; import type { LocaleModule } from "@lit/localize";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { AkLocale, LocaleRow } from "./types"; import { AKLocaleDefinition, LocaleRow } from "./types.js";
export const DEFAULT_FALLBACK = "en"; /**
* The default ISO 639-1 language code.
*/
export const DEFAULT_LANGUAGE_CODE = "en";
const enLocale: LocaleModule = _enLocale; /**
* The default English locale module.
export { enLocale }; */
export const DefaultLocaleModule: LocaleModule = EnglishLocaleModule;
// NOTE: This table cannot be made any shorter, despite all the repetition of syntax. Bundlers look // NOTE: This table cannot be made any shorter, despite all the repetition of syntax. Bundlers look
// for the `await import` string as a *string target* for doing alias substitution, so putting // for the `await import` string as a *string target* for doing alias substitution, so putting
@ -35,34 +39,44 @@ export { enLocale };
// - Text Label // - Text Label
// - Locale loader. // - Locale loader.
// prettier-ignore
const debug: LocaleRow = [ const debug: LocaleRow = [
"pseudo-LOCALE", /^pseudo/i, () => msg("Pseudolocale (for testing)"), async () => await import("@goauthentik/locales/pseudo-LOCALE"), "pseudo-LOCALE",
/^pseudo/i,
() => msg("Pseudolocale (for testing)"),
() => import("@goauthentik/locales/pseudo-LOCALE"),
]; ];
// prettier-ignore // prettier-ignore
const LOCALE_TABLE: LocaleRow[] = [ const LOCALE_TABLE: readonly LocaleRow[] = [
["de", /^de([_-]|$)/i, () => msg("German"), async () => await import("@goauthentik/locales/de")], // English loaded when the application is first instantiated.
["en", /^en([_-]|$)/i, () => msg("English"), async () => await import("@goauthentik/locales/en")], ["en", /^en([_-]|$)/i, () => msg("English"), () => Promise.resolve(DefaultLocaleModule)],
["es", /^es([_-]|$)/i, () => msg("Spanish"), async () => await import("@goauthentik/locales/es")], ["de", /^de([_-]|$)/i, () => msg("German"), () => import("@goauthentik/locales/de")],
["fr", /^fr([_-]|$)/i, () => msg("French"), async () => await import("@goauthentik/locales/fr")], ["es", /^es([_-]|$)/i, () => msg("Spanish"), () => import("@goauthentik/locales/es")],
["it", /^it([_-]|$)/i, () => msg("Italian"), async () => await import("@goauthentik/locales/it")], ["fr", /^fr([_-]|$)/i, () => msg("French"), () => import("@goauthentik/locales/fr")],
["ko", /^ko([_-]|$)/i, () => msg("Korean"), async () => await import("@goauthentik/locales/ko")], ["it", /^it([_-]|$)/i, () => msg("Italian"), () => import("@goauthentik/locales/it")],
["nl", /^nl([_-]|$)/i, () => msg("Dutch"), async () => await import("@goauthentik/locales/nl")], ["ko", /^ko([_-]|$)/i, () => msg("Korean"), () => import("@goauthentik/locales/ko")],
["pl", /^pl([_-]|$)/i, () => msg("Polish"), async () => await import("@goauthentik/locales/pl")], ["nl", /^nl([_-]|$)/i, () => msg("Dutch"), () => import("@goauthentik/locales/nl")],
["ru", /^ru([_-]|$)/i, () => msg("Russian"), async () => await import("@goauthentik/locales/ru")], ["pl", /^pl([_-]|$)/i, () => msg("Polish"), () => import("@goauthentik/locales/pl")],
["tr", /^tr([_-]|$)/i, () => msg("Turkish"), async () => await import("@goauthentik/locales/tr")], ["ru", /^ru([_-]|$)/i, () => msg("Russian"), () => import("@goauthentik/locales/ru")],
["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), async () => await import("@goauthentik/locales/zh_TW")], ["tr", /^tr([_-]|$)/i, () => msg("Turkish"), () => import("@goauthentik/locales/tr")],
["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), async () => await import("@goauthentik/locales/zh-Hans")], ["zh_TW", /^zh[_-]TW$/i, () => msg("Taiwanese Mandarin"), () => import("@goauthentik/locales/zh_TW")],
["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), async () => await import("@goauthentik/locales/zh-Hant")], ["zh-Hans", /^zh(\b|_)/i, () => msg("Chinese (simplified)"), () => import("@goauthentik/locales/zh-Hans")],
debug ["zh-Hant", /^zh[_-](HK|Hant)/i, () => msg("Chinese (traditional)"), () => import("@goauthentik/locales/zh-Hant")],
debug,
]; ];
export const LOCALES: AkLocale[] = LOCALE_TABLE.map(([code, match, label, locale]) => ({ /**
code, * Available locales, identified by their ISO 639-1 language code.
match, */
label, export const AKLocalDefinitions: readonly AKLocaleDefinition[] = LOCALE_TABLE.map(
locale, ([languageCode, pattern, formatLabel, fetch]) => {
})); return {
languageCode,
pattern,
formatLabel,
fetch,
};
},
);
export default LOCALES; export default AKLocalDefinitions;

View File

@ -0,0 +1,6 @@
export const EVENT_LOCALE_REQUEST = "ak-locale-request";
export const EVENT_LOCALE_CHANGE = "ak-locale-change";
export interface LocaleContextEventDetail {
locale: string;
}

View File

@ -1,59 +1,80 @@
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { LOCALES as RAW_LOCALES, enLocale } from "./definitions"; import { AKLocalDefinitions } from "./definitions.js";
import { AkLocale } from "./types"; import { AKLocaleDefinition } from "./types.js";
export const DEFAULT_LOCALE = "en"; export const DEFAULT_LOCALE = "en";
export const EVENT_REQUEST_LOCALE = "ak-request-locale"; export const EVENT_REQUEST_LOCALE = "ak-request-locale";
const TOMBSTONE = "⛼⛼tombstone⛼⛼"; /**
* Find the locale definition for a given language code.
*/
export function findLocaleDefinition(languageCode: string): AKLocaleDefinition | null {
for (const locale of AKLocalDefinitions) {
if (locale.pattern.test(languageCode)) {
return locale;
}
}
// NOTE: This is the definition of the LOCALES table that most of the code uses. The 'definitions' return null;
// file is relatively pure, but here we establish that we want the English locale to loaded when an
// application is first instantiated.
export const LOCALES = RAW_LOCALES.map((locale) =>
locale.code === "en" ? { ...locale, locale: async () => enLocale } : locale,
);
export function getBestMatchLocale(locale: string): AkLocale | undefined {
return LOCALES.find((l) => l.match.test(locale));
} }
// This looks weird, but it's sensible: we have several candidates, and we want to find the first // This looks weird, but it's sensible: we have several candidates, and we want to find the first
// one that has a supported locale. Then, from *that*, we have to extract that first supported // one that has a supported locale. Then, from *that*, we have to extract that first supported
// locale. // locale.
export function findSupportedLocale(candidates: string[]) { export function findSupportedLocale(candidates: string[]): AKLocaleDefinition | null {
const candidate = candidates.find((candidate: string) => getBestMatchLocale(candidate)); for (const candidate of candidates) {
return candidate ? getBestMatchLocale(candidate) : undefined; const locale = findLocaleDefinition(candidate);
if (locale) return locale;
}
return null;
} }
export function localeCodeFromUrl(param = "locale") { export function localeCodeFromURL(param = "locale") {
const url = new URL(window.location.href); const searchParams = new URLSearchParams(window.location.search);
return url.searchParams.get(param) || "";
return searchParams.get(param);
} }
// Get all locales we can, in order function isLocaleCodeCandidate(input: unknown): input is string {
// - Global authentik settings (contains user settings) if (typeof input !== "string") return false;
// - URL parameter
// - A requested code passed in, if any
// - Navigator
// - Fallback (en)
const isLocaleCandidate = (v: unknown): v is string => return !!input;
typeof v === "string" && v !== "" && v !== TOMBSTONE; }
export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): string { /**
const localeCandidates: string[] = [ * Auto-detect the most appropriate locale.
localeCodeFromUrl("locale"), *
userReq, * @remarks
window.navigator?.language ?? TOMBSTONE, *
brandReq, * The order of precedence is:
globalAK()?.locale ?? TOMBSTONE, *
DEFAULT_LOCALE, * 1. URL parameter `locale`.
].filter(isLocaleCandidate); * 2. User's preferred locale, if any.
* 3. Browser's preferred locale, if any.
* 4. Brand's preferred locale, if any.
* 5. Default locale.
*
* @param requestedLanguageCode - The user's preferred locale, if any.
* @param brandLanguageCode - The brand's preferred locale, if any.
*
* @returns The most appropriate locale.
*/
export function autoDetectLanguage(
requestedLanguageCode?: string,
brandLanguageCode?: string,
): string {
const localeCandidates = [
localeCodeFromURL("locale"),
requestedLanguageCode,
window.navigator?.language,
brandLanguageCode,
globalAK()?.locale,
].filter(isLocaleCodeCandidate);
const firstSupportedLocale = findSupportedLocale(localeCandidates); const firstSupportedLocale = findSupportedLocale(localeCandidates);
@ -61,10 +82,11 @@ export function autoDetectLanguage(userReq = TOMBSTONE, brandReq = TOMBSTONE): s
console.debug( console.debug(
`authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`, `authentik/locale: No locale found for '[${localeCandidates}.join(',')]', falling back to ${DEFAULT_LOCALE}`,
); );
return DEFAULT_LOCALE; return DEFAULT_LOCALE;
} }
return firstSupportedLocale.code; return firstSupportedLocale.languageCode;
} }
export default autoDetectLanguage; export default autoDetectLanguage;

View File

@ -1,10 +1,21 @@
import type { LocaleModule } from "@lit/localize"; import type { LocaleModule } from "@lit/localize";
export type LocaleRow = [string, RegExp, () => string, () => Promise<LocaleModule>]; /**
* - ISO 639-1 code for the locale.
* - Pattern to match the user-supplied locale.
* - Human-readable label for the locale.
* - Locale loader.
*/
export type LocaleRow = [
languageCode: string,
pattern: RegExp,
formatLabel: () => string,
fetch: () => Promise<LocaleModule>,
];
export type AkLocale = { export interface AKLocaleDefinition {
code: string; languageCode: string;
match: RegExp; pattern: RegExp;
label: () => string; formatLabel(): string;
locale: () => Promise<LocaleModule>; fetch(): Promise<LocaleModule>;
}; }

View File

@ -86,7 +86,7 @@ export class AggregateCard extends AKElement implements IAggregateCard {
font-size: var(--pf-global--FontSize--sm); font-size: var(--pf-global--FontSize--sm);
} }
.pf-c-card__body { .pf-c-card__body {
overflow-x: scroll; overflow-x: auto;
padding-left: calc(var(--pf-c-card--child--PaddingLeft) / 2); padding-left: calc(var(--pf-c-card--child--PaddingLeft) / 2);
padding-right: calc(var(--pf-c-card--child--PaddingRight) / 2); padding-right: calc(var(--pf-c-card--child--PaddingRight) / 2);
} }

View File

@ -14,8 +14,6 @@ import "@goauthentik/flow/stages/password/PasswordStage";
// end of stage import // end of stage import
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) { if (process.env.NODE_ENV === "development") {
const { ESBuildObserver } = await import("@goauthentik/common/client"); await import("@goauthentik/esbuild-plugin-live-reload/client");
new ESBuildObserver(process.env.WATCHER_URL);
} }

View File

@ -4,7 +4,7 @@ import {
CapabilitiesEnum, CapabilitiesEnum,
WithCapabilitiesConfig, WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider"; } from "@goauthentik/elements/Interface/capabilitiesProvider";
import { LOCALES } from "@goauthentik/elements/ak-locale-context/definitions"; import { AKLocalDefinitions } from "@goauthentik/elements/ak-locale-context/definitions";
import "@goauthentik/elements/forms/FormElement"; import "@goauthentik/elements/forms/FormElement";
import { BaseStage } from "@goauthentik/flow/stages/base"; import { BaseStage } from "@goauthentik/flow/stages/base";
@ -199,15 +199,15 @@ ${prompt.initialValue}</textarea
})}`; })}`;
case PromptTypeEnum.AkLocale: { case PromptTypeEnum.AkLocale: {
const locales = this.can(CapabilitiesEnum.CanDebug) const locales = this.can(CapabilitiesEnum.CanDebug)
? LOCALES ? AKLocalDefinitions
: LOCALES.filter((locale) => locale.code !== "debug"); : AKLocalDefinitions.filter((locale) => locale.languageCode !== "debug");
const options = locales.map( const options = locales.map(
(locale) => (locale) =>
html`<option html`<option
value=${locale.code} value=${locale.languageCode}
?selected=${locale.code === prompt.initialValue} ?selected=${locale.languageCode === prompt.initialValue}
> >
${locale.code.toUpperCase()} - ${locale.label()} ${locale.languageCode.toUpperCase()} - ${locale.formatLabel()}
</option> `, </option> `,
); );

View File

@ -43,6 +43,10 @@ import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
import { CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api"; import { CurrentBrand, EventsApi, SessionUser } from "@goauthentik/api";
if (process.env.NODE_ENV === "development") {
await import("@goauthentik/esbuild-plugin-live-reload/client");
}
const customStyles = css` const customStyles = css`
.pf-c-page__main, .pf-c-page__main,
.pf-c-drawer__content, .pf-c-drawer__content,
@ -291,12 +295,6 @@ export class UserInterface extends AuthenticatedInterface {
window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer); window.addEventListener(EVENT_NOTIFICATION_DRAWER_TOGGLE, this.toggleNotificationDrawer);
window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer); window.addEventListener(EVENT_API_DRAWER_TOGGLE, this.toggleApiDrawer);
window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails); window.addEventListener(EVENT_WS_MESSAGE, this.fetchConfigurationDetails);
if (process.env.NODE_ENV === "development" && process.env.WATCHER_URL) {
const { ESBuildObserver } = await import("@goauthentik/common/client");
new ESBuildObserver(process.env.WATCHER_URL);
}
} }
disconnectedCallback() { disconnectedCallback() {

View File

@ -1,71 +0,0 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"esModuleInterop": true,
"paths": {
"@goauthentik/docs/*": ["../website/docs/*"]
},
"types": [
"node",
"@wdio/mocha-framework",
"@wdio/types",
"expect-webdriverio",
"grecaptcha"
],
"jsx": "react-jsx",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"sourceMap": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"lib": [
"ES5",
"ES2015",
"ES2016",
"ES2017",
"ES2018",
"ES2019",
"ES2020",
"ESNext",
"DOM",
"DOM.Iterable",
"WebWorker"
],
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"useDefineForClassFields": false,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noImplicitAny": true,
"plugins": [
{
"name": "ts-lit-plugin",
"strict": true,
"rules": {
"no-unknown-tag-name": "off",
"no-missing-import": "off",
"no-incompatible-type-binding": "off",
"no-unknown-property": "off",
"no-unknown-attribute": "off"
}
},
{
"name": "@genesiscommunitysuccess/custom-elements-lsp",
"designSystemPrefix": "ak-",
"parser": {
"timeout": 2000
}
}
]
},
"exclude": ["src/**/*.test.ts", "./tests"]
}

6
web/tsconfig.build.json Normal file
View File

@ -0,0 +1,6 @@
// @file TSConfig used by the web package during build.
{
"extends": "./tsconfig.json",
"exclude": ["src/**/*.test.ts", "./tests"]
}

View File

@ -1,7 +1,19 @@
// @file TSConfig used by the web package during development.
{ {
"extends": "./tsconfig.base.json", "extends": "@goauthentik/tsconfig",
"compilerOptions": { "compilerOptions": {
"types": ["@wdio/types", "grecaptcha", "node", "@wdio/mocha-framework", "expect-webdriverio"], "allowSyntheticDefaultImports": true,
"emitDeclarationOnly": true,
"experimentalDecorators": true,
// See https://lit.dev/docs/components/properties/
"useDefineForClassFields": false,
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"baseUrl": ".",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
// TODO: We should enable this when when we're ready to enforce it.
"noUncheckedIndexedAccess": false,
"paths": { "paths": {
"@goauthentik/admin/*": ["./src/admin/*"], "@goauthentik/admin/*": ["./src/admin/*"],
"@goauthentik/common/*": ["./src/common/*"], "@goauthentik/common/*": ["./src/common/*"],
@ -13,6 +25,41 @@
"@goauthentik/polyfill/*": ["./src/polyfill/*"], "@goauthentik/polyfill/*": ["./src/polyfill/*"],
"@goauthentik/standalone/*": ["./src/standalone/*"], "@goauthentik/standalone/*": ["./src/standalone/*"],
"@goauthentik/user/*": ["./src/user/*"] "@goauthentik/user/*": ["./src/user/*"]
} },
"plugins": [
{
"name": "ts-lit-plugin",
"strict": true,
"rules": {
"no-unknown-tag-name": "off",
"no-missing-import": "off",
"no-incompatible-type-binding": "off",
"no-unknown-property": "off",
"no-unknown-attribute": "off"
}
},
{
"name": "@genesiscommunitysuccess/custom-elements-lsp",
"designSystemPrefix": "ak-",
"parser": {
"timeout": 2000
}
}
]
}, },
"exclude": [
// ---
"./out/**/*",
"./dist/**/*",
"src/**/*.test.ts",
"./tests",
// TODO: Remove after monorepo cleanup.
"src/**/*.comp.ts"
],
"references": [
{
"path": "./packages/esbuild-plugin-live-reload"
}
],
} }

View File

@ -1,3 +1,4 @@
// @file TSConfig used during tests.
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",

26
web/types/global.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
/**
* @file Environment variables available via ESBuild.
*/
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "production" | "development";
/**
*
* @todo Determine where this is used and if it is needed,
* give it a better name.
* @deprecated
*/
CWD: string;
/**
* @todo Determine where this is used and if it is needed,
* give it a better name.
* @deprecated
*/
AK_API_BASE_PATH: string;
}
}
}
}

View File

@ -336,6 +336,7 @@ export const config: Options.Testrunner = {
{ error: _error, result: _result, duration: _duration, passed: _passed, retries: _retries }, { error: _error, result: _result, duration: _duration, passed: _passed, retries: _retries },
) { ) {
if (lemmeSee) { if (lemmeSee) {
// @ts-expect-error TODO
await browser.pause(500); await browser.pause(500);
} }
}, },

View File

@ -9146,6 +9146,21 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -7679,6 +7679,21 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9238,6 +9238,21 @@ Las vinculaciones a grupos o usuarios se comparan con el usuario del evento.</ta
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9772,15 +9772,34 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
</trans-unit> </trans-unit>
<trans-unit id="s4d5cb134999b50df"> <trans-unit id="s4d5cb134999b50df">
<source>HTTP Basic Auth</source> <source>HTTP Basic Auth</source>
<target>HTTP Basic Auth</target>
</trans-unit> </trans-unit>
<trans-unit id="s6927635d1c339cfc"> <trans-unit id="s6927635d1c339cfc">
<source>Include the client ID and secret as request parameters</source> <source>Include the client ID and secret as request parameters</source>
<target>Inclure le client ID et secret comme paramètres de la requête</target>
</trans-unit> </trans-unit>
<trans-unit id="s4fca384c634e1a92"> <trans-unit id="s4fca384c634e1a92">
<source>Authorization code authentication method</source> <source>Authorization code authentication method</source>
<target>Méthode d'authentification pour authorization_code</target>
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
<target>Comment effectuer l'authentification lors d'une demande de jeton pour le flux authorization_code</target>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9763,6 +9763,21 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9146,6 +9146,21 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9048,6 +9048,21 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9473,6 +9473,21 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9481,4 +9481,19 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit> </trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit>
</body></file></xliff> </body></file></xliff>

View File

@ -9566,6 +9566,21 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -9537,6 +9537,21 @@ Gruplara/kullanıcılara yapılan bağlamalar, etkinliğin kullanıcısına kar
</trans-unit> </trans-unit>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit> </trans-unit>
</body> </body>
</file> </file>

View File

@ -6287,6 +6287,21 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="sdc02c276ed429008"> <trans-unit id="sdc02c276ed429008">
<source>How to perform authentication during an authorization_code token request flow</source> <source>How to perform authentication during an authorization_code token request flow</source>
</trans-unit> </trans-unit>
<trans-unit id="s844baf19a6c4a9b4">
<source>Enable "Remember me on this device"</source>
</trans-unit>
<trans-unit id="sfa72bca733f40692">
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
</trans-unit>
<trans-unit id="s1c336c2d6cef77b3">
<source>Remember me on this device</source>
</trans-unit>
<trans-unit id="s86cf007b861152ca">
<source>Ensure that the user's new password is different from their previous passwords. The number of past passwords to check is configurable.</source>
</trans-unit>
<trans-unit id="s79b3fcd40dd63921">
<source>Number of previous passwords to check</source>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

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