Compare commits

..

29 Commits

Author SHA1 Message Date
76b9add6b4 Merge branch 'main' into web/update-provider-forms-for-invalidation
* main: (142 commits)
  core: bump goauthentik.io/api/v3 from 3.2024102.2 to 3.2024104.1 (#12149)
  core: bump debugpy from 1.8.8 to 1.8.9 (#12150)
  core: bump webauthn from 2.2.0 to 2.3.0 (#12151)
  core: bump pydantic from 2.10.0 to 2.10.1 (#12152)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#12156)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#12157)
  core: bump sentry-sdk from 2.18.0 to 2.19.0 (#12153)
  web: bump API Client version (#12147)
  root: Backport version change (#12146)
  website/docs: update info about footer links to match new UI (#12120)
  website/docs: prepare release notes (#12142)
  providers/oauth2: fix migration (#12138)
  providers/oauth2: fix migration dependencies (#12123)
  web: bump API Client version (#12129)
  providers/oauth2: fix redirect uri input (#12122)
  providers/proxy: fix redirect_uri (#12121)
  website/docs: prepare release notes (#12119)
  web: bump API Client version (#12118)
  security: fix CVE 2024 52289 (#12113)
  security: fix CVE 2024 52307 (#12115)
  ...
2024-11-22 10:01:43 -08:00
785403de18 core: bump goauthentik.io/api/v3 from 3.2024102.2 to 3.2024104.1 (#12149)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024102.2 to 3.2024104.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Changelog](https://github.com/goauthentik/client-go/blob/main/model_version_history.go)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024102.2...v3.2024104.1)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 15:22:41 +01:00
1c4165a373 core: bump debugpy from 1.8.8 to 1.8.9 (#12150)
Bumps [debugpy](https://github.com/microsoft/debugpy) from 1.8.8 to 1.8.9.
- [Release notes](https://github.com/microsoft/debugpy/releases)
- [Commits](https://github.com/microsoft/debugpy/compare/v1.8.8...v1.8.9)

---
updated-dependencies:
- dependency-name: debugpy
  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>
2024-11-22 14:17:36 +01:00
bbd03b2b05 core: bump webauthn from 2.2.0 to 2.3.0 (#12151)
Bumps [webauthn](https://github.com/duo-labs/py_webauthn) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.2.0...v2.3.0)

---
updated-dependencies:
- dependency-name: webauthn
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 14:17:28 +01:00
dd79aec5a6 core: bump pydantic from 2.10.0 to 2.10.1 (#12152)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.0 to 2.10.1.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.10.0...v2.10.1)

---
updated-dependencies:
- dependency-name: pydantic
  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>
2024-11-22 14:17:20 +01:00
3634ae3db9 translate: Updates for file web/xliff/en.xlf in zh_CN (#12156)
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>
2024-11-22 14:17:13 +01:00
12e1ee93ed translate: Updates for file web/xliff/en.xlf in zh-Hans (#12157)
Translate web/xliff/en.xlf in zh-Hans

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-11-22 14:16:52 +01:00
62aa3659b8 core: bump sentry-sdk from 2.18.0 to 2.19.0 (#12153)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.18.0 to 2.19.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.18.0...2.19.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-22 13:52:28 +01:00
23ec05a86c web: bump API Client version (#12147)
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>
2024-11-22 13:51:40 +01:00
520148bba4 root: Backport version change (#12146)
* release: 2024.10.3

* release: 2024.10.4
2024-11-22 01:51:30 +01:00
d76e3c8023 Unneeded console.log 2024-11-01 12:46:24 -07:00
fb40ee72a5 Fix merge bug; modify package to always build before testing to catch bugs of this kind. 2024-11-01 10:46:43 -07:00
24f52252ba Merge bug! Lesson of the day: always run npm run build && npm run test:e2e in that order! 2024-11-01 10:27:40 -07:00
26ceb3d6c9 How did that get through? 2024-11-01 10:20:44 -07:00
f756be2ece Merge branch 'main' into web/update-provider-forms-for-invalidation
* main:
  website: bump elliptic from 6.5.7 to 6.6.0 in /website (#11869)
  core: bump selenium from 4.25.0 to 4.26.0 (#11875)
  core: bump goauthentik.io/api/v3 from 3.2024083.14 to 3.2024100.1 (#11876)
  website/docs: add info about invalidation flow, default flows in general (#11800)
  website: fix docs redirect (#11873)
  website: remove RC disclaimer for version 2024.10 (#11871)
  website: update supported versions (#11841)
  web: bump API Client version (#11870)
  root: backport version bump 2024.10.0 (#11868)
  website/docs: 2024.8.4 release notes (#11862)
  web/admin: provide default invalidation flows for LDAP and Radius (#11861)
  core, web: update translations (#11858)
2024-10-31 09:33:14 -07:00
4bfd06e034 Labeling error. 2024-10-30 16:52:22 -07:00
107dff39af Updating these to correspond to changes in MAIN. 2024-10-30 10:36:54 -07:00
401850c5e2 Merge branch 'main' into web/update-provider-forms-for-invalidation
* main:
  web/admin: fix code-based MFA toggle not working in wizard (#11854)
  sources/kerberos: add kiprop to ignored system principals (#11852)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#11846)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in it (#11845)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#11847)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11848)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#11849)
  translate: Updates for file web/xliff/en.xlf in it (#11850)
  website: 2024.10 Release Notes (#11839)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11814)
  core, web: update translations (#11821)
  core: bump goauthentik.io/api/v3 from 3.2024083.13 to 3.2024083.14 (#11830)
  core: bump service-identity from 24.1.0 to 24.2.0 (#11831)
  core: bump twilio from 9.3.5 to 9.3.6 (#11832)
  core: bump pytest-randomly from 3.15.0 to 3.16.0 (#11833)
  website/docs: Update social-logins github (#11822)
  website/docs: remove � (#11823)
  lifecycle: fix kdc5-config missing (#11826)
  website/docs: update preview status of different features (#11817)
2024-10-29 15:56:05 -07:00
11bc9b8041 Not sure how *that* got lost, but... 2024-10-29 15:51:19 -07:00
807e2a9fb0 web/admin: Unify the forms for providers between the ./admin/providers and ./admin/applications/wizard
## What

- For LDAP, OAuth2, Radius, SAML, SCIM, and Proxy providers, extract the literal form rendering
  component of each provider into a function.  After all, that's what they are: they take input (the
  render state) and produce output (HTML with event handlers).
- Rip out all of the forms in the wizard and replace them with ☝️
- Write E2E tests that exercise *all* of the components in *all* of the forms mentioned. See test
  results.  These tests come in two flavors, "simple" (minimum amount needed to make the provider
  "pass" the backend's parsers) and "complete" (touches every legal field in the form according to
  the authentik `./schema.yml` file).  As a result, every field is validated against the schema
  (although the schema is currently ported into the test by hand.
- Fixed some serious bugginess in the way the wizard `commit` phase handles errors.

## Details

### Providers

In some cases, I broke up the forms into smaller units:

- Proxy, especially, with standalone units now for `renderHttpBasic`, `renderModeSelector`,
`renderSettings`, and the differing modes)
- SAML now has a `renderHasSigningKp` object, which makes that part of the code much more readable.

I also extracted a few of static `options` collections into static const objects, so that the form
object itself would be a bit more readable.

### Wizard

Just ripped out all of the Provider forms.  All of them.  They weren't going to be needed in our
glorious new future.

Using the information provided by the `providerTypes` object, it was easy to extract all of the
information that had once been in `ak-application-wizard-authentication-method-choice.choices`. The
only thing left now is the renderers, one for each of the forms ripped out. Everything else is just
gone.

As a result, though, that's no longer a static list. It has to be derived from information sent via
the API.  So now it's in a context that's built when the wizard is initialized, and accessed by the
`createTypes` pass as well as the specific provider.

The error handling in the `commit` pass was just broken.  I have improved it quite a bit, and now it
actually displays helpful messages when things go wrong.

### Tests

Wrote a simple test runner that iterates through a collection of fields, setting their values via
field-type instructions contained in each line. For example, the "simple" OAuth2 Provider test looks
like this:

```
export const simpleOAuth2ProviderForm: TestProvider = () => [
    [setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
    [clickButton, "Next"],
    [setTextInput, "name", newObjectName("New Oauth2 Provider")],
    [setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
];
```

Each control checks for the existence of the object, and in most cases its current `display`.
(SearchSelect only checks existence, due to the oddness of the portaled popup.)  Where a field can't
reasonably be modified and still pass, we at least verify that the name provided in `schema.yml`
corresponds to an existing, available control on the form or wizard panel.

Combined with a routine for logging in and navigating to the Provider page, and another one to
validate that a new and uniqute "Successfully Created Provider" notification appeared, this makes
testing each provider a simple message of filling out the table of fields you want populated.

Equally simple: these *exact same tests* can be incorporated into a wrapper for logging in,
navigating to the Application page, and filling out an Application, and then a new and unique
Provider for that Application, by Provider Type.

As a special case, the Wizard variant checks the `TestSequence` object returned by the
`TestProvider` function and removes the `name` field, since the Wizard pre-populates that
automatically.

As a result of this, the contents of `./web/src` has lost 1,504 lines of code. And results like
these, where the behavior has been cross-checked three ways (the forms, the tests (and so the
back-end), *and the schema* all agree on field names and behaviors, gives me much more confidence
that the refactor works as expected:

```
[chrome 130.0.6723.70 mac #0-1] Running: chrome (v130.0.6723.70) on mac
[chrome 130.0.6723.70 mac #0-1] Session ID: 039c70690eebc83ffbc2eef97043c774
[chrome 130.0.6723.70 mac #0-1]
[chrome 130.0.6723.70 mac #0-1] » /tests/specs/providers.ts
[chrome 130.0.6723.70 mac #0-1] Configuring Providers
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple LDAP provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple OAuth2 provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple Radius provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple SAML provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple SCIM provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple Proxy provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple Forward Auth (single application) provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple Forward Auth (domain level) provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete OAuth2 provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete LDAP provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete Radius provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete SAML provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete SCIM provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete Proxy provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete Forward Auth (single application) provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete Forward Auth (domain level) provider
[chrome 130.0.6723.70 mac #0-1]
[chrome 130.0.6723.70 mac #0-1] 16 passing (1m 48.5s)
------------------------------------------------------------------
[chrome 130.0.6723.70 mac #0-2] Running: chrome (v130.0.6723.70) on mac
[chrome 130.0.6723.70 mac #0-2] Session ID: 5a3ae12c851eff8fffd2686096759146
[chrome 130.0.6723.70 mac #0-2]
[chrome 130.0.6723.70 mac #0-2] » /tests/specs/new-application-by-wizard.ts
[chrome 130.0.6723.70 mac #0-2] Configuring Applications Via the Wizard
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple LDAP provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple OAuth2 provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple Radius provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple SAML provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple SCIM provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple Proxy provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple Forward Auth (single) provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple Forward Auth (domain) provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete OAuth2 provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete LDAP provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete Radius provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete SAML provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete SCIM provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete Proxy provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete Forward Auth (single) provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete Forward Auth (domain) provider
[chrome 130.0.6723.70 mac #0-2]
[chrome 130.0.6723.70 mac #0-2] 16 passing (2m 3s)
```

🎉
2024-10-29 15:50:51 -07:00
5bd7cedaba Merge branch 'main' into web/update-provider-forms-for-invalidation
* main: (22 commits)
  lifecycle: fix missing krb5 deps for full testing in image (#11815)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11810)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#11809)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#11808)
  web: bump API Client version (#11807)
  core: bump goauthentik.io/api/v3 from 3.2024083.12 to 3.2024083.13 (#11806)
  core: bump ruff from 0.7.0 to 0.7.1 (#11805)
  core: bump twilio from 9.3.4 to 9.3.5 (#11804)
  core, web: update translations (#11803)
  providers/scim: handle no members in group in consistency check (#11801)
  stages/identification: add captcha to identification stage (#11711)
  website/docs: improve root page and redirect (#11798)
  providers/scim: clamp batch size for patch requests (#11797)
  web/admin: fix missing div in wizard forms (#11794)
  providers/proxy: fix handling of AUTHENTIK_HOST_BROWSER (#11722)
  core, web: update translations (#11789)
  core: bump goauthentik.io/api/v3 from 3.2024083.11 to 3.2024083.12 (#11790)
  core: bump gssapi from 1.8.3 to 1.9.0 (#11791)
  web: bump API Client version (#11792)
  stages/authenticator_validate: autoselect last used 2fa device (#11087)
  ...
2024-10-28 09:37:16 -07:00
c0814ad279 Almost there! 2024-10-25 10:27:06 -07:00
99af95b10c Committed harmony on SAML. Streamlined the tests even further. 2024-10-24 09:35:31 -07:00
a36cc820bd Radius form has been isolated. 2024-10-23 15:24:47 -07:00
6ff260df01 Merge branch 'main' into web/update-provider-forms-for-invalidation
* main:
  web/admin: Add InvalidationFlow to Radius Provider dialogues (#11786)
  core, web: update translations (#11782)
  providers/oauth2: fix amr claim not set due to login event not associated (#11780)
2024-10-23 14:45:21 -07:00
e497dbc314 Merge branch 'main' into web/update-provider-forms-for-invalidation
* main: (44 commits)
  web/admin: add strict dompurify config for diagram (#11783)
  core: bump cryptography from 43.0.1 to 43.0.3 (#11750)
  web: bump API Client version (#11781)
  sources: add Kerberos (#10815)
  root: rework CSRF middleware to set secure flag (#11753)
  web/admin: improve invalidation flow default & field grouping (#11769)
  providers/scim: add comparison with existing group on update and delta update users (#11414)
  website: bump mermaid from 10.6.0 to 10.9.3 in /website (#11766)
  web/flows: use dompurify for footer links (#11773)
  core, web: update translations (#11775)
  core: bump goauthentik.io/api/v3 from 3.2024083.10 to 3.2024083.11 (#11776)
  website: bump @types/react from 18.3.11 to 18.3.12 in /website (#11777)
  website: bump http-proxy-middleware from 2.0.6 to 2.0.7 in /website (#11771)
  web: bump API Client version (#11770)
  stages: authenticator_endpoint_gdtc (#10477)
  core: add prompt_data to auth flow (#11702)
  tests/e2e: fix dex tests failing (#11761)
  web/rac: disable DPI scaling (#11757)
  web/admin: update flow background (#11758)
  website/docs: fix some broken links (#11742)
  ...
2024-10-23 14:00:31 -07:00
f9f849574b We have working tests!!!!!! 2024-10-23 10:50:27 -07:00
4439b298bd Still trying to find components by internal text. Still not working. 2024-10-22 14:21:16 -07:00
4af6ecf629 web: Isolate the OAuth2 Provider Form into a reusable rendering function
- Pull the OAuth2 Provider Form `render()` method out into a standalone function.
  - Why: So it can be shared by both the Wizard and the Provider function. The renderer is (or at
    least, can be) a pure function: you give it input and it produces HTML, *and then it stops*.
- Provide a test harness that can test the OAuth2 provider form.
2024-10-22 07:13:04 -07:00
59 changed files with 3607 additions and 4914 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.10.2
current_version = 2024.10.4
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.10.2"
__version__ = "2024.10.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.10.2 Blueprint schema",
"title": "authentik 2024.10.4 Blueprint schema",
"required": [
"version",
"entries"

View File

@ -31,7 +31,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4}
restart: unless-stopped
command: server
environment:
@ -52,7 +52,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.2}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.4}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

@ -29,7 +29,7 @@ require (
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024102.2
goauthentik.io/api/v3 v3.2024104.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.9.0

4
go.sum
View File

@ -299,8 +299,8 @@ 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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2024102.2 h1:k2sIU7TkT2fOomBYo5KEc/mz5ipzaZUp5TuEOJLPX4g=
goauthentik.io/api/v3 v3.2024102.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024104.1 h1:N09HAJ66W965QEYpx6sJzcaQxPsnFykVwkzVjVK/zH0=
goauthentik.io/api/v3 v3.2024104.1/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2024.10.2"
const VERSION = "2024.10.4"

View File

@ -1,5 +1,5 @@
{
"name": "@goauthentik/authentik",
"version": "2024.10.2",
"version": "2024.10.4",
"private": true
}

371
poetry.lock generated
View File

@ -560,48 +560,55 @@ files = [
[[package]]
name = "cbor2"
version = "5.6.4"
version = "5.6.5"
description = "CBOR (de)serializer with extensive tag support"
optional = false
python-versions = ">=3.8"
files = [
{file = "cbor2-5.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c40c68779a363f47a11ded7b189ba16767391d5eae27fac289e7f62b730ae1fc"},
{file = "cbor2-5.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0625c8d3c487e509458459de99bf052f62eb5d773cc9fc141c6a6ea9367726d"},
{file = "cbor2-5.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de7137622204168c3a57882f15dd09b5135bda2bcb1cf8b56b58d26b5150dfca"},
{file = "cbor2-5.6.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3545e1e62ec48944b81da2c0e0a736ca98b9e4653c2365cae2f10ae871e9113"},
{file = "cbor2-5.6.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6749913cd00a24eba17406a0bfc872044036c30a37eb2fcde7acfd975317e8a"},
{file = "cbor2-5.6.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:57db966ab08443ee54b6f154f72021a41bfecd4ba897fe108728183ad8784a2a"},
{file = "cbor2-5.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:380e0c7f4db574dcd86e6eee1b0041863b0aae7efd449d49b0b784cf9a481b9b"},
{file = "cbor2-5.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c763d50a1714e0356b90ad39194fc8ef319356b89fb001667a2e836bfde88e3"},
{file = "cbor2-5.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:58a7ac8861857a9f9b0de320a4808a2a5f68a2599b4c14863e2748d5a4686c99"},
{file = "cbor2-5.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d715b2f101730335e84a25fe0893e2b6adf049d6d44da123bf243b8c875ffd8"},
{file = "cbor2-5.6.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f53a67600038cb9668720b309fdfafa8c16d1a02570b96d2144d58d66774318"},
{file = "cbor2-5.6.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f898bab20c4f42dca3688c673ff97c2f719b1811090430173c94452603fbcf13"},
{file = "cbor2-5.6.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e5d50fb9f47d295c1b7f55592111350424283aff4cc88766c656aad0300f11f"},
{file = "cbor2-5.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:7f9d867dcd814ab8383ad132eb4063e2b69f6a9f688797b7a8ca34a4eadb3944"},
{file = "cbor2-5.6.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e0860ca88edf8aaec5461ce0e498eb5318f1bcc70d93f90091b7a1f1d351a167"},
{file = "cbor2-5.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c38a0ed495a63a8bef6400158746a9cb03c36f89aeed699be7ffebf82720bf86"},
{file = "cbor2-5.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c8d8c2f208c223a61bed48dfd0661694b891e423094ed30bac2ed75032142aa"},
{file = "cbor2-5.6.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cd2ce6136e1985da989e5ba572521023a320dcefad5d1fff57fba261de80ca"},
{file = "cbor2-5.6.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7facce04aed2bf69ef43bdffb725446fe243594c2451921e89cc305bede16f02"},
{file = "cbor2-5.6.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f9c8ee0d89411e5e039a4f3419befe8b43c0dd8746eedc979e73f4c06fe0ef97"},
{file = "cbor2-5.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:9b45d554daa540e2f29f1747df9f08f8d98ade65a67b1911791bc193d33a5923"},
{file = "cbor2-5.6.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0a5cb2c16687ccd76b38cfbfdb34468ab7d5635fb92c9dc5e07831c1816bd0a9"},
{file = "cbor2-5.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6f985f531f7495527153c4f66c8c143e4cf8a658ec9e87b14bc5438e0a8d0911"},
{file = "cbor2-5.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d9c7b4bd7c3ea7e5587d4f1bbe073b81719530ddadb999b184074f064896e2"},
{file = "cbor2-5.6.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d06184dcdc275c389fee3cd0ea80b5e1769763df15f93ecd0bf4c281817365"},
{file = "cbor2-5.6.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e9ba7116f201860fb4c3e80ef36be63851ec7e4a18af70fea22d09cab0b000bf"},
{file = "cbor2-5.6.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:341468ae58bdedaa05c907ab16e90dd0d5c54d7d1e66698dfacdbc16a31e815b"},
{file = "cbor2-5.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:bcb4994be1afcc81f9167c220645d878b608cae92e19f6706e770f9bc7bbff6c"},
{file = "cbor2-5.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41c43abffe217dce70ae51c7086530687670a0995dfc90cc35f32f2cf4d86392"},
{file = "cbor2-5.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:227a7e68ba378fe53741ed892b5b03fe472b5bd23ef26230a71964accebf50a2"},
{file = "cbor2-5.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13521b7c9a0551fcc812d36afd03fc554fa4e1b193659bb5d4d521889aa81154"},
{file = "cbor2-5.6.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4816d290535d20c7b7e2663b76da5b0deb4237b90275c202c26343d8852b8a"},
{file = "cbor2-5.6.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1e98d370106821335efcc8fbe4136ea26b4747bf29ca0e66512b6c4f6f5cc59f"},
{file = "cbor2-5.6.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:68743a18e16167ff37654a29321f64f0441801dba68359c82dc48173cc6c87e1"},
{file = "cbor2-5.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:7ba5e9c6ed17526d266a1116c045c0941f710860c5f2495758df2e0d848c1b6d"},
{file = "cbor2-5.6.4-py3-none-any.whl", hash = "sha256:fe411c4bf464f5976605103ebcd0f60b893ac3e4c7c8d8bc8f4a0cb456e33c60"},
{file = "cbor2-5.6.4.tar.gz", hash = "sha256:1c533c50dde86bef1c6950602054a0ffa3c376e8b0e20c7b8f5b108793f6983e"},
{file = "cbor2-5.6.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e16c4a87fc999b4926f5c8f6c696b0d251b4745bc40f6c5aee51d69b30b15ca2"},
{file = "cbor2-5.6.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:87026fc838370d69f23ed8572939bd71cea2b3f6c8f8bb8283f573374b4d7f33"},
{file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88f029522aec5425fc2f941b3df90da7688b6756bd3f0472ab886d21208acbd"},
{file = "cbor2-5.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d15b638539b68aa5d5eacc56099b4543a38b2d2c896055dccf7e83d24b7955"},
{file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:47261f54a024839ec649b950013c4de5b5f521afe592a2688eebbe22430df1dc"},
{file = "cbor2-5.6.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:559dcf0d897260a9e95e7b43556a62253e84550b77147a1ad4d2c389a2a30192"},
{file = "cbor2-5.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:5b856fda4c50c5bc73ed3664e64211fa4f015970ed7a15a4d6361bd48462feaf"},
{file = "cbor2-5.6.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:863e0983989d56d5071270790e7ed8ddbda88c9e5288efdb759aba2efee670bc"},
{file = "cbor2-5.6.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5cff06464b8f4ca6eb9abcba67bda8f8334a058abc01005c8e616728c387ad32"},
{file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c7dbcdc59ea7f5a745d3e30ee5e6b6ff5ce7ac244aa3de6786391b10027bb3"},
{file = "cbor2-5.6.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34cf5ab0dc310c3d0196caa6ae062dc09f6c242e2544bea01691fe60c0230596"},
{file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6797b824b26a30794f2b169c0575301ca9b74ae99064e71d16e6ba0c9057de51"},
{file = "cbor2-5.6.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73b9647eed1493097db6aad61e03d8f1252080ee041a1755de18000dd2c05f37"},
{file = "cbor2-5.6.5-cp311-cp311-win_amd64.whl", hash = "sha256:6e14a1bf6269d25e02ef1d4008e0ce8880aa271d7c6b4c329dba48645764f60e"},
{file = "cbor2-5.6.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e25c2aebc9db99af7190e2261168cdde8ed3d639ca06868e4f477cf3a228a8e9"},
{file = "cbor2-5.6.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fde21ac1cf29336a31615a2c469a9cb03cf0add3ae480672d4d38cda467d07fc"},
{file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8947c102cac79d049eadbd5e2ffb8189952890df7cbc3ee262bbc2f95b011a9"},
{file = "cbor2-5.6.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38886c41bebcd7dca57739439455bce759f1e4c551b511f618b8e9c1295b431b"},
{file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae2b49226224e92851c333b91d83292ec62eba53a19c68a79890ce35f1230d70"},
{file = "cbor2-5.6.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2764804ffb6553283fc4afb10a280715905a4cea4d6dc7c90d3e89c4a93bc8d"},
{file = "cbor2-5.6.5-cp312-cp312-win_amd64.whl", hash = "sha256:a3ac50485cf67dfaab170a3e7b527630e93cb0a6af8cdaa403054215dff93adf"},
{file = "cbor2-5.6.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f0d0a9c5aabd48ecb17acf56004a7542a0b8d8212be52f3102b8218284bd881e"},
{file = "cbor2-5.6.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61ceb77e6aa25c11c814d4fe8ec9e3bac0094a1f5bd8a2a8c95694596ea01e08"},
{file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97a7e409b864fecf68b2ace8978eb5df1738799a333ec3ea2b9597bfcdd6d7d2"},
{file = "cbor2-5.6.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6d69f38f7d788b04c09ef2b06747536624b452b3c8b371ab78ad43b0296fab"},
{file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f91e6d74fa6917df31f8757fdd0e154203b0dd0609ec53eb957016a2b474896a"},
{file = "cbor2-5.6.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce13a27ef8fddf643fc17a753fe34aa72b251d03c23da6a560c005dc171085b"},
{file = "cbor2-5.6.5-cp313-cp313-win_amd64.whl", hash = "sha256:54c72a3207bb2d4480c2c39dad12d7971ce0853a99e3f9b8d559ce6eac84f66f"},
{file = "cbor2-5.6.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4586a4f65546243096e56a3f18f29d60752ee9204722377021b3119a03ed99ff"},
{file = "cbor2-5.6.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d1a18b3a58dcd9b40ab55c726160d4a6b74868f2a35b71f9e726268b46dc6a2"},
{file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a83b76367d1c3e69facbcb8cdf65ed6948678e72f433137b41d27458aa2a40cb"},
{file = "cbor2-5.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90bfa36944caccec963e6ab7e01e64e31cc6664535dc06e6295ee3937c999cbb"},
{file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:37096663a5a1c46a776aea44906cbe5fa3952f29f50f349179c00525d321c862"},
{file = "cbor2-5.6.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:93676af02bd9a0b4a62c17c5b20f8e9c37b5019b1a24db70a2ee6cb770423568"},
{file = "cbor2-5.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:8f747b7a9aaa58881a0c5b4cd4a9b8fb27eca984ed261a769b61de1f6b5bd1e6"},
{file = "cbor2-5.6.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:94885903105eec66d7efb55f4ce9884fdc5a4d51f3bd75b6fedc68c5c251511b"},
{file = "cbor2-5.6.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fe11c2eb518c882cfbeed456e7a552e544893c17db66fe5d3230dbeaca6b615c"},
{file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:66dd25dd919cddb0b36f97f9ccfa51947882f064729e65e6bef17c28535dc459"},
{file = "cbor2-5.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa61a02995f3a996c03884cf1a0b5733f88cbfd7fa0e34944bf678d4227ee712"},
{file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:824f202b556fc204e2e9a67d6d6d624e150fbd791278ccfee24e68caec578afd"},
{file = "cbor2-5.6.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7488aec919f8408f9987a3a32760bd385d8628b23a35477917aa3923ff6ad45f"},
{file = "cbor2-5.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a34ee99e86b17444ecbe96d54d909dd1a20e2da9f814ae91b8b71cf1ee2a95e4"},
{file = "cbor2-5.6.5-py3-none-any.whl", hash = "sha256:3038523b8fc7de312bb9cdcbbbd599987e64307c4db357cd2030c472a6c7d468"},
{file = "cbor2-5.6.5.tar.gz", hash = "sha256:b682820677ee1dbba45f7da11898d2720f92e06be36acec290867d5ebf3d7e09"},
]
[package.extras]
@ -1139,37 +1146,37 @@ tests = ["django", "hypothesis", "pytest", "pytest-asyncio"]
[[package]]
name = "debugpy"
version = "1.8.8"
version = "1.8.9"
description = "An implementation of the Debug Adapter Protocol for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6"},
{file = "debugpy-1.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d"},
{file = "debugpy-1.8.8-cp310-cp310-win32.whl", hash = "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f"},
{file = "debugpy-1.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9"},
{file = "debugpy-1.8.8-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318"},
{file = "debugpy-1.8.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba"},
{file = "debugpy-1.8.8-cp311-cp311-win32.whl", hash = "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98"},
{file = "debugpy-1.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4"},
{file = "debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996"},
{file = "debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9"},
{file = "debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9"},
{file = "debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864"},
{file = "debugpy-1.8.8-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804"},
{file = "debugpy-1.8.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f"},
{file = "debugpy-1.8.8-cp313-cp313-win32.whl", hash = "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add"},
{file = "debugpy-1.8.8-cp313-cp313-win_amd64.whl", hash = "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b"},
{file = "debugpy-1.8.8-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:143ef07940aeb8e7316de48f5ed9447644da5203726fca378f3a6952a50a9eae"},
{file = "debugpy-1.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95651bdcbfd3b27a408869a53fbefcc2bcae13b694daee5f1365b1b83a00113"},
{file = "debugpy-1.8.8-cp38-cp38-win32.whl", hash = "sha256:26b461123a030e82602a750fb24d7801776aa81cd78404e54ab60e8b5fecdad5"},
{file = "debugpy-1.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3cbf1833e644a3100eadb6120f25be8a532035e8245584c4f7532937edc652a"},
{file = "debugpy-1.8.8-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854"},
{file = "debugpy-1.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2"},
{file = "debugpy-1.8.8-cp39-cp39-win32.whl", hash = "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2"},
{file = "debugpy-1.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9"},
{file = "debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f"},
{file = "debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091"},
{file = "debugpy-1.8.9-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:cfe1e6c6ad7178265f74981edf1154ffce97b69005212fbc90ca22ddfe3d017e"},
{file = "debugpy-1.8.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada7fb65102a4d2c9ab62e8908e9e9f12aed9d76ef44880367bc9308ebe49a0f"},
{file = "debugpy-1.8.9-cp310-cp310-win32.whl", hash = "sha256:c36856343cbaa448171cba62a721531e10e7ffb0abff838004701454149bc037"},
{file = "debugpy-1.8.9-cp310-cp310-win_amd64.whl", hash = "sha256:17c5e0297678442511cf00a745c9709e928ea4ca263d764e90d233208889a19e"},
{file = "debugpy-1.8.9-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:b74a49753e21e33e7cf030883a92fa607bddc4ede1aa4145172debc637780040"},
{file = "debugpy-1.8.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62d22dacdb0e296966d7d74a7141aaab4bec123fa43d1a35ddcb39bf9fd29d70"},
{file = "debugpy-1.8.9-cp311-cp311-win32.whl", hash = "sha256:8138efff315cd09b8dcd14226a21afda4ca582284bf4215126d87342bba1cc66"},
{file = "debugpy-1.8.9-cp311-cp311-win_amd64.whl", hash = "sha256:ff54ef77ad9f5c425398efb150239f6fe8e20c53ae2f68367eba7ece1e96226d"},
{file = "debugpy-1.8.9-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:957363d9a7a6612a37458d9a15e72d03a635047f946e5fceee74b50d52a9c8e2"},
{file = "debugpy-1.8.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e565fc54b680292b418bb809f1386f17081d1346dca9a871bf69a8ac4071afe"},
{file = "debugpy-1.8.9-cp312-cp312-win32.whl", hash = "sha256:3e59842d6c4569c65ceb3751075ff8d7e6a6ada209ceca6308c9bde932bcef11"},
{file = "debugpy-1.8.9-cp312-cp312-win_amd64.whl", hash = "sha256:66eeae42f3137eb428ea3a86d4a55f28da9bd5a4a3d369ba95ecc3a92c1bba53"},
{file = "debugpy-1.8.9-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:957ecffff80d47cafa9b6545de9e016ae8c9547c98a538ee96ab5947115fb3dd"},
{file = "debugpy-1.8.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1efbb3ff61487e2c16b3e033bc8595aea578222c08aaf3c4bf0f93fadbd662ee"},
{file = "debugpy-1.8.9-cp313-cp313-win32.whl", hash = "sha256:7c4d65d03bee875bcb211c76c1d8f10f600c305dbd734beaed4077e902606fee"},
{file = "debugpy-1.8.9-cp313-cp313-win_amd64.whl", hash = "sha256:e46b420dc1bea64e5bbedd678148be512442bc589b0111bd799367cde051e71a"},
{file = "debugpy-1.8.9-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:472a3994999fe6c0756945ffa359e9e7e2d690fb55d251639d07208dbc37caea"},
{file = "debugpy-1.8.9-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365e556a4772d7d0d151d7eb0e77ec4db03bcd95f26b67b15742b88cacff88e9"},
{file = "debugpy-1.8.9-cp38-cp38-win32.whl", hash = "sha256:54a7e6d3014c408eb37b0b06021366ee985f1539e12fe49ca2ee0d392d9ceca5"},
{file = "debugpy-1.8.9-cp38-cp38-win_amd64.whl", hash = "sha256:8e99c0b1cc7bf86d83fb95d5ccdc4ad0586d4432d489d1f54e4055bcc795f693"},
{file = "debugpy-1.8.9-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:7e8b079323a56f719977fde9d8115590cb5e7a1cba2fcee0986ef8817116e7c1"},
{file = "debugpy-1.8.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6953b335b804a41f16a192fa2e7851bdcfd92173cbb2f9f777bb934f49baab65"},
{file = "debugpy-1.8.9-cp39-cp39-win32.whl", hash = "sha256:7e646e62d4602bb8956db88b1e72fe63172148c1e25c041e03b103a25f36673c"},
{file = "debugpy-1.8.9-cp39-cp39-win_amd64.whl", hash = "sha256:3d9755e77a2d680ce3d2c5394a444cf42be4a592caaf246dbfbdd100ffcf7ae5"},
{file = "debugpy-1.8.9-py2.py3-none-any.whl", hash = "sha256:cc37a6c9987ad743d9c3a14fa1b1a14b7e4e6041f9dd0c8abf8895fe7a97b899"},
{file = "debugpy-1.8.9.zip", hash = "sha256:1339e14c7d980407248f09824d1b25ff5c5616651689f1e0f0e51bdead3ea13e"},
]
[[package]]
@ -3718,19 +3725,19 @@ files = [
[[package]]
name = "pydantic"
version = "2.10.0"
version = "2.10.1"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.10.0-py3-none-any.whl", hash = "sha256:5e7807ba9201bdf61b1b58aa6eb690916c40a47acfb114b1b4fef3e7fd5b30fc"},
{file = "pydantic-2.10.0.tar.gz", hash = "sha256:0aca0f045ff6e2f097f1fe89521115335f15049eeb8a7bef3dafe4b19a74e289"},
{file = "pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e"},
{file = "pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
pydantic-core = "2.27.0"
pydantic-core = "2.27.1"
typing-extensions = ">=4.12.2"
[package.extras]
@ -3739,111 +3746,111 @@ timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
version = "2.27.0"
version = "2.27.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2ac6b919f7fed71b17fe0b4603c092a4c9b5bae414817c9c81d3c22d1e1bcc"},
{file = "pydantic_core-2.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e015833384ca3e1a0565a79f5d953b0629d9138021c27ad37c92a9fa1af7623c"},
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db72e40628967f6dc572020d04b5f800d71264e0531c6da35097e73bdf38b003"},
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df45c4073bed486ea2f18757057953afed8dd77add7276ff01bccb79982cf46c"},
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:836a4bfe0cc6d36dc9a9cc1a7b391265bf6ce9d1eb1eac62ac5139f5d8d9a6fa"},
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bf1340ae507f6da6360b24179c2083857c8ca7644aab65807023cf35404ea8d"},
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ab325fc86fbc077284c8d7f996d904d30e97904a87d6fb303dce6b3de7ebba9"},
{file = "pydantic_core-2.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1da0c98a85a6c6ed702d5556db3b09c91f9b0b78de37b7593e2de8d03238807a"},
{file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b0202ebf2268954090209a84f9897345719e46a57c5f2c9b7b250ca0a9d3e63"},
{file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:35380671c3c921fe8adf31ad349dc6f7588b7e928dbe44e1093789734f607399"},
{file = "pydantic_core-2.27.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b4c19525c3538fbc0bbda6229f9682fb8199ce9ac37395880e6952798e00373"},
{file = "pydantic_core-2.27.0-cp310-none-win32.whl", hash = "sha256:333c840a1303d1474f491e7be0b718226c730a39ead0f7dab2c7e6a2f3855555"},
{file = "pydantic_core-2.27.0-cp310-none-win_amd64.whl", hash = "sha256:99b2863c1365f43f74199c980a3d40f18a218fbe683dd64e470199db426c4d6a"},
{file = "pydantic_core-2.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4523c4009c3f39d948e01962223c9f5538602e7087a628479b723c939fab262d"},
{file = "pydantic_core-2.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84af1cf7bfdcbc6fcf5a5f70cc9896205e0350306e4dd73d54b6a18894f79386"},
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e65466b31be1070b4a5b7dbfbd14b247884cb8e8b79c64fb0f36b472912dbaea"},
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a5c022bb0d453192426221605efc865373dde43b17822a264671c53b068ac20c"},
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bb69bf3b6500f195c3deb69c1205ba8fc3cb21d1915f1f158a10d6b1ef29b6a"},
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aa4d1b2eba9a325897308b3124014a142cdccb9f3e016f31d3ebee6b5ea5e75"},
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e96ca781e0c01e32115912ebdf7b3fb0780ce748b80d7d28a0802fa9fbaf44e"},
{file = "pydantic_core-2.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b872c86d8d71827235c7077461c502feb2db3f87d9d6d5a9daa64287d75e4fa0"},
{file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:82e1ad4ca170e8af4c928b67cff731b6296e6a0a0981b97b2eb7c275cc4e15bd"},
{file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:eb40f828bc2f73f777d1eb8fee2e86cd9692a4518b63b6b5aa8af915dfd3207b"},
{file = "pydantic_core-2.27.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9a8fbf506fde1529a1e3698198fe64bfbe2e0c09557bc6a7dcf872e7c01fec40"},
{file = "pydantic_core-2.27.0-cp311-none-win32.whl", hash = "sha256:24f984fc7762ed5f806d9e8c4c77ea69fdb2afd987b4fd319ef06c87595a8c55"},
{file = "pydantic_core-2.27.0-cp311-none-win_amd64.whl", hash = "sha256:68950bc08f9735306322bfc16a18391fcaac99ded2509e1cc41d03ccb6013cfe"},
{file = "pydantic_core-2.27.0-cp311-none-win_arm64.whl", hash = "sha256:3eb8849445c26b41c5a474061032c53e14fe92a11a5db969f722a2716cd12206"},
{file = "pydantic_core-2.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8117839a9bdbba86e7f9df57018fe3b96cec934c3940b591b0fd3fbfb485864a"},
{file = "pydantic_core-2.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a291d0b4243a259c8ea7e2b84eb9ccb76370e569298875a7c5e3e71baf49057a"},
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84e35afd9e10b2698e6f2f32256678cb23ca6c1568d02628033a837638b3ed12"},
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58ab0d979c969983cdb97374698d847a4acffb217d543e172838864636ef10d9"},
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d06b667e53320332be2bf6f9461f4a9b78092a079b8ce8634c9afaa7e10cd9f"},
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78f841523729e43e3928a364ec46e2e3f80e6625a4f62aca5c345f3f626c6e8a"},
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:400bf470e4327e920883b51e255617dfe4496d4e80c3fea0b5a5d0bf2c404dd4"},
{file = "pydantic_core-2.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:951e71da6c89d354572098bada5ba5b5dc3a9390c933af8a614e37755d3d1840"},
{file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a51ce96224eadd1845150b204389623c8e129fde5a67a84b972bd83a85c6c40"},
{file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:483c2213a609e7db2c592bbc015da58b6c75af7360ca3c981f178110d9787bcf"},
{file = "pydantic_core-2.27.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:359e7951f04ad35111b5ddce184db3391442345d0ab073aa63a95eb8af25a5ef"},
{file = "pydantic_core-2.27.0-cp312-none-win32.whl", hash = "sha256:ee7d9d5537daf6d5c74a83b38a638cc001b648096c1cae8ef695b0c919d9d379"},
{file = "pydantic_core-2.27.0-cp312-none-win_amd64.whl", hash = "sha256:2be0ad541bb9f059954ccf8877a49ed73877f862529575ff3d54bf4223e4dd61"},
{file = "pydantic_core-2.27.0-cp312-none-win_arm64.whl", hash = "sha256:6e19401742ed7b69e51d8e4df3c03ad5ec65a83b36244479fd70edde2828a5d9"},
{file = "pydantic_core-2.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5f2b19b8d6fca432cb3acf48cf5243a7bf512988029b6e6fd27e9e8c0a204d85"},
{file = "pydantic_core-2.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c86679f443e7085ea55a7376462553996c688395d18ef3f0d3dbad7838f857a2"},
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:510b11e9c3b1a852876d1ccd8d5903684336d635214148637ceb27366c75a467"},
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb704155e73b833801c247f39d562229c0303f54770ca14fb1c053acb376cf10"},
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ce048deb1e033e7a865ca384770bccc11d44179cf09e5193a535c4c2f497bdc"},
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58560828ee0951bb125c6f2862fbc37f039996d19ceb6d8ff1905abf7da0bf3d"},
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb4785894936d7682635726613c44578c420a096729f1978cd061a7e72d5275"},
{file = "pydantic_core-2.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2883b260f7a93235488699d39cbbd94fa7b175d3a8063fbfddd3e81ad9988cb2"},
{file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6fcb3fa3855d583aa57b94cf146f7781d5d5bc06cb95cb3afece33d31aac39b"},
{file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e851a051f7260e6d688267eb039c81f05f23a19431bd7dfa4bf5e3cb34c108cd"},
{file = "pydantic_core-2.27.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edb1bfd45227dec8d50bc7c7d86463cd8728bcc574f9b07de7369880de4626a3"},
{file = "pydantic_core-2.27.0-cp313-none-win32.whl", hash = "sha256:678f66462058dd978702db17eb6a3633d634f7aa0deaea61e0a674152766d3fc"},
{file = "pydantic_core-2.27.0-cp313-none-win_amd64.whl", hash = "sha256:d28ca7066d6cdd347a50d8b725dc10d9a1d6a1cce09836cf071ea6a2d4908be0"},
{file = "pydantic_core-2.27.0-cp313-none-win_arm64.whl", hash = "sha256:6f4a53af9e81d757756508b57cae1cf28293f0f31b9fa2bfcb416cc7fb230f9d"},
{file = "pydantic_core-2.27.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:e9f9feee7f334b72ceae46313333d002b56f325b5f04271b4ae2aadd9e993ae4"},
{file = "pydantic_core-2.27.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:225bfff5d425c34e1fd562cef52d673579d59b967d9de06178850c4802af9039"},
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921ad596ff1a82f9c692b0758c944355abc9f0de97a4c13ca60ffc6d8dc15d4"},
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6354e18a9be37bfa124d6b288a87fb30c673745806c92956f1a25e3ae6e76b96"},
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ee4c2a75af9fe21269a4a0898c5425afb01af1f5d276063f57e2ae1bc64e191"},
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c91e3c04f5191fd3fb68764bddeaf02025492d5d9f23343b283870f6ace69708"},
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a6ebfac28fd51890a61df36ef202adbd77d00ee5aca4a3dadb3d9ed49cfb929"},
{file = "pydantic_core-2.27.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:36aa167f69d8807ba7e341d67ea93e50fcaaf6bc433bb04939430fa3dab06f31"},
{file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e8d89c276234579cd3d095d5fa2a44eb10db9a218664a17b56363cddf226ff3"},
{file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:5cc822ab90a70ea3a91e6aed3afac570b276b1278c6909b1d384f745bd09c714"},
{file = "pydantic_core-2.27.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e15315691fe2253eb447503153acef4d7223dfe7e7702f9ed66539fcd0c43801"},
{file = "pydantic_core-2.27.0-cp38-none-win32.whl", hash = "sha256:dfa5f5c0a4c8fced1422dc2ca7eefd872d5d13eb33cf324361dbf1dbfba0a9fe"},
{file = "pydantic_core-2.27.0-cp38-none-win_amd64.whl", hash = "sha256:513cb14c0cc31a4dfd849a4674b20c46d87b364f997bbcb02282306f5e187abf"},
{file = "pydantic_core-2.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:4148dc9184ab79e356dc00a4199dc0ee8647973332cb385fc29a7cced49b9f9c"},
{file = "pydantic_core-2.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5fc72fbfebbf42c0856a824b8b0dc2b5cd2e4a896050281a21cfa6fed8879cb1"},
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:185ef205256cd8b38431205698531026979db89a79587725c1e55c59101d64e9"},
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:395e3e1148fa7809016231f8065f30bb0dc285a97b4dc4360cd86e17bab58af7"},
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33d14369739c5d07e2e7102cdb0081a1fa46ed03215e07f097b34e020b83b1ae"},
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7820bb0d65e3ce1e3e70b6708c2f66143f55912fa02f4b618d0f08b61575f12"},
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b61989068de9ce62296cde02beffabcadb65672207fc51e7af76dca75e6636"},
{file = "pydantic_core-2.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15e350efb67b855cd014c218716feea4986a149ed1f42a539edd271ee074a196"},
{file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:433689845288f9a1ee5714444e65957be26d30915f7745091ede4a83cfb2d7bb"},
{file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:3fd8bc2690e7c39eecdf9071b6a889ce7b22b72073863940edc2a0a23750ca90"},
{file = "pydantic_core-2.27.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:884f1806609c2c66564082540cffc96868c5571c7c3cf3a783f63f2fb49bd3cd"},
{file = "pydantic_core-2.27.0-cp39-none-win32.whl", hash = "sha256:bf37b72834e7239cf84d4a0b2c050e7f9e48bced97bad9bdf98d26b8eb72e846"},
{file = "pydantic_core-2.27.0-cp39-none-win_amd64.whl", hash = "sha256:31a2cae5f059329f9cfe3d8d266d3da1543b60b60130d186d9b6a3c20a346361"},
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4fb49cfdb53af5041aba909be00cccfb2c0d0a2e09281bf542371c5fd36ad04c"},
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49633583eb7dc5cba61aaf7cdb2e9e662323ad394e543ee77af265736bcd3eaa"},
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153017e3d6cd3ce979de06d84343ca424bb6092727375eba1968c8b4693c6ecb"},
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff63a92f6e249514ef35bc795de10745be0226eaea06eb48b4bbeaa0c8850a4a"},
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5982048129f40b082c2654de10c0f37c67a14f5ff9d37cf35be028ae982f26df"},
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:91bc66f878557313c2a6bcf396e7befcffe5ab4354cfe4427318968af31143c3"},
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:68ef5377eb582fa4343c9d0b57a5b094046d447b4c73dd9fbd9ffb216f829e7d"},
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c5726eec789ee38f2c53b10b1821457b82274f81f4f746bb1e666d8741fcfadb"},
{file = "pydantic_core-2.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0c431e4be5c1a0c6654e0c31c661cd89e0ca956ef65305c3c3fd96f4e72ca39"},
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8e21d927469d04b39386255bf00d0feedead16f6253dcc85e9e10ddebc334084"},
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b51f964fcbb02949fc546022e56cdb16cda457af485e9a3e8b78ac2ecf5d77e"},
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a7fd4de38f7ff99a37e18fa0098c3140286451bc823d1746ba80cec5b433a1"},
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fda87808429c520a002a85d6e7cdadbf58231d60e96260976c5b8f9a12a8e13"},
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a150392102c402c538190730fda06f3bce654fc498865579a9f2c1d2b425833"},
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c9ed88b398ba7e3bad7bd64d66cc01dcde9cfcb7ec629a6fd78a82fa0b559d78"},
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:9fe94d9d2a2b4edd7a4b22adcd45814b1b59b03feb00e56deb2e89747aec7bfe"},
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d8b5ee4ae9170e2775d495b81f414cc20268041c42571530513496ba61e94ba3"},
{file = "pydantic_core-2.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d29e235ce13c91902ef3efc3d883a677655b3908b1cbc73dee816e5e1f8f7739"},
{file = "pydantic_core-2.27.0.tar.gz", hash = "sha256:f57783fbaf648205ac50ae7d646f27582fc706be3977e87c3c124e7a92407b10"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"},
{file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"},
{file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"},
{file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"},
{file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"},
{file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"},
{file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"},
{file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"},
{file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"},
{file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"},
{file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"},
{file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"},
{file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"},
{file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"},
{file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"},
{file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"},
{file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"},
]
[package.dependencies]
@ -4547,13 +4554,13 @@ websocket-client = ">=1.8,<2.0"
[[package]]
name = "sentry-sdk"
version = "2.18.0"
version = "2.19.0"
description = "Python client for Sentry (https://sentry.io)"
optional = false
python-versions = ">=3.6"
files = [
{file = "sentry_sdk-2.18.0-py2.py3-none-any.whl", hash = "sha256:ee70e27d1bbe4cd52a38e1bd28a5fadb9b17bc29d91b5f2b97ae29c0a7610442"},
{file = "sentry_sdk-2.18.0.tar.gz", hash = "sha256:0dc21febd1ab35c648391c664df96f5f79fb0d92d7d4225cd9832e53a617cafd"},
{file = "sentry_sdk-2.19.0-py2.py3-none-any.whl", hash = "sha256:7b0b3b709dee051337244a09a30dbf6e95afe0d34a1f8b430d45e0982a7c125b"},
{file = "sentry_sdk-2.19.0.tar.gz", hash = "sha256:ee4a4d2ae8bfe3cac012dcf3e4607975904c137e1738116549fc3dbbb6ff0e36"},
]
[package.dependencies]
@ -4579,7 +4586,7 @@ grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"]
http2 = ["httpcore[http2] (==1.*)"]
httpx = ["httpx (>=0.16.0)"]
huey = ["huey (>=2)"]
huggingface-hub = ["huggingface-hub (>=0.22)"]
huggingface-hub = ["huggingface_hub (>=0.22)"]
langchain = ["langchain (>=0.0.210)"]
launchdarkly = ["launchdarkly-server-sdk (>=9.8.0)"]
litestar = ["litestar (>=2.0.0)"]
@ -4588,7 +4595,7 @@ openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"]
openfeature = ["openfeature-sdk (>=0.7.1)"]
opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
opentelemetry-experimental = ["opentelemetry-distro"]
pure-eval = ["asttokens", "executing", "pure-eval"]
pure-eval = ["asttokens", "executing", "pure_eval"]
pymongo = ["pymongo (>=3.1)"]
pyspark = ["pyspark (>=2.4.4)"]
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
@ -5279,20 +5286,20 @@ files = [
[[package]]
name = "webauthn"
version = "2.2.0"
version = "2.3.0"
description = "Pythonic WebAuthn"
optional = false
python-versions = "*"
files = [
{file = "webauthn-2.2.0-py3-none-any.whl", hash = "sha256:e8e2daace85dde8f6fb436c1bca9aa72d5931dac8829ecc1562cc4e7cc169f6c"},
{file = "webauthn-2.2.0.tar.gz", hash = "sha256:70e4f318d293125e3a8609838be0561119f4f8846bc430d524f8da4052ee18cc"},
{file = "webauthn-2.3.0-py3-none-any.whl", hash = "sha256:872668fd8f32e256e76e4251e04eb0737e77e0760b1db3912af11346cbacef9e"},
{file = "webauthn-2.3.0.tar.gz", hash = "sha256:79fca835027d3b39290bfd175d09ca7a2bd6e12163790feb6d9c0b746e4c2ede"},
]
[package.dependencies]
asn1crypto = ">=1.4.0"
cbor2 = ">=5.4.6"
cryptography = ">=41.0.7"
pyOpenSSL = ">=23.3.0"
asn1crypto = ">=1.5.1"
cbor2 = ">=5.6.5"
cryptography = ">=43.0.3"
pyOpenSSL = ">=24.2.1"
[[package]]
name = "websocket-client"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.10.2"
version = "2024.10.4"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.10.2
version: 2024.10.4
description: Making authentication simple.
contact:
email: hello@goauthentik.io

1628
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2024.10.2-1732206118",
"@goauthentik/api": "^2024.10.4-1732236707",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
@ -135,6 +135,7 @@
"storybook:build": "wireit",
"storybook:build-import-map": "wireit",
"test": "wireit",
"test:e2e": "wireit",
"test:e2e:watch": "wireit",
"test:watch": "wireit",
"tsc": "wireit",
@ -321,11 +322,24 @@
},
"test": {
"command": "wdio ./wdio.conf.ts --logLevel=warn",
"dependencies": [
"build"
],
"env": {
"CI": "true",
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"test:e2e": {
"command": "wdio run ./tests/wdio.conf.ts",
"dependencies": [
"build"
],
"env": {
"CI": "true",
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
}
},
"test:e2e:watch": {
"command": "wdio run ./tests/wdio.conf.ts",
"dependencies": [

View File

@ -29,7 +29,7 @@ export class ApplicationWizardPageBase
return AwadStyles;
}
@consume({ context: applicationWizardContext })
@consume({ context: applicationWizardContext, subscribe: true })
public wizard!: ApplicationWizardState;
@query("form")

View File

@ -1,7 +1,12 @@
import { createContext } from "@lit/context";
import { LocalTypeCreate } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.choices.js";
import { ApplicationWizardState } from "./types";
export const applicationWizardContext = createContext<ApplicationWizardState>(
Symbol("ak-application-wizard-state-context"),
);
export const applicationWizardProvidersContext = createContext<LocalTypeCreate[]>(
Symbol("ak-application-wizard-providers-context"),
);

View File

@ -1,3 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
@ -5,7 +6,10 @@ import { ContextProvider } from "@lit/context";
import { msg } from "@lit/localize";
import { customElement, state } from "lit/decorators.js";
import { applicationWizardContext } from "./ContextIdentity";
import { ProvidersApi, ProxyMode } from "@goauthentik/api";
import { applicationWizardContext, applicationWizardProvidersContext } from "./ContextIdentity";
import { providerTypeRenderers } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.choices.js";
import { newSteps } from "./steps";
import {
ApplicationStep,
@ -19,6 +23,7 @@ const freshWizardState = (): ApplicationWizardState => ({
app: {},
provider: {},
errors: {},
proxyMode: ProxyMode.Proxy,
});
@customElement("ak-application-wizard")
@ -46,6 +51,11 @@ export class ApplicationWizard extends CustomListenerElement(
initialValue: this.wizardState,
});
wizardProviderProvider = new ContextProvider(this, {
context: applicationWizardProvidersContext,
initialValue: [],
});
/**
* One of our steps has multiple display variants, one for each type of service provider. We
* want to *preserve* a customer's decisions about different providers; never make someone "go
@ -56,6 +66,21 @@ export class ApplicationWizard extends CustomListenerElement(
*/
providerCache: Map<string, OneOfProvider> = new Map();
connectedCallback() {
super.connectedCallback();
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
const wizardReadyProviders = Object.keys(providerTypeRenderers);
this.wizardProviderProvider.setValue(
providerTypes
.filter((providerType) => wizardReadyProviders.includes(providerType.modelName))
.map((providerType) => ({
...providerType,
renderer: providerTypeRenderers[providerType.modelName],
})),
);
});
}
// And this is where all the special cases go...
handleUpdate(detail: ApplicationWizardStateUpdate) {
if (detail.status === "submitted") {

View File

@ -1,176 +1,28 @@
import "@goauthentik/admin/common/ak-license-notice";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import type { ProviderModelEnum as ProviderModelEnumType, TypeCreate } from "@goauthentik/api";
import { ProviderModelEnum, ProxyMode } from "@goauthentik/api";
import type {
LDAPProviderRequest,
ModelRequest,
OAuth2ProviderRequest,
ProxyProviderRequest,
RACProviderRequest,
RadiusProviderRequest,
SAMLProviderRequest,
SCIMProviderRequest,
} from "@goauthentik/api";
import { OneOfProvider } from "../types";
import type { TypeCreate } from "@goauthentik/api";
type ProviderRenderer = () => TemplateResult;
type ModelConverter = (provider: OneOfProvider) => ModelRequest;
type ProviderNoteProvider = () => TemplateResult | undefined;
type ProviderNote = ProviderNoteProvider | undefined;
export type LocalTypeCreate = TypeCreate & {
formName: string;
modelName: ProviderModelEnumType;
converter: ModelConverter;
note?: ProviderNote;
renderer: ProviderRenderer;
};
export const providerModelsList: LocalTypeCreate[] = [
{
formName: "oauth2provider",
name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
description: msg("Modern applications, APIs and Single-page applications."),
renderer: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
modelName: ProviderModelEnum.Oauth2Oauth2provider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
...(provider as OAuth2ProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/openidconnect.svg",
},
{
formName: "ldapprovider",
name: msg("LDAP (Lightweight Directory Access Protocol)"),
description: msg(
"Provide an LDAP interface for applications and users to authenticate against.",
),
renderer: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
modelName: ProviderModelEnum.LdapLdapprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.LdapLdapprovider,
...(provider as LDAPProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/ldap.png",
},
{
formName: "proxyprovider-proxy",
name: msg("Transparent Reverse Proxy"),
description: msg("For transparent reverse proxies with required authentication"),
renderer: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.Proxy,
}),
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwardsingle",
name: msg("Forward Auth (Single Application)"),
description: msg("For nginx's auth_request or traefik's forwardAuth"),
renderer: () =>
html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardSingle,
}),
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwarddomain",
name: msg("Forward Auth (Domain Level)"),
description: msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
renderer: () =>
html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardDomain,
}),
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "racprovider",
name: msg("Remote Access Provider"),
description: msg("Remotely access computers/servers via RDP/SSH/VNC"),
renderer: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
modelName: ProviderModelEnum.RacRacprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RacRacprovider,
...(provider as RACProviderRequest),
}),
note: () => html`<ak-license-notice></ak-license-notice>`,
requiresEnterprise: true,
component: "",
iconUrl: "/static/authentik/sources/rac.svg",
},
{
formName: "samlprovider",
name: msg("SAML (Security Assertion Markup Language)"),
description: msg("Configure SAML provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
modelName: ProviderModelEnum.SamlSamlprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.SamlSamlprovider,
...(provider as SAMLProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/saml.png",
},
{
formName: "radiusprovider",
name: msg("RADIUS (Remote Authentication Dial-In User Service)"),
description: msg("Configure RADIUS provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
modelName: ProviderModelEnum.RadiusRadiusprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RadiusRadiusprovider,
...(provider as RadiusProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/radius.svg",
},
{
formName: "scimprovider",
name: msg("SCIM (System for Cross-domain Identity Management)"),
description: msg("Configure SCIM provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
modelName: ProviderModelEnum.ScimScimprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ScimScimprovider,
...(provider as SCIMProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/scim.png",
},
];
export const providerRendererList = new Map<string, ProviderRenderer>(
providerModelsList.map((tc) => [tc.formName, tc.renderer]),
);
export default providerModelsList;
export const providerTypeRenderers: Record<string, () => TemplateResult> = {
oauth2provider: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
ldapprovider: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
proxyprovider: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
racprovider: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
samlprovider: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
radiusprovider: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
scimprovider: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
};

View File

@ -7,41 +7,37 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { consume } from "@lit/context";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html } from "lit";
import BasePanel from "../BasePanel";
import { applicationWizardProvidersContext } from "../ContextIdentity";
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
@customElement("ak-application-wizard-authentication-method-choice")
export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) {
@consume({ context: applicationWizardProvidersContext })
public providerModelsList!: LocalTypeCreate[];
render() {
const selectedTypes = providerModelsList.filter(
(t) => t.formName === this.wizard.providerModel,
const selectedTypes = this.providerModelsList.filter(
(t) => t.modelName === this.wizard.providerModel,
);
// As a hack, the Application wizard has separate provider paths for our three types of
// proxy providers. This patch swaps the form we want to be directed to on page 3 from the
// modelName to the formName, so we get the right one. This information isn't modified
// or forwarded, so the proxy-plus-subtype is correctly mapped on submission.
const typesForWizard = providerModelsList.map((provider) => ({
...provider,
modelName: provider.formName,
}));
return providerModelsList.length > 0
return this.providerModelsList.length > 0
? html`<form class="pf-c-form pf-m-horizontal">
<ak-wizard-page-type-create
.types=${typesForWizard}
.types=${this.providerModelsList}
name="selectProviderType"
layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
this.dispatchWizardUpdate({
update: {
...this.wizard,
providerModel: ev.detail.formName,
providerModel: ev.detail.modelName,
errors: {},
},
status: this.valid ? "valid" : "invalid",

View File

@ -22,6 +22,9 @@ import {
type ApplicationRequest,
CoreApi,
type ModelRequest,
ProviderModelEnum,
ProxyMode,
type ProxyProviderRequest,
type TransactionApplicationRequest,
type TransactionApplicationResponse,
ValidationError,
@ -29,7 +32,6 @@ import {
} from "@goauthentik/api";
import BasePanel from "../BasePanel";
import providerModelsList from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest {
return {
@ -39,14 +41,19 @@ function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest
};
}
type ProviderModelType = Exclude<ModelRequest["providerModel"], "11184809">;
type State = {
state: "idle" | "running" | "error" | "success";
label: string | TemplateResult;
icon: string[];
};
const providerMap: Map<string, string> = Object.values(ProviderModelEnum)
.filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value))
.reduce((acc: Map<string, string>, value) => {
acc.set(value.split(".")[1], value);
return acc;
}, new Map());
const idleState: State = {
state: "idle",
label: "",
@ -70,6 +77,7 @@ const successState: State = {
icon: ["fa-check-circle", "pf-m-success"],
};
type StrictProviderModelEnum = Exclude<ProviderModelEnum, "11184809">;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isValidationError = (v: any): v is ValidationError => instanceOfValidationError(v);
@ -102,19 +110,28 @@ export class ApplicationWizardCommitApplication extends BasePanel {
if (this.commitState === idleState) {
this.response = undefined;
this.commitState = runningState;
const providerModel = providerModelsList.find(
({ formName }) => formName === this.wizard.providerModel,
);
if (!providerModel) {
throw new Error(
`Could not determine provider model from user request: ${JSON.stringify(this.wizard, null, 2)}`,
);
// Stringly-based API. Not the best, but it works. Just be aware that it is
// stringly-based.
const providerModel = providerMap.get(
this.wizard.providerModel,
) as StrictProviderModelEnum;
const provider = this.wizard.provider as ModelRequest;
provider.providerModel = providerModel;
// Special case for the Proxy provider.
if (this.wizard.providerModel === "proxyprovider") {
(provider as ProxyProviderRequest).mode = this.wizard.proxyMode;
if ((provider as ProxyProviderRequest).mode !== ProxyMode.ForwardDomain) {
(provider as ProxyProviderRequest).cookieDomain = "";
}
}
const request: TransactionApplicationRequest = {
providerModel: providerModel.modelName as ProviderModelType,
app: cleanApplication(this.wizard.app),
provider: providerModel.converter(this.wizard.provider),
providerModel,
provider,
};
this.send(request);
@ -125,6 +142,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
data: TransactionApplicationRequest,
): Promise<TransactionApplicationResponse | void> {
this.errors = undefined;
this.commitState = idleState;
new CoreApi(DEFAULT_CONFIG)
.coreTransactionalApplicationsUpdate({
transactionApplicationRequest: data,
@ -138,6 +156,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch(async (resolution: any) => {
const errors = await parseAPIError(resolution);
console.log(errors);
// THIS is a really gross special case; if the user is duplicating the name of an
// existing provider, the error appears on the `app` (!) error object. We have to
@ -164,11 +183,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
});
}
renderErrors(errors?: ValidationError) {
if (!errors) {
return nothing;
}
renderErrors(errors: ValidationError) {
const navTo = (step: number) => () =>
this.dispatchCustomEvent("ak-wizard-nav", {
command: "goto",
@ -219,7 +234,9 @@ export class ApplicationWizardCommitApplication extends BasePanel {
>
${this.commitState.label}
</h1>
${this.renderErrors(this.errors)}
${this.commitState === errorState
? this.renderErrors(this.errors ?? {})
: nothing}
</div>
</div>
</div>

View File

@ -1,12 +1,12 @@
import { consume } from "@lit/context";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import BasePanel from "../BasePanel";
import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
import { applicationWizardProvidersContext } from "../ContextIdentity";
import type { LocalTypeCreate } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
import "./ldap/ak-application-wizard-authentication-by-ldap";
import "./oauth/ak-application-wizard-authentication-by-oauth";
import "./proxy/ak-application-wizard-authentication-for-forward-domain-proxy";
import "./proxy/ak-application-wizard-authentication-for-reverse-proxy";
import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
import "./rac/ak-application-wizard-authentication-for-rac";
import "./radius/ak-application-wizard-authentication-by-radius";
import "./saml/ak-application-wizard-authentication-by-saml-configuration";
@ -14,14 +14,19 @@ import "./scim/ak-application-wizard-authentication-by-scim";
@customElement("ak-application-wizard-authentication-method")
export class ApplicationWizardApplicationDetails extends BasePanel {
@consume({ context: applicationWizardProvidersContext })
public providerModelsList!: LocalTypeCreate[];
render() {
const handler = providerRendererList.get(this.wizard.providerModel);
const handler: LocalTypeCreate | undefined = this.providerModelsList.find(
({ modelName }) => modelName === this.wizard.providerModel,
);
if (!handler) {
throw new Error(
"Unrecognized authentication method in ak-application-wizard-authentication-method",
);
}
return handler();
return handler.renderer();
}
}

View File

@ -1,33 +1,14 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { renderForm } from "@goauthentik/admin/providers/ldap/LDAPProviderFormForm.js";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { html } from "lit";
import { FlowsInstancesListDesignationEnum } from "@goauthentik/api";
import type { LDAPProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
import {
bindModeOptions,
cryptoCertificateHelp,
gidStartNumberHelp,
mfaSupportHelp,
searchModeOptions,
tlsServerNameHelp,
uidStartNumberHelp,
} from "./LDAPOptionsAndHelp";
@customElement("ak-application-wizard-authentication-by-ldap")
export class ApplicationWizardApplicationDetails extends WithBrandConfig(BaseProviderPanel) {
@ -37,129 +18,7 @@ export class ApplicationWizardApplicationDetails extends WithBrandConfig(BasePro
return html` <ak-wizard-title>${msg("Configure LDAP Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
help=${msg("Method's display Name.")}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Bind flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${this.brand.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for users to authenticate.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Unbind flow")}
name="invalidationFlow"
required
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.brandFlow=${this.brand.flowInvalidation}
defaultFlowSlug="default-invalidation-flow"
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for unbinding users.")}</p>
</ak-form-element-horizontal>
<ak-radio-input
label=${msg("Bind mode")}
name="bindMode"
.options=${bindModeOptions}
.value=${provider?.bindMode}
help=${msg("Configure how the outpost authenticates requests.")}
>
</ak-radio-input>
<ak-radio-input
label=${msg("Search mode")}
name="searchMode"
.options=${searchModeOptions}
.value=${provider?.searchMode}
help=${msg(
"Configure how the outpost queries the core authentik server's users.",
)}
>
</ak-radio-input>
<ak-switch-input
name="mfaSupport"
label=${msg("Code-based MFA Support")}
?checked=${provider?.mfaSupport ?? true}
help=${mfaSupportHelp}
>
</ak-switch-input>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="baseDn"
label=${msg("Base DN")}
required
value="${first(provider?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
.errorMessages=${errors?.baseDn ?? []}
help=${msg(
"LDAP DN under which bind requests and search requests can be made.",
)}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Certificate")}
name="certificate"
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.certificate ?? nothing)}
name="certificate"
>
</ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
</ak-form-element-horizontal>
<ak-text-input
label=${msg("TLS Server name")}
name="tlsServerName"
value="${first(provider?.tlsServerName, "")}"
.errorMessages=${errors?.tlsServerName ?? []}
help=${tlsServerNameHelp}
></ak-text-input>
<ak-number-input
label=${msg("UID start number")}
required
name="uidStartNumber"
value="${first(provider?.uidStartNumber, 2000)}"
.errorMessages=${errors?.uidStartNumber ?? []}
help=${uidStartNumberHelp}
></ak-number-input>
<ak-number-input
label=${msg("GID start number")}
required
name="gidStartNumber"
value="${first(provider?.gidStartNumber, 4000)}"
.errorMessages=${errors?.gidStartNumber ?? []}
help=${gidStartNumberHelp}
></ak-number-input>
</div>
</ak-form-group>
${renderForm(provider, errors, this.brand)}
</form>`;
}
}

View File

@ -1,47 +1,11 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2PropertyMappings.js";
import {
clientTypeOptions,
issuerModeOptions,
redirectUriHelp,
subjectModeOptions,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import {
IRedirectURIInput,
akOAuthRedirectURIInput,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { renderForm } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormForm.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { html } from "lit";
import {
ClientTypeEnum,
FlowsInstancesListDesignationEnum,
MatchingModeEnum,
RedirectURI,
SourcesApi,
} from "@goauthentik/api";
import { SourcesApi } from "@goauthentik/api";
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@ -69,258 +33,17 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
render() {
const provider = this.wizard.provider as OAuth2Provider | undefined;
const errors = this.wizard.errors.provider;
return html`<ak-wizard-title>${msg("Configure OAuth2/OpenId Provider")}</ak-wizard-title>
const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show;
};
return html` <ak-wizard-title>${msg("Configure OAuth2 Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
.errorMessages=${errors?.authorizationFlow ?? []}
?required=${true}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-radio-input
name="clientType"
label=${msg("Client type")}
.value=${provider?.clientType}
required
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
}}
.options=${clientTypeOptions}
>
</ak-radio-input>
<ak-text-input
name="clientId"
label=${msg("Client ID")}
value=${provider?.clientId ?? randomString(40, ascii_letters + digits)}
.errorMessages=${errors?.clientId ?? []}
required
>
</ak-text-input>
<ak-text-input
name="clientSecret"
label=${msg("Client Secret")}
value=${provider?.clientSecret ??
randomString(128, ascii_letters + digits)}
.errorMessages=${errors?.clientSecret ?? []}
?hidden=${!this.showClientSecret}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Redirect URIs/Origins")}
required
name="redirectUris"
>
<ak-array-input
.items=${[]}
.newItem=${() => ({
matchingMode: MatchingModeEnum.Strict,
url: "",
})}
.row=${(f?: RedirectURI) =>
akOAuthRedirectURIInput({
".redirectURI": f,
"style": "width: 100%",
"name": "oauth2-redirect-uri",
} as unknown as IRedirectURIInput)}
>
</ak-array-input>
${redirectUriHelp}
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Signing Key")}
name="signingKey"
.errorMessages=${errors?.signingKey ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.signingKey ?? nothing)}
name="certificate"
singleton
>
</ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg("Key used to sign the tokens.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="accessCodeValidity"
label=${msg("Access code validity")}
required
value="${first(provider?.accessCodeValidity, "minutes=1")}"
.errorMessages=${errors?.accessCodeValidity ?? []}
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Configure how long access codes are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="accessTokenValidity"
label=${msg("Access Token validity")}
value="${first(provider?.accessTokenValidity, "minutes=5")}"
required
.errorMessages=${errors?.accessTokenValidity ?? []}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long access tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="refreshTokenValidity"
label=${msg("Refresh Token validity")}
value="${first(provider?.refreshTokenValidity, "days=30")}"
.errorMessages=${errors?.refreshTokenValidity ?? []}
?required=${true}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long refresh tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Scopes")}
name="propertyMappings"
.errorMessages=${errors?.propertyMappings ?? []}
>
<ak-dual-select-dynamic-selected
.provider=${oauth2PropertyMappingsProvider}
.selector=${makeOAuth2PropertyMappingsSelector(
provider?.propertyMappings,
)}
available-label=${msg("Available Scopes")}
selected-label=${msg("Selected Scopes")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
)}
</p>
</ak-form-element-horizontal>
<ak-radio-input
name="subMode"
label=${msg("Subject mode")}
required
.options=${subjectModeOptions}
.value=${provider?.subMode}
help=${msg(
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
)}
>
</ak-radio-input>
<ak-switch-input
name="includeClaimsInIdToken"
label=${msg("Include claims in id_token")}
?checked=${first(provider?.includeClaimsInIdToken, true)}
help=${msg(
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
)}
></ak-switch-input>
<ak-radio-input
name="issuerMode"
label=${msg("Issuer mode")}
required
.options=${issuerModeOptions}
.value=${provider?.issuerMode}
help=${msg(
"Configure how the issuer field of the ID Token should be filled.",
)}
>
</ak-radio-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []}
>
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
${renderForm(
provider ?? {},
errors,
this.showClientSecret,
showClientSecretCallback,
)}
</form>`;
}
}

View File

@ -1,267 +0,0 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "@goauthentik/admin/providers/proxy/ProxyProviderPropertyMappings.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { state } from "@lit/reactive-element/decorators.js";
import { TemplateResult, html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
PaginatedOAuthSourceList,
PaginatedScopeMappingList,
ProxyMode,
ProxyProvider,
SourcesApi,
} from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
type MaybeTemplateResult = TemplateResult | typeof nothing;
export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
constructor() {
super();
new SourcesApi(DEFAULT_CONFIG)
.sourcesOauthList({
ordering: "name",
hasJwks: true,
})
.then((oauthSources: PaginatedOAuthSourceList) => {
this.oauthSources = oauthSources;
});
}
propertyMappings?: PaginatedScopeMappingList;
oauthSources?: PaginatedOAuthSourceList;
@state()
showHttpBasic = true;
@state()
mode: ProxyMode = ProxyMode.Proxy;
get instance(): ProxyProvider | undefined {
return this.wizard.provider as ProxyProvider;
}
renderModeDescription(): MaybeTemplateResult {
return nothing;
}
renderProxyMode(): TemplateResult {
throw new Error("Must be implemented in a child class.");
}
renderHttpBasic() {
return html`<ak-text-input
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
help=${msg(
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
)}
>
</ak-text-input>
<ak-text-input
name="basicAuthPasswordAttribute"
label=${msg("HTTP-Basic Password Key")}
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
help=${msg(
"User/Group Attribute used for the password part of the HTTP-Basic Header.",
)}
>
</ak-text-input>`;
}
render() {
const errors = this.wizard.errors.provider;
return html` <ak-wizard-title>${msg("Configure Proxy Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
${this.renderModeDescription()}
<ak-text-input
name="name"
value=${ifDefined(this.instance?.name)}
required
.errorMessages=${errors?.name ?? []}
label=${msg("Name")}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
${this.renderProxyMode()}
<ak-text-input
name="accessTokenValidity"
value=${first(this.instance?.accessTokenValidity, "hours=24")}
label=${msg("Token validity")}
help=${msg("Configure how long tokens are valid for.")}
.errorMessages=${errors?.accessTokenValidity ?? []}
></ak-text-input>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Certificate")}
name="certificate"
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(this.instance?.certificate ?? undefined)}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Additional scopes")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${proxyPropertyMappingsProvider}
.selector=${makeProxyPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label="${msg("Available Scopes")}"
selected-label="${msg("Selected Scopes")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Additional scope mappings, which are passed to the proxy.")}
</p>
</ak-form-element-horizontal>
<ak-textarea-input
name="skipPathRegex"
label=${this.mode === ProxyMode.ForwardDomain
? msg("Unauthenticated URLs")
: msg("Unauthenticated Paths")}
value=${ifDefined(this.instance?.skipPathRegex)}
.errorMessages=${errors?.skipPathRegex ?? []}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg(
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
)}
</p>`}
>
</ak-textarea-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="interceptHeaderAuth"
?checked=${first(this.instance?.interceptHeaderAuth, true)}
label=${msg("Intercept header authentication")}
help=${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
></ak-switch-input>
<ak-switch-input
name="basicAuthEnabled"
?checked=${first(this.instance?.basicAuthEnabled, false)}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
}}
label=${msg("Send HTTP-Basic Authentication")}
help=${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
></ak-switch-input>
${this.showHttpBasic ? this.renderHttpBasic() : html``}
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []}
>
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}
export default AkTypeProxyApplicationWizardPage;

View File

@ -1,74 +0,0 @@
import "@goauthentik/components/ak-text-input";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import PFList from "@patternfly/patternfly/components/List/list.css";
import { ProxyProvider } from "@goauthentik/api";
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
@customElement("ak-application-wizard-authentication-for-forward-proxy-domain")
export class AkForwardDomainProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
static get styles() {
return super.styles.concat(PFList);
}
renderModeDescription() {
return html`<p>
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
)}
</p>
<div>
${msg("An example setup can look like this:")}
<ul class="pf-c-list">
<li>${msg("authentik running on auth.example.com")}</li>
<li>${msg("app1 running on app1.example.com")}</li>
</ul>
${msg(
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
)}
</div>`;
}
renderProxyMode() {
const provider = this.wizard.provider as ProxyProvider | undefined;
const errors = this.wizard.errors.provider;
return html`
<ak-text-input
name="externalHost"
label=${msg("External host")}
value=${ifDefined(provider?.externalHost)}
.errorMessages=${errors?.externalHost ?? []}
required
help=${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
>
</ak-text-input>
<ak-text-input
name="cookieDomain"
label=${msg("Cookie domain")}
value="${ifDefined(provider?.cookieDomain)}"
.errorMessages=${errors?.cookieDomain ?? []}
required
help=${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
></ak-text-input>
`;
}
}
export default AkForwardDomainProxyApplicationWizardPage;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-for-forward-proxy-domain": AkForwardDomainProxyApplicationWizardPage;
}
}

View File

@ -1,55 +1,50 @@
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import {
ProxyModeValue,
type SetMode,
type SetShowHttpBasic,
renderForm,
} from "@goauthentik/admin/providers/proxy/ProxyProviderFormForm.js";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { ProxyProvider } from "@goauthentik/api";
import { ProxyMode } from "@goauthentik/api";
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
import BaseProviderPanel from "../BaseProviderPanel.js";
@customElement("ak-application-wizard-authentication-for-reverse-proxy")
export class AkReverseProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
renderModeDescription() {
return html`<p class="pf-u-mb-xl">
${msg(
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
)}
</p>`;
}
export class AkReverseProxyApplicationWizardPage extends BaseProviderPanel {
@state()
showHttpBasic = true;
renderProxyMode() {
const provider = this.wizard.provider as ProxyProvider | undefined;
const errors = this.wizard.errors.provider;
render() {
const onSetMode: SetMode = (ev: CustomEvent<ProxyModeValue>) => {
this.dispatchWizardUpdate({
update: {
...this.wizard,
proxyMode: ev.detail.value,
},
});
// We deliberately chose not to make the forms "controlled," but we do need this form to
// respond immediately to a state change in the wizard.
window.setTimeout(() => this.requestUpdate(), 0);
};
return html` <ak-text-input
name="externalHost"
value=${ifDefined(provider?.externalHost)}
required
label=${msg("External host")}
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>
<ak-text-input
name="internalHost"
value=${ifDefined(provider?.internalHost)}
.errorMessages=${errors?.internalHost ?? []}
required
label=${msg("Internal host")}
help=${msg("Upstream host that the requests are forwarded to.")}
></ak-text-input>
<ak-switch-input
name="internalHostSslValidation"
?checked=${first(provider?.internalHostSslValidation, true)}
label=${msg("Internal host SSL Validation")}
help=${msg("Validate SSL Certificates of upstream servers.")}
>
</ak-switch-input>`;
const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
};
return html` <ak-wizard-title>${msg("Configure Proxy Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
${renderForm(this.wizard.provider ?? {}, this.wizard.errors.provider ?? [], {
mode: this.wizard.proxyMode ?? ProxyMode.Proxy,
onSetMode,
showHttpBasic: this.showHttpBasic,
onSetShowHttpBasic,
})}
</form>`;
}
}

View File

@ -1,48 +0,0 @@
import "@goauthentik/components/ak-text-input";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { ProxyProvider } from "@goauthentik/api";
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
@customElement("ak-application-wizard-authentication-for-single-forward-proxy")
export class AkForwardSingleProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
renderModeDescription() {
return html`<p class="pf-u-mb-xl">
${msg(
html`Use this provider with nginx's <code>auth_request</code> or traefik's
<code>forwardAuth</code>. Each application/domain needs its own provider.
Additionally, on each domain, <code>/outpost.goauthentik.io</code> must be
routed to the outpost (when using a managed outpost, this is done for you).`,
)}
</p>`;
}
renderProxyMode() {
const provider = this.wizard.provider as ProxyProvider | undefined;
const errors = this.wizard.errors.provider;
return html`<ak-text-input
name="externalHost"
value=${ifDefined(provider?.externalHost)}
required
label=${msg("External host")}
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>`;
}
}
export default AkForwardSingleProxyApplicationWizardPage;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-for-single-forward-proxy": AkForwardSingleProxyApplicationWizardPage;
}
}

View File

@ -1,7 +1,7 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { renderForm } from "@goauthentik/admin/providers/radius/RadiusProviderFormForm.js";
import "@goauthentik/components/ak-text-input";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/forms/FormGroup";
@ -10,91 +10,21 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowsInstancesListDesignationEnum, RadiusProvider } from "@goauthentik/api";
import { RadiusProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@customElement("ak-application-wizard-authentication-by-radius")
export class ApplicationWizardAuthenticationByRadius extends WithBrandConfig(BaseProviderPanel) {
render() {
const provider = this.wizard.provider as RadiusProvider | undefined;
const errors = this.wizard.errors.provider;
return html`<ak-wizard-title>${msg("Configure Radius Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Authentication flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${this.brand.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for users to authenticate.")}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="sharedSecret"
label=${msg("Shared secret")}
.errorMessages=${errors?.sharedSecret ?? []}
value=${first(
provider?.sharedSecret,
randomString(128, ascii_letters + digits),
)}
required
></ak-text-input>
<ak-text-input
name="clientNetworks"
label=${msg("Client Networks")}
value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
.errorMessages=${errors?.clientNetworks ?? []}
required
help=${msg(`List of CIDRs (comma-seperated) that clients can connect from. A more specific
CIDR will match before a looser one. Clients connecting from a non-specified CIDR
will be dropped.`)}
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div></ak-form-group
>
${renderForm(
this.wizard.provider as RadiusProvider | undefined,
this.wizard.errors.provider,
this.brand,
)}
</form>`;
}
}

View File

@ -1,356 +1,35 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-multi-select";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { renderForm } from "@goauthentik/admin/providers/saml/SAMLProviderFormForm.js";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { html } from "lit";
import {
FlowsInstancesListDesignationEnum,
PaginatedSAMLPropertyMappingList,
PropertymappingsApi,
SAMLProvider,
} from "@goauthentik/api";
import { SAMLProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
import {
digestAlgorithmOptions,
signatureAlgorithmOptions,
spBindingOptions,
} from "./SamlProviderOptions";
import "./saml-property-mappings-search";
@customElement("ak-application-wizard-authentication-by-saml-configuration")
export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPanel {
@state()
propertyMappings?: PaginatedSAMLPropertyMappingList;
@state()
hasSigningKp = false;
constructor() {
super();
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsProviderSamlList({
ordering: "saml_name",
})
.then((propertyMappings: PaginatedSAMLPropertyMappingList) => {
this.propertyMappings = propertyMappings;
});
}
propertyMappingConfiguration(provider?: SAMLProvider) {
const propertyMappings = this.propertyMappings?.results ?? [];
const configuredMappings = (providerMappings: string[]) =>
propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk));
const managedMappings = () =>
propertyMappings
.filter((pm) => (pm?.managed ?? "").startsWith("goauthentik.io/providers/saml"))
.map((pm) => pm.pk);
const pmValues = provider?.propertyMappings
? configuredMappings(provider?.propertyMappings ?? [])
: managedMappings();
const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]);
return { pmValues, propertyPairs };
}
render() {
const provider = this.wizard.provider as SAMLProvider | undefined;
const errors = this.wizard.errors.provider;
const { pmValues, propertyPairs } = this.propertyMappingConfiguration(provider);
const setHasSigningKp = (ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
};
return html` <ak-wizard-title>${msg("Configure SAML Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
required
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="acsUrl"
value=${ifDefined(provider?.acsUrl)}
required
label=${msg("ACS URL")}
.errorMessages=${errors?.acsUrl ?? []}
></ak-text-input>
<ak-text-input
name="issuer"
value=${provider?.issuer || "authentik"}
required
label=${msg("Issuer")}
help=${msg("Also known as EntityID.")}
.errorMessages=${errors?.issuer ?? []}
></ak-text-input>
<ak-radio-input
name="spBinding"
label=${msg("Service Provider Binding")}
required
.options=${spBindingOptions}
.value=${provider?.spBinding}
help=${msg(
"Determines how authentik sends the response back to the Service Provider.",
)}
>
</ak-radio-input>
<ak-text-input
name="audience"
value=${ifDefined(provider?.audience)}
label=${msg("Audience")}
.errorMessages=${errors?.audience ?? []}
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Signing Certificate")}
name="signingKp"
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.signingKp ?? undefined)}
@input=${(ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
}}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"Certificate used to sign outgoing Responses going to the Service Provider.",
)}
</p>
</ak-form-element-horizontal>
${this.hasSigningKp
? html` <ak-form-element-horizontal name="signAssertion">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.signAssertion, true)}
/>
<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("Sign assertions")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="signResponse">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.signResponse, 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("Sign responses")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>`
: nothing}
<ak-form-element-horizontal
label=${msg("Verification Certificate")}
name="verificationKp"
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.verificationKp ?? undefined)}
nokey
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Encryption Certificate")}
name="encryptionKp"
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.encryptionKp ?? undefined)}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, encrypted assertions will be decrypted using this keypair.",
)}
</p>
</ak-form-element-horizontal>
<ak-multi-select
label=${msg("Property Mappings")}
name="propertyMappings"
.options=${propertyPairs}
.values=${pmValues}
.richhelp=${html` <p class="pf-c-form__helper-text">
${msg("Property mappings used for user mapping.")}
</p>`}
></ak-multi-select>
<ak-form-element-horizontal
label=${msg("NameID Property Mapping")}
name="nameIdMapping"
>
<ak-saml-property-mapping-search
name="nameIdMapping"
propertymapping=${ifDefined(provider?.nameIdMapping ?? undefined)}
></ak-saml-property-mapping-search>
<p class="pf-c-form__helper-text">
${msg(
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
)}
</p>
</ak-form-element-horizontal>
<ak-text-input
name="assertionValidNotBefore"
value=${provider?.assertionValidNotBefore || "minutes=-5"}
required
label=${msg("Assertion valid not before")}
help=${msg(
"Configure the maximum allowed time drift for an assertion.",
)}
.errorMessages=${errors?.assertionValidNotBefore ?? []}
></ak-text-input>
<ak-text-input
name="assertionValidNotOnOrAfter"
value=${provider?.assertionValidNotOnOrAfter || "minutes=5"}
required
label=${msg("Assertion valid not on or after")}
help=${msg(
"Assertion not valid on or after current time + this value.",
)}
.errorMessages=${errors?.assertionValidNotOnOrAfter ?? []}
></ak-text-input>
<ak-text-input
name="sessionValidNotOnOrAfter"
value=${provider?.sessionValidNotOnOrAfter || "minutes=86400"}
required
label=${msg("Session valid not on or after")}
help=${msg("Session not valid on or after current time + this value.")}
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
></ak-text-input>
<ak-radio-input
name="digestAlgorithm"
label=${msg("Digest algorithm")}
required
.options=${digestAlgorithmOptions}
.value=${provider?.digestAlgorithm}
>
</ak-radio-input>
<ak-radio-input
name="signatureAlgorithm"
label=${msg("Signature algorithm")}
required
.options=${signatureAlgorithmOptions}
.value=${provider?.signatureAlgorithm}
>
</ak-radio-input>
</div>
</ak-form-group>
${renderForm(
(this.wizard.provider as SAMLProvider) ?? {},
this.wizard.errors.provider,
setHasSigningKp,
this.hasSigningKp,
)}
</form>`;
}
}

View File

@ -1,21 +1,10 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-core-group-search";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-multi-select";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { renderForm } from "@goauthentik/admin/providers/scim/SCIMProviderFormForm.js";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { PaginatedSCIMMappingList, PropertymappingsApi, type SCIMProvider } from "@goauthentik/api";
import { PaginatedSCIMMappingList, type SCIMProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@ -26,125 +15,15 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
constructor() {
super();
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsProviderScimList({
ordering: "managed",
})
.then((propertyMappings: PaginatedSCIMMappingList) => {
this.propertyMappings = propertyMappings;
});
}
propertyMappingConfiguration(provider?: SCIMProvider) {
const propertyMappings = this.propertyMappings?.results ?? [];
const configuredMappings = (providerMappings: string[]) =>
propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk));
const managedMappings = (key: string) =>
propertyMappings
.filter((pm) => pm.managed === `goauthentik.io/providers/scim/${key}`)
.map((pm) => pm.pk);
const pmUserValues = provider?.propertyMappings
? configuredMappings(provider?.propertyMappings ?? [])
: managedMappings("user");
const pmGroupValues = provider?.propertyMappingsGroup
? configuredMappings(provider?.propertyMappingsGroup ?? [])
: managedMappings("group");
const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]);
return { pmUserValues, pmGroupValues, propertyPairs };
}
render() {
const provider = this.wizard.provider as SCIMProvider | undefined;
const errors = this.wizard.errors.provider;
const { pmUserValues, pmGroupValues, propertyPairs } =
this.propertyMappingConfiguration(provider);
return html`<ak-wizard-title>${msg("Configure SCIM Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="url"
label=${msg("URL")}
value="${first(provider?.url, "")}"
required
help=${msg("SCIM base url, usually ends in /v2.")}
.errorMessages=${errors?.url ?? []}
>
</ak-text-input>
<ak-text-input
name="token"
label=${msg("Token")}
value="${first(provider?.token, "")}"
.errorMessages=${errors?.token ?? []}
required
help=${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
>
</ak-text-input>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="excludeUsersServiceAccount"
?checked=${first(provider?.excludeUsersServiceAccount, true)}
label=${msg("Exclude service accounts")}
></ak-switch-input>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-core-group-search
.group=${provider?.filterGroup}
></ak-core-group-search>
<p class="pf-c-form__helper-text">
${msg("Only sync users within the selected group.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group ?expanded=${true}>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-multi-select
label=${msg("User Property Mappings")}
name="propertyMappings"
.options=${propertyPairs}
.values=${pmUserValues}
.richhelp=${html`
<p class="pf-c-form__helper-text">
${msg("Property mappings used for user mapping.")}
</p>
`}
></ak-multi-select>
<ak-multi-select
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup"
.options=${propertyPairs}
.values=${pmGroupValues}
.richhelp=${html`
<p class="pf-c-form__helper-text">
${msg("Property mappings used for group creation.")}
</p>
`}
></ak-multi-select>
</div>
</ak-form-group>
${renderForm(
(this.wizard.provider as SCIMProvider) ?? {},
this.wizard.errors.provider,
)}
</form>`;
}
}

View File

@ -5,6 +5,7 @@ import {
type LDAPProviderRequest,
type OAuth2ProviderRequest,
type ProvidersSamlImportMetadataCreateRequest,
ProxyMode,
type ProxyProviderRequest,
type RACProviderRequest,
type RadiusProviderRequest,
@ -27,6 +28,7 @@ export interface ApplicationWizardState {
providerModel: string;
app: Partial<ApplicationRequest>;
provider: OneOfProvider;
proxyMode?: ProxyMode;
errors: ValidationError;
}

View File

@ -5,8 +5,8 @@ import "@goauthentik/elements/forms/SearchSelect";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
import { property, query } from "lit/decorators.js";
import { customElement, property, query } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CertificateKeyPair,
@ -114,6 +114,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
render() {
return html`
<ak-search-select
name=${ifDefined(this.name ?? undefined)}
.fetchObjects=${this.fetchObjects}
.renderElement=${renderElement}
.value=${renderValue}

View File

@ -7,6 +7,7 @@ import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"
import { html } from "lit";
import { property, query } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowsApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
import type { Flow, FlowsInstancesListRequest } from "@goauthentik/api";
@ -133,7 +134,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
.renderElement=${renderElement}
.renderDescription=${renderDescription}
.value=${getFlowValue}
.name=${this.name}
name=${ifDefined(this.name ?? undefined)}
@ak-change=${this.handleSearchUpdate}
?blankable=${!this.required}
>

View File

@ -43,8 +43,11 @@ export class ProviderWizard extends AKElement {
@query("ak-wizard")
wizard?: Wizard;
async firstUpdated(): Promise<void> {
this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList();
connectedCallback() {
super.connectedCallback();
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
this.providerTypes = providerTypes;
});
}
render(): TemplateResult {
@ -58,6 +61,7 @@ export class ProviderWizard extends AKElement {
}}
>
<ak-wizard-page-type-create
name="selectProviderType"
slot="initial"
layout=${TypeCreateWizardPageLayouts.grid}
.types=${this.providerTypes}

View File

@ -2,24 +2,17 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
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 {
FlowsInstancesListDesignationEnum,
LDAPAPIAccessMode,
LDAPProvider,
ProvidersApi,
} from "@goauthentik/api";
import { LDAPProvider, ProvidersApi } from "@goauthentik/api";
import { renderForm } from "./LDAPProviderFormForm.js";
@customElement("ak-provider-ldap-form")
export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPProvider>) {
@ -42,212 +35,8 @@ export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPP
}
}
// All Provider objects have an Authorization flow, but not all providers have an Authentication
// flow. LDAP needs only one field, but it is not an Authorization field, it is an
// Authentication field. So, yeah, we're using the authorization field to store the
// authentication information, which is why the ak-branded-flow-search call down there looks so
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
// field of the target Provider.
renderForm(): TemplateResult {
return html` <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 label=${msg("Bind mode")} name="bindMode">
<ak-radio
.options=${[
{
label: msg("Cached binding"),
value: LDAPAPIAccessMode.Cached,
default: true,
description: html`${msg(
"Flow is executed and session is cached in memory. Flow is executed when session expires",
)}`,
},
{
label: msg("Direct binding"),
value: LDAPAPIAccessMode.Direct,
description: html`${msg(
"Always execute the configured bind flow to authenticate the user",
)}`,
},
]}
.value=${this.instance?.bindMode}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg("Configure how the outpost authenticates requests.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Search mode")} name="searchMode">
<ak-radio
.options=${[
{
label: msg("Cached querying"),
value: LDAPAPIAccessMode.Cached,
default: true,
description: html`${msg(
"The outpost holds all users and groups in-memory and will refresh every 5 Minutes",
)}`,
},
{
label: msg("Direct querying"),
value: LDAPAPIAccessMode.Direct,
description: html`${msg(
"Always returns the latest data, but slower than cached querying",
)}`,
},
]}
.value=${this.instance?.searchMode}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg("Configure how the outpost queries the core authentik server's users.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="mfaSupport">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.mfaSupport, true)}
/>
<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("Code-based MFA Support")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Bind flow")}
name="authorizationFlow"
required
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authorizationFlow}
.brandFlow=${this.brand?.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for users to authenticate.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Unbind flow")}
name="invalidationFlow"
required
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
.brandFlow=${this.brand.flowInvalidation}
defaultFlowSlug="default-invalidation-flow"
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for unbinding users.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Base DN")}
?required=${true}
name="baseDn"
>
<input
type="text"
value="${first(this.instance?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"LDAP DN under which bind requests and search requests can be made.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${this.instance?.certificate}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("TLS Server name")}
name="tlsServerName"
>
<input
type="text"
value="${first(this.instance?.tlsServerName, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"DNS name for which the above configured certificate should be used. The certificate cannot be detected based on the base DN, as the SSL/TLS negotiation happens before such data is exchanged.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("UID start number")}
?required=${true}
name="uidStartNumber"
>
<input
type="number"
value="${first(this.instance?.uidStartNumber, 2000)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("GID start number")}
?required=${true}
name="gidStartNumber"
>
<input
type="number"
value="${first(this.instance?.gidStartNumber, 4000)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
renderForm() {
return renderForm(this.instance ?? {}, [], this.brand);
}
}

View File

@ -0,0 +1,178 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CurrentBrand,
FlowsInstancesListDesignationEnum,
LDAPProvider,
ValidationError,
} from "@goauthentik/api";
import {
bindModeOptions,
cryptoCertificateHelp,
gidStartNumberHelp,
mfaSupportHelp,
searchModeOptions,
tlsServerNameHelp,
uidStartNumberHelp,
} from "./LDAPOptionsAndHelp.js";
// All Provider objects have an Authorization flow, but not all providers have an Authentication
// flow. LDAP needs only one field, but it is not an Authorization field, it is an Authentication
// field. So, yeah, we're using the authorization field to store the authentication information,
// which is why the ak-branded-flow-search call down there looks so weird-- we're looking up
// Authentication flows, but we're storing them in the Authorization field of the target Provider.
export function renderForm(
provider?: Partial<LDAPProvider>,
errors: ValidationError = {},
brand?: CurrentBrand,
) {
return html`
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
help=${msg("Method's display Name.")}
></ak-text-input>
<ak-radio-input
label=${msg("Bind mode")}
name="bindMode"
.options=${bindModeOptions}
.value=${provider?.bindMode}
help=${msg("Configure how the outpost authenticates requests.")}
>
</ak-radio-input>
<ak-radio-input
label=${msg("Search mode")}
name="searchMode"
.options=${searchModeOptions}
.value=${provider?.searchMode}
help=${msg("Configure how the outpost queries the core authentik server's users.")}
>
</ak-radio-input>
<ak-switch-input
name="mfaSupport"
label=${msg("Code-based MFA Support")}
?checked=${provider?.mfaSupport ?? true}
help=${mfaSupportHelp}
>
</ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Bind flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for users to authenticate.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Unbind flow")}
name="invalidationFlow"
required
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.brandFlow=${brand?.flowInvalidation}
defaultFlowSlug="default-invalidation-flow"
.errorMessages=${errors?.invalidationFlow ?? []}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for unbinding users.")}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="baseDn"
label=${msg("Base DN")}
required
value="${provider?.baseDn ?? "DC=ldap,DC=goauthentik,DC=io"}"
.errorMessages=${errors?.baseDn ?? []}
help=${msg(
"LDAP DN under which bind requests and search requests can be made.",
)}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Certificate")}
name="certificate"
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.certificate ?? nothing)}
name="certificate"
>
</ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
</ak-form-element-horizontal>
<ak-text-input
label=${msg("TLS Server name")}
name="tlsServerName"
value="${provider?.tlsServerName ?? ""}"
.errorMessages=${errors?.tlsServerName ?? []}
help=${tlsServerNameHelp}
></ak-text-input>
<ak-number-input
label=${msg("UID start number")}
required
name="uidStartNumber"
value="${provider?.uidStartNumber ?? 2000}"
.errorMessages=${errors?.uidStartNumber ?? []}
help=${uidStartNumberHelp}
></ak-number-input>
<ak-number-input
label=${msg("GID start number")}
required
name="gidStartNumber"
value="${provider?.gidStartNumber ?? 4000}"
.errorMessages=${errors?.gidStartNumber ?? []}
help=${gidStartNumberHelp}
></ak-number-input>
</div>
</ak-form-group>
`;
}

View File

@ -1,12 +1,5 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import {
IRedirectURIInput,
akOAuthRedirectURIInput,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
@ -19,105 +12,12 @@ import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { TemplateResult, css, html } from "lit";
import { css } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
ClientTypeEnum,
FlowsInstancesListDesignationEnum,
IssuerModeEnum,
MatchingModeEnum,
OAuth2Provider,
ProvidersApi,
RedirectURI,
SubModeEnum,
} from "@goauthentik/api";
import { ClientTypeEnum, OAuth2Provider, ProvidersApi } from "@goauthentik/api";
import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "./OAuth2PropertyMappings.js";
import { makeSourceSelector, oauth2SourcesProvider } from "./OAuth2Sources.js";
export const clientTypeOptions = [
{
label: msg("Confidential"),
value: ClientTypeEnum.Confidential,
default: true,
description: html`${msg(
"Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
)}`,
},
{
label: msg("Public"),
value: ClientTypeEnum.Public,
description: html`${msg(
"Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
)}`,
},
];
export const subjectModeOptions = [
{
label: msg("Based on the User's hashed ID"),
value: SubModeEnum.HashedUserId,
default: true,
},
{
label: msg("Based on the User's ID"),
value: SubModeEnum.UserId,
},
{
label: msg("Based on the User's UUID"),
value: SubModeEnum.UserUuid,
},
{
label: msg("Based on the User's username"),
value: SubModeEnum.UserUsername,
},
{
label: msg("Based on the User's Email"),
value: SubModeEnum.UserEmail,
description: html`${msg("This is recommended over the UPN mode.")}`,
},
{
label: msg("Based on the User's UPN"),
value: SubModeEnum.UserUpn,
description: html`${msg(
"Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
)}`,
},
];
export const issuerModeOptions = [
{
label: msg("Each provider has a different issuer, based on the application slug"),
value: IssuerModeEnum.PerProvider,
default: true,
},
{
label: msg("Same identifier is used for all providers"),
value: IssuerModeEnum.Global,
},
];
const redirectUriHelpMessages = [
msg(
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
),
msg(
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
),
msg(
'To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.',
),
];
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
)}`;
import { renderForm } from "./OAuth2ProviderFormForm.js";
/**
* Form page for OAuth2 Authentication Method
@ -131,9 +31,6 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
@state()
showClientSecret = true;
@state()
redirectUris: RedirectURI[] = [];
static get styles() {
return super.styles.concat(css`
ak-array-input {
@ -147,7 +44,6 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
id: pk,
});
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
this.redirectUris = provider.redirectUris;
return provider;
}
@ -164,245 +60,11 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
}
}
renderForm(): TemplateResult {
const provider = this.instance;
return html` <ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
required
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
?required=${true}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-radio-input
name="clientType"
label=${msg("Client type")}
.value=${provider?.clientType}
required
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
}}
.options=${clientTypeOptions}
>
</ak-radio-input>
<ak-text-input
name="clientId"
label=${msg("Client ID")}
value="${first(
provider?.clientId,
randomString(40, ascii_letters + digits),
)}"
required
>
</ak-text-input>
<ak-text-input
name="clientSecret"
label=${msg("Client Secret")}
value="${first(
provider?.clientSecret,
randomString(128, ascii_letters + digits),
)}"
?hidden=${!this.showClientSecret}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Redirect URIs/Origins")}
required
name="redirectUris"
>
<ak-array-input
.items=${this.instance?.redirectUris ?? []}
.newItem=${() => ({ matchingMode: MatchingModeEnum.Strict, url: "" })}
.row=${(f?: RedirectURI) =>
akOAuthRedirectURIInput({
".redirectURI": f,
"style": "width: 100%",
"name": "oauth2-redirect-uri",
} as unknown as IRedirectURIInput)}
>
</ak-array-input>
${redirectUriHelp}
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
certificate=${ifDefined(this.instance?.signingKey ?? undefined)}
singleton
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey">
<!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
certificate=${ifDefined(this.instance?.encryptionKey ?? undefined)}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg("Key used to encrypt the tokens.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="accessCodeValidity"
label=${msg("Access code validity")}
required
value="${first(provider?.accessCodeValidity, "minutes=1")}"
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Configure how long access codes are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="accessTokenValidity"
label=${msg("Access Token validity")}
value="${first(provider?.accessTokenValidity, "minutes=5")}"
required
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long access tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="refreshTokenValidity"
label=${msg("Refresh Token validity")}
value="${first(provider?.refreshTokenValidity, "days=30")}"
?required=${true}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long refresh tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
<ak-dual-select-dynamic-selected
.provider=${oauth2PropertyMappingsProvider}
.selector=${makeOAuth2PropertyMappingsSelector(
provider?.propertyMappings,
)}
available-label=${msg("Available Scopes")}
selected-label=${msg("Selected Scopes")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
)}
</p>
</ak-form-element-horizontal>
<ak-radio-input
name="subMode"
label=${msg("Subject mode")}
required
.options=${subjectModeOptions}
.value=${provider?.subMode}
help=${msg(
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
)}
>
</ak-radio-input>
<ak-switch-input
name="includeClaimsInIdToken"
label=${msg("Include claims in id_token")}
?checked=${first(provider?.includeClaimsInIdToken, true)}
help=${msg(
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
)}
></ak-switch-input>
<ak-radio-input
name="issuerMode"
label=${msg("Issuer mode")}
required
.options=${issuerModeOptions}
.value=${provider?.issuerMode}
help=${msg(
"Configure how the issuer field of the ID Token should be filled.",
)}
>
</ak-radio-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
>
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
renderForm() {
const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show;
};
return renderForm(this.instance ?? {}, [], this.showClientSecret, showClientSecretCallback);
}
}

View File

@ -0,0 +1,350 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import {
IRedirectURIInput,
akOAuthRedirectURIInput,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/ak-array-input.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
ClientTypeEnum,
FlowsInstancesListDesignationEnum,
IssuerModeEnum,
MatchingModeEnum,
OAuth2Provider,
RedirectURI,
SubModeEnum,
ValidationError,
} from "@goauthentik/api";
import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "./OAuth2PropertyMappings.js";
import { makeSourceSelector, oauth2SourcesProvider } from "./OAuth2Sources.js";
export const clientTypeOptions = [
{
label: msg("Confidential"),
value: ClientTypeEnum.Confidential,
default: true,
description: html`${msg(
"Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
)}`,
},
{
label: msg("Public"),
value: ClientTypeEnum.Public,
description: html`${msg(
"Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
)}`,
},
];
export const subjectModeOptions = [
{
label: msg("Based on the User's hashed ID"),
value: SubModeEnum.HashedUserId,
default: true,
},
{
label: msg("Based on the User's ID"),
value: SubModeEnum.UserId,
},
{
label: msg("Based on the User's UUID"),
value: SubModeEnum.UserUuid,
},
{
label: msg("Based on the User's username"),
value: SubModeEnum.UserUsername,
},
{
label: msg("Based on the User's Email"),
value: SubModeEnum.UserEmail,
description: html`${msg("This is recommended over the UPN mode.")}`,
},
{
label: msg("Based on the User's UPN"),
value: SubModeEnum.UserUpn,
description: html`${msg(
"Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
)}`,
},
];
export const issuerModeOptions = [
{
label: msg("Each provider has a different issuer, based on the application slug"),
value: IssuerModeEnum.PerProvider,
default: true,
},
{
label: msg("Same identifier is used for all providers"),
value: IssuerModeEnum.Global,
},
];
const redirectUriHelpMessages = [
msg(
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
),
msg(
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
),
msg(
'To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.',
),
];
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
)}`;
type ShowClientSecret = (show: boolean) => void;
const defaultShowClientSecret: ShowClientSecret = (_show) => undefined;
export function renderForm(
provider: Partial<OAuth2Provider>,
errors: ValidationError,
showClientSecret = false,
showClientSecretCallback: ShowClientSecret = defaultShowClientSecret,
) {
return html` <ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
required
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
?required=${true}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-radio-input
name="clientType"
label=${msg("Client type")}
.value=${provider?.clientType}
required
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
showClientSecretCallback(ev.detail.value !== ClientTypeEnum.Public);
}}
.options=${clientTypeOptions}
>
</ak-radio-input>
<ak-text-input
name="clientId"
label=${msg("Client ID")}
value="${provider?.clientId ?? randomString(40, ascii_letters + digits)}"
required
>
</ak-text-input>
<ak-text-input
name="clientSecret"
label=${msg("Client Secret")}
value="${provider?.clientSecret ?? randomString(128, ascii_letters + digits)}"
?hidden=${!showClientSecret}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Redirect URIs/Origins")}
required
name="redirectUris"
>
<ak-array-input
name="redirectUris"
.items=${provider?.redirectUris ?? []}
.newItem=${() => ({ matchingMode: MatchingModeEnum.Strict, url: "" })}
.row=${(f?: RedirectURI) =>
akOAuthRedirectURIInput({
".redirectURI": f,
"style": "width: 100%",
"name": "oauth2-redirect-uri",
} as unknown as IRedirectURIInput)}
>
</ak-array-input>
${redirectUriHelp}
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.signingKey ?? undefined)}
singleton
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey">
<!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.encryptionKey ?? undefined)}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to encrypt the tokens.")}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="accessCodeValidity"
label=${msg("Access code validity")}
required
value="${provider?.accessCodeValidity ?? "minutes=1"}"
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Configure how long access codes are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="accessTokenValidity"
label=${msg("Access Token validity")}
value="${provider?.accessTokenValidity ?? "minutes=5"}"
required
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long access tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="refreshTokenValidity"
label=${msg("Refresh Token validity")}
value="${provider?.refreshTokenValidity ?? "days=30"}"
?required=${true}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long refresh tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
<ak-dual-select-dynamic-selected
.provider=${oauth2PropertyMappingsProvider}
.selector=${makeOAuth2PropertyMappingsSelector(provider?.propertyMappings)}
available-label=${msg("Available Scopes")}
selected-label=${msg("Selected Scopes")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
)}
</p>
</ak-form-element-horizontal>
<ak-radio-input
name="subMode"
label=${msg("Subject mode")}
required
.options=${subjectModeOptions}
.value=${provider?.subMode}
help=${msg(
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
)}
>
</ak-radio-input>
<ak-switch-input
name="includeClaimsInIdToken"
label=${msg("Include claims in id_token")}
?checked=${provider?.includeClaimsInIdToken ?? true}
help=${msg(
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
)}
></ak-switch-input>
<ak-radio-input
name="issuerMode"
label=${msg("Issuer mode")}
required
.options=${issuerModeOptions}
.value=${provider?.issuerMode}
help=${msg("Configure how the issuer field of the ID Token should be filled.")}
>
</ak-radio-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Trusted OIDC Sources")} name="jwksSources">
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}

View File

@ -1,39 +1,18 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { CSSResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
import {
FlowsInstancesListDesignationEnum,
ProvidersApi,
ProxyMode,
ProxyProvider,
} from "@goauthentik/api";
import { ProvidersApi, ProxyMode, ProxyProvider } from "@goauthentik/api";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "./ProxyProviderPropertyMappings.js";
import { SetMode, SetShowHttpBasic, renderForm } from "./ProxyProviderFormForm.js";
@customElement("ak-provider-proxy-form")
export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
@ -45,8 +24,8 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
const provider = await new ProvidersApi(DEFAULT_CONFIG).providersProxyRetrieve({
id: pk,
});
this.showHttpBasic = first(provider.basicAuthEnabled, true);
this.mode = first(provider.mode, ProxyMode.Proxy);
this.showHttpBasic = provider.basicAuthEnabled ?? true;
this.mode = provider.mode ?? ProxyMode.Proxy;
return provider;
}
@ -73,376 +52,22 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
}
}
renderHttpBasic(): TemplateResult {
return html`<ak-text-input
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
help=${msg(
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
)}
>
</ak-text-input>
<ak-text-input
name="basicAuthPasswordAttribute"
label=${msg("HTTP-Basic Password Key")}
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
help=${msg(
"User/Group Attribute used for the password part of the HTTP-Basic Header.",
)}
>
</ak-text-input>`;
}
renderModeSelector(): TemplateResult {
const setMode = (ev: CustomEvent<{ value: ProxyMode }>) => {
renderForm() {
const onSetMode: SetMode = (ev) => {
this.mode = ev.detail.value;
};
// prettier-ignore
return html`
<ak-toggle-group value=${this.mode} @ak-toggle=${setMode}>
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>
</ak-toggle-group>
`;
}
const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
};
renderSettings(): TemplateResult {
switch (this.mode) {
case ProxyMode.Proxy:
return html`<p class="pf-u-mb-xl">
${msg(
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
)}
</p>
<ak-form-element-horizontal
label=${msg("External host")}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Internal host")}
?required=${true}
name="internalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.internalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Upstream host that the requests are forwarded to.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="internalHostSslValidation">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.internalHostSslValidation, true)}
/>
<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("Internal host SSL Validation")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg("Validate SSL Certificates of upstream servers.")}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardSingle:
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).",
)}
</p>
<ak-form-element-horizontal
label=${msg("External host")}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardDomain:
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
)}
</p>
<div class="pf-u-mb-xl">
${msg("An example setup can look like this:")}
<ul class="pf-c-list">
<li>${msg("authentik running on auth.example.com")}</li>
<li>${msg("app1 running on app1.example.com")}</li>
</ul>
${msg(
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
)}
</div>
<ak-form-element-horizontal
label=${msg("Authentication URL")}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${first(this.instance?.externalHost, window.location.origin)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Cookie domain")}
name="cookieDomain"
?required=${true}
>
<input
type="text"
value="${ifDefined(this.instance?.cookieDomain)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.UnknownDefaultOpenApi:
return html`<p>${msg("Unknown proxy mode")}</p>`;
}
}
renderForm(): TemplateResult {
return html`
<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
label=${msg("Authorization flow")}
required
name="authorizationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body">${this.renderModeSelector()}</div>
<div class="pf-c-card__footer">${this.renderSettings()}</div>
</div>
<ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity">
<input
type="text"
value="${first(this.instance?.accessTokenValidity, "hours=24")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg("Configure how long tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${this.instance?.certificate}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Additional scopes")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${proxyPropertyMappingsProvider}
.selector=${makeProxyPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label="${msg("Available Scopes")}"
selected-label="${msg("Selected Scopes")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Additional scope mappings, which are passed to the proxy.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label="${this.mode === ProxyMode.ForwardDomain
? msg("Unauthenticated URLs")
: msg("Unauthenticated Paths")}"
name="skipPathRegex"
>
<textarea class="pf-c-form-control">
${this.instance?.skipPathRegex}</textarea
>
<p class="pf-c-form__helper-text">
${msg(
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="interceptHeaderAuth">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.interceptHeaderAuth, true)}
/>
<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("Intercept header authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="basicAuthEnabled">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.basicAuthEnabled, false)}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
}}
/>
<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("Send HTTP-Basic Authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
</p>
</ak-form-element-horizontal>
${this.showHttpBasic ? this.renderHttpBasic() : html``}
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
>
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
?required=${false}
name="authenticationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
return renderForm(this.instance ?? {}, [], {
mode: this.mode,
onSetMode,
showHttpBasic: this.showHttpBasic,
onSetShowHttpBasic,
});
}
}

View File

@ -0,0 +1,343 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { match } from "ts-pattern";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
ProxyMode,
ProxyProvider,
ValidationError,
} from "@goauthentik/api";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "./ProxyProviderPropertyMappings.js";
export type ProxyModeValue = { value: ProxyMode };
export type SetMode = (ev: CustomEvent<ProxyModeValue>) => void;
export type SetShowHttpBasic = (ev: Event) => void;
export interface ProxyModeExtraArgs {
mode: ProxyMode;
onSetMode: SetMode;
showHttpBasic: boolean;
onSetShowHttpBasic: SetShowHttpBasic;
}
function renderHttpBasic(provider: Partial<ProxyProvider>) {
return html`<ak-text-input
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
value="${ifDefined(provider?.basicAuthUserAttribute)}"
help=${msg(
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
)}
>
</ak-text-input>
<ak-text-input
name="basicAuthPasswordAttribute"
label=${msg("HTTP-Basic Password Key")}
value="${ifDefined(provider?.basicAuthPasswordAttribute)}"
help=${msg("User/Group Attribute used for the password part of the HTTP-Basic Header.")}
>
</ak-text-input>`;
}
function renderModeSelector(mode: ProxyMode, onSet: SetMode) {
// prettier-ignore
return html` <ak-toggle-group
value=${mode}
@ak-toggle=${onSet}
data-ouid-component-name="proxy-type-toggle"
>
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>
</ak-toggle-group>`;
}
function renderProxySettings(provider: Partial<ProxyProvider>, errors?: ValidationError) {
return html`<p class="pf-u-mb-xl">
${msg(
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
)}
</p>
<ak-text-input
name="externalHost"
label=${msg("External host")}
value="${ifDefined(provider?.externalHost)}"
required
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>
<ak-text-input
name="internalHost"
label=${msg("Internal host")}
value="${ifDefined(provider?.internalHost)}"
required
.errorMessages=${errors?.internalHost ?? []}
help=${msg("Upstream host that the requests are forwarded to.")}
></ak-text-input>
<ak-switch-input
name="internalHostSslValidation"
label=${msg("Internal host SSL Validation")}
?checked=${provider?.internalHostSslValidation ?? true}
help=${msg("Validate SSL Certificates of upstream servers.")}
>
</ak-switch-input>`;
}
function renderForwardSingleSettings(provider: Partial<ProxyProvider>, errors?: ValidationError) {
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).",
)}
</p>
<ak-text-input
name="externalHost"
label=${msg("External host")}
value="${ifDefined(provider?.externalHost)}"
required
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>`;
}
function renderForwardDomainSettings(provider: Partial<ProxyProvider>, errors?: ValidationError) {
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
)}
</p>
<div class="pf-u-mb-xl">
${msg("An example setup can look like this:")}
<ul class="pf-c-list">
<li>${msg("authentik running on auth.example.com")}</li>
<li>${msg("app1 running on app1.example.com")}</li>
</ul>
${msg(
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
)}
</div>
<ak-text-input
name="externalHost"
label=${msg("Authentication URL")}
value="${provider?.externalHost ?? window.location.origin}"
required
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
></ak-text-input>
<ak-text-input
label=${msg("Cookie domain")}
name="cookieDomain"
value="${ifDefined(provider?.cookieDomain)}"
required
.errorMessages=${errors?.cookieDomain ?? []}
help=${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
></ak-text-input> `;
}
type StrictProxyMode = Omit<ProxyMode, "11184809">;
function renderSettings(provider: Partial<ProxyProvider>, mode: ProxyMode) {
return match(mode as StrictProxyMode)
.with(ProxyMode.Proxy, () => renderProxySettings(provider))
.with(ProxyMode.ForwardSingle, () => renderForwardSingleSettings(provider))
.with(ProxyMode.ForwardDomain, () => renderForwardDomainSettings(provider))
.otherwise(() => {
throw new Error("Unrecognized proxy mode");
});
}
export function renderForm(
provider: Partial<ProxyProvider> = {},
errors: ValidationError = {},
args: ProxyModeExtraArgs,
) {
const { mode, onSetMode, showHttpBasic, onSetShowHttpBasic } = args;
return html`
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
name="authorizationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body">${renderModeSelector(mode, onSetMode)}</div>
<div class="pf-c-card__footer">${renderSettings(provider, mode)}</div>
</div>
<ak-text-input
label=${msg("Token validity")}
name="accessTokenValidity"
value="${provider?.accessTokenValidity ?? "hours=24"}"
.errorMessages=${errors?.accessTokenValidity ?? []}
required
.help=${msg("Configure how long tokens are valid for.")}
></ak-text-input>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${provider?.certificate}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Additional scopes")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${proxyPropertyMappingsProvider}
.selector=${makeProxyPropertyMappingsSelector(provider?.propertyMappings)}
available-label="${msg("Available Scopes")}"
selected-label="${msg("Selected Scopes")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Additional scope mappings, which are passed to the proxy.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label="${mode === ProxyMode.ForwardDomain
? msg("Unauthenticated URLs")
: msg("Unauthenticated Paths")}"
name="skipPathRegex"
>
<textarea class="pf-c-form-control">${provider?.skipPathRegex}</textarea>
<p class="pf-c-form__helper-text">
${msg(
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="interceptHeaderAuth"
label=${msg("Intercept header authentication")}
?checked=${provider?.interceptHeaderAuth ?? true}
help=${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
>
</ak-switch-input>
<ak-switch-input
name="basicAuthEnabled"
label=${msg("Send HTTP-Basic Authentication")}
?checked=${provider?.basicAuthEnabled ?? false}
help=${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
@change=${onSetShowHttpBasic}
>
</ak-switch-input>
${showHttpBasic ? renderHttpBasic(provider) : nothing}
<ak-form-element-horizontal label=${msg("Trusted OIDC Sources")} name="jwksSources">
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}

View File

@ -1,48 +1,12 @@
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { ifDefined } from "lit-html/directives/if-defined.js";
import { customElement } from "lit/decorators.js";
import {
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
ProvidersApi,
RadiusProvider,
RadiusProviderPropertyMapping,
} from "@goauthentik/api";
import { ProvidersApi, RadiusProvider } from "@goauthentik/api";
export async function radiusPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderRadiusList({
ordering: "name",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeRadiusPropertyMappingsSelector(instanceMappings?: string[]) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, _]: DualSelectPair<RadiusProviderPropertyMapping>) => [];
}
import { renderForm } from "./RadiusProviderFormForm.js";
@customElement("ak-provider-radius-form")
export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm<RadiusProvider>) {
@ -65,127 +29,8 @@ export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm<Rad
}
}
// All Provider objects have an Authorization flow, but not all providers have an Authentication
// flow. Radius needs only one field, but it is not the Authorization field, it is an
// Authentication field. So, yeah, we're using the authorization field to store the
// authentication information, which is why the ak-branded-flow-search call down there looks so
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
// field of the target Provider.
renderForm(): TemplateResult {
return html`
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Authentication flow")}
required
name="authorizationFlow"
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authorizationFlow}
.brandFlow=${this.brand?.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="mfaSupport">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.mfaSupport, true)}
/>
<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("Code-based MFA Support")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Shared secret")}
required
name="sharedSecret"
>
<input
type="text"
value="${first(
this.instance?.sharedSecret,
randomString(128, ascii_letters + digits),
)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Client Networks")}
required
name="clientNetworks"
>
<input
type="text"
value="${first(this.instance?.clientNetworks, "0.0.0.0/0, ::/0")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(`List of CIDRs (comma-seperated) that clients can connect from. A more specific
CIDR will match before a looser one. Clients connecting from a non-specified CIDR
will be dropped.`)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${radiusPropertyMappingsProvider}
.selector=${makeRadiusPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label=${msg("Available Property Mappings")}
selected-label=${msg("Selected Property Mappings")}
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div></ak-form-group
>
`;
renderForm() {
return renderForm(this.instance ?? {}, [], this.brand);
}
}

View File

@ -0,0 +1,154 @@
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CurrentBrand,
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
RadiusProvider,
RadiusProviderPropertyMapping,
ValidationError,
} from "@goauthentik/api";
export async function radiusPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderRadiusList({
ordering: "name",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeRadiusPropertyMappingsSelector(instanceMappings?: string[]) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, _]: DualSelectPair<RadiusProviderPropertyMapping>) => [];
}
const mfaSupportHelp = msg(
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
);
const clientNetworksHelp = msg(
"List of CIDRs (comma-seperated) that clients can connect from. A more specific CIDR will match before a looser one. Clients connecting from a non-specified CIDR will be dropped.",
);
// All Provider objects have an Authorization flow, but not all providers have an Authentication
// flow. Radius needs only one field, but it is not the Authorization field, it is an
// Authentication field. So, yeah, we're using the authorization field to store the
// authentication information, which is why the ak-branded-flow-search call down there looks so
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
// field of the target Provider.
export function renderForm(
provider?: Partial<RadiusProvider>,
errors: ValidationError = {},
brand?: CurrentBrand,
) {
return html`
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Authentication flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
</ak-form-element-horizontal>
<ak-switch-input
name="mfaSupport"
label=${msg("Code-based MFA Support")}
?checked=${provider?.mfaSupport ?? true}
help=${mfaSupportHelp}
>
</ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="sharedSecret"
label=${msg("Shared secret")}
.errorMessages=${errors?.sharedSecret ?? []}
value=${first(
provider?.sharedSecret,
randomString(128, ascii_letters + digits),
)}
required
></ak-text-input>
<ak-text-input
name="clientNetworks"
label=${msg("Client Networks")}
value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
.errorMessages=${errors?.clientNetworks ?? []}
required
help=${clientNetworksHelp}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${radiusPropertyMappingsProvider}
.selector=${makeRadiusPropertyMappingsSelector(provider?.propertyMappings)}
available-label=${msg("Available Property Mappings")}
selected-label=${msg("Selected Property Mappings")}
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.errorMessages=${errors?.invalidationFlow ?? []}
defaultFlowSlug="default-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div></ak-form-group
>
`;
}

View File

@ -1,58 +1,12 @@
import {
digestAlgorithmOptions,
signatureAlgorithmOptions,
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { type AkCryptoCertificateSearch } from "@goauthentik/admin/common/ak-crypto-certificate-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
PropertymappingsProviderSamlListRequest,
ProvidersApi,
SAMLPropertyMapping,
SAMLProvider,
SpBindingEnum,
} from "@goauthentik/api";
import { ProvidersApi, SAMLProvider } from "@goauthentik/api";
export async function samlPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderSamlList({
ordering: "saml_name",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SAMLPropertyMapping>) =>
mapping?.managed?.startsWith("goauthentik.io/providers/saml");
}
import { renderForm } from "./SAMLProviderFormForm.js";
@customElement("ak-provider-saml-form")
export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
@ -80,368 +34,14 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
}
}
renderForm(): TemplateResult {
return html` <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
label=${msg("Authorization flow")}
required
name="authorizationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
renderForm() {
const setHasSigningKp = (ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
};
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("ACS URL")}
?required=${true}
name="acsUrl"
>
<input
type="text"
value="${ifDefined(this.instance?.acsUrl)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Issuer")}
?required=${true}
name="issuer"
>
<input
type="text"
value="${this.instance?.issuer || "authentik"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">${msg("Also known as EntityID.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Service Provider Binding")}
?required=${true}
name="spBinding"
>
<ak-radio
.options=${[
{
label: msg("Redirect"),
value: SpBindingEnum.Redirect,
default: true,
},
{
label: msg("Post"),
value: SpBindingEnum.Post,
},
]}
.value=${this.instance?.spBinding}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg(
"Determines how authentik sends the response back to the Service Provider.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Audience")} name="audience">
<input
type="text"
value="${ifDefined(this.instance?.audience)}"
class="pf-c-form-control"
/>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
?required=${false}
name="authenticationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Signing Certificate")}
name="signingKp"
>
<ak-crypto-certificate-search
.certificate=${this.instance?.signingKp}
@input=${(ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
}}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"Certificate used to sign outgoing Responses going to the Service Provider.",
)}
</p>
</ak-form-element-horizontal>
${this.hasSigningKp
? html` <ak-form-element-horizontal name="signAssertion">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.signAssertion, true)}
/>
<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("Sign assertions")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="signResponse">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.signResponse, 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("Sign responses")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>`
: nothing}
<ak-form-element-horizontal
label=${msg("Verification Certificate")}
name="verificationKp"
>
<ak-crypto-certificate-search
.certificate=${this.instance?.verificationKp}
nokey
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Encryption Certificate")}
name="encryptionKp"
>
<ak-crypto-certificate-search
.certificate=${this.instance?.encryptionKp}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, assertions will be encrypted using this keypair.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${samlPropertyMappingsProvider}
.selector=${makeSAMLPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("NameID Property Mapping")}
name="nameIdMapping"
>
<ak-search-select
.fetchObjects=${async (
query?: string,
): Promise<SAMLPropertyMapping[]> => {
const args: PropertymappingsProviderSamlListRequest = {
ordering: "saml_name",
};
if (query !== undefined) {
args.search = query;
}
const items = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderSamlList(args);
return items.results;
}}
.renderElement=${(item: SAMLPropertyMapping): string => {
return item.name;
}}
.value=${(
item: SAMLPropertyMapping | undefined,
): string | undefined => {
return item?.pk;
}}
.selected=${(item: SAMLPropertyMapping): boolean => {
return this.instance?.nameIdMapping === item.pk;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Assertion valid not before")}
?required=${true}
name="assertionValidNotBefore"
>
<input
type="text"
value="${this.instance?.assertionValidNotBefore || "minutes=-5"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Configure the maximum allowed time drift for an assertion.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Assertion valid not on or after")}
?required=${true}
name="assertionValidNotOnOrAfter"
>
<input
type="text"
value="${this.instance?.assertionValidNotOnOrAfter || "minutes=5"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Assertion not valid on or after current time + this value.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Session valid not on or after")}
?required=${true}
name="sessionValidNotOnOrAfter"
>
<input
type="text"
value="${this.instance?.sessionValidNotOnOrAfter || "minutes=86400"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Session not valid on or after current time + this value.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Default relay state")}
?required=${true}
name="defaultRelayState"
>
<input
type="text"
value="${this.instance?.defaultRelayState || ""}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"When using IDP-initiated logins, the relay state will be set to this value.",
)}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Digest algorithm")}
?required=${true}
name="digestAlgorithm"
>
<ak-radio
.options=${digestAlgorithmOptions}
.value=${this.instance?.digestAlgorithm}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Signature algorithm")}
?required=${true}
name="signatureAlgorithm"
>
<ak-radio
.options=${signatureAlgorithmOptions}
.value=${this.instance?.signatureAlgorithm}
>
</ak-radio>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
return renderForm(this.instance ?? {}, [], setHasSigningKp, this.hasSigningKp);
}
}

View File

@ -0,0 +1,330 @@
import {
digestAlgorithmOptions,
signatureAlgorithmOptions,
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
PropertymappingsProviderSamlListRequest,
SAMLPropertyMapping,
SAMLProvider,
SpBindingEnum,
ValidationError,
} from "@goauthentik/api";
export async function samlPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderSamlList({
ordering: "saml_name",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SAMLPropertyMapping>) =>
mapping?.managed?.startsWith("goauthentik.io/providers/saml");
}
const serviceProviderBindingOptions = [
{
label: msg("Redirect"),
value: SpBindingEnum.Redirect,
default: true,
},
{
label: msg("Post"),
value: SpBindingEnum.Post,
},
];
function renderHasSigningKp(provider?: Partial<SAMLProvider>) {
return html` <ak-switch-input
name="signAssertion"
label=${msg("Sign assertions")}
?checked=${provider?.signAssertion ?? true}
help=${msg("When enabled, the assertion element of the SAML response will be signed.")}
>
</ak-switch-input>
<ak-switch-input
name="signResponse"
label=${msg("Sign responses")}
?checked=${provider?.signResponse ?? false}
help=${msg("When enabled, the assertion element of the SAML response will be signed.")}
>
</ak-switch-input>`;
}
export function renderForm(
provider: Partial<SAMLProvider> = {},
errors: ValidationError,
setHasSigningKp: (ev: InputEvent) => void,
hasSigningKp: boolean,
) {
return html` <ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
required
.errorMessages=${errors?.name ?? []}
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
.errorMessages=${errors?.authorizationFlow ?? []}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="acsUrl"
label=${msg("ACS URL")}
value="${ifDefined(provider?.acsUrl)}"
required
.errorMessages=${errors?.acsUrl ?? []}
></ak-text-input>
<ak-text-input
label=${msg("Issuer")}
name="issuer"
value="${provider?.issuer || "authentik"}"
required
.errorMessages=${errors?.issuer ?? []}
help=${msg("Also known as EntityID.")}
></ak-text-input>
<ak-radio-input
label=${msg("Service Provider Binding")}
name="spBinding"
required
.options=${serviceProviderBindingOptions}
.value=${provider?.spBinding}
help=${msg(
"Determines how authentik sends the response back to the Service Provider.",
)}
>
</ak-radio-input>
<ak-text-input
name="audience"
label=${msg("Audience")}
value="${ifDefined(provider?.audience)}"
.errorMessages=${errors?.audience ?? []}
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
?required=${false}
name="authenticationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Signing Certificate")} name="signingKp">
<ak-crypto-certificate-search
.certificate=${provider?.signingKp}
@input=${setHasSigningKp}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"Certificate used to sign outgoing Responses going to the Service Provider.",
)}
</p>
</ak-form-element-horizontal>
${hasSigningKp ? renderHasSigningKp(provider) : nothing}
<ak-form-element-horizontal
label=${msg("Verification Certificate")}
name="verificationKp"
>
<ak-crypto-certificate-search
.certificate=${provider?.verificationKp}
nokey
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Encryption Certificate")}
name="encryptionKp"
>
<ak-crypto-certificate-search
.certificate=${provider?.encryptionKp}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg("When selected, assertions will be encrypted using this keypair.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${samlPropertyMappingsProvider}
.selector=${makeSAMLPropertyMappingsSelector(provider?.propertyMappings)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("NameID Property Mapping")}
name="nameIdMapping"
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<SAMLPropertyMapping[]> => {
const args: PropertymappingsProviderSamlListRequest = {
ordering: "saml_name",
};
if (query !== undefined) {
args.search = query;
}
const items = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderSamlList(args);
return items.results;
}}
.renderElement=${(item: SAMLPropertyMapping): string => {
return item.name;
}}
.value=${(item: SAMLPropertyMapping | undefined): string | undefined => {
return item?.pk;
}}
.selected=${(item: SAMLPropertyMapping): boolean => {
return provider?.nameIdMapping === item.pk;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
)}
</p>
</ak-form-element-horizontal>
<ak-text-input
name="assertionValidNotBefore"
label=${msg("Assertion valid not before")}
value="${provider?.assertionValidNotBefore || "minutes=-5"}"
required
.errorMessages=${errors?.assertionValidNotBefore ?? []}
help=${msg("Configure the maximum allowed time drift for an assertion.")}
></ak-text-input>
<ak-text-input
name="assertionValidNotOnOrAfter"
label=${msg("Assertion valid not on or after")}
value="${provider?.assertionValidNotOnOrAfter || "minutes=5"}"
required
.errorMessages=${errors?.assertionValidNotBefore ?? []}
help=${msg("Assertion not valid on or after current time + this value.")}
></ak-text-input>
<ak-text-input
name="sessionValidNotOnOrAfter"
label=${msg("Session valid not on or after")}
value="${provider?.sessionValidNotOnOrAfter || "minutes=86400"}"
required
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
help=${msg("Session not valid on or after current time + this value.")}
></ak-text-input>
<ak-text-input
name="defaultRelayState"
label=${msg("Default relay state")}
value="${provider?.defaultRelayState || ""}"
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
help=${msg(
"When using IDP-initiated logins, the relay state will be set to this value.",
)}
></ak-text-input>
<ak-radio-input
name="digestAlgorithm"
label=${msg("Digest algorithm")}
.options=${digestAlgorithmOptions}
.value=${provider?.digestAlgorithm}
required
>
</ak-radio-input>
<ak-radio-input
name="signatureAlgorithm"
label=${msg("Signature algorithm")}
.options=${signatureAlgorithmOptions}
.value=${provider?.signatureAlgorithm}
required
>
</ak-radio-input>
</div>
</ak-form-group>`;
}

View File

@ -1,53 +1,11 @@
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
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 {
CoreApi,
CoreGroupsListRequest,
Group,
PropertymappingsApi,
ProvidersApi,
SCIMMapping,
SCIMProvider,
} from "@goauthentik/api";
import { ProvidersApi, SCIMProvider } from "@goauthentik/api";
export async function scimPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderScimList({
ordering: "managed",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSCIMPropertyMappingsSelector(
instanceMappings: string[] | undefined,
defaultSelected: string,
) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SCIMMapping>) =>
mapping?.managed === defaultSelected;
}
import { renderForm } from "./SCIMProviderFormForm.js";
@customElement("ak-provider-scim-form")
export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
@ -70,156 +28,8 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
}
}
renderForm(): TemplateResult {
return html` <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-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("URL")} ?required=${true} name="url">
<input
type="text"
value="${first(this.instance?.url, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("SCIM base url, usually ends in /v2.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="verifyCertificates">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.verifyCertificates, true)}
/>
<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("Verify SCIM server's certificates")}</span
>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Token")}
?required=${true}
name="token"
>
<input
type="text"
value="${first(this.instance?.token, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group ?expanded=${true}>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.excludeUsersServiceAccount, true)}
/>
<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("Exclude service accounts")}</span
>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
includeUsers: false,
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
args,
);
return groups.results;
}}
.renderElement=${(group: Group): string => {
return group.name;
}}
.value=${(group: Group | undefined): string | undefined => {
return group ? group.pk : undefined;
}}
.selected=${(group: Group): boolean => {
return group.pk === this.instance?.filterGroup;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg("Only sync users within the selected group.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group ?expanded=${true}>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings">
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
this.instance?.propertyMappings,
"goauthentik.io/providers/scim/user",
)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
</select>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to user mapping.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup">
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
this.instance?.propertyMappingsGroup,
"goauthentik.io/providers/scim/group",
)}
available-label=${msg("Available Group Property Mappings")}
selected-label=${msg("Selected Group Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to group creation.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
renderForm() {
return renderForm(this.instance ?? {}, []);
}
}

View File

@ -0,0 +1,173 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CoreApi,
CoreGroupsListRequest,
Group,
PropertymappingsApi,
SCIMMapping,
SCIMProvider,
ValidationError,
} from "@goauthentik/api";
export async function scimPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderScimList({
ordering: "managed",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSCIMPropertyMappingsSelector(
instanceMappings: string[] | undefined,
defaultSelected: string,
) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SCIMMapping>) =>
mapping?.managed === defaultSelected;
}
export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationError = {}) {
return html`
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
help=${msg("Method's display Name.")}
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="url"
label=${msg("URL")}
value="${first(provider?.url, "")}"
.errorMessages=${errors?.url ?? []}
required
help=${msg("SCIM base url, usually ends in /v2.")}
></ak-text-input>
<ak-switch-input
name="verifyCertificates"
label=${msg("Verify SCIM server's certificates")}
?checked=${provider?.verifyCertificates ?? true}
>
</ak-switch-input>
<ak-text-input
name="token"
label=${msg("Token")}
value="${provider?.token ?? ""}"
.errorMessages=${errors?.token ?? []}
required
help=${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="excludeUsersServiceAccount"
label=${msg("Exclude service accounts")}
?checked=${first(provider?.excludeUsersServiceAccount, true)}
>
</ak-switch-input>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
includeUsers: false,
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
return groups.results;
}}
.renderElement=${(group: Group): string => {
return group.name;
}}
.value=${(group: Group | undefined): string | undefined => {
return group ? group.pk : undefined;
}}
.selected=${(group: Group): boolean => {
return group.pk === provider?.filterGroup;
}}
blankable
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg("Only sync users within the selected group.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
provider?.propertyMappings,
"goauthentik.io/providers/scim/user",
)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to user mapping.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup"
>
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
provider?.propertyMappingsGroup,
"goauthentik.io/providers/scim/group",
)}
available-label=${msg("Available Group Property Mappings")}
selected-label=${msg("Selected Group Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to group creation.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2024.10.2";
export const VERSION = "2024.10.4";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -44,37 +44,43 @@ export class FormGroup extends AKElement {
}
render(): TemplateResult {
return html`<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
<div class="pf-c-form__field-group-toggle">
<div class="pf-c-form__field-group-toggle-button">
<button
class="pf-c-button pf-m-plain"
type="button"
aria-expanded="${this.expanded}"
aria-label=${this.ariaLabel}
@click=${() => {
this.expanded = !this.expanded;
}}
>
<span class="pf-c-form__field-group-toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</button>
return html` <div class="pf-c-form">
<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
<div class="pf-c-form__field-group-toggle">
<div class="pf-c-form__field-group-toggle-button">
<button
class="pf-c-button pf-m-plain"
type="button"
aria-expanded="${this.expanded}"
aria-label=${this.ariaLabel}
@click=${() => {
this.expanded = !this.expanded;
}}
>
<span class="pf-c-form__field-group-toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</button>
</div>
</div>
</div>
<div class="pf-c-form__field-group-header">
<div class="pf-c-form__field-group-header-main">
<div class="pf-c-form__field-group-header-title">
<div class="pf-c-form__field-group-header-title-text">
<slot name="header"></slot>
<div class="pf-c-form__field-group-header">
<div class="pf-c-form__field-group-header-main">
<div class="pf-c-form__field-group-header-title">
<div class="pf-c-form__field-group-header-title-text">
<slot name="header"></slot>
</div>
</div>
<div class="pf-c-form__field-group-header-description">
<slot name="description"></slot>
</div>
</div>
<div class="pf-c-form__field-group-header-description">
<slot name="description"></slot>
</div>
</div>
<slot
?hidden=${!this.expanded}
class="pf-c-form__field-group-body"
name="body"
></slot>
</div>
<slot ?hidden=${!this.expanded} class="pf-c-form__field-group-body" name="body"></slot>
</div>`;
}
}

View File

@ -5,14 +5,15 @@ import {
TITLE_DEFAULT,
} from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
import { purify } from "@goauthentik/common/purify";
import { configureSentry } from "@goauthentik/common/sentry";
import { first } from "@goauthentik/common/utils";
import { WebsocketClient } from "@goauthentik/common/ws";
import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/LoadingOverlay";
import "@goauthentik/elements/ak-locale-context";
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
import { themeImage } from "@goauthentik/elements/utils/images";
import "@goauthentik/flow/components/ak-brand-footer";
import "@goauthentik/flow/sources/apple/AppleLoginInit";
import "@goauthentik/flow/sources/plex/PlexLoginInit";
import "@goauthentik/flow/stages/FlowErrorStage";
@ -25,7 +26,6 @@ import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "l
import { customElement, property, state } from "lit/decorators.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { until } from "lit/directives/until.js";
import { html as staticHtml, unsafeStatic } from "lit/static-html.js";
import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -49,52 +49,6 @@ import {
UiThemeEnum,
} from "@goauthentik/api";
type StageRenderer = {
// Provide the lit-element tag if it's different from the challenge.component name
tag?: string;
// Provide a dynamic import whenever possible; otherwise, make sure you include it in the
// build-time imports above.
import?: () => Promise<unknown>;
};
type StageRenderers = { [key: string]: StageRenderer };
// authentik's standard stages and the Lit components that handle them. A "standard stage" conforms
// to an API that takes two properties:
// `.host=${host: StageHost} .challenge=${challenge: ChallengeTypes}`
// Exceptions are handled in a switch/case statement below the renderer for these.
// All of that `async () => await import("@goauthentik/flow/...")` boilerplate cannot be abstracted
// away because [import is not a function](https://v8.dev/features/dynamic-import), it is a
// _statement_, and its contents are statically analyzed by bundlers, compilers, and the V8
// interpreter.
// Prettier ignore to keep the table looking like a table:
// prettier-ignore
const allStages: StageRenderers = {
"ak-stage-access-denied": { import: async () => await import("@goauthentik/flow/stages/access_denied/AccessDeniedStage") },
"ak-stage-identification": { import: async () => await import("@goauthentik/flow/stages/identification/IdentificationStage") },
"ak-stage-password": { import: async () => await import("@goauthentik/flow/stages/password/PasswordStage") },
"ak-stage-captcha": { import: async () => await import("@goauthentik/flow/stages/captcha/CaptchaStage") },
"ak-stage-consent": { import: async () => await import("@goauthentik/flow/stages/consent/ConsentStage") },
"ak-stage-dummy": { import: async () => await import("@goauthentik/flow/stages/dummy/DummyStage") },
"ak-stage-email": { import: async () => await import("@goauthentik/flow/stages/email/EmailStage") },
"ak-stage-autosubmit": { import: async () => await import("@goauthentik/flow/stages/autosubmit/AutosubmitStage") },
"ak-stage-prompt": { import: async () => await import("@goauthentik/flow/stages/prompt/PromptStage") },
"ak-stage-authenticator-totp": { import: async () => await import("@goauthentik/flow/stages/authenticator_totp/AuthenticatorTOTPStage") },
"ak-stage-authenticator-duo": { import: async () => await import("@goauthentik/flow/stages/authenticator_duo/AuthenticatorDuoStage") },
"ak-stage-authenticator-static": { import: async () => await import("@goauthentik/flow/stages/authenticator_static/AuthenticatorStaticStage") },
"ak-stage-authenticator-webauthn": { },
"ak-stage-authenticator-sms": { import: async () => await import("@goauthentik/flow/stages/authenticator_sms/AuthenticatorSMSStage") },
"ak-stage-authenticator-validate": { import: async () => await import("@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage") },
"ak-stage-user-login": { import: async () => await import("@goauthentik/flow/stages/user_login/UserLoginStage") },
"ak-source-plex": { tag: "ak-flow-source-plex" },
"ak-source-oauth-apple": { tag: "ak-flow-source-oauth-apple" },
"ak-provider-oauth2-device-code": { tag: "ak-flow-provider-oauth2-code", import: async () => await import("@goauthentik/flow/providers/oauth2/DeviceCode") },
"ak-provider-oauth2-device-code-finish": { tag: "ak-flow-provider-oauth2-code-finish", import: async () => await import("@goauthentik/flow/providers/oauth2/DeviceCodeFinish") },
"ak-stage-session-end": { import: async () => await import("@goauthentik/flow/providers/SessionEnd") },
"ak-stage-flow-error": { },
} as const;
@customElement("ak-flow-executor")
export class FlowExecutor extends Interface implements StageHost {
@property()
@ -345,21 +299,142 @@ export class FlowExecutor extends Interface implements StageHost {
if (!this.challenge) {
return html`<ak-empty-state loading> </ak-empty-state>`;
}
const stage = allStages[this.challenge.component];
if (stage) {
if (stage.import) {
await stage.import();
}
const tag = stage.tag ?? this.challenge.component;
// Prettier doesn't know what `staticHTML` is, will try to format it by
// prettier-ignore
return staticHtml`<${unsafeStatic(tag)}
.host=${this as StageHost}
.challenge=${this.challenge}
></${unsafeStatic(tag)}>`;
}
switch (this.challenge?.component) {
case "ak-stage-access-denied":
await import("@goauthentik/flow/stages/access_denied/AccessDeniedStage");
return html`<ak-stage-access-denied
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-access-denied>`;
case "ak-stage-identification":
await import("@goauthentik/flow/stages/identification/IdentificationStage");
return html`<ak-stage-identification
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-identification>`;
case "ak-stage-password":
await import("@goauthentik/flow/stages/password/PasswordStage");
return html`<ak-stage-password
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-password>`;
case "ak-stage-captcha":
await import("@goauthentik/flow/stages/captcha/CaptchaStage");
return html`<ak-stage-captcha
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-captcha>`;
case "ak-stage-consent":
await import("@goauthentik/flow/stages/consent/ConsentStage");
return html`<ak-stage-consent
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-consent>`;
case "ak-stage-dummy":
await import("@goauthentik/flow/stages/dummy/DummyStage");
return html`<ak-stage-dummy
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-dummy>`;
case "ak-stage-email":
await import("@goauthentik/flow/stages/email/EmailStage");
return html`<ak-stage-email
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-email>`;
case "ak-stage-autosubmit":
await import("@goauthentik/flow/stages/autosubmit/AutosubmitStage");
return html`<ak-stage-autosubmit
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-autosubmit>`;
case "ak-stage-prompt":
await import("@goauthentik/flow/stages/prompt/PromptStage");
return html`<ak-stage-prompt
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-prompt>`;
case "ak-stage-authenticator-totp":
await import("@goauthentik/flow/stages/authenticator_totp/AuthenticatorTOTPStage");
return html`<ak-stage-authenticator-totp
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-totp>`;
case "ak-stage-authenticator-duo":
await import("@goauthentik/flow/stages/authenticator_duo/AuthenticatorDuoStage");
return html`<ak-stage-authenticator-duo
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-duo>`;
case "ak-stage-authenticator-static":
await import(
"@goauthentik/flow/stages/authenticator_static/AuthenticatorStaticStage"
);
return html`<ak-stage-authenticator-static
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-static>`;
case "ak-stage-authenticator-webauthn":
return html`<ak-stage-authenticator-webauthn
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-webauthn>`;
case "ak-stage-authenticator-sms":
await import("@goauthentik/flow/stages/authenticator_sms/AuthenticatorSMSStage");
return html`<ak-stage-authenticator-sms
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-sms>`;
case "ak-stage-authenticator-validate":
await import(
"@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage"
);
return html`<ak-stage-authenticator-validate
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-validate>`;
case "ak-stage-user-login":
await import("@goauthentik/flow/stages/user_login/UserLoginStage");
return html`<ak-stage-user-login
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-user-login>`;
// Sources
case "ak-source-plex":
return html`<ak-flow-source-plex
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-source-plex>`;
case "ak-source-oauth-apple":
return html`<ak-flow-source-oauth-apple
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-source-oauth-apple>`;
// Providers
case "ak-provider-oauth2-device-code":
await import("@goauthentik/flow/providers/oauth2/DeviceCode");
return html`<ak-flow-provider-oauth2-code
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-provider-oauth2-code>`;
case "ak-provider-oauth2-device-code-finish":
await import("@goauthentik/flow/providers/oauth2/DeviceCodeFinish");
return html`<ak-flow-provider-oauth2-code-finish
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-provider-oauth2-code-finish>`;
case "ak-stage-session-end":
await import("@goauthentik/flow/providers/SessionEnd");
return html`<ak-stage-session-end
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-session-end>`;
// Internal stages
case "ak-stage-flow-error":
return html`<ak-stage-flow-error
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-flow-error>`;
case "xak-flow-redirect":
return html`<ak-stage-redirect
.host=${this as StageHost}
@ -429,9 +504,11 @@ export class FlowExecutor extends Interface implements StageHost {
>
<img
src="${themeImage(
this.brand?.brandingLogo ??
globalAK()?.brand.brandingLogo ??
first(
this.brand?.brandingLogo,
globalAK()?.brand.brandingLogo,
DefaultBrand.brandingLogo,
),
)}"
alt="authentik Logo"
/>
@ -439,9 +516,25 @@ export class FlowExecutor extends Interface implements StageHost {
${until(this.renderChallenge())}
</div>
<footer class="pf-c-login__footer">
<ak-brand-links
.links=${this.brand?.uiFooterLinks ?? []}
></ak-brand-links>
<ul class="pf-c-list pf-m-inline">
${this.brand?.uiFooterLinks?.map((link) => {
if (link.href) {
return html`${purify(
html`<li>
<a href="${link.href}"
>${link.name}</a
>
</li>`,
)}`;
}
return html`<li>
<span>${link.name}</span>
</li>`;
})}
<li>
<span>${msg("Powered by authentik")}</span>
</li>
</ul>
</footer>
</div>
</div>

View File

@ -1,51 +0,0 @@
import { purify } from "@goauthentik/common/purify";
import { AKElement } from "@goauthentik/elements/Base.js";
import { msg } from "@lit/localize";
import { css, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { FooterLink } from "@goauthentik/api";
const styles = css`
.pf-c-list a {
color: unset;
}
ul.pf-c-list.pf-m-inline {
justify-content: center;
padding: calc(var(--pf-global--spacer--xs) / 2) 0px;
}
`;
const salesMark: FooterLink = { name: msg("Powered by authentik"), href: "" };
@customElement("ak-brand-links")
export class BrandLinks extends AKElement {
static get styles() {
return [PFBase, PFList, styles];
}
@property({ type: Array, attribute: false })
links: FooterLink[] = [];
render() {
const links = [...(this.links ?? []), salesMark];
return html` <ul class="pf-c-list pf-m-inline">
${map(links, (link) =>
link.href
? purify(html`<li><a href="${link.href}">${link.name}</a></li>`)
: html`<li><span>${link.name}</span></li>`,
)}
</ul>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-brand-links": BrandLinks;
}
}

View File

@ -0,0 +1,225 @@
import { browser } from "@wdio/globals";
import { match } from "ts-pattern";
import { Key } from "webdriverio";
export async function doBlur(el: WebdriverIO.Element | ChainablePromiseElement) {
const element = await el;
browser.execute((element) => element.blur(), element);
}
export function tap<A>(a: A) {
console.log("TAP:", a);
return a;
}
const makeComparator = (value: string | RegExp) =>
typeof value === "string"
? (sample: string) => sample === value
: (sample: string) => value.test(sample);
export async function checkIsPresent(name: string) {
await expect(await $(name)).toBeDisplayed();
}
export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
const context = ctx ?? browser;
const button = await (async () => {
for await (const button of context.$$("button")) {
if ((await button.isDisplayed()) && (await button.getText()).indexOf(name) !== -1) {
return button;
}
}
})();
if (!(button && (await button.isDisplayed()))) {
throw new Error(`Unable to find button '${name}'`);
}
await button.scrollIntoView();
await button.click();
await doBlur(button);
}
export async function clickToggleGroup(name: string, value: string | RegExp) {
const comparator = makeComparator(value);
const button = await (async () => {
for await (const button of $(`[data-ouid-component-name=${name}]`).$$(
".pf-c-toggle-group__button",
)) {
if (comparator(await button.$(".pf-c-toggle-group__text").getText())) {
return button;
}
}
})();
if (!(button && (await button?.isDisplayed()))) {
throw new Error(`Unable to locate toggle button ${name}:${value.toString()}`);
}
await button.scrollIntoView();
await button.click();
await doBlur(button);
}
export async function setFormGroup(name: string | RegExp, setting: "open" | "closed") {
const comparator = makeComparator(name);
const formGroup = await (async () => {
for await (const group of browser.$$("ak-form-group")) {
// Delightfully, wizards may have slotted elements that *exist* but are not *attached*,
// and this can break the damn tests.
if (!(await group.isDisplayed())) {
continue;
}
if (
comparator(await group.$("div.pf-c-form__field-group-header-title-text").getText())
) {
return group;
}
}
})();
if (!(formGroup && (await formGroup.isDisplayed()))) {
throw new Error(`Unable to find ak-form-group[name="${name}"]`);
}
await formGroup.scrollIntoView();
const toggle = await formGroup.$("div.pf-c-form__field-group-toggle-button button");
await match([await toggle.getAttribute("aria-expanded"), setting])
.with(["false", "open"], async () => await toggle.click())
.with(["true", "closed"], async () => await toggle.click())
.otherwise(async () => {});
await doBlur(formGroup);
}
export async function setRadio(name: string, value: string | RegExp) {
const control = await $(`ak-radio[name="${name}"]`);
await control.scrollIntoView();
const comparator = makeComparator(value);
const item = await (async () => {
for await (const item of control.$$("div.pf-c-radio")) {
if (comparator(await item.$(".pf-c-radio__label").getText())) {
return item;
}
}
})();
if (!(item && (await item.isDisplayed()))) {
throw new Error(`Unable to find a radio that matches ${name}:${value.toString()}`);
}
await item.scrollIntoView();
await item.click();
await doBlur(control);
}
export async function setSearchSelect(name: string, value: string | RegExp) {
const control = await (async () => {
try {
const control = await $(`ak-search-select[name="${name}"]`);
await control.waitForExist({ timeout: 500 });
return control;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
} catch (_e: any) {
const control = await $(`ak-search-selects-ez[name="${name}"]`);
return control;
}
})();
if (!(control && (await control.isExisting()))) {
throw new Error(`Unable to find an ak-search-select variant matching ${name}}`);
}
// Find the search select input control and activate it.
const view = await control.$("ak-search-select-view");
const input = await view.$('input[type="text"]');
await input.scrollIntoView();
await input.click();
const comparator = makeComparator(value);
const button = await (async () => {
for await (const button of $(`div[data-managed-for*="${name}"]`)
.$("ak-list-select")
.$$("button")) {
if (comparator(await button.getText())) {
return button;
}
}
})();
if (!(button && (await button.isDisplayed()))) {
throw new Error(
`Unable to find an ak-search-select entry matching ${name}:${value.toString()}`,
);
}
await (await button).click();
await browser.keys(Key.Tab);
await doBlur(control);
}
export async function setTextInput(name: string, value: string) {
const control = await $(`input[name="${name}"]`);
await control.scrollIntoView();
await control.setValue(value);
await doBlur(control);
}
export async function setTextareaInput(name: string, value: string) {
const control = await $(`textarea[name="${name}"]`);
await control.scrollIntoView();
await control.setValue(value);
await doBlur(control);
}
export async function setToggle(name: string, set: boolean) {
const toggle = await $(`input[name="${name}"]`);
await toggle.scrollIntoView();
await expect(await toggle.getAttribute("type")).toBe("checkbox");
const state = await toggle.isSelected();
if (set !== state) {
const control = await (await toggle.parentElement()).$(".pf-c-switch__toggle");
await control.click();
await doBlur(control);
}
}
export async function setTypeCreate(name: string, value: string | RegExp) {
const control = await $(`ak-wizard-page-type-create[name="${name}"]`);
await control.scrollIntoView();
const comparator = makeComparator(value);
const card = await (async () => {
for await (const card of $("ak-wizard-page-type-create").$$(
'[data-ouid-component-type="ak-type-create-grid-card"]',
)) {
if (comparator(await card.$(".pf-c-card__title").getText())) {
return card;
}
}
})();
if (!(card && (await card.isDisplayed()))) {
throw new Error(`Unable to locate radio card ${name}:${value.toString()}`);
}
await card.scrollIntoView();
await card.click();
await doBlur(control);
}
export type TestInteraction =
| [typeof checkIsPresent, ...Parameters<typeof checkIsPresent>]
| [typeof clickButton, ...Parameters<typeof clickButton>]
| [typeof clickToggleGroup, ...Parameters<typeof clickToggleGroup>]
| [typeof setFormGroup, ...Parameters<typeof setFormGroup>]
| [typeof setRadio, ...Parameters<typeof setRadio>]
| [typeof setSearchSelect, ...Parameters<typeof setSearchSelect>]
| [typeof setTextInput, ...Parameters<typeof setTextInput>]
| [typeof setTextareaInput, ...Parameters<typeof setTextareaInput>]
| [typeof setToggle, ...Parameters<typeof setToggle>]
| [typeof setTypeCreate, ...Parameters<typeof setTypeCreate>];
export type TestSequence = TestInteraction[];
export type TestProvider = () => TestSequence;

View File

@ -1,4 +1,5 @@
import { browser } from "@wdio/globals";
import { match } from "ts-pattern";
import { Key } from "webdriverio";
const CLICK_TIME_DELAY = 250;
@ -7,6 +8,7 @@ const CLICK_TIME_DELAY = 250;
* Main page object containing all methods, selectors and functionality that is shared across all
* page objects
*/
export default class Page {
/**
* Opens a sub page of the page
@ -31,7 +33,6 @@ export default class Page {
* why it would be hard to simplify this further (`flow` vs `tentanted-flow` vs a straight-up
* SearchSelect each have different a `searchSelector`).
*/
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
const inputBind = await $(searchSelector);
const inputMain = await inputBind.$('input[type="text"]');
@ -55,6 +56,79 @@ export default class Page {
await browser.keys(Key.Tab);
}
async setSearchSelect(name: string, value: string) {
const control = await (async () => {
try {
const control = await $(`ak-search-select[name="${name}"]`);
await control.waitForExist({ timeout: 500 });
return control;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
} catch (_e: any) {
const control = await $(`ak-search-selects-ez[name="${name}"]`);
return control;
}
})();
// Find the search select input control and activate it.
const view = await control.$("ak-search-select-view");
const input = await view.$('input[type="text"]');
await input.scrollIntoView();
await input.click();
// Weirdly necessary because it's portals!
const searchBlock = await (
await $(`div[data-managed-for="${name}"]`).$("ak-list-select")
).shadow$$("button");
let target: WebdriverIO.Element;
// @ts-expect-error "Types break on shadow$$"
for (const button of searchBlock) {
if ((await button.getText()).includes(value)) {
target = button;
break;
}
}
// @ts-expect-error "TSC cannot tell if the `for` loop actually performs the assignment."
if (!target) {
throw new Error(`Expected to find an entry matching the spec ${value}`);
}
await (await target).click();
await browser.keys(Key.Tab);
}
async setTextInput(name: string, value: string) {
const control = await $(`input[name="${name}"}`);
await control.scrollIntoView();
await control.setValue(value);
}
async setRadio(name: string, value: string) {
const control = await $(`ak-radio[name="${name}"]`);
await control.scrollIntoView();
const item = await control.$(`label.*=${value}`).parentElement();
await item.scrollIntoView();
await item.click();
}
async setTypeCreate(name: string, value: string) {
const control = await $(`ak-wizard-page-type-create[name="${name}"]`);
await control.scrollIntoView();
const selection = await $(`.pf-c-card__.*=${value}`);
await selection.scrollIntoView();
await selection.click();
}
async setFormGroup(name: string, setting: "open" | "closed") {
const formGroup = await $(`ak-form-group span[slot="header"].*=${name}`).parentElement();
await formGroup.scrollIntoView();
const toggle = await formGroup.$("div.pf-c-form__field-group-toggle-button button");
await match([await toggle.getAttribute("expanded"), setting])
.with(["false", "open"], async () => await toggle.click())
.with(["true", "closed"], async () => await toggle.click())
.otherwise(async () => {});
}
public async logout() {
await browser.url("http://localhost:9000/flows/-/default/invalidation/");
return await this.pause();

View File

@ -9,29 +9,50 @@ import ApplicationWizardView from "../pageobjects/application-wizard.page.js";
import ApplicationsListPage from "../pageobjects/applications-list.page.js";
import { randomId } from "../utils/index.js";
import { login } from "../utils/login.js";
import {
completeForwardAuthDomainProxyProviderForm,
completeForwardAuthProxyProviderForm,
completeLDAPProviderForm,
completeOAuth2ProviderForm,
completeProxyProviderForm,
completeRadiusProviderForm,
completeSAMLProviderForm,
completeSCIMProviderForm,
simpleForwardAuthDomainProxyProviderForm,
simpleForwardAuthProxyProviderForm,
simpleLDAPProviderForm,
simpleOAuth2ProviderForm,
simpleProxyProviderForm,
simpleRadiusProviderForm,
simpleSAMLProviderForm,
simpleSCIMProviderForm,
} from "./provider-shared-sequences.js";
import { type TestSequence } from "./shared-sequences";
async function reachTheProvider(title: string) {
const newPrefix = randomId();
const SUCCESS_MESSAGE = "Your application has been saved";
async function reachTheApplicationsPage() {
await ApplicationsListPage.logout();
await login();
await ApplicationsListPage.open();
await ApplicationsListPage.pause("ak-page-header");
await expect(await ApplicationsListPage.pageHeader()).toBeDisplayed();
await expect(await ApplicationsListPage.pageHeader()).toHaveText("Applications");
}
async function fillOutTheApplication(title: string) {
const newPrefix = randomId();
await (await ApplicationsListPage.startWizardButton()).click();
await (await ApplicationWizardView.wizardTitle()).waitForDisplayed();
await expect(await ApplicationWizardView.wizardTitle()).toHaveText("New application");
await (await ApplicationWizardView.app.name()).setValue(`${title} - ${newPrefix}`);
await (await ApplicationWizardView.app.uiSettings()).scrollIntoView();
await (await ApplicationWizardView.app.uiSettings()).click();
await (await ApplicationWizardView.app.launchUrl()).scrollIntoView();
await (await ApplicationWizardView.app.launchUrl()).setValue("http://example.goauthentik.io");
await (await ApplicationWizardView.nextButton()).click();
return await ApplicationWizardView.pause();
await ApplicationWizardView.pause();
}
async function getCommitMessage() {
@ -39,136 +60,51 @@ async function getCommitMessage() {
return await ApplicationWizardView.successMessage();
}
const SUCCESS_MESSAGE = "Your application has been saved";
const EXPLICIT_CONSENT = "default-provider-authorization-explicit-consent";
async function fillOutTheProviderAndCommit(provider: TestSequence) {
// The wizard automagically provides a name. If it doesn't, that's a bug.
const wizardProvider = provider.filter((p) => p.length < 2 || p[1] !== "name");
await $("ak-wizard-page-type-create").waitForDisplayed();
for await (const field of wizardProvider) {
const thefunc = field[0];
const args = field.slice(1);
// @ts-expect-error "This is a pretty alien call; I'm not surprised Typescript hates it."
await thefunc.apply($, args);
}
describe("Configure Applications with the Application Wizard", () => {
it("Should configure a simple LDAP Application", async () => {
await reachTheProvider("New LDAP Application");
await $("ak-wizard-frame").$("footer button.pf-m-primary").click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
}
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.ldapProvider).scrollIntoView();
await (await ApplicationWizardView.ldapProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.ldap.setBindFlow("default-authentication-flow");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
async function itShouldConfigureApplicationsViaTheWizard(name: string, provider: TestSequence) {
it(`Should successfully configure an application with a ${name} provider`, async () => {
await reachTheApplicationsPage();
await fillOutTheApplication(name);
await fillOutTheProviderAndCommit(provider);
});
}
it("Should configure a simple Oauth2 Application", async () => {
await reachTheProvider("New Oauth2 Application");
const providers = [
["Simple LDAP", simpleLDAPProviderForm],
["Simple OAuth2", simpleOAuth2ProviderForm],
["Simple Radius", simpleRadiusProviderForm],
["Simple SAML", simpleSAMLProviderForm],
["Simple SCIM", simpleSCIMProviderForm],
["Simple Proxy", simpleProxyProviderForm],
["Simple Forward Auth (single)", simpleForwardAuthProxyProviderForm],
["Simple Forward Auth (domain)", simpleForwardAuthDomainProxyProviderForm],
["Complete OAuth2", completeOAuth2ProviderForm],
["Complete LDAP", completeLDAPProviderForm],
["Complete Radius", completeRadiusProviderForm],
["Complete SAML", completeSAMLProviderForm],
["Complete SCIM", completeSCIMProviderForm],
["Complete Proxy", completeProxyProviderForm],
["Complete Forward Auth (single)", completeForwardAuthProxyProviderForm],
["Complete Forward Auth (domain)", completeForwardAuthDomainProxyProviderForm],
];
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.oauth2Provider).scrollIntoView();
await (await ApplicationWizardView.oauth2Provider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.oauth.setAuthorizationFlow(EXPLICIT_CONSENT);
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple SAML Application", async () => {
await reachTheProvider("New SAML Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.samlProvider).scrollIntoView();
await (await ApplicationWizardView.samlProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.saml.setAuthorizationFlow(EXPLICIT_CONSENT);
await ApplicationWizardView.saml.acsUrl.setValue("http://example.com:8000/");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple SCIM Application", async () => {
await reachTheProvider("New SCIM Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.scimProvider).scrollIntoView();
await (await ApplicationWizardView.scimProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.scim.url.setValue("http://example.com:8000/");
await ApplicationWizardView.scim.token.setValue("a-very-basic-token");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple Radius Application", async () => {
await reachTheProvider("New Radius Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.radiusProvider).scrollIntoView();
await (await ApplicationWizardView.radiusProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.radius.setAuthenticationFlow("default-authentication-flow");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple Transparent Proxy Application", async () => {
await reachTheProvider("New Transparent Proxy Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.proxyProviderProxy).scrollIntoView();
await (await ApplicationWizardView.proxyProviderProxy).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.transparentProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
await ApplicationWizardView.transparentProxy.externalHost.setValue(
"http://external.example.com",
);
await ApplicationWizardView.transparentProxy.internalHost.setValue(
"http://internal.example.com",
);
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple Forward Proxy Application", async () => {
await reachTheProvider("New Forward Proxy Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.proxyProviderForwardsingle).scrollIntoView();
await (await ApplicationWizardView.proxyProviderForwardsingle).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.forwardProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
await ApplicationWizardView.forwardProxy.externalHost.setValue(
"http://external.example.com",
);
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
describe("Configuring Applications Via the Wizard", () => {
for (const [name, provider] of providers) {
itShouldConfigureApplicationsViaTheWizard(name, provider());
}
});

View File

@ -1,47 +0,0 @@
import { expect } from "@wdio/globals";
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
import ProvidersListPage from "../pageobjects/providers-list.page.js";
import { randomId } from "../utils/index.js";
import { login } from "../utils/login.js";
async function reachTheProvider() {
await ProvidersListPage.logout();
await login();
await ProvidersListPage.open();
await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers");
await ProvidersListPage.startWizardButton.click();
await ProviderWizardView.wizardTitle.waitForDisplayed();
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
}
describe("Configure Oauth2 Providers", () => {
it("Should configure a simple LDAP Application", async () => {
const newProviderName = `New OAuth2 Provider - ${randomId()}`;
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await $('div[data-ouid-component-name="oauth2provider"]').scrollIntoView();
await $('div[data-ouid-component-name="oauth2provider"]').click();
await ProviderWizardView.nextButton.click();
await ProviderWizardView.pause();
return await $('ak-form-element-horizontal[name="name"]').$("input");
await ProviderWizardView.oauth.setAuthorizationFlow(
"default-provider-authorization-explicit-consent",
);
await ProviderWizardView.nextButton.click();
await ProviderWizardView.pause();
await ProvidersListPage.searchInput.setValue(newProviderName);
await ProvidersListPage.clickSearchButton();
await ProvidersListPage.pause();
const newProvider = await ProvidersListPage.findProviderRow();
await newProvider.waitForDisplayed();
expect(newProvider).toExist();
expect(await newProvider.getText()).toHaveText(newProviderName);
});
});

View File

@ -0,0 +1,323 @@
import {
type TestProvider,
type TestSequence,
checkIsPresent,
clickButton,
clickToggleGroup,
setFormGroup,
setRadio,
setSearchSelect,
setTextInput,
setTextareaInput,
setToggle,
setTypeCreate,
} from "pageobjects/controls.js";
import { ascii_letters, digits, randomString } from "../utils";
import { randomId } from "../utils/index.js";
const newObjectName = (prefix: string) => `${prefix} - ${randomId()}`;
// components.schemas.OAuth2ProviderRequest
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - property_mappings
// - client_type
// - client_id
// - client_secret
// - access_code_validity
// - access_token_validity
// - refresh_token_validity
// - include_claims_in_id_token
// - signing_key
// - encryption_key
// - redirect_uris
// - sub_mode
// - issuer_mode
// - jwks_sources
//
export const simpleOAuth2ProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Oauth2 Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
];
export const completeOAuth2ProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Oauth2 Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[setFormGroup, /Protocol settings/, "open"],
[setRadio, "clientType", "Public"],
// Switch back so we can make sure `clientSecret` is available.
[setRadio, "clientType", "Confidential"],
[checkIsPresent, '[name="clientId"]'],
[checkIsPresent, '[name="clientSecret"]'],
[setSearchSelect, "signingKey", /authentik Self-signed Certificate/],
[setSearchSelect, "encryptionKey", /authentik Self-signed Certificate/],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setFormGroup, /Advanced protocol settings/, "open"],
[setTextInput, "accessCodeValidity", "minutes=2"],
[setTextInput, "accessTokenValidity", "minutes=10"],
[setTextInput, "refreshTokenValidity", "days=40"],
[setToggle, "includeClaimsInIdToken", false],
[checkIsPresent, '[name="redirectUris"]'],
[setRadio, "subMode", "Based on the User's username"],
[setRadio, "issuerMode", "Same identifier is used for all providers"],
[setFormGroup, /Machine-to-Machine authentication settings/, "open"],
[checkIsPresent, '[name="jwksSources"]'],
];
// components.schemas.LDAPProviderRequest
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - base_dn
// - certificate
// - tls_server_name
// - uid_start_number
// - gid_start_number
// - search_mode
// - bind_mode
// - mfa_support
//
export const simpleLDAPProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "LDAP Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New LDAP Provider")],
// This will never not weird me out.
[setFormGroup, /Flow settings/, "open"],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
];
export const completeLDAPProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "LDAP Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New LDAP Provider")],
[setFormGroup, /Flow settings/, "open"],
[setFormGroup, /Protocol settings/, "open"],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setTextInput, "baseDn", "DC=ldap-2,DC=goauthentik,DC=io"],
[setSearchSelect, "certificate", /authentik Self-signed Certificate/],
[checkIsPresent, '[name="tlsServerName"]'],
[setTextInput, "uidStartNumber", "2001"],
[setTextInput, "gidStartNumber", "4001"],
[setRadio, "searchMode", "Direct querying"],
[setRadio, "bindMode", "Direct binding"],
[setToggle, "mfaSupport", false],
];
export const simpleRadiusProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Radius Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Radius Provider")],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
];
export const completeRadiusProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Radius Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Radius Provider")],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setFormGroup, /Protocol settings/, "open"],
[setToggle, "mfaSupport", false],
[setTextInput, "clientNetworks", ""],
[setTextInput, "clientNetworks", "0.0.0.0/0, ::/0"],
[setTextInput, "sharedSecret", randomString(128, ascii_letters + digits)],
[checkIsPresent, '[name="propertyMappings"]'],
];
// provider_components.schemas.SAMLProviderRequest.yml
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - property_mappings
// - acs_url
// - audience
// - issuer
// - assertion_valid_not_before
// - assertion_valid_not_on_or_after
// - session_valid_not_on_or_after
// - name_id_mapping
// - digest_algorithm
// - signature_algorithm
// - signing_kp
// - verification_kp
// - encryption_kp
// - sign_assertion
// - sign_response
// - sp_binding
// - default_relay_state
//
export const simpleSAMLProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SAML Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SAML Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[setTextInput, "acsUrl", "http://example.com:8000/"],
];
export const completeSAMLProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SAML Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SAML Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[setTextInput, "acsUrl", "http://example.com:8000/"],
[setTextInput, "issuer", "someone-else"],
[setRadio, "spBinding", "Post"],
[setTextInput, "audience", ""],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
[setFormGroup, /Advanced protocol settings/, "open"],
[checkIsPresent, '[name="propertyMappings"]'],
[setSearchSelect, "signingKp", /authentik Self-signed Certificate/],
[setSearchSelect, "verificationKp", /authentik Self-signed Certificate/],
[setSearchSelect, "encryptionKp", /authentik Self-signed Certificate/],
[setSearchSelect, "nameIdMapping", /authentik default SAML Mapping. Username/],
[setTextInput, "assertionValidNotBefore", "minutes=-10"],
[setTextInput, "assertionValidNotOnOrAfter", "minutes=10"],
[setTextInput, "sessionValidNotOnOrAfter", "minutes=172800"],
[checkIsPresent, '[name="defaultRelayState"]'],
[setRadio, "digestAlgorithm", "SHA512"],
[setRadio, "signatureAlgorithm", "RSA-SHA512"],
// These are only available after the signingKp is defined.
[setToggle, "signAssertion", true],
[setToggle, "signResponse", true],
];
// provider_components.schemas.SCIMProviderRequest.yml
//
// - name
// - property_mappings
// - property_mappings_group
// - url
// - verify_certificates
// - token
// - exclude_users_service_account
// - filter_group
//
export const simpleSCIMProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SCIM Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SCIM Provider")],
[setTextInput, "url", "http://example.com:8000/"],
[setTextInput, "token", "insert-real-token-here"],
];
export const completeSCIMProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SCIM Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SCIM Provider")],
[setTextInput, "url", "http://example.com:8000/"],
[setToggle, "verifyCertificates", false],
[setTextInput, "token", "insert-real-token-here"],
[setFormGroup, /Protocol settings/, "open"],
[setFormGroup, /User filtering/, "open"],
[setToggle, "excludeUsersServiceAccount", false],
[setSearchSelect, "filterGroup", /authentik Admins/],
[setFormGroup, /Attribute mapping/, "open"],
[checkIsPresent, '[name="propertyMappings"]'],
[checkIsPresent, '[name="propertyMappingsGroup"]'],
];
// provider_components.schemas.ProxyProviderRequest.yml
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - property_mappings
// - internal_host
// - external_host
// - internal_host_ssl_validation
// - certificate
// - skip_path_regex
// - basic_auth_enabled
// - basic_auth_password_attribute
// - basic_auth_user_attribute
// - mode
// - intercept_header_auth
// - cookie_domain
// - jwks_sources
// - access_token_validity
// - refresh_token_validity
// - refresh_token_validity is not handled in any of our forms. On purpose.
// - internal_host_ssl_validation
// - only on ProxyMode
export const simpleProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Proxy Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[clickToggleGroup, "proxy-type-toggle", "Proxy"],
[setTextInput, "externalHost", "http://example.com:8000/"],
[setTextInput, "internalHost", "http://example.com:8001/"],
];
export const simpleForwardAuthProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Forward Auth Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[clickToggleGroup, "proxy-type-toggle", "Forward auth (single application)"],
[setTextInput, "externalHost", "http://example.com:8000/"],
];
export const simpleForwardAuthDomainProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Forward Auth Domain Level Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[clickToggleGroup, "proxy-type-toggle", "Forward auth (domain level)"],
[setTextInput, "externalHost", "http://example.com:8000/"],
[setTextInput, "cookieDomain", "somedomain.tld"],
];
const proxyModeCompletions: TestSequence = [
[setTextInput, "accessTokenValidity", "hours=36"],
[setFormGroup, /Advanced protocol settings/, "open"],
[setSearchSelect, "certificate", /authentik Self-signed Certificate/],
[checkIsPresent, '[name="propertyMappings"]'],
[setTextareaInput, "skipPathRegex", "."],
[setFormGroup, /Authentication settings/, "open"],
[setToggle, "interceptHeaderAuth", false],
[setToggle, "basicAuthEnabled", true],
[setTextInput, "basicAuthUserAttribute", "authorized-user"],
[setTextInput, "basicAuthPasswordAttribute", "authorized-user-password"],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[checkIsPresent, '[name="jwksSources"]'],
];
export const completeProxyProviderForm: TestProvider = () => [
...simpleProxyProviderForm(),
[setToggle, "internalHostSslValidation", false],
...proxyModeCompletions,
];
export const completeForwardAuthProxyProviderForm: TestProvider = () => [
...simpleForwardAuthProxyProviderForm(),
...proxyModeCompletions,
];
export const completeForwardAuthDomainProxyProviderForm: TestProvider = () => [
...simpleForwardAuthProxyProviderForm(),
...proxyModeCompletions,
];

View File

@ -0,0 +1,98 @@
import { expect } from "@wdio/globals";
import { type TestProvider, type TestSequence } from "../pageobjects/controls";
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
import ProvidersListPage from "../pageobjects/providers-list.page.js";
import { login } from "../utils/login.js";
import {
completeForwardAuthDomainProxyProviderForm,
completeForwardAuthProxyProviderForm,
completeLDAPProviderForm,
completeOAuth2ProviderForm,
completeProxyProviderForm,
completeRadiusProviderForm,
completeSAMLProviderForm,
completeSCIMProviderForm,
simpleForwardAuthDomainProxyProviderForm,
simpleForwardAuthProxyProviderForm,
simpleLDAPProviderForm,
simpleOAuth2ProviderForm,
simpleProxyProviderForm,
simpleRadiusProviderForm,
simpleSAMLProviderForm,
simpleSCIMProviderForm,
} from "./provider-shared-sequences.js";
async function reachTheProvider() {
await ProvidersListPage.logout();
await login();
await ProvidersListPage.open();
await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers");
await expect(await containedMessages()).not.toContain("Successfully created provider.");
await ProvidersListPage.startWizardButton.click();
await ProviderWizardView.wizardTitle.waitForDisplayed();
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
}
const containedMessages = async () =>
await (async () => {
const messages = [];
for await (const alert of $("ak-message-container").$$("ak-message")) {
messages.push(await alert.$("p.pf-c-alert__title").getText());
}
return messages;
})();
const hasProviderSuccessMessage = async () =>
await browser.waitUntil(
async () => (await containedMessages()).includes("Successfully created provider."),
{ timeout: 1000, timeoutMsg: "Expected to see provider success message." },
);
async function fillOutFields(fields: TestSequence) {
for (const field of fields) {
const thefunc = field[0];
const args = field.slice(1);
// @ts-expect-error "This is a pretty alien call, so I'm not surprised Typescript doesn't like it."
await thefunc.apply($, args);
}
}
async function itShouldConfigureASimpleProvider(name: string, provider: TestSequence) {
it(`Should successfully configure a ${name} provider`, async () => {
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await fillOutFields(provider);
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
}
type ProviderTest = [string, TestProvider];
describe("Configuring Providers", () => {
const providers: ProviderTest[] = [
["Simple LDAP", simpleLDAPProviderForm],
["Simple OAuth2", simpleOAuth2ProviderForm],
["Simple Radius", simpleRadiusProviderForm],
["Simple SAML", simpleSAMLProviderForm],
["Simple SCIM", simpleSCIMProviderForm],
["Simple Proxy", simpleProxyProviderForm],
["Simple Forward Auth (single application)", simpleForwardAuthProxyProviderForm],
["Simple Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm],
["Complete OAuth2", completeOAuth2ProviderForm],
["Complete LDAP", completeLDAPProviderForm],
["Complete Radius", completeRadiusProviderForm],
["Complete SAML", completeSAMLProviderForm],
["Complete SCIM", completeSCIMProviderForm],
["Complete Proxy", completeProxyProviderForm],
["Complete Forward Auth (single application)", completeForwardAuthProxyProviderForm],
["Complete Forward Auth (domain level)", completeForwardAuthDomainProxyProviderForm],
];
for (const [name, provider] of providers) {
itShouldConfigureASimpleProvider(name, provider());
}
});

View File

@ -1,3 +1,22 @@
// Taken from python's string module
export const ascii_lowercase = "abcdefghijklmnopqrstuvwxyz";
export const ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
export const ascii_letters = ascii_lowercase + ascii_uppercase;
export const digits = "0123456789";
export const hexdigits = digits + "abcdef" + "ABCDEF";
export const octdigits = "01234567";
export const punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
export function randomString(len: number, charset: string): string {
const chars = [];
const array = new Uint8Array(len);
globalThis.crypto.getRandomValues(array);
for (let index = 0; index < len; index++) {
chars.push(charset[Math.floor(charset.length * (array[index] / Math.pow(2, 8)))]);
}
return chars.join("");
}
export function randomId() {
let dt = new Date().getTime();
return "xxxxxxxx".replace(/x/g, (c) => {

View File

@ -1,4 +1,4 @@
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
<body>
<trans-unit id="s4caed5b7a7e5d89b">
@ -596,9 +596,9 @@
</trans-unit>
<trans-unit id="saa0e2675da69651b">
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
<target>未找到 URL "
<x id="0" equiv-text="${this.url}"/>"。</target>
<source>The URL &quot;<x id="0" equiv-text="${this.url}"/>&quot; was not found.</source>
<target>未找到 URL &quot;
<x id="0" equiv-text="${this.url}"/>&quot;。</target>
</trans-unit>
<trans-unit id="s58cd9c2fe836d9c6">
@ -1030,8 +1030,8 @@
</trans-unit>
<trans-unit id="sa8384c9c26731f83">
<source>To allow any redirect URI, set this value to ".*". Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
<source>To allow any redirect URI, set this value to &quot;.*&quot;. Be aware of the possible security implications this can have.</source>
<target>要允许任何重定向 URI请将此值设置为 &quot;.*&quot;。请注意这可能带来的安全影响。</target>
</trans-unit>
<trans-unit id="s55787f4dfcdce52b">
@ -1752,8 +1752,8 @@
</trans-unit>
<trans-unit id="sa90b7809586c35ce">
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon &quot;fa-test&quot;.</source>
<target>输入完整 URL、相对路径或者使用 'fa://fa-test' 来使用 Font Awesome 图标 &quot;fa-test&quot;。</target>
</trans-unit>
<trans-unit id="s0410779cb47de312">
@ -2916,8 +2916,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s76768bebabb7d543">
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
<source>Field which contains members of a group. Note that if using the &quot;memberUid&quot; field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
<target>包含组成员的字段。请注意,如果使用 &quot;memberUid&quot; 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
</trans-unit>
<trans-unit id="s026555347e589f0e">
@ -3663,8 +3663,8 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s7b1fba26d245cb1c">
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
<source>When using an external logging solution for archiving, this can be set to &quot;minutes=5&quot;.</source>
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 &quot;minutes=5&quot;。</target>
</trans-unit>
<trans-unit id="s44536d20bb5c8257">
@ -3840,10 +3840,10 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sa95a538bfbb86111">
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> &quot;<x id="1" equiv-text="${this.obj?.name}"/>&quot;?</source>
<target>您确定要更新
<x id="0" equiv-text="${this.objectLabel}"/>"
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
<x id="0" equiv-text="${this.objectLabel}"/>&quot;
<x id="1" equiv-text="${this.obj?.name}"/>&quot; 吗?</target>
</trans-unit>
<trans-unit id="sc92d7cfb6ee1fec6">
@ -4919,7 +4919,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="sdf1d8edef27236f0">
<source>A "roaming" authenticator, like a YubiKey</source>
<source>A &quot;roaming&quot; authenticator, like a YubiKey</source>
<target>像 YubiKey 这样的“漫游”身份验证器</target>
</trans-unit>
@ -5298,7 +5298,7 @@ doesn't pass when either or both of the selected options are equal or above the
</trans-unit>
<trans-unit id="s1608b2f94fa0dbd4">
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
<source>If set to a duration above 0, the user will have the option to choose to &quot;stay signed in&quot;, which will extend their session by the time specified here.</source>
<target>如果设置时长大于 0用户可以选择“保持登录”选项这将使用户的会话延长此处设置的时间。</target>
</trans-unit>
@ -7709,7 +7709,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
</trans-unit>
<trans-unit id="s824e0943a7104668">
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
<source>This user will be added to the group &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</source>
<target>此用户将会被添加到组 &amp;quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&amp;quot;。</target>
</trans-unit>
<trans-unit id="s62e7f6ed7d9cb3ca">
@ -9059,7 +9059,7 @@ Bindings to groups/users are checked against the user of the event.</source>
<target>同步组</target>
</trans-unit>
<trans-unit id="s2d5f69929bb7221d">
<source><x id="0" equiv-text="${prompt.name}"/> ("<x id="1" equiv-text="${prompt.fieldKey}"/>", of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<source><x id="0" equiv-text="${prompt.name}"/> (&quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&quot;, of type <x id="2" equiv-text="${prompt.type}"/>)</source>
<target><x id="0" equiv-text="${prompt.name}"/>&amp;quot;<x id="1" equiv-text="${prompt.fieldKey}"/>&amp;quot;,类型为 <x id="2" equiv-text="${prompt.type}"/></target>
</trans-unit>
<trans-unit id="sa38c5a2731be3a46">
@ -9296,7 +9296,8 @@ Bindings to groups/users are checked against the user of the event.</source>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<target>通过 OAuth2 和 SAML 等协议,使用 <x id="0" equiv-text="${this.brand.brandingTitle || &quot;authentik&quot;}"/> 作为身份提供程序的外部应用程序。此处显示了所有应用程序,即使您无法访问的也包括在内。</target>
</trans-unit>
</body>
</file>
</xliff>
</xliff>

View File

@ -7544,10 +7544,6 @@ Bindings to groups/users are checked against the user of the event.</source>
<source>One hint, 'New Application Wizard', is currently hidden</source>
<target>“新应用程序向导”提示目前已隐藏</target>
</trans-unit>
<trans-unit id="s61bd841e66966325">
<source>External applications that use authentik as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<target>通过 OAuth2 和 SAML 等协议,使用 authentik 作为身份提供程序的外部应用程序。此处显示了所有应用程序,即使您无法访问的也包括在内。</target>
</trans-unit>
<trans-unit id="s1cc306d8e28c4464">
<source>Deny message</source>
<target>拒绝消息</target>
@ -9297,6 +9293,10 @@ Bindings to groups/users are checked against the user of the event.</source>
<trans-unit id="s92205c10ba1f0f4c">
<source>This option configures the footer links on the flow executor pages. The URL is limited to web and mail addresses. If the name is left blank, the URL will be shown.</source>
<target>此选项配置流程执行器页面上的页脚链接。URL 限为 Web 和电子邮件地址。如果名称留空,则显示 URL 自身。</target>
</trans-unit>
<trans-unit id="s66f572bec2bde9c4">
<source>External applications that use <x id="0" equiv-text="${this.brand.brandingTitle || &quot;authentik&quot;}"/> as an identity provider via protocols like OAuth2 and SAML. All applications are shown here, even ones you cannot access.</source>
<target>通过 OAuth2 和 SAML 等协议,使用 <x id="0" equiv-text="${this.brand.brandingTitle || &quot;authentik&quot;}"/> 作为身份提供程序的外部应用程序。此处显示了所有应用程序,即使您无法访问的也包括在内。</target>
</trans-unit>
</body>
</file>