Compare commits

...

42 Commits

Author SHA1 Message Date
f1b7a9f934 release: 2024.12.3 2025-01-29 21:47:30 +01:00
4af75d0979 ci: fix missing dockerhub login
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-29 21:47:23 +01:00
af0a314e0b ci: fix permissions for release-publish pipeline
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-29 19:24:17 +01:00
6c7f901220 ci: fix test_docker.sh (cherry-pick #12880) (#12881)
ci: fix test_docker.sh (#12880)

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

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

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

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

* move pre-release docker test to script



* set pipefail in ak



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



* fix image



* fix config error on startup



---------

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

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

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



* make flow executor invocation correct



* actually we can do this centrally



* make sure the state is really clean



---------

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

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

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

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

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

* ci: fix missing build args for dev and release



* fix?



---------

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

* lifecycle: make it work



* sigh



* I dont know why this works but it works



---------

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

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

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

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

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

* build duo_client from scratch, sigh

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

* deps for kadmin

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

* ok fine

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

* run on arm runner?

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

* fix yaml format

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

* rewrite release pipeline to use re-usable workflows

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

* fix typo

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

* re-usable multi-arch build?

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

* also add suffix for amd64

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

* parameterise image name

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

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

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

* fix missing checkout

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

* inherit secrets

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

* temp build directly

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

* get cache-to from python script

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

* better name?

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

* matrix for merging images?

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

* re-add build dep

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

* use multi-image tag

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

* include arch in buildcache

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

---------

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

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

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



* set fixed DPI



* automatically set resize method for RDP



---------

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

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



* validate against schema too



---------

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

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

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

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

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

* fix service connection health not updating fully

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

* fix logo alt not translated

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

---------

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

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

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

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

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

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

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

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

Translate web/xliff/en.xlf in zh_CN

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

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

* ci: dont run codeql on cherry-picked prs

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

---------

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

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

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

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

Translate django.po in zh-Hans

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

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

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

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-19 12:43:04 +01:00
88dc616c5e release: 2024.12.0-rc1 2024-12-18 19:35:21 +01:00
f06534cdf0 website/docs: release: 2024.12: add latest changes (#12397) 2024-12-18 18:35:07 +00:00
c528a6c336 web/admin: add application bindings to the application wizard (#11462)
* web: fix Flash of Unstructured Content while SearchSelect is loading from the backend

Provide an alternative, readonly, disabled, unindexed input object with the text "Loading...", to be
replaced with the _real_ input element after the content is loaded.

This provides the correct appearance and spacing so the content doesn't jiggle about between the
start of loading and the SearchSelect element being finalized.  It was visually distracting and
unappealing.

* web: comment on state management in API layer, move file to point to correct component under test.

* web: test for flash of unstructured content

- Add a unit test to ensure the "Loading..." element is displayed correctly before data arrives
- Demo how to mock a `fetchObjects()` call in testing. Very cool.
- Make distinguishing rule sets for code, tests, and scripts in nightmare mode
- In SearchSelect, Move the `styles()` declaration to the top of the class for consistency.

- To test for the FLOUC issue in SearchSelect.

This is both an exercise in mocking @beryju's `fetchObjects()` protocol, and shows how we can unit
test generic components that render API objects.

* web: interim commit of the basic sortable & selectable table.

* web: added basic unit testing to API-free tables

Mostly these tests assert that the table renders and that the content we give it
is where we expect it to be after sorting. For select tables, it also asserts that
the overall value of the table is what we expect it to be when we click on a
single row, or on the "select all" button.

* web: finalize testing for tables

Includes documentation updates and better tests for select-table.

* Provide unit test accessibility to Firefox and Safari; wrap calls to manipulate test DOMs directly in a browser.exec call so they run in the proper context and be await()ed properly

* web: repeat is needed to make sure sub-elements move around correctly. Map does not do full tracking.

* web: Update HorizontalLightComponent to accurately convey its value "upwards."

* interim commit, gods, the CSS is finally working.

* web: update

Got the binding editor in.  The tests complete.  Removed sonarjs.

* web: fixed tests to complete.

* web: fixed round-trip between binding list and binding editor. Fixed 'delete'.  TODO: Fix error reporting on home page, the edit button is ugly, and the height is off somehow, but I'm not yet sure how. I just know it bugs my eyes.

* core: add support to set policy bindings in transactional endpoint

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

* improve permission checks

especially since we'll be using the wizard as default in the future, it shouldn't be superuser only

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

* web: update api-less tables

- Replace `th` with `td` in `thead` components. Because Patternfly.
- Add @beryju's styling to the tables, which make it much better looking

* web: wizard for applications, now with bindings!

- Add policy bindings to the application wizard

- Restructures the Wizard base code.
  - ak-wizard-steps holds the steps and listens for NavigationRequest events to move
    from one step to the next.
  - WizardStep is a base class (no component registration provided) that provides the *whole frame*,
    not just the form.  It receives the navigation content for the sidebar from ak-wizard-steps,
    and provides the styling for the header, footer, sidebar, and main form.  It has abstractions
    for `buttons`, `renderMain()`, `handleButton()`, `handleEnable()`, in a section well-marked as
    "Public API".  Steps inherit from this class.

Conceptually:

- A wizard is a series of pages ("steps") with a distinct beginning and end, linked in a series,
  to complete a task.
- Later steps in the series are inaccessible until an earlier steps has granted access to it.
- Access is predicated on the earlier step being complete and valid. The developer is responsible
  for determining what "complete and valid" means.
- The series is visible, giving the customer a sense of how much effort is needed to complete the
  task.
- A parent object maintains (and can modify as needed) the list of steps. It *can* maintain the
  information being collected from the user. Alternatively, that information can be kept in each
  step.

Details:

- Keeping with the Lit paradigm, "requests to change the system flow up, information changed by
  valid requests flows down."
- The information flows up using events: WizardNavigation, WizardUpdate, WizardClose.
- The information flows down using properties.

- ak-application-wizard-main holds the list of steps, providing a unique slot name for each.
  - It maintains the ApplicationWizardState object.
- ApplicationWizardStep inherits from WizardStep and provides:
  - A means of extraction information from forms
  - A convenience method for updating the ApplicationWizardState object, enabling future steps, and
    navigating to a future step, in the correct order.
  - A method for cleaning error from the error reporting mechanism as the user navigates from an
    error-handling state.
  - The title, description, and cancelability of the wizard.
- Steps:
  - step: Handles the application. A good starting point for understanding the point of
    the Wizard.  Check the `handleButton()` method to understand how we enable or disable access to
    future steps.
  - provider-choice: Just a list. Shows validation without the form.
  - provider: Uses a *very* esoteric Lit feature, `unsafeStaticTag`, which enables
    the display to show anything that conforms to the expectations of ApplicationWizardProviderForm.
    - ApplicationWizardProviderForm repeats some of the base of ApplicationWizardStep, but allows us
      to provide multiple variants on a single form without having to create separate steps for each
      form.
    - The forms (`provider-for-ldap`, `provider-for-radius`) are therefore *just* the form and any
      fetchers needed to populate it.
  - bindings: Shows the table of bindings.  Has a custom display for "This table is empty."
  - edit-binding: Showcase for the `SearchSelectEZ` configuration format. Has an override on the
    `handleButton` feature to figure out which binding is about to be overridden. Is also a
    `.hidden` page; it doesn't show up on the navigation sidebar, as is only navigable-to by buttons
    not associated with the button bar at the bottom.
  - submit: Has a lot of machinery of state: Reviewing with errors, reviewing without errors,
    running submission, and success. Uses `ts-pattern` a lot to make sure the state/request pairs
    make sense.

The key insight is that, even though a wizard is a series in order, that order can't be simply
maintained in a list. The parent needs various strategies for swapping pages in and out of the
sequence, while still maintaining a coherent idea of "flow" and providing the visual cues the user
needs to feel confident that the work can be completed and completed quickly. The entire mechanism
for using an array and index to navigate, with index numbering, blocked the implementation of the
bindings pages.

One thing led to another.  *Sigh*  Really wish this hadn't been as much of a mess as it turned out.
The end result is pretty good, though.  Definitely re-usable.

One important feature to note is that the wizard is *not* tied to the ModalButton object; it's
simply embedded in a modal as-needed.  This allows us to use wizards in other places, such as just
being in a DIV, or just a page on its own.

* web: rollback dependabot "upgrade" that broke testing

Dependabot rolled us into WebdriverIO 9.  While that's probably the
right thing to do, right now it breaks out end-to-end tests badly.
Dependabot's mucking with infrastructure should not be taken lightly,
especially in cases when the infrastructure is for DX, not UX, and
doesn't create a bigger attack surface on the running product.

* web: small fixes for wdio and lint

- Roll back another dependabot breaking change, this time to WebdriverIO
- Remove the redundant scripts wrapping ESLint for Precommit mode. Access to those modes is
  available through the flags to the `./web/scripts/eslint.mjs` script.
- Remove SonarJS checks until SonarJS is ESLint 9 compatible.
- Minor nitpicking.

* web: not sure where all these getElement() additions come from; did I add them?  Anyway, they were breaking the tests, they're a Wdio9-ism.

* package-lock.json update

* web: small fixes for wdio and lint

**PLEASE** Stop trying to upgrade WebdriverIO following Dependabot's instructions. The changes
between wdio8 and wdio9 are extensive enough to require a lot more manual intervention. The unit
tests fail in wdio 9, with the testbed driver Wdio uses to compile content to push to the browser
([vite](https://vitejs.dev) complaining:

```
2024-09-27T15:30:03.672Z WARN @wdio/browser-runner:vite: warning: Unrecognized default export in file /Users/ken/projects/dev/web/node_modules/@patternfly/patternfly/components/Dropdown/dropdown.css
  Plugin: postcss-lit
  File: /Users/ken/projects/dev/web/node_modules/@patternfly/patternfly/components/Dropdown/dropdown.css
[0-6] 2024-09-27T15:30:04.083Z INFO webdriver: BIDI COMMAND script.callFunction {"functionDeclaration":"<Function[976 bytes]>","awaitPromise":true,"arguments":[],"target":{"context":"8E608E6D13E355DFFC28112C236B73AF"}}
[0-6]  Error:  Test failed due to following error(s):
  - ak-search-select.test.ts: The requested module '/src/common/styles/authentik.css' does not provide an export named 'default': SyntaxError: The requested module '/src/common/styles/authentik.css' does not provide an export named 'default'

```

So until we can figure out why the Vite installation isn't liking our CSS import scheme, we'll
have to soldier on with what we have.  At least with Wdio 8, we get:

```
Spec Files:      7 passed, 7 total (100% completed) in 00:00:19
```

* Forgot to run prettier.

* web: small fixes for elements and forms

- provides a new utility, `_isSlug_`, used to verify a user input
- extends the ak-horizontal-component wrapper to have a stronger identity and available value
- updates the types that use the wrapper to be typed more strongly
  - (Why) The above are used in the wizard to get and store values
- fixes a bug in SearchSelectEZ that broke the display if the user didn't supply a `groupBy` field.
- Adds `@wdio/types` to the package file so eslint is satisfied wdio builds correctly
- updates the end-to-end test to understand the revised button identities on the login page
  - Running the end-to-end tests verifies that changes to the components listed above did not break
    the semantics of those components.

* Prettier had opinions

* Fix the oauth2 provider test.

* web: fix oauth2 provider.  Fix resolutions in package-lock.json

* Provide an error field for the form errors on the OAuth2 form.  Unfortunately, this does not solve the general problem that we have a UX issue with which stage bindings to show where now that we've introduced the Invalidation Stage.

* web: Add InvalidationFlow to Radius Provider dialogues

## What

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

## Note

Yes, I know I'm going to have to do more magic when we harmonize the forms, and no, I didn't add the
Property Mappings to the wizard, and yes, I know I'm going to have pain with the *new* version of
the wizard. But this is a serious bug; you can't make Radius servers with *either* of the current
dialogues at the moment.

* web/admin: provide default invalidation flows for LDAP provider.

* admin/web: the default invalidation flows for LDAP and Radius are different from the others.

* Updating the SAML Wizard page to correspond to the provider page.  *This is an intermediate fix to get the tests passing. It will probably be mooted with the next revision.*

* Making progress...

* web/admin: provider formectomy complete

* fix minor issues

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

* custom ordering for provider types

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

* fix css

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

* fix missing PFBase causing wrong font

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

* fix missing card for type select

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

* fix padding on last page

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

* add card to bindings

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

* web/element/wizard: fix the CSS cascade so the modifications to the title display don't affect the wiard header.

* web/elements/wizard: fix logic on unavailable / available / current indicators in nav bar.

* Debugging code is not needed.

* web: small visual fixes

As requested by reviewers:

- Fixed the height to 75% of the viewport
- Put 1rem of whitespace between the hint label and the Wizard startup button.

* web: disable lint check for cAsEfUnNy AtTrIbUtE nAmEs.

* Apply suggestions from code review

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Jens L. <jens@beryju.org>

* rework title

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

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-18 18:44:27 +01:00
175 changed files with 9569 additions and 6265 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.10.5
current_version = 2024.12.3
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

@ -35,14 +35,6 @@ runs:
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
```
For arm64, use these values:
```shell
AUTHENTIK_IMAGE=ghcr.io/goauthentik/dev-server
AUTHENTIK_TAG=${{ inputs.tag }}-arm64
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
```
Afterwards, run the upgrade commands from the latest release notes.
</details>
<details>
@ -60,18 +52,6 @@ runs:
tag: ${{ inputs.tag }}
```
For arm64, use these values:
```yaml
authentik:
outposts:
container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
global:
image:
repository: ghcr.io/goauthentik/dev-server
tag: ${{ inputs.tag }}-arm64
```
Afterwards, run the upgrade commands from the latest release notes.
</details>
edit-mode: replace

View File

@ -9,6 +9,9 @@ inputs:
image-arch:
required: false
description: "Docker image arch"
release:
required: true
description: "True if this is a release build, false if this is a dev/PR build"
outputs:
shouldPush:
@ -29,15 +32,24 @@ outputs:
imageTags:
description: "Docker image tags"
value: ${{ steps.ev.outputs.imageTags }}
imageTagsJSON:
description: "Docker image tags, as a JSON array"
value: ${{ steps.ev.outputs.imageTagsJSON }}
attestImageNames:
description: "Docker image names used for attestation"
value: ${{ steps.ev.outputs.attestImageNames }}
cacheTo:
description: "cache-to value for the docker build step"
value: ${{ steps.ev.outputs.cacheTo }}
imageMainTag:
description: "Docker image main tag"
value: ${{ steps.ev.outputs.imageMainTag }}
imageMainName:
description: "Docker image main name"
value: ${{ steps.ev.outputs.imageMainName }}
imageBuildArgs:
description: "Docker image build args"
value: ${{ steps.ev.outputs.imageBuildArgs }}
runs:
using: "composite"
@ -48,6 +60,8 @@ runs:
env:
IMAGE_NAME: ${{ inputs.image-name }}
IMAGE_ARCH: ${{ inputs.image-arch }}
RELEASE: ${{ inputs.release }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
REF: ${{ github.ref }}
run: |
python3 ${{ github.action_path }}/push_vars.py

View File

@ -2,6 +2,7 @@
import configparser
import os
from json import dumps
from time import time
parser = configparser.ConfigParser()
@ -48,7 +49,7 @@ if is_release:
]
else:
suffix = ""
if image_arch and image_arch != "amd64":
if image_arch:
suffix = f"-{image_arch}"
for name in image_names:
image_tags += [
@ -70,12 +71,31 @@ def get_attest_image_names(image_with_tags: list[str]):
return ",".join(set(image_tags))
# Generate `cache-to` param
cache_to = ""
if should_push:
_cache_tag = "buildcache"
if image_arch:
_cache_tag += f"-{image_arch}"
cache_to = f"type=registry,ref={get_attest_image_names(image_tags)}:{_cache_tag},mode=max"
image_build_args = []
if os.getenv("RELEASE", "false").lower() == "true":
image_build_args = [f"VERSION={os.getenv('REF')}"]
else:
image_build_args = [f"GIT_BUILD_HASH={sha}"]
image_build_args = "\n".join(image_build_args)
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"shouldPush={str(should_push).lower()}", file=_output)
print(f"sha={sha}", file=_output)
print(f"version={version}", file=_output)
print(f"prerelease={prerelease}", file=_output)
print(f"imageTags={','.join(image_tags)}", file=_output)
print(f"imageTagsJSON={dumps(image_tags)}", file=_output)
print(f"attestImageNames={get_attest_image_names(image_tags)}", file=_output)
print(f"imageMainTag={image_main_tag}", file=_output)
print(f"imageMainName={image_tags[0]}", file=_output)
print(f"cacheTo={cache_to}", file=_output)
print(f"imageBuildArgs={image_build_args}", file=_output)

View File

@ -1,7 +1,18 @@
#!/bin/bash -x
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# Non-pushing PR
GITHUB_OUTPUT=/dev/stdout \
GITHUB_REF=ref \
GITHUB_SHA=sha \
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
GITHUB_REPOSITORY=goauthentik/authentik \
python $SCRIPT_DIR/push_vars.py
# Pushing PR/main
GITHUB_OUTPUT=/dev/stdout \
GITHUB_REF=ref \
GITHUB_SHA=sha \
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
GITHUB_REPOSITORY=goauthentik/authentik \
DOCKER_USERNAME=foo \
python $SCRIPT_DIR/push_vars.py

View File

@ -0,0 +1,95 @@
# Re-usable workflow for a single-architecture build
name: Single-arch Container build
on:
workflow_call:
inputs:
image_name:
required: true
type: string
image_arch:
required: true
type: string
runs-on:
required: true
type: string
registry_dockerhub:
default: false
type: boolean
registry_ghcr:
default: false
type: boolean
release:
default: false
type: boolean
outputs:
image-digest:
value: ${{ jobs.build.outputs.image-digest }}
jobs:
build:
name: Build ${{ inputs.image_arch }}
runs-on: ${{ inputs.runs-on }}
outputs:
image-digest: ${{ steps.push.outputs.digest }}
permissions:
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3.3.0
- uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ${{ inputs.image_name }}
image-arch: ${{ inputs.image_arch }}
release: ${{ inputs.release }}
- name: Login to Docker Hub
if: ${{ inputs.registry_dockerhub }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ inputs.registry_ghcr }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: make empty clients
if: ${{ inputs.release }}
run: |
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: generate ts client
if: ${{ !inputs.release }}
run: make gen-client-ts
- name: Build Docker Image
uses: docker/build-push-action@v6
id: push
with:
context: .
push: true
secrets: |
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
build-args: |
${{ steps.ev.outputs.imageBuildArgs }}
tags: ${{ steps.ev.outputs.imageTags }}
platforms: linux/${{ inputs.image_arch }}
cache-from: type=registry,ref=${{ steps.ev.outputs.attestImageNames }}:buildcache-${{ inputs.image_arch }}
cache-to: ${{ steps.ev.outputs.cacheTo }}
- uses: actions/attest-build-provenance@v2
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@ -0,0 +1,102 @@
# Re-usable workflow for a multi-architecture build
name: Multi-arch container build
on:
workflow_call:
inputs:
image_name:
required: true
type: string
registry_dockerhub:
default: false
type: boolean
registry_ghcr:
default: true
type: boolean
release:
default: false
type: boolean
outputs: {}
jobs:
build-server-amd64:
uses: ./.github/workflows/_reusable-docker-build-single.yaml
secrets: inherit
with:
image_name: ${{ inputs.image_name }}
image_arch: amd64
runs-on: ubuntu-latest
registry_dockerhub: ${{ inputs.registry_dockerhub }}
registry_ghcr: ${{ inputs.registry_ghcr }}
release: ${{ inputs.release }}
build-server-arm64:
uses: ./.github/workflows/_reusable-docker-build-single.yaml
secrets: inherit
with:
image_name: ${{ inputs.image_name }}
image_arch: arm64
runs-on: ubuntu-22.04-arm
registry_dockerhub: ${{ inputs.registry_dockerhub }}
registry_ghcr: ${{ inputs.registry_ghcr }}
release: ${{ inputs.release }}
get-tags:
runs-on: ubuntu-latest
needs:
- build-server-amd64
- build-server-arm64
outputs:
tags: ${{ steps.ev.outputs.imageTagsJSON }}
steps:
- uses: actions/checkout@v4
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ${{ inputs.image_name }}
merge-server:
runs-on: ubuntu-latest
needs:
- get-tags
- build-server-amd64
- build-server-arm64
strategy:
fail-fast: false
matrix:
tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
steps:
- uses: actions/checkout@v4
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ${{ inputs.image_name }}
- name: Login to Docker Hub
if: ${{ inputs.registry_dockerhub }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ inputs.registry_ghcr }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: int128/docker-manifest-create-action@v2
id: build
with:
tags: ${{ matrix.tag }}
sources: |
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-amd64.outputs.image-digest }}
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-arm64.outputs.image-digest }}
- uses: actions/attest-build-provenance@v2
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true

View File

@ -223,68 +223,18 @@ jobs:
with:
jobs: ${{ toJSON(needs) }}
build:
strategy:
fail-fast: false
matrix:
arch:
- amd64
- arm64
needs: ci-core-mark
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/dev-server
image-arch: ${{ matrix.arch }}
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: generate ts client
run: make gen-client-ts
- name: Build Docker Image
uses: docker/build-push-action@v6
id: push
with:
context: .
secrets: |
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
tags: ${{ steps.ev.outputs.imageTags }}
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max' || '' }}
platforms: linux/${{ matrix.arch }}
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
needs: ci-core-mark
uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit
with:
image_name: ghcr.io/goauthentik/dev-server
release: false
pr-comment:
needs:
- build

View File

@ -72,7 +72,7 @@ jobs:
- rac
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write

View File

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

View File

@ -7,64 +7,23 @@ on:
jobs:
build-server:
runs-on: ubuntu-latest
uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit
permissions:
# Needed to upload contianer images to ghcr.io
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/server,beryju/authentik
- name: Docker Login Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: make empty clients
run: |
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: Build Docker Image
uses: docker/build-push-action@v6
id: push
with:
context: .
push: true
secrets: |
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
build-args: |
VERSION=${{ github.ref }}
tags: ${{ steps.ev.outputs.imageTags }}
platforms: linux/amd64,linux/arm64
- uses: actions/attest-build-provenance@v2
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
with:
image_name: ghcr.io/goauthentik/server,beryju/authentik
release: true
registry_dockerhub: true
registry_ghcr: true
build-outpost:
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
@ -188,8 +147,8 @@ jobs:
aws-region: ${{ env.AWS_REGION }}
- name: Upload template
run: |
aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
test-release:
needs:
- build-server

View File

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

View File

@ -94,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 5: Python dependencies
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS python-deps
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-deps
ARG TARGETARCH
ARG TARGETVARIANT
@ -116,15 +116,30 @@ RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
--mount=type=cache,target=/root/.cache/pip \
--mount=type=cache,target=/root/.cache/pypoetry \
pip install --no-cache cffi && \
apt-get update && \
apt-get install -y --no-install-recommends \
build-essential libffi-dev \
# Required for cryptography
curl pkg-config \
# Required for lxml
libxslt-dev zlib1g-dev \
# Required for xmlsec
libltdl-dev \
# Required for kadmin
sccache clang && \
curl https://sh.rustup.rs -sSf | sh -s -- -y && \
. "$HOME/.cargo/env" && \
python -m venv /ak-root/venv/ && \
bash -c "source ${VENV_PATH}/bin/activate && \
pip3 install --upgrade pip && \
pip3 install poetry && \
pip3 install --upgrade pip poetry && \
poetry config --local installer.no-binary cryptography,xmlsec,lxml,python-kadmin-rs && \
poetry install --only=main --no-ansi --no-interaction --no-root && \
pip install --force-reinstall /wheels/*"
pip uninstall cryptography -y && \
poetry install --only=main --no-ansi --no-interaction --no-root"
# Stage 6: Run
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS final-image
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS final-image
ARG VERSION
ARG GIT_BUILD_HASH
@ -141,7 +156,7 @@ WORKDIR /
# We cannot cache this layer otherwise we'll end up with a bigger image
RUN apt-get update && \
# Required for runtime
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 && \
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 libltdl7 libxslt1.1 && \
# Required for bootstrap & healtcheck
apt-get install -y --no-install-recommends runit && \
apt-get clean && \
@ -176,9 +191,8 @@ ENV TMPDIR=/dev/shm/ \
PYTHONUNBUFFERED=1 \
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
VENV_PATH="/ak-root/venv" \
POETRY_VIRTUALENVS_CREATE=false
ENV GOFIPS=1
POETRY_VIRTUALENVS_CREATE=false \
GOFIPS=1
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]

View File

@ -45,15 +45,6 @@ help: ## Show this help
go-test:
go test -timeout 0 -v -race -cover ./...
test-docker: ## Run all tests in a docker-compose
echo "PG_PASS=$(shell openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64 -w 0)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis
docker compose run -u root server test-all
rm -f .env
test: ## Run the server tests and produce a coverage report (locally)
coverage run manage.py test --keepdb authentik
coverage html
@ -263,6 +254,9 @@ docker: ## Build a docker image of the current source tree
mkdir -p ${GEN_API_TS}
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
test-docker:
./scripts/test_docker.sh
#########################
## CI
#########################

View File

@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
| Version | Supported |
| --------- | --------- |
| 2024.8.x | ✅ |
| 2024.10.x | ✅ |
| 2024.12.x | ✅ |
## Reporting a Vulnerability

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.10.5"
__version__ = "2024.12.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,15 +1,16 @@
"""Application Roles API Viewset"""
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import (
Application,
ApplicationEntitlement,
User,
)
@ -18,7 +19,10 @@ class ApplicationEntitlementSerializer(ModelSerializer):
def validate_app(self, app: Application) -> Application:
"""Ensure user has permission to view"""
user: User = self._context["request"].user
request: HttpRequest = self.context.get("request")
if not request and SERIALIZER_CONTEXT_BLUEPRINT in self.context:
return app
user = request.user
if user.has_perm("view_application", app) or user.has_perm(
"authentik_core.view_application"
):

View File

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

View File

@ -159,9 +159,9 @@ class ConnectionToken(ExpiringModel):
default_settings["port"] = str(port)
else:
default_settings["hostname"] = self.endpoint.host
default_settings["client-name"] = "authentik"
# default_settings["enable-drive"] = "true"
# default_settings["drive-name"] = "authentik"
if self.endpoint.protocol == Protocols.RDP:
default_settings["resize-method"] = "display-update"
default_settings["client-name"] = f"authentik - {self.session.user}"
settings = {}
always_merger.merge(settings, default_settings)
always_merger.merge(settings, self.endpoint.provider.settings)

View File

@ -50,9 +50,10 @@ class TestModels(TransactionTestCase):
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": "authentik",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"resize-method": "display-update",
},
)
# Set settings in provider
@ -63,10 +64,11 @@ class TestModels(TransactionTestCase):
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": "authentik",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"level": "provider",
"resize-method": "display-update",
},
)
# Set settings in endpoint
@ -79,10 +81,11 @@ class TestModels(TransactionTestCase):
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": "authentik",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"level": "endpoint",
"resize-method": "display-update",
},
)
# Set settings in token
@ -95,10 +98,11 @@ class TestModels(TransactionTestCase):
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": "authentik",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"level": "token",
"resize-method": "display-update",
},
)
# Set settings in property mapping (provider)
@ -114,10 +118,11 @@ class TestModels(TransactionTestCase):
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": "authentik",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"level": "property_mapping_provider",
"resize-method": "display-update",
},
)
# Set settings in property mapping (endpoint)
@ -135,11 +140,12 @@ class TestModels(TransactionTestCase):
{
"hostname": self.endpoint.host.split(":")[0],
"port": "1324",
"client-name": "authentik",
"client-name": f"authentik - {self.user}",
"drive-path": path,
"create-drive-path": "true",
"level": "property_mapping_endpoint",
"foo": "true",
"bar": "6",
"resize-method": "display-update",
},
)

View File

@ -138,7 +138,6 @@ def notification_cleanup(self: SystemTask):
"""Cleanup seen notifications and notifications whose event expired."""
notifications = Notification.objects.filter(Q(event=None) | Q(seen=True))
amount = notifications.count()
for notification in notifications:
notification.delete()
notifications.delete()
LOGGER.debug("Expired notifications", amount=amount)
self.set_status(TaskStatus.SUCCESSFUL, f"Expired {amount} Notifications")

View File

@ -109,6 +109,8 @@ class FlowPlan:
def pop(self):
"""Pop next pending stage from bottom of list"""
if not self.markers and not self.bindings:
return
self.markers.pop(0)
self.bindings.pop(0)
@ -156,8 +158,13 @@ class FlowPlan:
final_stage: type[StageView] = self.bindings[-1].stage.view
temp_exec = FlowExecutorView(flow=flow, request=request, plan=self)
temp_exec.current_stage = self.bindings[-1].stage
temp_exec.current_stage_view = final_stage
temp_exec.setup(request, flow.slug)
stage = final_stage(request=request, executor=temp_exec)
return stage.dispatch(request)
response = stage.dispatch(request)
# Ensure we clean the flow state we have in the session before we redirect away
temp_exec.stage_ok()
return response
return redirect_with_qs(
"authentik_core:if-flow",

View File

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

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

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

View File

@ -280,9 +280,25 @@ class ConfigLoader:
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
return default
def get_optional_int(self, path: str, default=None) -> int | None:
"""Wrapper for get that converts value into int or None if set"""
value = self.get(path, default)
if value is UNSET:
return default
try:
return int(value)
except (ValueError, TypeError) as exc:
if value is None or (isinstance(value, str) and value.lower() == "null"):
return None
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
return default
def get_bool(self, path: str, default=False) -> bool:
"""Wrapper for get that converts value into boolean"""
return str(self.get(path, default)).lower() == "true"
value = self.get(path, UNSET)
if value is UNSET:
return default
return str(self.get(path)).lower() == "true"
def get_keys(self, path: str, sep=".") -> list[str]:
"""List attribute keys by using yaml path"""
@ -354,20 +370,33 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
"sslcert": config.get("postgresql.sslcert"),
"sslkey": config.get("postgresql.sslkey"),
},
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
"postgresql.disable_server_side_cursors", False
),
"TEST": {
"NAME": config.get("postgresql.test.name"),
},
}
}
conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET)
disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET)
if config.get_bool("postgresql.use_pgpool", False):
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
if disable_server_side_cursors is not UNSET:
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = disable_server_side_cursors
if config.get_bool("postgresql.use_pgbouncer", False):
# https://docs.djangoproject.com/en/4.0/ref/databases/#transaction-pooling-server-side-cursors
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
# https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections
db["default"]["CONN_MAX_AGE"] = None # persistent
if disable_server_side_cursors is not UNSET:
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = disable_server_side_cursors
if conn_max_age is not UNSET:
db["default"]["CONN_MAX_AGE"] = conn_max_age
for replica in config.get_keys("postgresql.read_replicas"):
_database = deepcopy(db["default"])

View File

@ -6,8 +6,6 @@ postgresql:
user: authentik
port: 5432
password: "env://POSTGRES_PASSWORD"
use_pgbouncer: false
use_pgpool: false
test:
name: test_authentik
read_replicas: {}

View File

@ -214,6 +214,9 @@ class TestConfig(TestCase):
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
}
},
)
@ -251,6 +254,9 @@ class TestConfig(TestCase):
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
},
"replica_0": {
"ENGINE": "authentik.root.db",
@ -266,6 +272,72 @@ class TestConfig(TestCase):
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
},
},
)
def test_db_read_replicas_pgbouncer(self):
"""Test read replicas"""
config = ConfigLoader()
config.set("postgresql.host", "foo")
config.set("postgresql.name", "foo")
config.set("postgresql.user", "foo")
config.set("postgresql.password", "foo")
config.set("postgresql.port", "foo")
config.set("postgresql.sslmode", "foo")
config.set("postgresql.sslrootcert", "foo")
config.set("postgresql.sslcert", "foo")
config.set("postgresql.sslkey", "foo")
config.set("postgresql.test.name", "foo")
config.set("postgresql.use_pgbouncer", True)
# Read replica
config.set("postgresql.read_replicas.0.host", "bar")
# Override conn_max_age
config.set("postgresql.read_replicas.0.conn_max_age", 10)
# This isn't supported
config.set("postgresql.read_replicas.0.use_pgbouncer", False)
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"DISABLE_SERVER_SIDE_CURSORS": True,
"CONN_MAX_AGE": None,
"CONN_HEALTH_CHECKS": False,
"ENGINE": "authentik.root.db",
"HOST": "foo",
"NAME": "foo",
"OPTIONS": {
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
"sslrootcert": "foo",
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
},
"replica_0": {
"DISABLE_SERVER_SIDE_CURSORS": True,
"CONN_MAX_AGE": 10,
"CONN_HEALTH_CHECKS": False,
"ENGINE": "authentik.root.db",
"HOST": "bar",
"NAME": "foo",
"OPTIONS": {
"sslcert": "foo",
"sslkey": "foo",
"sslmode": "foo",
"sslrootcert": "foo",
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
},
},
)
@ -294,6 +366,8 @@ class TestConfig(TestCase):
{
"default": {
"DISABLE_SERVER_SIDE_CURSORS": True,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"ENGINE": "authentik.root.db",
"HOST": "foo",
"NAME": "foo",
@ -310,6 +384,8 @@ class TestConfig(TestCase):
},
"replica_0": {
"DISABLE_SERVER_SIDE_CURSORS": True,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"ENGINE": "authentik.root.db",
"HOST": "bar",
"NAME": "foo",
@ -362,6 +438,9 @@ class TestConfig(TestCase):
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"DISABLE_SERVER_SIDE_CURSORS": False,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
},
"replica_0": {
"ENGINE": "authentik.root.db",
@ -377,6 +456,9 @@ class TestConfig(TestCase):
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"DISABLE_SERVER_SIDE_CURSORS": False,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
},
},
)

View File

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

View File

@ -256,7 +256,7 @@ class AssertionProcessor:
assertion.attrib["IssueInstant"] = self._issue_instant
assertion.append(self.get_issuer())
if self.provider.signing_kp:
if self.provider.signing_kp and self.provider.sign_assertion:
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
)
@ -295,6 +295,18 @@ class AssertionProcessor:
response.append(self.get_issuer())
if self.provider.signing_kp and self.provider.sign_response:
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
)
signature = xmlsec.template.create(
response,
xmlsec.constants.TransformExclC14N,
sign_algorithm_transform,
ns=xmlsec.constants.DSigNs,
)
response.append(signature)
status = SubElement(response, f"{{{NS_SAML_PROTOCOL}}}Status")
status_code = SubElement(status, f"{{{NS_SAML_PROTOCOL}}}StatusCode")
status_code.attrib["Value"] = "urn:oasis:names:tc:SAML:2.0:status:Success"

View File

@ -2,8 +2,10 @@
from base64 import b64encode
from defusedxml.lxml import fromstring
from django.http.request import QueryDict
from django.test import TestCase
from lxml import etree # nosec
from authentik.blueprints.tests import apply_blueprint
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
@ -11,12 +13,14 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import get_request
from authentik.lib.xml import lxml_from_string
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
from authentik.sources.saml.exceptions import MismatchedRequestID
from authentik.sources.saml.models import SAMLSource
from authentik.sources.saml.processors.constants import (
NS_MAP,
SAML_BINDING_REDIRECT,
SAML_NAME_ID_FORMAT_EMAIL,
SAML_NAME_ID_FORMAT_UNSPECIFIED,
@ -185,6 +189,19 @@ class TestAuthNRequest(TestCase):
self.assertEqual(response.count(response_proc._assertion_id), 2)
self.assertEqual(response.count(response_proc._response_id), 2)
schema = etree.XMLSchema(
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
)
self.assertTrue(schema.validate(lxml_from_string(response)))
response_xml = fromstring(response)
self.assertEqual(
len(response_xml.xpath("//saml:Assertion/ds:Signature", namespaces=NS_MAP)), 1
)
self.assertEqual(
len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1
)
# Now parse the response (source)
http_request.POST = QueryDict(mutable=True)
http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode()

View File

@ -2,9 +2,10 @@
from django.apps import apps
from django.contrib.auth.models import Permission
from django.db.models import QuerySet
from django.db.models import Q, QuerySet
from django_filters.filters import ModelChoiceFilter
from django_filters.filterset import FilterSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
CharField,
@ -13,8 +14,11 @@ from rest_framework.fields import (
ReadOnlyField,
SerializerMethodField,
)
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet
from authentik.blueprints.v1.importer import excluded_models
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.models import User
from authentik.lib.validators import RequiredTogetherValidator
@ -92,7 +96,9 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
queryset = Permission.objects.none()
serializer_class = PermissionSerializer
ordering = ["name"]
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
filterset_class = PermissionFilter
permission_classes = [IsAuthenticated]
search_fields = [
"codename",
"content_type__model",
@ -100,13 +106,13 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
]
def get_queryset(self) -> QuerySet:
return (
Permission.objects.all()
.select_related("content_type")
.filter(
content_type__app_label__startswith="authentik",
query = Q()
for model in excluded_models():
query |= Q(
content_type__app_label=model._meta.app_label,
content_type__model=model._meta.model_name,
)
)
return Permission.objects.all().select_related("content_type").exclude(query)
class PermissionAssignSerializer(PassiveSerializer):

View File

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

View File

@ -38,7 +38,9 @@ class KerberosBackend(InbuiltBackend):
self, username: str, realm: str | None, password: str, **filters
) -> tuple[User | None, KerberosSource | None]:
sources = KerberosSource.objects.filter(enabled=True)
user = User.objects.filter(usersourceconnection__source__in=sources, **filters).first()
user = User.objects.filter(
usersourceconnection__source__in=sources, username=username, **filters
).first()
if user is not None:
# User found, let's get its connections for the sources that are available
@ -77,7 +79,7 @@ class KerberosBackend(InbuiltBackend):
password, sender=user_source_connection.source
)
user_source_connection.user.save()
return user, user_source_connection.source
return user_source_connection.user, user_source_connection.source
# Password doesn't match, onto next source
LOGGER.debug(
"failed to kinit, password invalid",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ from authentik.flows.planner import (
FlowPlanner,
)
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN, InvalidStageError
from authentik.flows.views.executor import SESSION_KEY_GET, SESSION_KEY_PLAN, InvalidStageError
from authentik.lib.utils.urls import reverse_with_qs
from authentik.stages.redirect.models import RedirectMode, RedirectStage
@ -72,7 +72,9 @@ class RedirectStageView(ChallengeStageView):
self.request.session[SESSION_KEY_PLAN] = plan
kwargs = self.executor.kwargs
kwargs.update({"flow_slug": flow.slug})
return reverse_with_qs("authentik_core:if-flow", self.request.GET, kwargs=kwargs)
return reverse_with_qs(
"authentik_core:if-flow", self.request.session[SESSION_KEY_GET], kwargs=kwargs
)
def get_challenge(self, *args, **kwargs) -> Challenge:
"""Get the redirect target. Prioritize `redirect_stage_target` if present."""

View File

@ -1,5 +1,7 @@
"""Test Redirect stage"""
from urllib.parse import urlencode
from django.urls.base import reverse
from rest_framework.exceptions import ValidationError
@ -58,6 +60,23 @@ class TestRedirectStage(FlowTestCase):
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": self.target_flow.slug})
)
def test_flow_query(self):
self.stage.mode = RedirectMode.FLOW
self.stage.save()
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
+ "?"
+ urlencode({"query": urlencode({"test": "foo"})})
)
self.assertStageRedirects(
response,
reverse("authentik_core:if-flow", kwargs={"flow_slug": self.target_flow.slug})
+ "?"
+ urlencode({"test": "foo"}),
)
def test_override_static(self):
policy = ExpressionPolicy.objects.create(
name=generate_id(),

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.5 Blueprint schema",
"title": "authentik 2024.12.3 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.5}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3}
restart: unless-stopped
command: server
environment:
@ -54,7 +54,7 @@ services:
redis:
condition: service_healthy
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.5}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.3}
restart: unless-stopped
command: worker
environment:

View File

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

View File

@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"fmt"
"maps"
"net/http"
"net/url"
"strconv"
@ -16,11 +17,22 @@ import (
"goauthentik.io/internal/constants"
)
func (ac *APIController) getWebsocketURL(akURL url.URL, outpostUUID string, query url.Values) *url.URL {
wsUrl := &url.URL{}
wsUrl.Scheme = strings.ReplaceAll(akURL.Scheme, "http", "ws")
wsUrl.Host = akURL.Host
_p, _ := url.JoinPath(akURL.Path, "ws/outpost/", outpostUUID, "/")
wsUrl.Path = _p
v := url.Values{}
maps.Insert(v, maps.All(akURL.Query()))
maps.Insert(v, maps.All(query))
wsUrl.RawQuery = v.Encode()
return wsUrl
}
func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
pathTemplate := "%s://%s%sws/outpost/%s/?%s"
query := akURL.Query()
query.Set("instance_uuid", ac.instanceUUID.String())
scheme := strings.ReplaceAll(akURL.Scheme, "http", "ws")
authHeader := fmt.Sprintf("Bearer %s", ac.token)
@ -37,7 +49,9 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
},
}
ws, _, err := dialer.Dial(fmt.Sprintf(pathTemplate, scheme, akURL.Host, akURL.Path, outpostUUID, akURL.Query().Encode()), header)
wsu := ac.getWebsocketURL(akURL, outpostUUID, query).String()
ac.logger.WithField("url", wsu).Debug("connecting to websocket")
ws, _, err := dialer.Dial(wsu, header)
if err != nil {
ac.logger.WithError(err).Warning("failed to connect websocket")
return err

View File

@ -0,0 +1,42 @@
package ak
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
)
func URLMustParse(u string) *url.URL {
ur, err := url.Parse(u)
if err != nil {
panic(err)
}
return ur
}
func TestWebsocketURL(t *testing.T) {
u := URLMustParse("http://localhost:9000?foo=bar")
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
ac := &APIController{}
nu := ac.getWebsocketURL(*u, uuid, url.Values{})
assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?foo=bar", nu.String())
}
func TestWebsocketURL_Query(t *testing.T) {
u := URLMustParse("http://localhost:9000?foo=bar")
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
ac := &APIController{}
v := url.Values{}
v.Set("bar", "baz")
nu := ac.getWebsocketURL(*u, uuid, v)
assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?bar=baz&foo=bar", nu.String())
}
func TestWebsocketURL_Subpath(t *testing.T) {
u := URLMustParse("http://localhost:9000/foo/bar/")
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
ac := &APIController{}
nu := ac.getWebsocketURL(*u, uuid, url.Values{})
assert.Equal(t, "ws://localhost:9000/foo/bar/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/", nu.String())
}

View File

@ -1,4 +1,5 @@
#!/usr/bin/env -S bash -e
#!/usr/bin/env -S bash
set -e -o pipefail
MODE_FILE="${TMPDIR}/authentik-mode"
function log {
@ -87,7 +88,6 @@ elif [[ "$1" == "bash" ]]; then
elif [[ "$1" == "test-all" ]]; then
prepare_debug
chmod 777 /root
pip install --force-reinstall /wheels/*
check_if_root "python -m manage test authentik"
elif [[ "$1" == "healthcheck" ]]; then
run_authentik healthcheck $(cat $MODE_FILE)

View File

@ -15,7 +15,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
@ -1898,6 +1898,10 @@ msgstr "Kerberos 领域"
msgid "Custom krb5.conf to use. Uses the system one by default"
msgstr "要使用的自定义 krb5.conf。默认使用系统自带"
#: authentik/sources/kerberos/models.py
msgid "KAdmin server type"
msgstr "KAdmin 服务器类型"
#: authentik/sources/kerberos/models.py
msgid "Sync users from Kerberos into authentik"
msgstr "从 Kerberos 同步用户到 authentik"
@ -2858,7 +2862,7 @@ msgstr ""
#, python-format
msgid ""
"\n"
" If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
" If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
" "
msgstr ""
"\n"
@ -2882,7 +2886,7 @@ msgstr ""
#, python-format
msgid ""
"\n"
"If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
"If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
msgstr ""
"\n"
"如果您没有请求更改密码,请忽略此电子邮件。上面的链接在 %(expires)s 内有效。\n"
@ -3151,6 +3155,22 @@ msgstr "输入阶段"
msgid "Passwords don't match."
msgstr "密码不匹配。"
#: authentik/stages/redirect/api.py
msgid "Target URL should be present when mode is Static."
msgstr "当模式为静态时,目标 URL 应存在。"
#: authentik/stages/redirect/api.py
msgid "Target Flow should be present when mode is Flow."
msgstr "当模式为流程时,目标流程应存在。"
#: authentik/stages/redirect/models.py
msgid "Redirect Stage"
msgstr "重定向阶段"
#: authentik/stages/redirect/models.py
msgid "Redirect Stages"
msgstr "重定向阶段"
#: authentik/stages/user_delete/models.py
msgid "User Delete Stage"
msgstr "用户删除阶段"

View File

@ -14,7 +14,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: deluxghost, 2024\n"
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
@ -1897,6 +1897,10 @@ msgstr "Kerberos 领域"
msgid "Custom krb5.conf to use. Uses the system one by default"
msgstr "要使用的自定义 krb5.conf。默认使用系统自带"
#: authentik/sources/kerberos/models.py
msgid "KAdmin server type"
msgstr "KAdmin 服务器类型"
#: authentik/sources/kerberos/models.py
msgid "Sync users from Kerberos into authentik"
msgstr "从 Kerberos 同步用户到 authentik"
@ -2857,7 +2861,7 @@ msgstr ""
#, python-format
msgid ""
"\n"
" If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
" If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
" "
msgstr ""
"\n"
@ -2881,7 +2885,7 @@ msgstr ""
#, python-format
msgid ""
"\n"
"If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
"If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
msgstr ""
"\n"
"如果您没有请求更改密码,请忽略此电子邮件。上面的链接在 %(expires)s 内有效。\n"
@ -3150,6 +3154,22 @@ msgstr "输入阶段"
msgid "Passwords don't match."
msgstr "密码不匹配。"
#: authentik/stages/redirect/api.py
msgid "Target URL should be present when mode is Static."
msgstr "当模式为静态时,目标 URL 应存在。"
#: authentik/stages/redirect/api.py
msgid "Target Flow should be present when mode is Flow."
msgstr "当模式为流程时,目标流程应存在。"
#: authentik/stages/redirect/models.py
msgid "Redirect Stage"
msgstr "重定向阶段"
#: authentik/stages/redirect/models.py
msgid "Redirect Stages"
msgstr "重定向阶段"
#: authentik/stages/user_delete/models.py
msgid "User Delete Stage"
msgstr "用户删除阶段"

View File

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

View File

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

View File

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

29
scripts/test_docker.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/bash
set -e -x -o pipefail
hash="$(git rev-parse HEAD || openssl rand -base64 36)"
AUTHENTIK_IMAGE="xghcr.io/goauthentik/server"
AUTHENTIK_TAG="$(echo "$hash" | cut -c1-15)"
if [ -f .env ]; then
echo "Existing .env file, aborting"
exit 1
fi
echo PG_PASS="$(openssl rand -base64 36 | tr -d '\n')" >.env
echo AUTHENTIK_SECRET_KEY="$(openssl rand -base64 60 | tr -d '\n')" >>.env
echo AUTHENTIK_IMAGE="${AUTHENTIK_IMAGE}" >>.env
echo AUTHENTIK_TAG="${AUTHENTIK_TAG}" >>.env
export COMPOSE_PROJECT_NAME="authentik-test-${AUTHENTIK_TAG}"
# Ensure buildx is installed
docker buildx install
# For release builds we have an empty client here as we use the NPM package
mkdir -p ./gen-ts-api
touch .env
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" .
docker compose up --no-start
docker compose start postgresql redis
docker compose run -u root server test-all
docker compose down -v

View File

@ -41,6 +41,7 @@ export default [
},
files: ["src/**"],
rules: {
"lit/attribute-names": "off",
// "lit/attribute-names": "error",
"lit/no-private-properties": "error",
// "lit/prefer-nothing": "warn",

1353
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -68,8 +68,8 @@
"@types/showdown": "^2.0.6",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@wdio/browser-runner": "^9.1.2",
"@wdio/cli": "^9.1.2",
"@wdio/browser-runner": "9.4",
"@wdio/cli": "9.4",
"@wdio/spec-reporter": "^9.1.2",
"chokidar": "^4.0.1",
"chromedriver": "^131.0.1",

View File

@ -9,6 +9,9 @@ const MAX_DEPTH = 4;
const MAX_NESTED_CALLBACKS = 4;
const MAX_PARAMS = 5;
// Waiting for SonarJS to be compatible
// const MAX_COGNITIVE_COMPLEXITY = 9;
const rules = {
"accessor-pairs": "error",
"array-callback-return": "error",
@ -126,6 +129,11 @@ const rules = {
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
// SonarJS is not yet compatible with ESLint 9. Commenting these out
// until it is.
// "sonarjs/cognitive-complexity": ["off", MAX_COGNITIVE_COMPLEXITY],
// "sonarjs/no-duplicate-string": "off",
// "sonarjs/no-nested-template-literals": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
@ -162,6 +170,7 @@ export default [
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
// sonar.configs.recommended,
{
languageOptions: {
parser: tsparser,

View File

@ -29,6 +29,7 @@ export default [
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
// sonar.configs.recommended,
{
languageOptions: {
parser: tsparser,
@ -41,6 +42,11 @@ export default [
rules: {
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
// SonarJS is not yet compatible with ESLint 9. Commenting these out
// until it is.
// "sonarjs/cognitive-complexity": ["off", 9],
// "sonarjs/no-duplicate-string": "off",
// "sonarjs/no-nested-template-literals": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",

View File

@ -25,25 +25,12 @@ import { TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { Application, CoreApi, PolicyEngineMode, Provider } from "@goauthentik/api";
import { Application, CoreApi, Provider } from "@goauthentik/api";
import { policyOptions } from "./PolicyOptions.js";
import "./components/ak-backchannel-input";
import "./components/ak-provider-search-input";
export const policyOptions = [
{
label: "any",
value: PolicyEngineMode.Any,
default: true,
description: html`${msg("Any policy must match to grant access")}`,
},
{
label: "all",
value: PolicyEngineMode.All,
description: html`${msg("All policies must match to grant access")}`,
},
];
@customElement("ak-application-form")
export class ApplicationForm extends WithCapabilitiesConfig(ModelForm<Application, string>) {
constructor() {

View File

@ -18,6 +18,7 @@ import { styleMap } from "lit/directives/style-map.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFLabel from "@patternfly/patternfly/components/Label/label.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
const closeButtonIcon = html`<svg
fill="currentColor"
@ -37,6 +38,7 @@ const closeButtonIcon = html`<svg
export class AkApplicationWizardHint extends AKElement implements ShowHintControllerHost {
static get styles() {
return [
PFBase,
PFButton,
PFPage,
PFLabel,
@ -45,6 +47,9 @@ export class AkApplicationWizardHint extends AKElement implements ShowHintContro
padding-top: 0;
padding-bottom: 0;
}
.ak-hint-text {
padding-bottom: var(--pf-global--spacer--md);
}
`,
];
}
@ -101,16 +106,20 @@ export class AkApplicationWizardHint extends AKElement implements ShowHintContro
return html` <section class="pf-c-page__main-section pf-m-no-padding-mobile">
<ak-hint>
<ak-hint-body>
<p>
<p class="ak-hint-text">
You can now configure both an application and its authentication provider at
the same time with our new Application Wizard.
<!-- <a href="(link to docs)">Learn more about the wizard here.</a> -->
</p>
<ak-application-wizard
.open=${getURLParam("createWizard", false)}
.showButton=${false}
></ak-application-wizard>
<ak-application-wizard .open=${getURLParam("createWizard", false)}>
<button
slot="trigger"
class="pf-c-button pf-m-primary"
data-ouia-component-id="start-application-wizard"
>
${msg("Create with wizard")}
</button>
</ak-application-wizard>
</ak-hint-body>
${this.showHintController.render()}
</ak-hint>

View File

@ -0,0 +1,18 @@
import { msg } from "@lit/localize";
import { html } from "lit";
import { PolicyEngineMode } from "@goauthentik/api";
export const policyOptions = [
{
label: "any",
value: PolicyEngineMode.Any,
default: true,
description: html`${msg("Any policy must match to grant access")}`,
},
{
label: "all",
value: PolicyEngineMode.All,
description: html`${msg("All policies must match to grant access")}`,
},
];

View File

@ -8,6 +8,7 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co
import PFInputGroup from "@patternfly/patternfly/components/InputGroup/input-group.css";
import PFRadio from "@patternfly/patternfly/components/Radio/radio.css";
import PFSwitch from "@patternfly/patternfly/components/Switch/switch.css";
import PFWizard from "@patternfly/patternfly/components/Wizard/wizard.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
export const styles = [
@ -20,6 +21,7 @@ export const styles = [
PFInputGroup,
PFFormControl,
PFSwitch,
PFWizard,
css`
select[multiple] {
height: 15em;

View File

@ -0,0 +1,86 @@
import { styles } from "@goauthentik/admin/applications/wizard/ApplicationWizardFormStepStyles.css.js";
import { WizardStep } from "@goauthentik/components/ak-wizard/WizardStep.js";
import {
NavigationUpdate,
WizardNavigationEvent,
WizardUpdateEvent,
} from "@goauthentik/components/ak-wizard/events";
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { property, query } from "lit/decorators.js";
import { ValidationError } from "@goauthentik/api";
import {
type ApplicationWizardState,
type ApplicationWizardStateUpdate,
ExtendedValidationError,
} from "./types";
export class ApplicationWizardStep extends WizardStep {
static get styles() {
return [...WizardStep.styles, ...styles];
}
@property({ type: Object, attribute: false })
wizard!: ApplicationWizardState;
// As recommended in [WizardStep](../../../components/ak-wizard/WizardStep.ts), we override
// these fields and provide them to all the child classes.
wizardTitle = msg("New application");
wizardDescription = msg("Create a new application");
canCancel = true;
// This should be overridden in the children for more precise targeting.
@query("form")
form!: HTMLFormElement;
get formValues(): KeyUnknown | undefined {
const elements = [
...Array.from(
this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
),
...Array.from(this.form.querySelectorAll<HTMLElement>("[data-ak-control=true]")),
];
return serializeForm(elements as unknown as NodeListOf<HorizontalFormElement>);
}
protected removeErrors(
keyToDelete: keyof ExtendedValidationError,
): ValidationError | undefined {
if (!this.wizard.errors) {
return undefined;
}
const empty = {};
const errors = Object.entries(this.wizard.errors).reduce(
(acc, [key, value]) =>
key === keyToDelete ||
value === undefined ||
(Array.isArray(this.wizard?.errors?.[key]) && this.wizard.errors[key].length === 0)
? acc
: { ...acc, [key]: value },
empty,
);
return errors;
}
// This pattern became visible during development, and the order is important: wizard updating
// and validation must complete before navigation is attempted.
public handleUpdate(
update?: ApplicationWizardStateUpdate,
destination?: string,
enable?: NavigationUpdate,
) {
// Inform ApplicationWizard of content state
if (update) {
this.dispatchEvent(new WizardUpdateEvent(update));
}
// Inform WizardStepManager of steps state
if (destination || enable) {
this.dispatchEvent(new WizardNavigationEvent(destination, enable));
}
}
}

View File

@ -1,72 +0,0 @@
import { WizardPanel } from "@goauthentik/components/ak-wizard-main/types";
import { AKElement } from "@goauthentik/elements/Base";
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { consume } from "@lit/context";
import { query } from "@lit/reactive-element/decorators.js";
import { styles as AwadStyles } from "./BasePanel.css";
import { applicationWizardContext } from "./ContextIdentity";
import type { ApplicationWizardState, ApplicationWizardStateUpdate } from "./types";
/**
* Application Wizard Base Panel
*
* All of the displays in our system inherit from this object, which supplies the basic CSS for all
* the inputs we display, as well as the values and validity state for the form currently being
* displayed.
*
*/
export class ApplicationWizardPageBase
extends CustomEmitterElement(AKElement)
implements WizardPanel
{
static get styles() {
return AwadStyles;
}
@consume({ context: applicationWizardContext })
public wizard!: ApplicationWizardState;
@query("form")
form!: HTMLFormElement;
/**
* Provide access to the values on the current form. Child implementations use this to craft the
* update that will be sent using `dispatchWizardUpdate` below.
*/
get formValues(): KeyUnknown | undefined {
const elements = [
...Array.from(
this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
),
...Array.from(this.form.querySelectorAll<HTMLElement>("[data-ak-control=true]")),
];
return serializeForm(elements as unknown as NodeListOf<HorizontalFormElement>);
}
/**
* Provide access to the validity of the current form. Child implementations use this to craft
* the update that will be sent using `dispatchWizardUpdate` below.
*/
get valid() {
return this.form.checkValidity();
}
rendered = false;
/**
* Provide a single source of truth for the token used to notify the orchestrator that an event
* happens. The token `ak-wizard-update` is used by the Wizard framework's reactive controller
* to route "data on the current step has changed" events to the orchestrator.
*/
dispatchWizardUpdate(update: ApplicationWizardStateUpdate) {
this.dispatchCustomEvent("ak-wizard-update", update);
}
}
export default ApplicationWizardPageBase;

View File

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

View File

@ -0,0 +1,109 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/components/ak-wizard/ak-wizard-steps.js";
import { WizardUpdateEvent } from "@goauthentik/components/ak-wizard/events";
import { AKElement } from "@goauthentik/elements/Base.js";
import { ContextProvider } from "@lit/context";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ProvidersApi, ProxyMode } from "@goauthentik/api";
import { applicationWizardProvidersContext } from "./ContextIdentity";
import { providerTypeRenderers } from "./steps/ProviderChoices.js";
import "./steps/ak-application-wizard-application-step.js";
import "./steps/ak-application-wizard-bindings-step.js";
import "./steps/ak-application-wizard-edit-binding-step.js";
import "./steps/ak-application-wizard-provider-choice-step.js";
import "./steps/ak-application-wizard-provider-step.js";
import "./steps/ak-application-wizard-submit-step.js";
import { type ApplicationWizardState, type ApplicationWizardStateUpdate } from "./types";
const freshWizardState = (): ApplicationWizardState => ({
providerModel: "",
currentBinding: -1,
app: {},
provider: {},
proxyMode: ProxyMode.Proxy,
bindings: [],
errors: {},
});
@customElement("ak-application-wizard-main")
export class AkApplicationWizardMain extends AKElement {
@state()
wizard: ApplicationWizardState = freshWizardState();
wizardProviderProvider = new ContextProvider(this, {
context: applicationWizardProvidersContext,
initialValue: [],
});
constructor() {
super();
this.addEventListener(WizardUpdateEvent.eventName, this.handleUpdate);
}
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].render,
}))
.sort(
(a, b) =>
providerTypeRenderers[a.modelName].order -
providerTypeRenderers[b.modelName].order,
)
.reverse(),
);
});
}
// This is the actual top of the Wizard; so this is where we accept the update information and
// incorporate it into the wizard.
handleUpdate(ev: WizardUpdateEvent<ApplicationWizardStateUpdate>) {
ev.stopPropagation();
const update = ev.content;
if (update !== undefined) {
this.wizard = {
...this.wizard,
...update,
};
}
}
render() {
return html`<ak-wizard-steps>
<ak-application-wizard-application-step
slot="application"
.wizard=${this.wizard}
></ak-application-wizard-application-step>
<ak-application-wizard-provider-choice-step
slot="provider-choice"
.wizard=${this.wizard}
></ak-application-wizard-provider-choice-step>
<ak-application-wizard-provider-step
slot="provider"
.wizard=${this.wizard}
></ak-application-wizard-provider-step>
<ak-application-wizard-bindings-step
slot="bindings"
.wizard=${this.wizard}
></ak-application-wizard-bindings-step>
<ak-application-wizard-edit-binding-step
slot="edit-binding"
.wizard=${this.wizard}
></ak-application-wizard-edit-binding-step>
<ak-application-wizard-submit-step
slot="submit"
.wizard=${this.wizard}
></ak-application-wizard-submit-step>
</ak-wizard-steps>`;
}
}

View File

@ -1,117 +1,32 @@
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { WizardCloseEvent } from "@goauthentik/components/ak-wizard/events.js";
import { ModalButton } from "@goauthentik/elements/buttons/ModalButton";
import { bound } from "@goauthentik/elements/decorators/bound.js";
import { ContextProvider } from "@lit/context";
import { msg } from "@lit/localize";
import { customElement, state } from "lit/decorators.js";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
import { applicationWizardContext } from "./ContextIdentity";
import { newSteps } from "./steps";
import {
ApplicationStep,
ApplicationWizardState,
ApplicationWizardStateUpdate,
OneOfProvider,
} from "./types";
const freshWizardState = (): ApplicationWizardState => ({
providerModel: "",
app: {},
provider: {},
errors: {},
});
import "./ak-application-wizard-main.js";
@customElement("ak-application-wizard")
export class ApplicationWizard extends CustomListenerElement(
AkWizard<ApplicationWizardStateUpdate, ApplicationStep>,
) {
export class AkApplicationWizard extends ModalButton {
constructor() {
super(msg("Create With Wizard"), msg("New application"), msg("Create a new application"));
this.steps = newSteps();
super();
this.addEventListener(WizardCloseEvent.eventName, this.onCloseEvent);
}
/**
* We're going to be managing the content of the forms by percolating all of the data up to this
* class, which will ultimately transmit all of it to the server as a transaction. The
* WizardFramework doesn't know anything about the nature of the data itself; it just forwards
* valid updates to us. So here we maintain a state object *and* update it so all child
* components can access the wizard state.
*
*/
@state()
wizardState: ApplicationWizardState = freshWizardState();
wizardStateProvider = new ContextProvider(this, {
context: applicationWizardContext,
initialValue: this.wizardState,
});
/**
* 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
* back and type it all back in," even if it's probably rare that someone will chose one
* provider, realize it's the wrong one, and go back to chose a different one, *and then go
* back*. Nonetheless, strive to *never* lose customer input.
*
*/
providerCache: Map<string, OneOfProvider> = new Map();
// And this is where all the special cases go...
handleUpdate(detail: ApplicationWizardStateUpdate) {
if (detail.status === "submitted") {
this.step.valid = true;
this.requestUpdate();
return;
}
this.step.valid = this.step.valid || detail.status === "valid";
const update = detail.update;
if (!update) {
return;
}
// When the providerModel enum changes, retrieve the customer's prior work for *this* wizard
// session (and only this wizard session) or provide an empty model with a default provider
// name.
if (update.providerModel && update.providerModel !== this.wizardState.providerModel) {
const requestedProvider = this.providerCache.get(update.providerModel) ?? {
name: `Provider for ${this.wizardState.app.name}`,
};
if (this.wizardState.providerModel) {
this.providerCache.set(this.wizardState.providerModel, this.wizardState.provider);
}
update.provider = requestedProvider;
}
this.wizardState = update as ApplicationWizardState;
this.wizardStateProvider.setValue(this.wizardState);
this.requestUpdate();
@bound
onCloseEvent(ev: WizardCloseEvent) {
ev.stopPropagation();
this.open = false;
}
close() {
this.steps = newSteps();
this.currentStep = 0;
this.wizardState = freshWizardState();
this.providerCache = new Map();
this.wizardStateProvider.setValue(this.wizardState);
this.frame.value!.open = false;
}
handleNav(stepId: number | undefined) {
if (stepId === undefined || this.steps[stepId] === undefined) {
throw new Error(`Attempt to navigate to undefined step: ${stepId}`);
}
if (stepId > this.currentStep && !this.step.valid) {
return;
}
this.currentStep = stepId;
this.requestUpdate();
renderModalInner() {
return html` <ak-application-wizard-main> </ak-application-wizard-main>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard": ApplicationWizard;
"ak-application-wizard": AkApplicationWizard;
}
}

View File

@ -1,103 +0,0 @@
import { policyOptions } from "@goauthentik/admin/applications/ApplicationForm";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-slug-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
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 { TemplateResult, html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import BasePanel from "../BasePanel";
@customElement("ak-application-wizard-application-details")
export class ApplicationWizardApplicationDetails extends BasePanel {
handleChange(_ev: Event) {
const formValues = this.formValues;
if (!formValues) {
throw new Error("No application values on form?");
}
this.dispatchWizardUpdate({
update: {
...this.wizard,
app: formValues,
},
status: this.valid ? "valid" : "invalid",
});
}
render(): TemplateResult {
return html` <form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
value=${ifDefined(this.wizard.app?.name)}
label=${msg("Name")}
required
help=${msg("Application's display Name.")}
id="ak-application-wizard-details-name"
.errorMessages=${this.wizard.errors.app?.name ?? []}
></ak-text-input>
<ak-slug-input
name="slug"
value=${ifDefined(this.wizard.app?.slug)}
label=${msg("Slug")}
source="#ak-application-wizard-details-name"
required
help=${msg("Internal application name used in URLs.")}
.errorMessages=${this.wizard.errors.app?.slug ?? []}
></ak-slug-input>
<ak-text-input
name="group"
value=${ifDefined(this.wizard.app?.group)}
label=${msg("Group")}
.errorMessages=${this.wizard.errors.app?.group ?? []}
help=${msg(
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
)}
></ak-text-input>
<ak-radio-input
label=${msg("Policy engine mode")}
required
name="policyEngineMode"
.options=${policyOptions}
.value=${this.wizard.app?.policyEngineMode}
.errorMessages=${this.wizard.errors.app?.policyEngineMode ?? []}
></ak-radio-input>
<ak-form-group aria-label=${msg("UI Settings")}>
<span slot="header"> ${msg("UI Settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="metaLaunchUrl"
label=${msg("Launch URL")}
value=${ifDefined(this.wizard.app?.metaLaunchUrl)}
help=${msg(
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
)}
.errorMessages=${this.wizard.errors.app?.metaLaunchUrl ?? []}
></ak-text-input>
<ak-switch-input
name="openInNewTab"
?checked=${first(this.wizard.app?.openInNewTab, false)}
label=${msg("Open in new tab")}
help=${msg(
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
)}
>
</ak-switch-input>
</div>
</ak-form-group>
</form>`;
}
}
export default ApplicationWizardApplicationDetails;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-application-details": ApplicationWizardApplicationDetails;
}
}

View File

@ -1,176 +0,0 @@
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";
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;

View File

@ -1,62 +0,0 @@
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html } from "lit";
import BasePanel from "../BasePanel";
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) {
render() {
const selectedTypes = providerModelsList.filter(
(t) => t.formName === 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
? html`<form class="pf-c-form pf-m-horizontal">
<ak-wizard-page-type-create
.types=${typesForWizard}
layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
this.dispatchWizardUpdate({
update: {
...this.wizard,
providerModel: ev.detail.formName,
errors: {},
},
status: this.valid ? "valid" : "invalid",
});
}}
></ak-wizard-page-type-create>
</form>`
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
}
}
export default ApplicationWizardAuthenticationMethodChoice;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-method-choice": ApplicationWizardAuthenticationMethodChoice;
}
}

View File

@ -1,237 +0,0 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { parseAPIError } from "@goauthentik/common/errors";
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/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { classMap } from "lit/directives/class-map.js";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
import {
type ApplicationRequest,
CoreApi,
type ModelRequest,
type TransactionApplicationRequest,
type TransactionApplicationResponse,
ValidationError,
instanceOfValidationError,
} 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 {
name: "",
slug: "",
...app,
};
}
type ProviderModelType = Exclude<ModelRequest["providerModel"], "11184809">;
type State = {
state: "idle" | "running" | "error" | "success";
label: string | TemplateResult;
icon: string[];
};
const idleState: State = {
state: "idle",
label: "",
icon: ["fa-cogs", "pf-m-pending"],
};
const runningState: State = {
state: "running",
label: msg("Saving Application..."),
icon: ["fa-cogs", "pf-m-info"],
};
const errorState: State = {
state: "error",
label: msg("authentik was unable to save this application:"),
icon: ["fa-times-circle", "pf-m-danger"],
};
const successState: State = {
state: "success",
label: msg("Your application has been saved"),
icon: ["fa-check-circle", "pf-m-success"],
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isValidationError = (v: any): v is ValidationError => instanceOfValidationError(v);
@customElement("ak-application-wizard-commit-application")
export class ApplicationWizardCommitApplication extends BasePanel {
static get styles() {
return [
...super.styles,
PFBullseye,
PFEmptyState,
PFTitle,
PFProgressStepper,
css`
.pf-c-title {
padding-bottom: var(--pf-global--spacer--md);
}
`,
];
}
@state()
commitState: State = idleState;
@state()
errors?: ValidationError;
response?: TransactionApplicationResponse;
willUpdate(_changedProperties: PropertyValues<this>) {
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)}`,
);
}
const request: TransactionApplicationRequest = {
providerModel: providerModel.modelName as ProviderModelType,
app: cleanApplication(this.wizard.app),
provider: providerModel.converter(this.wizard.provider),
};
this.send(request);
}
}
async send(
data: TransactionApplicationRequest,
): Promise<TransactionApplicationResponse | void> {
this.errors = undefined;
new CoreApi(DEFAULT_CONFIG)
.coreTransactionalApplicationsUpdate({
transactionApplicationRequest: data,
})
.then((response: TransactionApplicationResponse) => {
this.response = response;
this.dispatchCustomEvent(EVENT_REFRESH);
this.dispatchWizardUpdate({ status: "submitted" });
this.commitState = successState;
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch(async (resolution: any) => {
const errors = await parseAPIError(resolution);
// 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
// move that to the `provider.name` error field so it shows up in the right place.
if (isValidationError(errors) && Array.isArray(errors?.app?.provider)) {
const providerError = errors.app.provider;
errors.provider = errors.provider ?? {};
errors.provider.name = providerError;
delete errors.app.provider;
if (Object.keys(errors.app).length === 0) {
delete errors.app;
}
}
this.errors = errors;
this.dispatchWizardUpdate({
update: {
...this.wizard,
errors: this.errors,
},
status: "failed",
});
this.commitState = errorState;
});
}
renderErrors(errors?: ValidationError) {
if (!errors) {
return nothing;
}
const navTo = (step: number) => () =>
this.dispatchCustomEvent("ak-wizard-nav", {
command: "goto",
step,
});
if (errors.app) {
return html`<p>${msg("There was an error in the application.")}</p>
<p><a @click=${navTo(0)}>${msg("Review the application.")}</a></p>`;
}
if (errors.provider) {
return html`<p>${msg("There was an error in the provider.")}</p>
<p><a @click=${navTo(2)}>${msg("Review the provider.")}</a></p>`;
}
if (errors.detail) {
return html`<p>${msg("There was an error")}: ${errors.detail}</p>`;
}
if ((errors?.nonFieldErrors ?? []).length > 0) {
return html`<p>$(msg("There was an error")}:</p>
<ul>
${(errors.nonFieldErrors ?? []).map((e: string) => html`<li>${e}</li>`)}
</ul>`;
}
return html`<p>
${msg(
"There was an error creating the application, but no error message was sent. Please review the server logs.",
)}
</p>`;
}
render() {
const icon = classMap(
this.commitState.icon.reduce((acc, icon) => ({ ...acc, [icon]: true }), {}),
);
return html`
<div>
<div class="pf-l-bullseye">
<div class="pf-c-empty-state pf-m-lg">
<div class="pf-c-empty-state__content">
<i
class="fas fa- ${icon} pf-c-empty-state__icon"
aria-hidden="true"
></i>
<h1
data-commit-state=${this.commitState.state}
class="pf-c-title pf-m-lg"
>
${this.commitState.label}
</h1>
${this.renderErrors(this.errors)}
</div>
</div>
</div>
</div>
`;
}
}
export default ApplicationWizardCommitApplication;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-commit-application": ApplicationWizardCommitApplication;
}
}

View File

@ -1,19 +0,0 @@
import BasePanel from "../BasePanel";
export class ApplicationWizardProviderPageBase extends BasePanel {
handleChange(_ev: InputEvent) {
const formValues = this.formValues;
if (!formValues) {
throw new Error("No provider values on form?");
}
this.dispatchWizardUpdate({
update: {
...this.wizard,
provider: formValues,
},
status: this.valid ? "valid" : "invalid",
});
}
}
export default ApplicationWizardProviderPageBase;

View File

@ -1,34 +0,0 @@
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 "./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";
import "./scim/ak-application-wizard-authentication-by-scim";
@customElement("ak-application-wizard-authentication-method")
export class ApplicationWizardApplicationDetails extends BasePanel {
render() {
const handler = providerRendererList.get(this.wizard.providerModel);
if (!handler) {
throw new Error(
"Unrecognized authentication method in ak-application-wizard-authentication-method",
);
}
return handler();
}
}
export default ApplicationWizardApplicationDetails;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-method": ApplicationWizardApplicationDetails;
}
}

View File

@ -1,173 +0,0 @@
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 { 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 { 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) {
render() {
const provider = this.wizard.provider as LDAPProvider | undefined;
const errors = this.wizard.errors.provider;
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>
</form>`;
}
}
export default ApplicationWizardApplicationDetails;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-by-ldap": ApplicationWizardApplicationDetails;
}
}

View File

@ -1,332 +0,0 @@
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 {
clientTypeOptions,
issuerModeOptions,
redirectUriHelp,
subjectModeOptions,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import {
propertyMappingsProvider,
propertyMappingsSelector,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormHelpers.js";
import {
IRedirectURIInput,
akOAuthRedirectURIInput,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import {
oauth2SourcesProvider,
oauth2SourcesSelector,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.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 {
ClientTypeEnum,
FlowsInstancesListDesignationEnum,
MatchingModeEnum,
RedirectURI,
SourcesApi,
} from "@goauthentik/api";
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@customElement("ak-application-wizard-authentication-by-oauth")
export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
@state()
showClientSecret = true;
@state()
oauthSources?: PaginatedOAuthSourceList;
constructor() {
super();
new SourcesApi(DEFAULT_CONFIG)
.sourcesOauthList({
ordering: "name",
hasJwks: true,
})
.then((oauthSources: PaginatedOAuthSourceList) => {
this.oauthSources = oauthSources;
});
}
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>
<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=${propertyMappingsProvider}
.selector=${propertyMappingsSelector(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=${oauth2SourcesSelector(provider?.jwtFederationSources)}
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 ApplicationWizardAuthenticationByOauth;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-by-oauth": ApplicationWizardAuthenticationByOauth;
}
}

View File

@ -1,269 +0,0 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import {
oauth2SourcesProvider,
oauth2SourcesSelector,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import {
propertyMappingsProvider,
propertyMappingsSelector,
} from "@goauthentik/admin/providers/proxy/ProxyProviderFormHelpers.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=${propertyMappingsProvider}
.selector=${propertyMappingsSelector(
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=${oauth2SourcesSelector(
this.instance?.jwtFederationSources,
)}
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,62 +0,0 @@
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input";
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-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>`;
}
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>
<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>`;
}
}
export default AkReverseProxyApplicationWizardPage;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-for-reverse-proxy": AkReverseProxyApplicationWizardPage;
}
}

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,108 +0,0 @@
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 "@goauthentik/components/ak-text-input";
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.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowsInstancesListDesignationEnum, 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
>
</form>`;
}
}
export default ApplicationWizardAuthenticationByRadius;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-by-radius": ApplicationWizardAuthenticationByRadius;
}
}

View File

@ -1,364 +0,0 @@
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 { 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 {
FlowsInstancesListDesignationEnum,
PaginatedSAMLPropertyMappingList,
PropertymappingsApi,
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);
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>
</form>`;
}
}
export default ApplicationWizardProviderSamlConfiguration;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-by-saml-configuration": ApplicationWizardProviderSamlConfiguration;
}
}

View File

@ -1,120 +0,0 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AKElement } from "@goauthentik/elements/Base";
import { SearchSelect } from "@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 {
PropertymappingsApi,
PropertymappingsProviderSamlListRequest,
SAMLPropertyMapping,
} from "@goauthentik/api";
async function fetchObjects(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;
}
function renderElement(item: SAMLPropertyMapping): string {
return item.name;
}
function renderValue(item: SAMLPropertyMapping | undefined): string | undefined {
return item?.pk;
}
/**
* SAML Property Mapping Search
*
* @element ak-saml-property-mapping-search
*
* A wrapper around SearchSelect for the SAML Property Search. It's a unique search, but for the
* purpose of the form all you need to know is that it is being searched and selected. Let's put the
* how somewhere else.
*
*/
@customElement("ak-saml-property-mapping-search")
export class SAMLPropertyMappingSearch extends CustomListenerElement(AKElement) {
/**
* The current property mapping known to the caller.
*
* @attr
*/
@property({ type: String, reflect: true, attribute: "propertymapping" })
propertyMapping?: string;
@query("ak-search-select")
search!: SearchSelect<SAMLPropertyMapping>;
@property({ type: String })
name: string | null | undefined;
selectedPropertyMapping?: SAMLPropertyMapping;
constructor() {
super();
this.selected = this.selected.bind(this);
this.handleSearchUpdate = this.handleSearchUpdate.bind(this);
}
get value() {
return this.selectedPropertyMapping ? renderValue(this.selectedPropertyMapping) : undefined;
}
connectedCallback() {
super.connectedCallback();
const horizontalContainer = this.closest("ak-form-element-horizontal[name]");
if (!horizontalContainer) {
throw new Error("This search can only be used in a named ak-form-element-horizontal");
}
const name = horizontalContainer.getAttribute("name");
const myName = this.getAttribute("name");
if (name !== null && name !== myName) {
this.setAttribute("name", name);
}
}
handleSearchUpdate(ev: CustomEvent) {
ev.stopPropagation();
this.selectedPropertyMapping = ev.detail.value;
this.dispatchEvent(new InputEvent("input", { bubbles: true, composed: true }));
}
selected(item: SAMLPropertyMapping): boolean {
return this.propertyMapping === item.pk;
}
render() {
return html`
<ak-search-select
.fetchObjects=${fetchObjects}
.renderElement=${renderElement}
.value=${renderValue}
.selected=${this.selected}
@ak-change=${this.handleSearchUpdate}
blankable
>
</ak-search-select>
`;
}
}
export default SAMLPropertyMappingSearch;
declare global {
interface HTMLElementTagNameMap {
"ak-saml-property-mapping-search": SAMLPropertyMappingSearch;
}
}

View File

@ -1,158 +0,0 @@
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 { 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 BaseProviderPanel from "../BaseProviderPanel";
@customElement("ak-application-wizard-authentication-by-scim")
export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
@state()
propertyMappings?: PaginatedSCIMMappingList;
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>
</form>`;
}
}
export default ApplicationWizardAuthenticationBySCIM;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-by-scim": ApplicationWizardAuthenticationBySCIM;
}
}

View File

@ -1,89 +0,0 @@
import {
BackStep,
CancelWizard,
CloseWizard,
DisabledNextStep,
NextStep,
SubmitStep,
} from "@goauthentik/components/ak-wizard-main/commonWizardButtons";
import { msg } from "@lit/localize";
import { html } from "lit";
import "./application/ak-application-wizard-application-details";
import "./auth-method-choice/ak-application-wizard-authentication-method-choice";
import "./commit/ak-application-wizard-commit-application";
import "./methods/ak-application-wizard-authentication-method";
import { ApplicationStep as ApplicationStepType } from "./types";
/**
* In the current implementation, all of the child forms have access to the wizard's
* global context, into which all data is written, and which is updated by events
* flowing into the top-level orchestrator.
*/
class ApplicationStep implements ApplicationStepType {
id = "application";
label = msg("Application Details");
disabled = false;
valid = false;
get buttons() {
return [this.valid ? NextStep : DisabledNextStep, CancelWizard];
}
render() {
return html`<ak-application-wizard-application-details></ak-application-wizard-application-details>`;
}
}
class ProviderMethodStep implements ApplicationStepType {
id = "provider-method";
label = msg("Provider Type");
disabled = false;
valid = false;
get buttons() {
return [this.valid ? NextStep : DisabledNextStep, BackStep, CancelWizard];
}
render() {
// prettier-ignore
return html`<ak-application-wizard-authentication-method-choice
></ak-application-wizard-authentication-method-choice> `;
}
}
class ProviderStepDetails implements ApplicationStepType {
id = "provider-details";
label = msg("Provider Configuration");
disabled = true;
valid = false;
get buttons() {
return [this.valid ? SubmitStep : DisabledNextStep, BackStep, CancelWizard];
}
render() {
return html`<ak-application-wizard-authentication-method></ak-application-wizard-authentication-method>`;
}
}
class SubmitApplicationStep implements ApplicationStepType {
id = "submit";
label = msg("Submit Application");
disabled = true;
valid = false;
get buttons() {
return this.valid ? [CloseWizard] : [BackStep, CancelWizard];
}
render() {
return html`<ak-application-wizard-commit-application></ak-application-wizard-commit-application>`;
}
}
export const newSteps = (): ApplicationStep[] => [
new ApplicationStep(),
new ProviderMethodStep(),
new ProviderStepDetails(),
new SubmitApplicationStep(),
];

View File

@ -0,0 +1,52 @@
import "@goauthentik/admin/common/ak-license-notice";
import { TemplateResult, html } from "lit";
import type { TypeCreate } from "@goauthentik/api";
type ProviderRenderer = () => TemplateResult;
export type LocalTypeCreate = TypeCreate & {
renderer: ProviderRenderer;
};
export const providerTypeRenderers: Record<
string,
{ render: () => TemplateResult; order: number }
> = {
oauth2provider: {
render: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
order: 90,
},
ldapprovider: {
render: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
order: 70,
},
proxyprovider: {
render: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
order: 75,
},
racprovider: {
render: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
order: 80,
},
samlprovider: {
render: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
order: 80,
},
radiusprovider: {
render: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
order: 70,
},
scimprovider: {
render: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
order: 60,
},
};

View File

@ -0,0 +1,152 @@
import {
type DescriptionPair,
renderDescriptionList,
} from "@goauthentik/components/DescriptionList.js";
import { match } from "ts-pattern";
import { msg } from "@lit/localize";
import { html } from "lit";
import {
ClientTypeEnum,
LDAPProvider,
MatchingModeEnum,
OAuth2Provider,
ProviderModelEnum,
ProxyMode,
ProxyProvider,
RACProvider,
RadiusProvider,
RedirectURI,
SAMLProvider,
SCIMProvider,
} from "@goauthentik/api";
import { OneOfProvider } from "../types.js";
const renderSummary = (type: string, name: string, fields: DescriptionPair[]) =>
renderDescriptionList([[msg("Type"), type], [msg("Name"), name], ...fields], {
threecolumn: true,
});
function renderSAMLOverview(rawProvider: OneOfProvider) {
const provider = rawProvider as SAMLProvider;
return renderSummary("SAML", provider.name, [
[msg("ACS URL"), provider.acsUrl],
[msg("Audience"), provider.audience || "-"],
[msg("Issuer"), provider.issuer],
]);
}
function renderSCIMOverview(rawProvider: OneOfProvider) {
const provider = rawProvider as SCIMProvider;
return renderSummary("SCIM", provider.name, [[msg("URL"), provider.url]]);
}
function renderRadiusOverview(rawProvider: OneOfProvider) {
const provider = rawProvider as RadiusProvider;
return renderSummary("Radius", provider.name, [
[msg("Client Networks"), provider.clientNetworks],
]);
}
function renderRACOverview(rawProvider: OneOfProvider) {
// @ts-expect-error TS6133
const _provider = rawProvider as RACProvider;
}
function formatRedirectUris(uris: RedirectURI[] = []) {
return uris.length > 0
? html`<ul class="pf-c-list pf-m-plain">
${uris.map(
(uri) =>
html`<li>
${uri.url}
(${uri.matchingMode === MatchingModeEnum.Strict
? msg("strict")
: msg("regexp")})
</li>`,
)}
</ul>`
: "-";
}
const proxyModeToLabel = new Map([
[ProxyMode.Proxy, msg("Proxy")],
[ProxyMode.ForwardSingle, msg("Forward auth (single application)")],
[ProxyMode.ForwardDomain, msg("Forward auth (domain-level)")],
[ProxyMode.UnknownDefaultOpenApi, msg("Unknown proxy mode")],
]);
function renderProxyOverview(rawProvider: OneOfProvider) {
const provider = rawProvider as ProxyProvider;
return renderSummary("Proxy", provider.name, [
[msg("Mode"), proxyModeToLabel.get(provider.mode ?? ProxyMode.Proxy)],
...match(provider.mode)
.with(
ProxyMode.Proxy,
() =>
[
[msg("Internal Host"), provider.internalHost],
[msg("External Host"), provider.externalHost],
] as DescriptionPair[],
)
.with(
ProxyMode.ForwardSingle,
() => [[msg("External Host"), provider.externalHost]] as DescriptionPair[],
)
.with(
ProxyMode.ForwardDomain,
() =>
[
[msg("Authentication URL"), provider.externalHost],
[msg("Cookie domain"), provider.cookieDomain],
] as DescriptionPair[],
)
.otherwise(() => {
throw new Error(
`Unrecognized proxy mode: ${provider.mode?.toString() ?? "-- undefined __"}`,
);
}),
[
msg("Basic-Auth"),
html` <ak-status-label
type="info"
?good=${provider.basicAuthEnabled}
></ak-status-label>`,
],
]);
}
const clientTypeToLabel = new Map<ClientTypeEnum, string>([
[ClientTypeEnum.Confidential, msg("Confidential")],
[ClientTypeEnum.Public, msg("Public")],
[ClientTypeEnum.UnknownDefaultOpenApi, msg("Unknown type")],
]);
function renderOAuth2Overview(rawProvider: OneOfProvider) {
const provider = rawProvider as OAuth2Provider;
return renderSummary("OAuth2", provider.name, [
[msg("Client type"), provider.clientType ? clientTypeToLabel.get(provider.clientType) : ""],
[msg("Client ID"), provider.clientId],
[msg("Redirect URIs"), formatRedirectUris(provider.redirectUris)],
]);
}
function renderLDAPOverview(rawProvider: OneOfProvider) {
const provider = rawProvider as LDAPProvider;
return renderSummary("Proxy", provider.name, [[msg("Base DN"), provider.baseDn]]);
}
const providerName = (p: ProviderModelEnum): string => p.toString().split(".")[1];
export const providerRenderers = new Map([
[providerName(ProviderModelEnum.SamlSamlprovider), renderSAMLOverview],
[providerName(ProviderModelEnum.ScimScimprovider), renderSCIMOverview],
[providerName(ProviderModelEnum.RadiusRadiusprovider), renderRadiusOverview],
[providerName(ProviderModelEnum.RacRacprovider), renderRACOverview],
[providerName(ProviderModelEnum.ProxyProxyprovider), renderProxyOverview],
[providerName(ProviderModelEnum.Oauth2Oauth2provider), renderOAuth2Overview],
[providerName(ProviderModelEnum.LdapLdapprovider), renderLDAPOverview],
]);

View File

@ -0,0 +1,192 @@
import { policyOptions } from "@goauthentik/admin/applications/PolicyOptions.js";
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { isSlug } from "@goauthentik/common/utils.js";
import { camelToSnake } from "@goauthentik/common/utils.js";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-slug-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
import { type KeyUnknown } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { type ApplicationRequest } from "@goauthentik/api";
import { ApplicationWizardStateUpdate, ValidationRecord } from "../types";
const autoTrim = (v: unknown) => (typeof v === "string" ? v.trim() : v);
const trimMany = (o: KeyUnknown, vs: string[]) =>
Object.fromEntries(vs.map((v) => [v, autoTrim(o[v])]));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isStr = (v: any): v is string => typeof v === "string";
@customElement("ak-application-wizard-application-step")
export class ApplicationWizardApplicationStep extends ApplicationWizardStep {
label = msg("Application");
@state()
errors = new Map<string, string>();
@query("form#applicationform")
form!: HTMLFormElement;
constructor() {
super();
// This is the first step. Ensure it is always enabled.
this.enabled = true;
}
errorMessages(name: string) {
return this.errors.has(name)
? [this.errors.get(name)]
: (this.wizard.errors?.app?.[name] ??
this.wizard.errors?.app?.[camelToSnake(name)] ??
[]);
}
get buttons(): WizardButton[] {
return [{ kind: "next", destination: "provider-choice" }, { kind: "cancel" }];
}
get valid() {
this.errors = new Map();
const values = trimMany(this.formValues ?? {}, ["metaLaunchUrl", "name", "slug"]);
if (values["name"] === "") {
this.errors.set("name", msg("An application name is required"));
}
if (
!(
isStr(values["metaLaunchUrl"]) &&
(values["metaLaunchUrl"] === "" || URL.canParse(values["metaLaunchUrl"]))
)
) {
this.errors.set("metaLaunchUrl", msg("Not a valid URL"));
}
if (!(isStr(values["slug"]) && values["slug"] !== "" && isSlug(values["slug"]))) {
this.errors.set("slug", msg("Not a valid slug"));
}
return this.errors.size === 0;
}
override handleButton(button: NavigableButton) {
if (button.kind === "next") {
if (!this.valid) {
this.handleEnabling({
disabled: ["provider-choice", "provider", "bindings", "submit"],
});
return;
}
const app: Partial<ApplicationRequest> = this.formValues as Partial<ApplicationRequest>;
let payload: ApplicationWizardStateUpdate = {
app: this.formValues,
errors: this.removeErrors("app"),
};
if (app.name && (this.wizard.provider?.name ?? "").trim() === "") {
payload = {
...payload,
provider: { name: `Provider for ${app.name}` },
};
}
this.handleUpdate(payload, button.destination, {
enable: "provider-choice",
});
return;
}
super.handleButton(button);
}
renderForm(app: Partial<ApplicationRequest>, errors: ValidationRecord) {
return html` <ak-wizard-title>${msg("Configure The Application")}</ak-wizard-title>
<form id="applicationform" class="pf-c-form pf-m-horizontal" slot="form">
<ak-text-input
name="name"
value=${ifDefined(app.name)}
label=${msg("Name")}
required
?invalid=${this.errors.has("name")}
.errorMessages=${errors.name ?? this.errorMessages("name")}
help=${msg("Application's display Name.")}
id="ak-application-wizard-details-name"
></ak-text-input>
<ak-slug-input
name="slug"
value=${ifDefined(app.slug)}
label=${msg("Slug")}
source="#ak-application-wizard-details-name"
required
?invalid=${errors.slug ?? this.errors.has("slug")}
.errorMessages=${this.errorMessages("slug")}
help=${msg("Internal application name used in URLs.")}
></ak-slug-input>
<ak-text-input
name="group"
value=${ifDefined(app.group)}
label=${msg("Group")}
.errorMessages=${errors.group ?? []}
help=${msg(
"Optionally enter a group name. Applications with identical groups are shown grouped together.",
)}
></ak-text-input>
<ak-radio-input
label=${msg("Policy engine mode")}
required
name="policyEngineMode"
.options=${policyOptions}
.value=${app.policyEngineMode}
.errorMessages=${errors.policyEngineMode ?? []}
></ak-radio-input>
<ak-form-group aria-label=${msg("UI Settings")}>
<span slot="header"> ${msg("UI Settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="metaLaunchUrl"
label=${msg("Launch URL")}
value=${ifDefined(app.metaLaunchUrl)}
?invalid=${this.errors.has("metaLaunchUrl")}
.errorMessages=${errors.metaLaunchUrl ??
this.errorMessages("metaLaunchUrl")}
help=${msg(
"If left empty, authentik will try to extract the launch URL based on the selected provider.",
)}
></ak-text-input>
<ak-switch-input
name="openInNewTab"
?checked=${app.openInNewTab ?? false}
label=${msg("Open in new tab")}
help=${msg(
"If checked, the launch URL will open in a new browser tab or window from the user's application library.",
)}
>
</ak-switch-input>
</div>
</ak-form-group>
</form>`;
}
renderMain() {
if (!(this.wizard.app && this.wizard.errors)) {
throw new Error("Application Step received uninitialized wizard context.");
}
return this.renderForm(
this.wizard.app as ApplicationRequest,
this.wizard.errors?.app ?? {},
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-application-step": ApplicationWizardApplicationStep;
}
}

View File

@ -0,0 +1,163 @@
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-slug-input";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
import "@goauthentik/elements/ak-table/ak-select-table.js";
import { SelectTable } from "@goauthentik/elements/ak-table/ak-select-table.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { P, match } from "ts-pattern";
import { msg, str } from "@lit/localize";
import { css, html } from "lit";
import { customElement, query } from "lit/decorators.js";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import { makeEditButton } from "./bindings/ak-application-wizard-bindings-edit-button.js";
import "./bindings/ak-application-wizard-bindings-toolbar.js";
const COLUMNS = [
[msg("Order"), "order"],
[msg("Binding")],
[msg("Enabled"), "enabled"],
[msg("Timeout"), "timeout"],
[msg("Actions")],
];
@customElement("ak-application-wizard-bindings-step")
export class ApplicationWizardBindingsStep extends ApplicationWizardStep {
label = msg("Configure Bindings");
get buttons(): WizardButton[] {
return [
{ kind: "next", destination: "submit" },
{ kind: "back", destination: "provider" },
{ kind: "cancel" },
];
}
@query("ak-select-table")
selectTable!: SelectTable;
static get styles() {
return super.styles.concat(
PFCard,
css`
.pf-c-card {
margin-top: 1em;
}
`,
);
}
get bindingsAsColumns() {
return this.wizard.bindings.map((binding, index) => {
const { order, enabled, timeout } = binding;
const isSet = P.string.minLength(1);
const policy = match(binding)
.with({ policy: isSet }, (v) => msg(str`Policy ${v.policyObj?.name}`))
.with({ group: isSet }, (v) => msg(str`Group ${v.groupObj?.name}`))
.with({ user: isSet }, (v) => msg(str`User ${v.userObj?.name}`))
.otherwise(() => msg("-"));
return {
key: index,
content: [
order,
policy,
html`<ak-status-label type="warning" ?good=${enabled}></ak-status-label>`,
timeout,
makeEditButton(msg("Edit"), index, (ev: CustomEvent<number>) =>
this.onBindingEvent(ev.detail),
),
],
};
});
}
// TODO Fix those dispatches so that we handle them here, in this component, and *choose* how to
// forward them.
onBindingEvent(binding?: number) {
this.handleUpdate({ currentBinding: binding ?? -1 }, "edit-binding", {
enable: "edit-binding",
});
}
onDeleteBindings() {
const toDelete = this.selectTable
.json()
.map((i) => (typeof i === "string" ? parseInt(i, 10) : i));
const bindings = this.wizard.bindings.filter((binding, index) => !toDelete.includes(index));
this.handleUpdate({ bindings }, "bindings");
}
renderEmptyCollection() {
return html`<ak-wizard-title
>${msg("Configure Policy/User/Group Bindings")}</ak-wizard-title
>
<h6 class="pf-c-title pf-m-md">
${msg("These policies control which users can access this application.")}
</h6>
<div class="pf-c-card">
<ak-application-wizard-bindings-toolbar
@clickNew=${() => this.onBindingEvent()}
@clickDelete=${() => this.onDeleteBindings()}
></ak-application-wizard-bindings-toolbar>
<ak-select-table
multiple
id="bindings"
order="order"
.columns=${COLUMNS}
.content=${[]}
></ak-select-table>
<ak-empty-state header=${msg("No bound policies.")} icon="pf-icon-module">
<div slot="body">${msg("No policies are currently bound to this object.")}</div>
<div slot="primary">
<button
@click=${() => this.onBindingEvent()}
class="pf-c-button pf-m-primary"
>
${msg("Bind policy/group/user")}
</button>
</div>
</ak-empty-state>
</div>`;
}
renderCollection() {
return html` <ak-wizard-title>${msg("Configure Policy Bindings")}</ak-wizard-title>
<h6 class="pf-c-title pf-m-md">
${msg("These policies control which users can access this application.")}
</h6>
<ak-application-wizard-bindings-toolbar
@clickNew=${() => this.onBindingEvent()}
@clickDelete=${() => this.onDeleteBindings()}
?can-delete=${this.wizard.bindings.length > 0}
></ak-application-wizard-bindings-toolbar>
<ak-select-table
multiple
id="bindings"
order="order"
.columns=${COLUMNS}
.content=${this.bindingsAsColumns}
></ak-select-table>`;
}
renderMain() {
if ((this.wizard.bindings ?? []).length === 0) {
return this.renderEmptyCollection();
}
return this.renderCollection();
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-applications-step": ApplicationWizardBindingsStep;
}
}

View File

@ -0,0 +1,235 @@
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { groupBy } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-toggle-group";
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { type SearchSelectBase } from "@goauthentik/elements/forms/SearchSelect/SearchSelect.js";
import "@goauthentik/elements/forms/SearchSelect/ak-search-select-ez.js";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { CoreApi, Group, PoliciesApi, Policy, PolicyBinding, User } from "@goauthentik/api";
const withQuery = <T>(search: string | undefined, args: T) => (search ? { ...args, search } : args);
enum target {
policy = "policy",
group = "group",
user = "user",
}
const policyObjectKeys: Record<target, keyof PolicyBinding> = {
[target.policy]: "policyObj",
[target.group]: "groupObj",
[target.user]: "userObj",
};
const PASS_FAIL = [
[msg("Pass"), true, false],
[msg("Don't Pass"), false, true],
].map(([label, value, d]) => ({ label, value, default: d }));
@customElement("ak-application-wizard-edit-binding-step")
export class ApplicationWizardEditBindingStep extends ApplicationWizardStep {
label = msg("Edit Binding");
hide = true;
@query("form#bindingform")
form!: HTMLFormElement;
@query(".policy-search-select")
searchSelect!: SearchSelectBase<Policy> | SearchSelectBase<Group> | SearchSelectBase<User>;
@state()
policyGroupUser: target = target.policy;
instanceId = -1;
instance?: PolicyBinding;
get buttons(): WizardButton[] {
return [
{ kind: "next", label: msg("Save Binding"), destination: "bindings" },
{ kind: "back", destination: "bindings" },
{ kind: "cancel" },
];
}
override handleButton(button: NavigableButton) {
if (button.kind === "next") {
if (!this.form.checkValidity()) {
return;
}
const policyObject = this.searchSelect.selectedObject;
const policyKey = policyObjectKeys[this.policyGroupUser];
const newBinding: PolicyBinding = {
...(this.formValues as unknown as PolicyBinding),
[policyKey]: policyObject,
};
const bindings = [...(this.wizard.bindings ?? [])];
if (this.instanceId === -1) {
bindings.push(newBinding);
} else {
bindings[this.instanceId] = newBinding;
}
this.instanceId = -1;
this.handleUpdate({ bindings }, "bindings");
return;
}
super.handleButton(button);
}
// The search select configurations for the three different types of fetches that we care about,
// policy, user, and group, all using the SearchSelectEZ protocol.
searchSelectConfigs(kind: target) {
switch (kind) {
case target.policy:
return {
fetchObjects: async (query?: string): Promise<Policy[]> => {
const policies = await new PoliciesApi(DEFAULT_CONFIG).policiesAllList(
withQuery(query, {
ordering: "name",
}),
);
return policies.results;
},
groupBy: (items: Policy[]) =>
groupBy(items, (policy) => policy.verboseNamePlural),
renderElement: (policy: Policy): string => policy.name,
value: (policy: Policy | undefined): string | undefined => policy?.pk,
selected: (policy: Policy): boolean => policy.pk === this.instance?.policy,
};
case target.group:
return {
fetchObjects: async (query?: string): Promise<Group[]> => {
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
withQuery(query, {
ordering: "name",
includeUsers: false,
}),
);
return groups.results;
},
renderElement: (group: Group): string => group.name,
value: (group: Group | undefined): string | undefined => group?.pk,
selected: (group: Group): boolean => group.pk === this.instance?.group,
};
case target.user:
return {
fetchObjects: async (query?: string): Promise<User[]> => {
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(
withQuery(query, {
ordering: "username",
}),
);
return users.results;
},
renderElement: (user: User): string => user.username,
renderDescription: (user: User) => html`${user.name}`,
value: (user: User | undefined): number | undefined => user?.pk,
selected: (user: User): boolean => user.pk === this.instance?.user,
};
default:
throw new Error(`Unrecognized policy binding target ${kind}`);
}
}
renderSearch(title: string, policyKind: target) {
if (policyKind !== this.policyGroupUser) {
return nothing;
}
return html`<ak-form-element-horizontal label=${title} name=${policyKind}>
<ak-search-select-ez
.config=${this.searchSelectConfigs(policyKind)}
class="policy-search-select"
blankable
></ak-search-select-ez>
</ak-form-element-horizontal>`;
}
renderForm(instance?: PolicyBinding) {
return html`<ak-wizard-title>${msg("Create a Policy/User/Group Binding")}</ak-wizard-title>
<form id="bindingform" class="pf-c-form pf-m-horizontal" slot="form">
<div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body">
<ak-toggle-group
value=${this.policyGroupUser}
@ak-toggle=${(ev: CustomEvent<{ value: target }>) => {
this.policyGroupUser = ev.detail.value;
}}
>
<option value=${target.policy}>${msg("Policy")}</option>
<option value=${target.group}>${msg("Group")}</option>
<option value=${target.user}>${msg("User")}</option>
</ak-toggle-group>
</div>
<div class="pf-c-card__footer">
${this.renderSearch(msg("Policy"), target.policy)}
${this.renderSearch(msg("Group"), target.group)}
${this.renderSearch(msg("User"), target.user)}
</div>
</div>
<ak-switch-input
name="enabled"
?checked=${instance?.enabled ?? true}
label=${msg("Enabled")}
></ak-switch-input>
<ak-switch-input
name="negate"
?checked=${instance?.negate ?? false}
label=${msg("Negate result")}
help=${msg("Negates the outcome of the binding. Messages are unaffected.")}
></ak-switch-input>
<ak-number-input
label=${msg("Order")}
name="order"
value="${instance?.order ?? 0}"
required
></ak-number-input>
<ak-number-input
label=${msg("Timeout")}
name="timeout"
value="${instance?.timeout ?? 30}"
required
></ak-number-input>
<ak-radio-input
name="failureResult"
label=${msg("Failure result")}
.options=${PASS_FAIL}
></ak-radio-input>
</form>`;
}
renderMain() {
if (!(this.wizard.bindings && this.wizard.errors)) {
throw new Error("Application Step received uninitialized wizard context.");
}
const currentBinding = this.wizard.currentBinding ?? -1;
if (this.instanceId !== currentBinding) {
this.instanceId = currentBinding;
this.instance =
this.instanceId === -1 ? undefined : this.wizard.bindings[this.instanceId];
}
return this.renderForm(this.instance);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-edit-binding-step": ApplicationWizardEditBindingStep;
}
}

View File

@ -0,0 +1,94 @@
import { ApplicationWizardStep } from "@goauthentik/admin/applications/wizard/ApplicationWizardStep.js";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import type { NavigableButton, WizardButton } from "@goauthentik/components/ak-wizard/types";
import "@goauthentik/elements/EmptyState.js";
import { WithLicenseSummary } from "@goauthentik/elements/Interface/licenseSummaryProvider.js";
import { bound } from "@goauthentik/elements/decorators/bound.js";
import "@goauthentik/elements/forms/FormGroup.js";
import "@goauthentik/elements/forms/HorizontalFormElement.js";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage.js";
import "@goauthentik/elements/wizard/TypeCreateWizardPage.js";
import { consume } from "@lit/context";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { TypeCreate } from "@goauthentik/api";
import { applicationWizardProvidersContext } from "../ContextIdentity";
import { type LocalTypeCreate } from "./ProviderChoices.js";
@customElement("ak-application-wizard-provider-choice-step")
export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) {
label = msg("Choose A Provider");
@state()
failureMessage = "";
@consume({ context: applicationWizardProvidersContext, subscribe: true })
public providerModelsList!: LocalTypeCreate[];
get buttons(): WizardButton[] {
return [
{ kind: "next", destination: "provider" },
{ kind: "back", destination: "application" },
{ kind: "cancel" },
];
}
override handleButton(button: NavigableButton) {
this.failureMessage = "";
if (button.kind === "next") {
if (!this.wizard.providerModel) {
this.failureMessage = msg("Please choose a provider type before proceeding.");
this.handleEnabling({ disabled: ["provider", "bindings", "submit"] });
return;
}
this.handleUpdate(undefined, button.destination, { enable: "provider" });
return;
}
super.handleButton(button);
}
@bound
onSelect(ev: CustomEvent<LocalTypeCreate>) {
ev.stopPropagation();
const detail: TypeCreate = ev.detail;
this.handleUpdate({ providerModel: detail.modelName });
}
renderMain() {
const selectedTypes = this.providerModelsList.filter(
(t) => t.modelName === this.wizard.providerModel,
);
return this.providerModelsList.length > 0
? html` <ak-wizard-title>${msg("Choose a Provider Type")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal">
<ak-wizard-page-type-create
.types=${this.providerModelsList}
name="selectProviderType"
layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
this.handleUpdate(
{
...this.wizard,
providerModel: ev.detail.modelName,
},
undefined,
{ enable: "provider" },
);
}}
></ak-wizard-page-type-create>
</form>`
: html`<ak-empty-state loading header=${msg("Loading")}></ak-empty-state>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-provider-choice-step": ApplicationWizardProviderChoiceStep;
}
}

View File

@ -0,0 +1,113 @@
import { type NavigableButton, type WizardButton } from "@goauthentik/components/ak-wizard/types";
import { msg } from "@lit/localize";
import { PropertyValues, nothing } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import { html, unsafeStatic } from "lit/static-html.js";
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
import { OneOfProvider } from "../types.js";
import { ApplicationWizardProviderForm } from "./providers/ApplicationWizardProviderForm.js";
import "./providers/ak-application-wizard-provider-for-ldap.js";
import "./providers/ak-application-wizard-provider-for-oauth.js";
import "./providers/ak-application-wizard-provider-for-proxy.js";
import "./providers/ak-application-wizard-provider-for-rac.js";
import "./providers/ak-application-wizard-provider-for-radius.js";
import "./providers/ak-application-wizard-provider-for-saml.js";
import "./providers/ak-application-wizard-provider-for-scim.js";
const providerToTag = new Map([
["ldapprovider", "ak-application-wizard-provider-for-ldap"],
["oauth2provider", "ak-application-wizard-provider-for-oauth"],
["proxyprovider", "ak-application-wizard-provider-for-proxy"],
["racprovider", "ak-application-wizard-provider-for-rac"],
["radiusprovider", "ak-application-wizard-provider-for-radius"],
["samlprovider", "ak-application-wizard-provider-for-saml"],
["scimprovider", "ak-application-wizard-provider-for-scim"],
]);
@customElement("ak-application-wizard-provider-step")
export class ApplicationWizardProviderStep extends ApplicationWizardStep {
@state()
label = msg("Configure Provider");
@query("#providerform")
element!: ApplicationWizardProviderForm<OneOfProvider>;
get valid() {
return this.element.valid;
}
get formValues() {
return this.element.formValues;
}
override handleButton(button: NavigableButton) {
if (button.kind === "next") {
if (!this.valid) {
this.handleEnabling({
disabled: ["bindings", "submit"],
});
return;
}
const payload = {
provider: {
...this.formValues,
mode: this.wizard.proxyMode,
},
errors: this.removeErrors("provider"),
};
this.handleUpdate(payload, button.destination, {
enable: ["bindings", "submit"],
});
return;
}
super.handleButton(button);
}
get buttons(): WizardButton[] {
return [
{ kind: "next", destination: "bindings" },
{ kind: "back", destination: "provider-choice" },
{ kind: "cancel" },
];
}
renderMain() {
if (!this.wizard.providerModel) {
throw new Error("Attempted to access provider page without providing a provider type.");
}
// This is, I'm afraid, some rather esoteric bit of Lit-ing, and it makes ESLint
// sad. It does allow us to get away with specifying very little about the
// provider here.
const tag = providerToTag.get(this.wizard.providerModel);
return tag
? // eslint-disable-next-line lit/binding-positions,lit/no-invalid-html
html`<${unsafeStatic(tag)}
id="providerform"
.wizard=${this.wizard}
.errors=${this.wizard.errors?.provider ?? {}}
></${
/* eslint-disable-next-line lit/binding-positions,lit/no-invalid-html */
unsafeStatic(tag)
}>`
: nothing;
}
updated(changed: PropertyValues<this>) {
if (changed.has("wizard")) {
const label = this.element?.label ?? this.label;
if (label !== this.label) {
this.label = label;
}
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-provider-step": ApplicationWizardProviderStep;
}
}

View File

@ -0,0 +1,360 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import { parseAPIError } from "@goauthentik/common/errors";
import { WizardNavigationEvent } from "@goauthentik/components/ak-wizard/events.js";
import { type WizardButton } from "@goauthentik/components/ak-wizard/types";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { P, match } from "ts-pattern";
import { msg } from "@lit/localize";
import { TemplateResult, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
// import { map } from "lit/directives/map.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
import PFProgressStepper from "@patternfly/patternfly/components/ProgressStepper/progress-stepper.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
import {
type ApplicationRequest,
CoreApi,
type ModelRequest,
type PolicyBinding,
ProviderModelEnum,
ProxyMode,
type ProxyProviderRequest,
type TransactionApplicationRequest,
type TransactionApplicationResponse,
type TransactionPolicyBindingRequest,
} from "@goauthentik/api";
import { ApplicationWizardStep } from "../ApplicationWizardStep.js";
import { ExtendedValidationError, OneOfProvider } from "../types.js";
import { providerRenderers } from "./SubmitStepOverviewRenderers.js";
const _submitStates = ["reviewing", "running", "submitted"] as const;
type SubmitStates = (typeof _submitStates)[number];
type StrictProviderModelEnum = Exclude<ProviderModelEnum, "11184809">;
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());
type NonEmptyArray<T> = [T, ...T[]];
type MaybeTemplateResult = TemplateResult | typeof nothing;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isNotEmpty = (arr: any): arr is NonEmptyArray<any> => Array.isArray(arr) && arr.length > 0;
const cleanApplication = (app: Partial<ApplicationRequest>): ApplicationRequest => ({
name: "",
slug: "",
...app,
});
const cleanBinding = (binding: PolicyBinding): TransactionPolicyBindingRequest => ({
policy: binding.policy,
group: binding.group,
user: binding.user,
negate: binding.negate,
enabled: binding.enabled,
order: binding.order,
timeout: binding.timeout,
failureResult: binding.failureResult,
});
@customElement("ak-application-wizard-submit-step")
export class ApplicationWizardSubmitStep extends CustomEmitterElement(ApplicationWizardStep) {
static get styles() {
return [
...ApplicationWizardStep.styles,
PFBullseye,
PFEmptyState,
PFTitle,
PFProgressStepper,
PFDescriptionList,
css`
.ak-wizard-main-content .pf-c-title {
padding-bottom: var(--pf-global--spacer--md);
padding-top: var(--pf-global--spacer--md);
}
`,
];
}
label = msg("Review and Submit Application");
@state()
state: SubmitStates = "reviewing";
async send() {
const app = this.wizard.app;
const provider = this.wizard.provider as ModelRequest;
if (app === undefined) {
throw new Error("Reached the submit state with the app undefined");
}
if (provider === undefined) {
throw new Error("Reached the submit state with the provider undefined");
}
// 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;
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 = {
app: cleanApplication(this.wizard.app),
providerModel,
provider,
policyBindings: (this.wizard.bindings ?? []).map(cleanBinding),
};
this.state = "running";
return (
new CoreApi(DEFAULT_CONFIG)
.coreTransactionalApplicationsUpdate({
transactionApplicationRequest: request,
})
.then((_response: TransactionApplicationResponse) => {
this.dispatchCustomEvent(EVENT_REFRESH);
this.state = "submitted";
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch(async (resolution: any) => {
const errors = (await parseAPIError(
await resolution,
)) as ExtendedValidationError;
// 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 move that to the `provider.name` error field so it shows up in the
// right place.
if (Array.isArray(errors?.app?.provider)) {
const providerError = errors.app.provider;
errors.provider = errors.provider ?? {};
errors.provider.name = providerError;
delete errors.app.provider;
if (Object.keys(errors.app).length === 0) {
delete errors.app;
}
}
this.handleUpdate({ errors });
this.state = "reviewing";
})
);
}
override handleButton(button: WizardButton) {
match([button.kind, this.state])
.with([P.union("back", "cancel"), P._], () => {
super.handleButton(button);
})
.with(["close", "submitted"], () => {
super.handleButton(button);
})
.with(["next", "reviewing"], () => {
this.send();
})
.with([P._, "running"], () => {
throw new Error("No buttons should be showing when running submit phase");
})
.otherwise(() => {
throw new Error(
`Submit step received incoherent button/state combination: ${[button.kind, state]}`,
);
});
}
get buttons(): WizardButton[] {
const forReview: WizardButton[] = [
{ kind: "next", label: msg("Submit"), destination: "here" },
{ kind: "back", destination: "bindings" },
{ kind: "cancel" },
];
const forSubmit: WizardButton[] = [{ kind: "close" }];
return match(this.state)
.with("submitted", () => forSubmit)
.with("running", () => [])
.with("reviewing", () => forReview)
.exhaustive();
}
renderInfo(
state: string,
label: string,
icons: string[],
extraInfo: MaybeTemplateResult = nothing,
) {
const icon = classMap(icons.reduce((acc, icon) => ({ ...acc, [icon]: true }), {}));
return html`<div data-ouid-component-state=${this.state} class="ak-wizard-main-content">
<div class="pf-l-bullseye">
<div class="pf-c-empty-state pf-m-lg">
<div class="pf-c-empty-state__content">
<i class="fas ${icon} pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 data-ouia-commit-state=${state} class="pf-c-title pf-m-lg">${label}</h1>
${extraInfo}
</div>
</div>
</div>
</div>`;
}
renderError() {
if (Object.keys(this.wizard.errors).length === 0) {
return nothing;
}
const navTo = (step: string) => () => this.dispatchEvent(new WizardNavigationEvent(step));
const errors = this.wizard.errors;
return html` <hr class="pf-c-divider" />
${match(errors as ExtendedValidationError)
.with(
{ app: P.nonNullable },
() =>
html`<p>${msg("There was an error in the application.")}</p>
<p>
<a @click=${navTo("application")}
>${msg("Review the application.")}</a
>
</p>`,
)
.with(
{ provider: P.nonNullable },
() =>
html`<p>${msg("There was an error in the provider.")}</p>
<p>
<a @click=${navTo("provider")}>${msg("Review the provider.")}</a>
</p>`,
)
.with(
{ detail: P.nonNullable },
() =>
`<p>${msg("There was an error. Please go back and review the application.")}: ${errors.detail}</p>`,
)
.with(
{
nonFieldErrors: P.when(isNotEmpty),
},
() =>
html`<p>${msg("There was an error:")}:</p>
<ul>
${(errors.nonFieldErrors ?? []).map(
(e: string) => html`<li>${e}</li>`,
)}
</ul>
<p>${msg("Please go back and review the application.")}</p>`,
)
.otherwise(
() =>
html`<p>
${msg(
"There was an error creating the application, but no error message was sent. Please review the server logs.",
)}
</p>`,
)}`;
}
renderReview(app: Partial<ApplicationRequest>, provider: OneOfProvider) {
const renderer = providerRenderers.get(this.wizard.providerModel);
if (!renderer) {
throw new Error(
`Provider ${this.wizard.providerModel ?? "-- undefined --"} has no summary renderer.`,
);
}
return html`
<div class="ak-wizard-main-content">
<ak-wizard-title>${msg("Review the Application and Provider")}</ak-wizard-title>
<h2 class="pf-c-title pf-m-xl">${msg("Application")}</h2>
<dl class="pf-c-description-list">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Name")}</dt>
<dt class="pf-c-description-list__description">${app.name}</dt>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Group")}</dt>
<dt class="pf-c-description-list__description">${app.group || msg("-")}</dt>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Policy engine mode")}</dt>
<dt class="pf-c-description-list__description">
${app.policyEngineMode?.toUpperCase()}
</dt>
</div>
${(app.metaLaunchUrl ?? "").trim() !== ""
? html` <div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">${msg("Launch URL")}</dt>
<dt class="pf-c-description-list__description">
${app.metaLaunchUrl}
</dt>
</div>`
: nothing}
</dl>
${renderer
? html` <h2 class="pf-c-title pf-m-xl pf-u-pt-xl">${msg("Provider")}</h2>
${renderer(provider)}`
: nothing}
</div>
`;
}
renderMain() {
const app = this.wizard.app;
const provider = this.wizard.provider;
if (!(this.wizard && app && provider)) {
throw new Error("Submit step received uninitialized wizard context");
}
// An empty object is truthy, an empty array is falsey. *WAT Javascript*.
const keys = Object.keys(this.wizard.errors);
return match([this.state, keys])
.with(["submitted", P._], () =>
this.renderInfo("success", msg("Your application has been saved"), [
"fa-check-circle",
"pf-m-success",
]),
)
.with(["running", P._], () =>
this.renderInfo("running", msg("Saving application..."), ["fa-cogs", "pf-m-info"]),
)
.with(["reviewing", []], () => this.renderReview(app, provider))
.with(["reviewing", [P.any, ...P.array()]], () =>
this.renderInfo(
"error",
msg("authentik was unable to complete this process."),
["fa-times-circle", "pf-m-danger"],
this.renderError(),
),
)
.exhaustive();
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-submit-step": ApplicationWizardSubmitStep;
}
}

View File

@ -0,0 +1,50 @@
import { AKElement } from "@goauthentik/elements/Base.js";
import { bound } from "@goauthentik/elements/decorators/bound.js";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@customElement("ak-application-wizard-binding-step-edit-button")
export class ApplicationWizardBindingStepEditButton extends AKElement {
static get styles() {
return [PFButton];
}
@property({ type: Number })
value = -1;
@bound
onClick(ev: Event) {
ev.stopPropagation();
this.dispatchEvent(
new CustomEvent<number>("click-edit", {
bubbles: true,
composed: true,
detail: this.value,
}),
);
}
render() {
return html`<button class="pf-c-button pf-c-secondary" @click=${this.onClick}>
${msg("Edit")}
</button>`;
}
}
export function makeEditButton(
label: string,
value: number,
handler: (_: CustomEvent<number>) => void,
) {
return html`<ak-application-wizard-binding-step-edit-button
class="pf-c-button pf-m-secondary"
.value=${value}
@click-edit=${handler}
>
${label}
</ak-application-wizard-binding-step-edit-button>`;
}

View File

@ -0,0 +1,53 @@
import { AKElement } from "@goauthentik/elements/Base";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFToolbar from "@patternfly/patternfly/components/Toolbar/toolbar.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-application-wizard-bindings-toolbar")
export class ApplicationWizardBindingsToolbar extends AKElement {
static get styles() {
return [PFBase, PFButton, PFToolbar];
}
@property({ type: Boolean, attribute: "can-delete", reflect: true })
canDelete = false;
notify(eventName: string) {
this.dispatchEvent(new Event(eventName, { bubbles: true, composed: true }));
}
render() {
return html`
<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__group">
<button
class="pf-c-button pf-m-primary"
@click=${() => this.notify("clickNew")}
>
${msg("Bind existing policy/group/user")}
</button>
</div>
<button
class="pf-c-button pf-m-danger"
?disabled=${!this.canDelete}
@click=${() => this.notify("clickDelete")}
>
${msg("Delete")}
</button>
</div>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-bindings-toolbar": ApplicationWizardBindingsToolbar;
}
}

View File

@ -0,0 +1,62 @@
import { camelToSnake } from "@goauthentik/common/utils.js";
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 { AKElement } from "@goauthentik/elements/Base.js";
import { KeyUnknown, serializeForm } from "@goauthentik/elements/forms/Form";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { HorizontalFormElement } from "@goauthentik/elements/forms/HorizontalFormElement";
import { property, query } from "lit/decorators.js";
import { styles as AwadStyles } from "../../ApplicationWizardFormStepStyles.css.js";
import { type ApplicationWizardState, type OneOfProvider } from "../../types";
export class ApplicationWizardProviderForm<T extends OneOfProvider> extends AKElement {
static get styles() {
return AwadStyles;
}
label = "";
@property({ type: Object, attribute: false })
wizard!: ApplicationWizardState;
@property({ type: Object, attribute: false })
errors: Map<string | number | symbol, string> = new Map();
@query("form#providerform")
form!: HTMLFormElement;
get formValues(): KeyUnknown | undefined {
const elements = [
...Array.from(
this.form.querySelectorAll<HorizontalFormElement>("ak-form-element-horizontal"),
),
...Array.from(this.form.querySelectorAll<HTMLElement>("[data-ak-control=true]")),
];
return serializeForm(elements as unknown as NodeListOf<HorizontalFormElement>);
}
get valid() {
this.errors = new Map();
return this.form.checkValidity();
}
errorMessages(name: string) {
return this.errors.has(name)
? [this.errors.get(name)]
: (this.wizard.errors?.provider?.[name] ??
this.wizard.errors?.provider?.[camelToSnake(name)] ??
[]);
}
isValid(name: keyof T) {
return !(
(this.wizard.errors?.provider?.[name as string] ?? []).length > 0 ||
this.errors.has(name)
);
}
}

View File

@ -0,0 +1,44 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { ValidationRecord } from "@goauthentik/admin/applications/wizard/types";
import { renderForm } from "@goauthentik/admin/providers/ldap/LDAPProviderFormForm.js";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider.js";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
import type { LDAPProvider } from "@goauthentik/api";
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
@customElement("ak-application-wizard-provider-for-ldap")
export class ApplicationWizardLdapProviderForm extends WithBrandConfig(
ApplicationWizardProviderForm<LDAPProvider>,
) {
label = msg("Configure LDAP Provider");
renderForm(provider: LDAPProvider, errors: ValidationRecord) {
return html`
<ak-wizard-title>${this.label}</ak-wizard-title>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm(provider ?? {}, errors, this.brand)}
</form>
`;
}
render() {
if (!(this.wizard.provider && this.wizard.errors)) {
throw new Error("LDAP Provider Step received uninitialized wizard context.");
}
return this.renderForm(
this.wizard.provider as LDAPProvider,
this.wizard.errors.provider ?? {},
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-provider-for-ldap": ApplicationWizardLdapProviderForm;
}
}

View File

@ -0,0 +1,64 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { renderForm } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormForm.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { OAuth2ProviderRequest, SourcesApi } from "@goauthentik/api";
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
import { ExtendedValidationError } from "../../types.js";
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
@customElement("ak-application-wizard-provider-for-oauth")
export class ApplicationWizardOauth2ProviderForm extends ApplicationWizardProviderForm<OAuth2ProviderRequest> {
label = msg("Configure OAuth2 Provider");
@state()
showClientSecret = true;
@state()
oauthSources?: PaginatedOAuthSourceList;
constructor() {
super();
new SourcesApi(DEFAULT_CONFIG)
.sourcesOauthList({
ordering: "name",
hasJwks: true,
})
.then((oauthSources: PaginatedOAuthSourceList) => {
this.oauthSources = oauthSources;
});
}
renderForm(provider: OAuth2Provider, errors: ExtendedValidationError) {
const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show;
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm(
provider ?? {},
errors,
this.showClientSecret,
showClientSecretCallback,
)}
</form>`;
}
render() {
if (!(this.wizard.provider && this.wizard.errors)) {
throw new Error("Oauth2 Provider Step received uninitialized wizard context.");
}
return this.renderForm(this.wizard.provider as OAuth2Provider, this.wizard.errors);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-provider-for-oauth": ApplicationWizardOauth2ProviderForm;
}
}

View File

@ -0,0 +1,67 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { ValidationRecord } from "@goauthentik/admin/applications/wizard/types";
import {
ProxyModeValue,
type SetMode,
type SetShowHttpBasic,
renderForm,
} from "@goauthentik/admin/providers/proxy/ProxyProviderFormForm.js";
import { WizardUpdateEvent } from "@goauthentik/components/ak-wizard/events.js";
import { msg } from "@lit/localize";
import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ProxyMode, ProxyProvider } from "@goauthentik/api";
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm";
@customElement("ak-application-wizard-provider-for-proxy")
export class ApplicationWizardProxyProviderForm extends ApplicationWizardProviderForm<ProxyProvider> {
label = msg("Configure Proxy Provider");
@state()
showHttpBasic = true;
renderForm(provider: ProxyProvider, errors: ValidationRecord) {
const onSetMode: SetMode = (ev: CustomEvent<ProxyModeValue>) => {
this.dispatchEvent(
new WizardUpdateEvent({ ...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);
};
const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm(provider ?? {}, errors ?? [], {
mode: this.wizard.proxyMode ?? ProxyMode.Proxy,
onSetMode,
showHttpBasic: this.showHttpBasic,
onSetShowHttpBasic,
})}
</form>`;
}
render() {
if (!(this.wizard.provider && this.wizard.errors)) {
throw new Error("Proxy Provider Step received uninitialized wizard context.");
}
return this.renderForm(
this.wizard.provider as ProxyProvider,
this.wizard.errors?.provider ?? {},
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-provider-for-proxy": ApplicationWizardProxyProviderForm;
}
}

View File

@ -1,4 +1,5 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import "@goauthentik/admin/common/ak-crypto-certificate-search.js";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import {
propertyMappingsProvider,
@ -7,33 +8,29 @@ import {
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/CodeMirror";
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 { html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowsInstancesListDesignationEnum, RACProvider } from "@goauthentik/api";
import { FlowsInstancesListDesignationEnum, type RACProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
@customElement("ak-application-wizard-authentication-for-rac")
export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel {
render() {
const provider = this.wizard.provider as RACProvider | undefined;
const errors = this.wizard.errors.provider;
@customElement("ak-application-wizard-provider-for-rac")
export class ApplicationWizardRACProviderForm extends ApplicationWizardProviderForm<RACProvider> {
label = msg("Configure Remote Access Provider");
return html`<ak-wizard-title
>${msg("Configure Remote Access Provider Provider")}</ak-wizard-title
>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
renderForm(provider: RACProvider) {
return html`
<ak-wizard-title>${this.label}</ak-wizard-title>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
value=${ifDefined(provider.name)}
.errorMessages=${this.errorMessages("name")}
required
></ak-text-input>
@ -44,7 +41,7 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel {
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
.currentFlow=${provider.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
@ -56,7 +53,7 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel {
name="connectionExpiry"
label=${msg("Connection expiry")}
required
value="${provider?.connectionExpiry ?? "hours=8"}"
value="${provider.connectionExpiry ?? "hours=8"}"
help=${msg(
"Determines how long a session lasts before being disconnected and requiring re-authorization.",
)}
@ -78,14 +75,20 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel {
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
</form>
`;
}
render() {
if (!(this.wizard.provider && this.wizard.errors)) {
throw new Error("RAC Provider Step received uninitialized wizard context.");
}
return this.renderForm(this.wizard.provider as RACProvider);
}
}
export default ApplicationWizardAuthenticationByRAC;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-for-rac": ApplicationWizardAuthenticationByRAC;
"ak-application-wizard-provider-for-rac": ApplicationWizardRACProviderForm;
}
}

View File

@ -0,0 +1,42 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { ValidationRecord } from "@goauthentik/admin/applications/wizard/types";
import { renderForm } from "@goauthentik/admin/providers/radius/RadiusProviderFormForm.js";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { RadiusProvider } from "@goauthentik/api";
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm.js";
@customElement("ak-application-wizard-provider-for-radius")
export class ApplicationWizardRadiusProviderForm extends WithBrandConfig(
ApplicationWizardProviderForm<RadiusProvider>,
) {
label = msg("Configure Radius Provider");
renderForm(provider: RadiusProvider, errors: ValidationRecord) {
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm(provider ?? {}, errors, this.brand)}
</form>`;
}
render() {
if (!(this.wizard.provider && this.wizard.errors)) {
throw new Error("RAC Provider Step received uninitialized wizard context.");
}
return this.renderForm(
this.wizard.provider as RadiusProvider,
this.wizard.errors?.provider ?? {},
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-provider-for-radius": ApplicationWizardRadiusProviderForm;
}
}

View File

@ -0,0 +1,51 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title.js";
import { type AkCryptoCertificateSearch } from "@goauthentik/admin/common/ak-crypto-certificate-search";
import { renderForm } from "@goauthentik/admin/providers/saml/SAMLProviderFormForm.js";
import "@goauthentik/elements/forms/FormGroup";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { SAMLProvider } from "@goauthentik/api";
import { ApplicationWizardProviderForm } from "./ApplicationWizardProviderForm";
@customElement("ak-application-wizard-provider-for-saml")
export class ApplicationWizardProviderSamlForm extends ApplicationWizardProviderForm<SAMLProvider> {
label = msg("Configure SAML Provider");
@state()
hasSigningKp = false;
renderForm() {
const setHasSigningKp = (ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
};
return html` <ak-wizard-title>${this.label}</ak-wizard-title>
<form id="providerform" class="pf-c-form pf-m-horizontal" slot="form">
${renderForm(
(this.wizard.provider as SAMLProvider) ?? {},
this.wizard.errors?.provider ?? {},
setHasSigningKp,
this.hasSigningKp,
)}
</form>`;
}
render() {
if (!(this.wizard.provider && this.wizard.errors)) {
throw new Error("SAML Provider Step received uninitialized wizard context.");
}
return this.renderForm();
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-provider-for-saml": ApplicationWizardProviderSamlForm;
}
}

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