Compare commits

...

46 Commits

Author SHA1 Message Date
7d0abbf072 core, events: reduce memory usage when batch deleting objects
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-20 11:57:53 +01:00
640d0a4a95 core, web: update translations (#12432)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-12-20 10:42:51 +01:00
6b8782556c blueprints: fix schema for meta models (#12421)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-20 03:27:28 +01:00
7f6f3b6602 web: bump API Client version (#12431)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-12-19 20:52:20 +00:00
3367ac0e08 root: backport version bump (#12426) 2024-12-19 21:27:13 +01:00
d5ea0ffdc6 website/docs: add content about bindings (#11787)
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:35:20 +01:00
93f1638b39 release: 2024.12.0 (#12423) 2024-12-19 19:15:34 +00:00
37525175fa providers/saml: provide generic metadata url when possible (#12413) 2024-12-19 20:00:44 +01:00
0db1e52f90 website/docs: add new section about impersonation (#12328)
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-19 19:58:58 +01:00
3e8620b686 website/docs: prepare for 2024.12.0 (#12420) 2024-12-19 18:17:14 +00:00
6687ffc6d2 root: expose CONN_MAX_AGE, CONN_HEALTH_CHECKS and DISABLE_SERVER_SIDE_CURSORS for PostgreSQL config (#10159)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
2024-12-19 17:44:18 +00:00
e265ee253b events: notification_cleanup: avoid unnecessary loop (#12417) 2024-12-19 17:20:04 +00:00
7763a3673c core: bump msgraph-sdk from 1.14.0 to 1.15.0 (#12403)
Bumps [msgraph-sdk](https://github.com/microsoftgraph/msgraph-sdk-python) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/microsoftgraph/msgraph-sdk-python/releases)
- [Changelog](https://github.com/microsoftgraph/msgraph-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoftgraph/msgraph-sdk-python/compare/v1.14.0...v1.15.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 12:02:42 +01:00
d99005e130 core: bump pydantic from 2.10.3 to 2.10.4 (#12404)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.3 to 2.10.4.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.10.3...v2.10.4)

---
updated-dependencies:
- dependency-name: pydantic
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 12:02:30 +01:00
c61f96e770 core: bump google-api-python-client from 2.155.0 to 2.156.0 (#12405)
Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.155.0 to 2.156.0.
- [Release notes](https://github.com/googleapis/google-api-python-client/releases)
- [Commits](https://github.com/googleapis/google-api-python-client/compare/v2.155.0...v2.156.0)

---
updated-dependencies:
- dependency-name: google-api-python-client
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 12:02:19 +01:00
83622dd934 core: bump goauthentik.io/api/v3 from 3.2024105.3 to 3.2024105.5 (#12406)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024105.3 to 3.2024105.5.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Changelog](https://github.com/goauthentik/client-go/blob/main/model_version_history.go)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024105.3...v3.2024105.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-19 12:02:09 +01:00
2eebd0eaa1 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>
2024-12-19 09:30:54 +01:00
b61d918c5c 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 09:30:48 +01:00
076a4f4772 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 09:30:35 +01:00
b3872b35f8 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 09:29:53 +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
821f06ffdf translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#12393)
Translate locale/en/LC_MESSAGES/django.po in fr

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-18 15:52:56 +00:00
e83d040a48 translate: Updates for file web/xliff/en.xlf in fr (#12394)
Translate web/xliff/en.xlf in fr

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-18 15:32:56 +00:00
9affd90850 root: add locale to codeowners (#12392)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-18 15:56:46 +01:00
80d84cb03f website/integrations: update argocd terraform examples (#12370) 2024-12-18 14:21:31 +00:00
a9cc5fdafe core, web: update translations (#12390)
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-12-18 15:17:49 +01:00
b45109afce web: bump API Client version (#12391)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-12-18 13:59:24 +00:00
c8711d9f8f website/docs: 2024.12 release notes (#12300)
Co-authored-by: Tana M Berry <tana@goauthentik.com>
2024-12-18 13:39:17 +00:00
40a7135c0c core: app entitlements (#12090)
* core: initial app entitlements

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

* base off of pbm

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

* add tests and oauth2

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

* add to proxy

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

* rewrite to use bindings

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

* make policy bindings form and list more customizable

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

* fix tests

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

* double fix

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

* refine permissions

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

* add missing rbac modal to app entitlements

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

* separate scope for app entitlements

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

* include entitlements mapping in proxy

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

* fix tests

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

* add API validation to prevent policies from being bound to entitlements

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

* make preview

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

* add initial docs

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

* fix

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

* remove duplicate docs

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-18 14:32:44 +01:00
675a4a6788 translate: Updates for file locale/en/LC_MESSAGES/django.po in it (#12388)
Translate locale/en/LC_MESSAGES/django.po in it

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

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-18 13:26:45 +00:00
98b5b75f29 blueprints: add AtIndex tag (#12386) 2024-12-18 13:10:37 +00:00
22b0a1bd23 web: bump API Client version (#12387)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-12-18 13:57:38 +01:00
1a1d499833 sources/oauth: allow creation of user connection objects with parameters (#12195)
* sources/oauth: allow creation of user connection objects with parameters

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

* fix web

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

* tix tests

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

* add for all

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

* align

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-18 13:28:22 +01:00
1573cfbaa1 website: bump docusaurus-theme-openapi-docs from 4.3.0 to 4.3.1 in /website (#12373)
website: bump docusaurus-theme-openapi-docs in /website

Bumps [docusaurus-theme-openapi-docs](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/tree/HEAD/packages/docusaurus-theme-openapi-docs) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/releases)
- [Changelog](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/commits/v4.3.1/packages/docusaurus-theme-openapi-docs)

---
updated-dependencies:
- dependency-name: docusaurus-theme-openapi-docs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 13:19:02 +01:00
b88ce32111 website: bump aws-cdk from 2.173.1 to 2.173.2 in /website (#12374)
Bumps [aws-cdk](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk) from 2.173.1 to 2.173.2.
- [Release notes](https://github.com/aws/aws-cdk/releases)
- [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md)
- [Commits](https://github.com/aws/aws-cdk/commits/v2.173.2/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 13:17:31 +01:00
a1965ceada website: bump docusaurus-plugin-openapi-docs from 4.3.0 to 4.3.1 in /website (#12375)
website: bump docusaurus-plugin-openapi-docs in /website

Bumps [docusaurus-plugin-openapi-docs](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/tree/HEAD/packages/docusaurus-plugin-openapi-docs) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/releases)
- [Changelog](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/commits/v4.3.1/packages/docusaurus-plugin-openapi-docs)

---
updated-dependencies:
- dependency-name: docusaurus-plugin-openapi-docs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 13:17:23 +01:00
9c536a1b4b core: bump django-pglock from 1.7.0 to 1.7.1 (#12376)
Bumps [django-pglock](https://github.com/AmbitionEng/django-pglock) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/AmbitionEng/django-pglock/releases)
- [Changelog](https://github.com/AmbitionEng/django-pglock/blob/main/CHANGELOG.md)
- [Commits](https://github.com/AmbitionEng/django-pglock/compare/1.7.0...1.7.1)

---
updated-dependencies:
- dependency-name: django-pglock
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 13:13:37 +01:00
f3e0ff2833 core: bump google-api-python-client from 2.154.0 to 2.155.0 (#12377)
Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.154.0 to 2.155.0.
- [Release notes](https://github.com/googleapis/google-api-python-client/releases)
- [Commits](https://github.com/googleapis/google-api-python-client/compare/v2.154.0...v2.155.0)

---
updated-dependencies:
- dependency-name: google-api-python-client
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 13:13:24 +01:00
06dc47b582 core: bump aws-cdk-lib from 2.172.0 to 2.173.2 (#12378)
Bumps [aws-cdk-lib](https://github.com/aws/aws-cdk) from 2.172.0 to 2.173.2.
- [Release notes](https://github.com/aws/aws-cdk/releases)
- [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.md)
- [Commits](https://github.com/aws/aws-cdk/compare/v2.172.0...v2.173.2)

---
updated-dependencies:
- dependency-name: aws-cdk-lib
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-18 13:13:18 +01:00
a4bf24a039 core: bump pdoc from 15.0.0 to 15.0.1 (#12379)
* core: bump pdoc from 15.0.0 to 15.0.1

Bumps [pdoc](https://github.com/mitmproxy/pdoc) from 15.0.0 to 15.0.1.
- [Changelog](https://github.com/mitmproxy/pdoc/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mitmproxy/pdoc/compare/v15...v15.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-18 13:12:49 +01:00
1715c3e268 core: bump ruff from 0.8.2 to 0.8.3 (#12380)
* core: bump ruff from 0.8.2 to 0.8.3

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.2 to 0.8.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.8.2...0.8.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-18 13:11:48 +01:00
feb3be7cee core: bump uvicorn from 0.32.1 to 0.34.0 (#12381)
* core: bump uvicorn from 0.32.1 to 0.34.0

Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.32.1 to 0.34.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.32.1...0.34.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-18 13:11:07 +01:00
db05232f12 core: bump twilio from 9.3.8 to 9.4.1 (#12382)
* core: bump twilio from 9.3.8 to 9.4.1

Bumps [twilio](https://github.com/twilio/twilio-python) from 9.3.8 to 9.4.1.
- [Release notes](https://github.com/twilio/twilio-python/releases)
- [Changelog](https://github.com/twilio/twilio-python/blob/main/CHANGES.md)
- [Commits](https://github.com/twilio/twilio-python/compare/9.3.8...9.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-18 13:10:46 +01:00
ebfa7dbcfc web/admin: fix prompt stage wording (#12384)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-18 13:07:51 +01:00
8c4dab7399 sources/saml: fix redirect not kept through SAML Source (#12372)
* fix missing name in tests

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

* fix redirect lost with saml source

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-18 13:07:17 +01:00
212 changed files with 14175 additions and 8816 deletions

View File

@ -1,16 +1,16 @@
[bumpversion]
current_version = 2024.10.5
current_version = 2024.12.0
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
serialize =
serialize =
{major}.{minor}.{patch}-{rc_t}{rc_n}
{major}.{minor}.{patch}
message = release: {new_version}
tag_name = version/{new_version}
[bumpversion:part:rc_t]
values =
values =
rc
final
optional_value = final

View File

@ -33,7 +33,8 @@
"!If sequence",
"!Index scalar",
"!KeyOf scalar",
"!Value scalar"
"!Value scalar",
"!AtIndex scalar"
],
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.importModuleSpecifierEnding": "index",

View File

@ -25,6 +25,9 @@ CODEOWNERS @goauthentik/infrastructure
# Web
web/ @goauthentik/frontend
tests/wdio/ @goauthentik/frontend
# Locale
locale/ @goauthentik/backend @goauthentik/frontend
web/xliff/ @goauthentik/backend @goauthentik/frontend
# Docs & Website
website/ @goauthentik/docs
CODE_OF_CONDUCT.md @goauthentik/docs

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.0"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -126,7 +126,7 @@ class Command(BaseCommand):
def_name_perm = f"model_{model_path}_permissions"
def_path_perm = f"#/$defs/{def_name_perm}"
self.schema["$defs"][def_name_perm] = self.model_permissions(model)
return {
template = {
"type": "object",
"required": ["model", "identifiers"],
"properties": {
@ -143,6 +143,11 @@ class Command(BaseCommand):
"identifiers": {"$ref": def_path},
},
}
# Meta models don't require identifiers, as there's no matching database model to find
if issubclass(model, BaseMetaModel):
del template["properties"]["identifiers"]
template["required"].remove("identifiers")
return template
def field_to_jsonschema(self, field: Field) -> dict:
"""Convert a single field to json schema"""

View File

@ -146,6 +146,10 @@ entries:
]
]
nested_context: !Context context2
at_index_sequence: !AtIndex [!Context sequence, 0]
at_index_sequence_default: !AtIndex [!Context sequence, 100, "non existent"]
at_index_mapping: !AtIndex [!Context mapping, "key2"]
at_index_mapping_default: !AtIndex [!Context mapping, "invalid", "non existent"]
identifiers:
name: test
conditions:

View File

@ -215,6 +215,10 @@ class TestBlueprintsV1(TransactionTestCase):
},
"nested_context": "context-nested-value",
"env_null": None,
"at_index_sequence": "foo",
"at_index_sequence_default": "non existent",
"at_index_mapping": 2,
"at_index_mapping_default": "non existent",
}
).exists()
)

View File

@ -24,6 +24,10 @@ from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.models import PolicyBindingModel
class UNSET:
"""Used to test whether a key has not been set."""
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
"""Get object's attributes via their serializer, and convert it to a normal dict"""
serializer: Serializer = obj.serializer(obj)
@ -556,6 +560,53 @@ class Value(EnumeratedItem):
raise EntryInvalidError.from_entry(f"Empty/invalid context: {context}", entry) from exc
class AtIndex(YAMLTag):
"""Get value at index of a sequence or mapping"""
obj: YAMLTag | dict | list | tuple
attribute: int | str | YAMLTag
default: Any | UNSET
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
super().__init__()
self.obj = loader.construct_object(node.value[0])
self.attribute = loader.construct_object(node.value[1])
if len(node.value) == 2: # noqa: PLR2004
self.default = UNSET
else:
self.default = loader.construct_object(node.value[2])
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
if isinstance(self.obj, YAMLTag):
obj = self.obj.resolve(entry, blueprint)
else:
obj = self.obj
if isinstance(self.attribute, YAMLTag):
attribute = self.attribute.resolve(entry, blueprint)
else:
attribute = self.attribute
if isinstance(obj, list | tuple):
try:
return obj[attribute]
except TypeError as exc:
raise EntryInvalidError.from_entry(
f"Invalid index for list: {attribute}", entry
) from exc
except IndexError as exc:
if self.default is UNSET:
raise EntryInvalidError.from_entry(
f"Index out of range: {attribute}", entry
) from exc
return self.default
if attribute in obj:
return obj[attribute]
else:
if self.default is UNSET:
raise EntryInvalidError.from_entry(f"Key does not exist: {attribute}", entry)
return self.default
class BlueprintDumper(SafeDumper):
"""Dump dataclasses to yaml"""
@ -606,6 +657,7 @@ class BlueprintLoader(SafeLoader):
self.add_constructor("!Enumerate", Enumerate)
self.add_constructor("!Value", Value)
self.add_constructor("!Index", Index)
self.add_constructor("!AtIndex", AtIndex)
class EntryInvalidError(SentryIgnoredException):

View File

@ -0,0 +1,54 @@
"""Application Roles API Viewset"""
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import (
Application,
ApplicationEntitlement,
User,
)
class ApplicationEntitlementSerializer(ModelSerializer):
"""ApplicationEntitlement Serializer"""
def validate_app(self, app: Application) -> Application:
"""Ensure user has permission to view"""
user: User = self._context["request"].user
if user.has_perm("view_application", app) or user.has_perm(
"authentik_core.view_application"
):
return app
raise ValidationError(_("User does not have access to application."), code="invalid")
class Meta:
model = ApplicationEntitlement
fields = [
"pbm_uuid",
"name",
"app",
"attributes",
]
class ApplicationEntitlementViewSet(UsedByMixin, ModelViewSet):
"""ApplicationEntitlement Viewset"""
queryset = ApplicationEntitlement.objects.all()
serializer_class = ApplicationEntitlementSerializer
search_fields = [
"pbm_uuid",
"name",
"app",
"attributes",
]
filterset_fields = [
"pbm_uuid",
"name",
"app",
]
ordering = ["name"]

View File

@ -159,9 +159,9 @@ class SourceViewSet(
class UserSourceConnectionSerializer(SourceSerializer):
"""OAuth Source Serializer"""
"""User source connection"""
source = SourceSerializer(read_only=True)
source_obj = SourceSerializer(read_only=True, source="source")
class Meta:
model = UserSourceConnection
@ -169,10 +169,10 @@ class UserSourceConnectionSerializer(SourceSerializer):
"pk",
"user",
"source",
"source_obj",
"created",
]
extra_kwargs = {
"user": {"read_only": True},
"created": {"read_only": True},
}
@ -197,9 +197,9 @@ class UserSourceConnectionViewSet(
class GroupSourceConnectionSerializer(SourceSerializer):
"""Group Source Connection Serializer"""
"""Group Source Connection"""
source = SourceSerializer(read_only=True)
source_obj = SourceSerializer(read_only=True)
class Meta:
model = GroupSourceConnection
@ -207,12 +207,11 @@ class GroupSourceConnectionSerializer(SourceSerializer):
"pk",
"group",
"source",
"source_obj",
"identifier",
"created",
]
extra_kwargs = {
"group": {"read_only": True},
"identifier": {"read_only": True},
"created": {"read_only": True},
}

View File

@ -22,7 +22,7 @@ from authentik.blueprints.v1.common import (
from authentik.blueprints.v1.importer import Importer
from authentik.core.api.applications import ApplicationSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider
from authentik.core.models import Application, Provider
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.bindings import PolicyBindingSerializer
@ -51,6 +51,13 @@ class TransactionProviderField(DictField):
class TransactionPolicyBindingSerializer(PolicyBindingSerializer):
"""PolicyBindingSerializer which does not require target as target is set implicitly"""
def validate(self, attrs):
# As the PolicyBindingSerializer checks that the correct things can be bound to a target
# but we don't have a target here as that's set by the blueprint, pass in an empty app
# which will have the correct allowed combination of group/user/policy.
attrs["target"] = Application()
return super().validate(attrs)
class Meta(PolicyBindingSerializer.Meta):
fields = [x for x in PolicyBindingSerializer.Meta.fields if x != "target"]

View File

@ -0,0 +1,45 @@
# Generated by Django 5.0.9 on 2024-11-20 15:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0040_provider_invalidation_flow"),
("authentik_policies", "0011_policybinding_failure_result_and_more"),
]
operations = [
migrations.CreateModel(
name="ApplicationEntitlement",
fields=[
(
"policybindingmodel_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_policies.policybindingmodel",
),
),
("attributes", models.JSONField(blank=True, default=dict)),
("name", models.TextField()),
(
"app",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.application"
),
),
],
options={
"verbose_name": "Application Entitlement",
"verbose_name_plural": "Application Entitlements",
"unique_together": {("app", "name")},
},
bases=("authentik_policies.policybindingmodel", models.Model),
),
]

View File

@ -314,6 +314,32 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
always_merger.merge(final_attributes, self.attributes)
return final_attributes
def app_entitlements(self, app: "Application | None") -> QuerySet["ApplicationEntitlement"]:
"""Get all entitlements this user has for `app`."""
if not app:
return []
all_groups = self.all_groups()
qs = app.applicationentitlement_set.filter(
Q(
Q(bindings__user=self) | Q(bindings__group__in=all_groups),
bindings__negate=False,
)
| Q(
Q(~Q(bindings__user=self), bindings__user__isnull=False)
| Q(~Q(bindings__group__in=all_groups), bindings__group__isnull=False),
bindings__negate=True,
),
bindings__enabled=True,
).order_by("name")
return qs
def app_entitlements_attributes(self, app: "Application | None") -> dict:
"""Get a dictionary containing all merged attributes from app entitlements for `app`."""
final_attributes = {}
for attrs in self.app_entitlements(app).values_list("attributes", flat=True):
always_merger.merge(final_attributes, attrs)
return final_attributes
@property
def serializer(self) -> Serializer:
from authentik.core.api.users import UserSerializer
@ -581,6 +607,31 @@ class Application(SerializerModel, PolicyBindingModel):
verbose_name_plural = _("Applications")
class ApplicationEntitlement(AttributesMixin, SerializerModel, PolicyBindingModel):
"""Application-scoped entitlement to control authorization in an application"""
name = models.TextField()
app = models.ForeignKey(Application, on_delete=models.CASCADE)
class Meta:
verbose_name = _("Application Entitlement")
verbose_name_plural = _("Application Entitlements")
unique_together = (("app", "name"),)
def __str__(self):
return f"Application Entitlement {self.name} for app {self.app_id}"
@property
def serializer(self) -> type[Serializer]:
from authentik.core.api.application_entitlements import ApplicationEntitlementSerializer
return ApplicationEntitlementSerializer
def supported_policy_binding_targets(self):
return ["group", "user"]
class SourceUserMatchingModes(models.TextChoices):
"""Different modes a source can handle new/returning users"""

View File

@ -238,13 +238,7 @@ class SourceFlowManager:
self.request.GET,
flow_slug=flow_slug,
)
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if PLAN_CONTEXT_REDIRECT not in flow_context:
flow_context[PLAN_CONTEXT_REDIRECT] = final_redirect
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
if not flow:
return bad_request_message(

View File

@ -18,6 +18,7 @@ from authentik.core.models import (
)
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
from authentik.lib.config import CONFIG
from authentik.lib.utils.db import qs_batch_iter
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@ -34,14 +35,14 @@ def clean_expired_models(self: SystemTask):
cls.objects.all().exclude(expiring=False).exclude(expiring=True, expires__gt=now())
)
amount = objects.count()
for obj in objects:
for obj in qs_batch_iter(objects):
obj.expire_action()
LOGGER.debug("Expired models", model=cls, amount=amount)
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
# Special case
amount = 0
for session in AuthenticatedSession.objects.all():
for session in qs_batch_iter(AuthenticatedSession.objects.all()):
match CONFIG.get("session_storage", "cache"):
case "cache":
cache_key = f"{KEY_PREFIX}{session.session_key}"

View File

@ -0,0 +1,153 @@
"""Test Application Entitlements API"""
from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase
from authentik.core.models import Application, ApplicationEntitlement, Group
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import OAuth2Provider
class TestApplicationEntitlements(APITestCase):
"""Test application entitlements"""
def setUp(self) -> None:
self.user = create_test_user()
self.other_user = create_test_user()
self.provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
)
self.app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
provider=self.provider,
)
def test_user(self):
"""Test user-direct assignment"""
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, user=self.user, order=0)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_group(self):
"""Test direct group"""
group = Group.objects.create(name=generate_id())
self.user.ak_groups.add(group)
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, group=group, order=0)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_group_indirect(self):
"""Test indirect group"""
parent = Group.objects.create(name=generate_id())
group = Group.objects.create(name=generate_id(), parent=parent)
self.user.ak_groups.add(group)
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, group=parent, order=0)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_negate_user(self):
"""Test with negate flag"""
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, user=self.other_user, order=0, negate=True)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_negate_group(self):
"""Test with negate flag"""
other_group = Group.objects.create(name=generate_id())
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
PolicyBinding.objects.create(target=ent, group=other_group, order=0, negate=True)
ents = self.user.app_entitlements(self.app)
self.assertEqual(len(ents), 1)
self.assertEqual(ents[0].name, ent.name)
def test_api_perms_global(self):
"""Test API creation with global permissions"""
assign_perm("authentik_core.add_applicationentitlement", self.user)
assign_perm("authentik_core.view_application", self.user)
self.client.force_login(self.user)
res = self.client.post(
reverse("authentik_api:applicationentitlement-list"),
data={
"name": generate_id(),
"app": self.app.pk,
},
)
self.assertEqual(res.status_code, 201)
def test_api_perms_scoped(self):
"""Test API creation with scoped permissions"""
assign_perm("authentik_core.add_applicationentitlement", self.user)
assign_perm("authentik_core.view_application", self.user, self.app)
self.client.force_login(self.user)
res = self.client.post(
reverse("authentik_api:applicationentitlement-list"),
data={
"name": generate_id(),
"app": self.app.pk,
},
)
self.assertEqual(res.status_code, 201)
def test_api_perms_missing(self):
"""Test API creation with no permissions"""
assign_perm("authentik_core.add_applicationentitlement", self.user)
self.client.force_login(self.user)
res = self.client.post(
reverse("authentik_api:applicationentitlement-list"),
data={
"name": generate_id(),
"app": self.app.pk,
},
)
self.assertEqual(res.status_code, 400)
self.assertJSONEqual(res.content, {"app": ["User does not have access to application."]})
def test_api_bindings_policy(self):
"""Test that API doesn't allow policies to be bound to this"""
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
policy = DummyPolicy.objects.create(name=generate_id())
admin = create_test_admin_user()
self.client.force_login(admin)
response = self.client.post(
reverse("authentik_api:policybinding-list"),
data={
"target": ent.pbm_uuid,
"policy": policy.pk,
"order": 0,
},
)
self.assertJSONEqual(
response.content.decode(),
{"non_field_errors": ["One of 'group', 'user' must be set."]},
)
def test_api_bindings_group(self):
"""Test that API doesn't allow policies to be bound to this"""
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
group = Group.objects.create(name=generate_id())
admin = create_test_admin_user()
self.client.force_login(admin)
response = self.client.post(
reverse("authentik_api:policybinding-list"),
data={
"target": ent.pbm_uuid,
"group": group.pk,
"order": 0,
},
)
self.assertEqual(response.status_code, 201)
self.assertTrue(PolicyBinding.objects.filter(target=ent.pbm_uuid).exists())

View File

@ -6,6 +6,7 @@ from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.urls import path
from authentik.core.api.application_entitlements import ApplicationEntitlementViewSet
from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
@ -69,6 +70,7 @@ urlpatterns = [
api_urlpatterns = [
("core/authenticated_sessions", AuthenticatedSessionViewSet),
("core/applications", ApplicationViewSet),
("core/application_entitlements", ApplicationEntitlementViewSet),
path(
"core/transactional/applications/",
TransactionalApplicationView.as_view(),

View File

@ -15,6 +15,7 @@ from authentik.events.models import (
TaskStatus,
)
from authentik.events.system_tasks import SystemTask, prefill_task
from authentik.lib.utils.db import qs_batch_iter
from authentik.policies.engine import PolicyEngine
from authentik.policies.models import PolicyBinding, PolicyEngineMode
from authentik.root.celery import CELERY_APP
@ -129,7 +130,8 @@ def gdpr_cleanup(user_pk: int):
"""cleanup events from gdpr_compliance"""
events = Event.objects.filter(user__pk=user_pk)
LOGGER.debug("GDPR cleanup, removing events from user", events=events.count())
events.delete()
for event in qs_batch_iter(events):
event.delete()
@CELERY_APP.task(bind=True, base=SystemTask)
@ -138,7 +140,7 @@ 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:
for notification in qs_batch_iter(notifications):
notification.delete()
LOGGER.debug("Expired notifications", amount=amount)
self.set_status(TaskStatus.SUCCESSFUL, f"Expired {amount} Notifications")

View File

@ -280,9 +280,24 @@ 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)
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 +369,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,
},
},
)

22
authentik/lib/utils/db.py Normal file
View File

@ -0,0 +1,22 @@
"""authentik database utilities"""
import gc
from django.db.models import QuerySet
def qs_batch_iter(qs: QuerySet, batch_size: int = 10_000, gc_collect: bool = True):
pk_iter = qs.values_list("pk", flat=True).order_by("pk").distinct().iterator()
eof = False
while not eof:
pk_buffer = []
i = 0
try:
while i < batch_size:
pk_buffer.append(pk_iter.next())
i += 1
except StopIteration:
eof = True
yield from qs.filter(pk__in=pk_buffer).order_by("pk").iterator()
if gc_collect:
gc.collect()

View File

@ -84,19 +84,17 @@ class PolicyBindingSerializer(ModelSerializer):
def validate(self, attrs: OrderedDict) -> OrderedDict:
"""Check that either policy, group or user is set."""
count = sum(
[
bool(attrs.get("policy", None)),
bool(attrs.get("group", None)),
bool(attrs.get("user", None)),
]
)
target: PolicyBindingModel = attrs.get("target")
supported = target.supported_policy_binding_targets()
supported.sort()
count = sum([bool(attrs.get(x, None)) for x in supported])
invalid = count > 1
empty = count < 1
warning = ", ".join(f"'{x}'" for x in supported)
if invalid:
raise ValidationError("Only one of 'policy', 'group' or 'user' can be set.")
raise ValidationError(f"Only one of {warning} can be set.")
if empty:
raise ValidationError("One of 'policy', 'group' or 'user' must be set.")
raise ValidationError(f"One of {warning} must be set.")
return attrs

View File

@ -1,4 +1,6 @@
# Generated by Django 4.2.5 on 2023-09-13 18:07
import authentik.lib.models
import django.db.models.deletion
from django.db import migrations, models
@ -23,4 +25,13 @@ class Migration(migrations.Migration):
default=30, help_text="Timeout after which Policy execution is terminated."
),
),
migrations.AlterField(
model_name="policybinding",
name="target",
field=authentik.lib.models.InheritanceForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="bindings",
to="authentik_policies.policybindingmodel",
),
),
]

View File

@ -47,6 +47,10 @@ class PolicyBindingModel(models.Model):
def __str__(self) -> str:
return f"PolicyBindingModel {self.pbm_uuid}"
def supported_policy_binding_targets(self):
"""Return the list of objects that can be bound to this object."""
return ["policy", "user", "group"]
class PolicyBinding(SerializerModel):
"""Relationship between a Policy and a PolicyBindingModel."""
@ -81,7 +85,9 @@ class PolicyBinding(SerializerModel):
blank=True,
)
target = InheritanceForeignKey(PolicyBindingModel, on_delete=models.CASCADE, related_name="+")
target = InheritanceForeignKey(
PolicyBindingModel, on_delete=models.CASCADE, related_name="bindings"
)
negate = models.BooleanField(
default=False,
help_text=_("Negates the outcome of the policy. Messages are unaffected."),

View File

@ -38,7 +38,7 @@ class TestBindingsAPI(APITestCase):
)
self.assertJSONEqual(
response.content.decode(),
{"non_field_errors": ["Only one of 'policy', 'group' or 'user' can be set."]},
{"non_field_errors": ["Only one of 'group', 'policy', 'user' can be set."]},
)
def test_invalid_too_little(self):
@ -49,5 +49,5 @@ class TestBindingsAPI(APITestCase):
)
self.assertJSONEqual(
response.content.decode(),
{"non_field_errors": ["One of 'policy', 'group' or 'user' must be set."]},
{"non_field_errors": ["One of 'group', 'policy', 'user' must be set."]},
)

View File

@ -127,6 +127,7 @@ class Traefik3MiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
authResponseHeaders=[
"X-authentik-username",
"X-authentik-groups",
"X-authentik-entitlements",
"X-authentik-email",
"X-authentik-name",
"X-authentik-uid",

View File

@ -147,6 +147,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
"goauthentik.io/providers/oauth2/scope-openid",
"goauthentik.io/providers/oauth2/scope-profile",
"goauthentik.io/providers/oauth2/scope-email",
"goauthentik.io/providers/oauth2/scope-entitlements",
"goauthentik.io/providers/proxy/scope-proxy",
]
)

View File

@ -54,9 +54,23 @@ class SAMLProviderSerializer(ProviderSerializer):
if "request" not in self._context:
return ""
request: HttpRequest = self._context["request"]._request
return request.build_absolute_uri(
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": instance.pk}) + "?download"
)
try:
return request.build_absolute_uri(
reverse(
"authentik_providers_saml:metadata-download",
kwargs={"application_slug": instance.application.slug},
)
)
except Provider.application.RelatedObjectDoesNotExist:
return request.build_absolute_uri(
reverse(
"authentik_api:samlprovider-metadata",
kwargs={
"pk": instance.pk,
},
)
+ "?download"
)
def get_url_sso_post(self, instance: SAMLProvider) -> str:
"""Get SSO Post URL"""

View File

@ -17,6 +17,7 @@ class TestMetadataProcessor(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.source = SAMLSource.objects.create(
name=generate_id(),
slug=generate_id(),
issuer="authentik",
signing_kp=create_test_cert(),

View File

@ -28,6 +28,7 @@ class TestPropertyMappings(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.source = SAMLSource.objects.create(
name=generate_id(),
slug=generate_id(),
issuer="authentik",
allow_idp_initiated=True,

View File

@ -20,6 +20,7 @@ class TestResponseProcessor(TestCase):
def setUp(self):
self.factory = RequestFactory()
self.source = SAMLSource.objects.create(
name=generate_id(),
slug=generate_id(),
issuer="authentik",
allow_idp_initiated=True,

View File

@ -0,0 +1,88 @@
"""SAML Source tests"""
from base64 import b64encode
from django.test import RequestFactory, TestCase
from django.urls import reverse
from authentik.core.tests.utils import create_test_flow
from authentik.flows.planner import PLAN_CONTEXT_REDIRECT, FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.sources.saml.models import SAMLSource
class TestViews(TestCase):
"""Test SAML Views"""
def setUp(self):
self.factory = RequestFactory()
self.source = SAMLSource.objects.create(
name=generate_id(),
slug=generate_id(),
issuer="authentik",
allow_idp_initiated=True,
pre_authentication_flow=create_test_flow(),
)
def test_enroll(self):
"""Enroll"""
flow = create_test_flow()
self.source.enrollment_flow = flow
self.source.save()
response = self.client.post(
reverse(
"authentik_sources_saml:acs",
kwargs={
"source_slug": self.source.slug,
},
),
data={
"SAMLResponse": b64encode(
load_fixture("fixtures/response_success.xml").encode()
).decode()
},
)
self.assertEqual(response.status_code, 302)
self.assertRedirects(
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
)
plan: FlowPlan = self.client.session.get(SESSION_KEY_PLAN)
self.assertIsNotNone(plan)
def test_enroll_redirect(self):
"""Enroll when attempting to access a provider"""
initial_redirect = f"http://{generate_id()}"
session = self.client.session
old_plan = FlowPlan(generate_id())
old_plan.context[PLAN_CONTEXT_REDIRECT] = initial_redirect
session[SESSION_KEY_PLAN] = old_plan
session.save()
flow = create_test_flow()
self.source.enrollment_flow = flow
self.source.save()
response = self.client.post(
reverse(
"authentik_sources_saml:acs",
kwargs={
"source_slug": self.source.slug,
},
),
data={
"SAMLResponse": b64encode(
load_fixture("fixtures/response_success.xml").encode()
).decode()
},
)
self.assertEqual(response.status_code, 302)
self.assertRedirects(
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
)
plan: FlowPlan = self.client.session.get(SESSION_KEY_PLAN)
self.assertIsNotNone(plan)
self.assertEqual(plan.context.get(PLAN_CONTEXT_REDIRECT), initial_redirect)

View File

@ -28,10 +28,11 @@ from authentik.flows.planner import (
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SOURCE,
PLAN_CONTEXT_SSO,
FlowPlan,
FlowPlanner,
)
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
@ -148,12 +149,15 @@ class ACSView(View):
processor = ResponseProcessor(source, request)
try:
processor.parse()
except MissingSAMLResponse as exc:
return bad_request_message(request, str(exc))
except VerificationError as exc:
except (MissingSAMLResponse, VerificationError) as exc:
return bad_request_message(request, str(exc))
try:
if SESSION_KEY_PLAN in request.session:
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
plan_redirect = plan.context.get(PLAN_CONTEXT_REDIRECT)
if plan_redirect:
self.request.session[SESSION_KEY_GET] = {NEXT_ARG_NAME: plan_redirect}
return processor.prepare_flow_manager().get_flow()
except (UnsupportedNameIDFormat, ValueError) as exc:
return bad_request_message(request, str(exc))

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.0 Blueprint schema",
"required": [
"version",
"entries"
@ -3201,6 +3201,46 @@
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_core.applicationentitlement"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_core.applicationentitlement_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_core.applicationentitlement"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_core.applicationentitlement"
}
}
},
{
"type": "object",
"required": [
@ -3844,8 +3884,7 @@
{
"type": "object",
"required": [
"model",
"identifiers"
"model"
],
"properties": {
"model": {
@ -3875,9 +3914,6 @@
},
"attrs": {
"$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_blueprints.metaapplyblueprint"
}
}
}
@ -4640,6 +4676,7 @@
"authentik_core.group",
"authentik_core.user",
"authentik_core.application",
"authentik_core.applicationentitlement",
"authentik_core.token",
"authentik_enterprise.license",
"authentik_providers_google_workspace.googleworkspaceprovider",
@ -6369,6 +6406,7 @@
"authentik_brands.delete_brand",
"authentik_brands.view_brand",
"authentik_core.add_application",
"authentik_core.add_applicationentitlement",
"authentik_core.add_authenticatedsession",
"authentik_core.add_group",
"authentik_core.add_groupsourceconnection",
@ -6381,6 +6419,7 @@
"authentik_core.add_usersourceconnection",
"authentik_core.assign_user_permissions",
"authentik_core.change_application",
"authentik_core.change_applicationentitlement",
"authentik_core.change_authenticatedsession",
"authentik_core.change_group",
"authentik_core.change_groupsourceconnection",
@ -6391,6 +6430,7 @@
"authentik_core.change_user",
"authentik_core.change_usersourceconnection",
"authentik_core.delete_application",
"authentik_core.delete_applicationentitlement",
"authentik_core.delete_authenticatedsession",
"authentik_core.delete_group",
"authentik_core.delete_groupsourceconnection",
@ -6406,6 +6446,7 @@
"authentik_core.reset_user_password",
"authentik_core.unassign_user_permissions",
"authentik_core.view_application",
"authentik_core.view_applicationentitlement",
"authentik_core.view_authenticatedsession",
"authentik_core.view_group",
"authentik_core.view_groupsourceconnection",
@ -7170,6 +7211,10 @@
"type": "integer",
"title": "User"
},
"source": {
"type": "integer",
"title": "Source"
},
"identifier": {
"type": "string",
"minLength": 1,
@ -7212,6 +7257,20 @@
"model_authentik_sources_kerberos.groupkerberossourceconnection": {
"type": "object",
"properties": {
"group": {
"type": "string",
"format": "uuid",
"title": "Group"
},
"source": {
"type": "integer",
"title": "Source"
},
"identifier": {
"type": "string",
"minLength": 1,
"title": "Identifier"
},
"icon": {
"type": "string",
"minLength": 1,
@ -7755,6 +7814,14 @@
"model_authentik_sources_oauth.useroauthsourceconnection": {
"type": "object",
"properties": {
"user": {
"type": "integer",
"title": "User"
},
"source": {
"type": "integer",
"title": "Source"
},
"identifier": {
"type": "string",
"maxLength": 255,
@ -7805,6 +7872,20 @@
"model_authentik_sources_oauth.groupoauthsourceconnection": {
"type": "object",
"properties": {
"group": {
"type": "string",
"format": "uuid",
"title": "Group"
},
"source": {
"type": "integer",
"title": "Source"
},
"identifier": {
"type": "string",
"minLength": 1,
"title": "Identifier"
},
"icon": {
"type": "string",
"minLength": 1,
@ -8038,6 +8119,14 @@
"model_authentik_sources_plex.userplexsourceconnection": {
"type": "object",
"properties": {
"user": {
"type": "integer",
"title": "User"
},
"source": {
"type": "integer",
"title": "Source"
},
"identifier": {
"type": "string",
"minLength": 1,
@ -8085,6 +8174,20 @@
"model_authentik_sources_plex.groupplexsourceconnection": {
"type": "object",
"properties": {
"group": {
"type": "string",
"format": "uuid",
"title": "Group"
},
"source": {
"type": "integer",
"title": "Source"
},
"identifier": {
"type": "string",
"minLength": 1,
"title": "Identifier"
},
"icon": {
"type": "string",
"minLength": 1,
@ -8395,6 +8498,14 @@
"model_authentik_sources_saml.usersamlsourceconnection": {
"type": "object",
"properties": {
"user": {
"type": "integer",
"title": "User"
},
"source": {
"type": "integer",
"title": "Source"
},
"identifier": {
"type": "string",
"minLength": 1,
@ -8437,6 +8548,20 @@
"model_authentik_sources_saml.groupsamlsourceconnection": {
"type": "object",
"properties": {
"group": {
"type": "string",
"format": "uuid",
"title": "Group"
},
"source": {
"type": "integer",
"title": "Source"
},
"identifier": {
"type": "string",
"minLength": 1,
"title": "Identifier"
},
"icon": {
"type": "string",
"minLength": 1,
@ -12530,6 +12655,7 @@
"authentik_brands.delete_brand",
"authentik_brands.view_brand",
"authentik_core.add_application",
"authentik_core.add_applicationentitlement",
"authentik_core.add_authenticatedsession",
"authentik_core.add_group",
"authentik_core.add_groupsourceconnection",
@ -12542,6 +12668,7 @@
"authentik_core.add_usersourceconnection",
"authentik_core.assign_user_permissions",
"authentik_core.change_application",
"authentik_core.change_applicationentitlement",
"authentik_core.change_authenticatedsession",
"authentik_core.change_group",
"authentik_core.change_groupsourceconnection",
@ -12552,6 +12679,7 @@
"authentik_core.change_user",
"authentik_core.change_usersourceconnection",
"authentik_core.delete_application",
"authentik_core.delete_applicationentitlement",
"authentik_core.delete_authenticatedsession",
"authentik_core.delete_group",
"authentik_core.delete_groupsourceconnection",
@ -12567,6 +12695,7 @@
"authentik_core.reset_user_password",
"authentik_core.unassign_user_permissions",
"authentik_core.view_application",
"authentik_core.view_applicationentitlement",
"authentik_core.view_authenticatedsession",
"authentik_core.view_group",
"authentik_core.view_groupsourceconnection",
@ -13179,6 +13308,52 @@
}
}
},
"model_authentik_core.applicationentitlement": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"app": {
"type": "integer",
"title": "App"
},
"attributes": {
"type": "object",
"additionalProperties": true,
"title": "Attributes"
}
},
"required": []
},
"model_authentik_core.applicationentitlement_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_applicationentitlement",
"change_applicationentitlement",
"delete_applicationentitlement",
"view_applicationentitlement"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_core.token": {
"type": "object",
"properties": {

View File

@ -42,9 +42,21 @@ entries:
"given_name": request.user.name,
"preferred_username": request.user.username,
"nickname": request.user.username,
# groups is not part of the official userinfo schema, but is a quasi-standard
"groups": [group.name for group in request.user.ak_groups.all()],
}
- identifiers:
managed: goauthentik.io/providers/oauth2/scope-entitlements
model: authentik_providers_oauth2.scopemapping
attrs:
name: "authentik default OAuth Mapping: Application Entitlements"
scope_name: entitlements
description: "Application entitlements"
expression: |
entitlements = [entitlement.name for entitlement in request.user.app_entitlements(provider.application)]
return {
"entitlements": entitlements,
"roles": entitlements,
}
- identifiers:
managed: goauthentik.io/providers/oauth2/scope-offline_access
model: authentik_providers_oauth2.scopemapping

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.0}
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.0}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

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

4
go.sum
View File

@ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
goauthentik.io/api/v3 v3.2024105.3 h1:Vl1vwPkCtA8hChsxwO3NUI8nupFC7r93jUHvqM+kYVw=
goauthentik.io/api/v3 v3.2024105.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024105.5 h1:zBDqIjWN5QNuL6iBLL4o9QwBsSkFQdAnyTjASsyE/fw=
goauthentik.io/api/v3 v3.2024105.5/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

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

View File

@ -14,6 +14,7 @@ type Claims struct {
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
Groups []string `json:"groups"`
Entitlements []string `json:"entitlements"`
Sid string `json:"sid"`
Proxy *ProxyClaims `json:"ak_proxy"`

View File

@ -41,6 +41,7 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
// https://goauthentik.io/docs/providers/proxy/proxy
headers.Set("X-authentik-username", c.PreferredUsername)
headers.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
headers.Set("X-authentik-entitlements", strings.Join(c.Entitlements, "|"))
headers.Set("X-authentik-email", c.Email)
headers.Set("X-authentik-name", c.Name)
headers.Set("X-authentik-uid", c.Sub)

View File

@ -8,7 +8,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-20 00:08+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -101,6 +101,10 @@ msgstr ""
msgid "Brands"
msgstr ""
#: authentik/core/api/application_entitlements.py
msgid "User does not have access to application."
msgstr ""
#: authentik/core/api/devices.py
msgid "Extra description not available"
msgstr ""
@ -225,6 +229,14 @@ msgstr ""
msgid "Applications"
msgstr ""
#: authentik/core/models.py
msgid "Application Entitlement"
msgstr ""
#: authentik/core/models.py
msgid "Application Entitlements"
msgstr ""
#: authentik/core/models.py
msgid "Use the source-specific identifier"
msgstr ""
@ -1873,6 +1885,10 @@ msgstr ""
msgid "Custom krb5.conf to use. Uses the system one by default"
msgstr ""
#: authentik/sources/kerberos/models.py
msgid "KAdmin server type"
msgstr ""
#: authentik/sources/kerberos/models.py
msgid "Sync users from Kerberos into authentik"
msgstr ""
@ -2812,7 +2828,7 @@ msgstr ""
#, python-format
msgid ""
"\n"
" If you did not request a password change, please ignore this Email. The "
" If you did not request a password change, please ignore this email. The "
"link above is valid for %(expires)s.\n"
" "
msgstr ""
@ -2833,7 +2849,7 @@ msgstr ""
#, python-format
msgid ""
"\n"
"If you did not request a password change, please ignore this Email. The link "
"If you did not request a password change, please ignore this email. The link "
"above is valid for %(expires)s.\n"
msgstr ""
@ -3098,6 +3114,22 @@ msgstr ""
msgid "Passwords don't match."
msgstr ""
#: authentik/stages/redirect/api.py
msgid "Target URL should be present when mode is Static."
msgstr ""
#: 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

@ -19,7 +19,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: Marc Schmitt, 2024\n"
"Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n"
@ -2084,6 +2084,10 @@ msgid "Custom krb5.conf to use. Uses the system one by default"
msgstr ""
"krb5.conf personnalisé à utiliser. Utilise celui du système par défault"
#: authentik/sources/kerberos/models.py
msgid "KAdmin server type"
msgstr "Type de serveur KAdmin"
#: authentik/sources/kerberos/models.py
msgid "Sync users from Kerberos into authentik"
msgstr "Synchroniser les utilisateurs Kerberos dans authentik"
@ -3105,7 +3109,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"
@ -3129,7 +3133,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"
"Si vous n'avez pas requis de changement de mot de passe, veuillez ignorer cet e-mail. Le lien ci-dessus est valide pendant %(expires)s.\n"
@ -3434,6 +3438,22 @@ msgstr "Étapes invite"
msgid "Passwords don't match."
msgstr "Les mots de passe ne correspondent pas."
#: authentik/stages/redirect/api.py
msgid "Target URL should be present when mode is Static."
msgstr "L'URL destination doit être présente lorsque le mode est Statique."
#: authentik/stages/redirect/api.py
msgid "Target Flow should be present when mode is Flow."
msgstr "Le flux destination doit être présent lorsque le mode est Flux."
#: authentik/stages/redirect/models.py
msgid "Redirect Stage"
msgstr "Étape de redirection"
#: authentik/stages/redirect/models.py
msgid "Redirect Stages"
msgstr "Étapes de redirection"
#: authentik/stages/user_delete/models.py
msgid "User Delete Stage"
msgstr "Étape de suppression utilisateur"

Binary file not shown.

View File

@ -13,15 +13,16 @@
# albanobattistella <albanobattistella@gmail.com>, 2024
# Nicola Mersi, 2024
# tom max, 2024
# Marc Schmitt, 2024
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-18 00:09+0000\n"
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
"Last-Translator: tom max, 2024\n"
"Last-Translator: Marc Schmitt, 2024\n"
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -89,9 +90,9 @@ msgid "authentik Export - {date}"
msgstr "Esportazione authentik - {date}"
#: authentik/blueprints/v1/tasks.py authentik/crypto/tasks.py
#, python-format
msgid "Successfully imported %(count)d files."
msgstr "Importato con successo %(count)d file."
#, python-brace-format
msgid "Successfully imported {count} files."
msgstr "Importato con successo {count} file."
#: authentik/brands/models.py
msgid ""
@ -635,7 +636,7 @@ msgstr "Fasi Sorgenti"
#: authentik/events/api/tasks.py
#, python-brace-format
msgid "Successfully started task {name}."
msgstr "Attività {nome} avviata correttamente."
msgstr "Attività {name} avviata correttamente."
#: authentik/events/models.py
msgid "Event"
@ -937,14 +938,14 @@ msgid "Starting full provider sync"
msgstr "Avvio della sincronizzazione completa del provider"
#: authentik/lib/sync/outgoing/tasks.py
#, python-format
msgid "Syncing page %(page)d of users"
msgstr "Sincronizzando pagina %(page)d degli utenti"
#, python-brace-format
msgid "Syncing page {page} of users"
msgstr "Sincronizzando pagina {page} degli utenti"
#: authentik/lib/sync/outgoing/tasks.py
#, python-format
msgid "Syncing page %(page)d of groups"
msgstr "Sincronizzando pagina %(page)d dei gruppi"
#, python-brace-format
msgid "Syncing page {page} of groups"
msgstr "Sincronizzando pagina {page} dei gruppi"
#: authentik/lib/sync/outgoing/tasks.py
#, python-brace-format
@ -1117,10 +1118,10 @@ msgid "Event Matcher Policies"
msgstr "Criteri Corrispondenza Evento"
#: authentik/policies/expiry/models.py
#, python-format
msgid "Password expired %(days)d days ago. Please update your password."
#, python-brace-format
msgid "Password expired {days} days ago. Please update your password."
msgstr ""
"Password scaduta %(days)d giorni fa. Si prega di aggiornare la password."
"Password scaduta {days} giorni fa. Si prega di aggiornare la password."
#: authentik/policies/expiry/models.py
msgid "Password has expired."
@ -1254,9 +1255,9 @@ msgid "Invalid password."
msgstr "Password invalida."
#: authentik/policies/password/models.py
#, python-format
msgid "Password exists on %(count)d online lists."
msgstr "Password esistente in %(count)d lite online."
#, python-brace-format
msgid "Password exists on {count} online lists."
msgstr "Password esistente in {count} lite online."
#: authentik/policies/password/models.py
msgid "Password is too weak."
@ -1383,6 +1384,11 @@ msgstr "Providers LDAP"
msgid "Search full LDAP directory"
msgstr "Ricerca completa nella directory LDAP"
#: authentik/providers/oauth2/api/providers.py
#, python-brace-format
msgid "Invalid Regex Pattern: {url}"
msgstr "Modello Regex non valido: {url}"
#: authentik/providers/oauth2/id_token.py
msgid "Based on the Hashed User ID"
msgstr "Basato sull'ID utente hashato"
@ -1428,6 +1434,14 @@ msgid "Each provider has a different issuer, based on the application slug."
msgstr ""
"Ogni provider ha un issuer differente, basato sullo slug dell'applicazione."
#: authentik/providers/oauth2/models.py
msgid "Strict URL comparison"
msgstr "Confronto URL rigoroso"
#: authentik/providers/oauth2/models.py
msgid "Regular Expression URL matching"
msgstr "Corrispondenza URL espressione regolare"
#: authentik/providers/oauth2/models.py
msgid "code (Authorization Code Flow)"
msgstr "code (Flusso di autorizzazione del codice)"
@ -1508,10 +1522,6 @@ msgstr "Client Secret"
msgid "Redirect URIs"
msgstr "URL di reindirizzamento"
#: authentik/providers/oauth2/models.py
msgid "Enter each URI on a new line."
msgstr "Inserisci ogni URI su una nuova riga."
#: authentik/providers/oauth2/models.py
msgid "Include claims in id_token"
msgstr "Includere le richieste in id_token"

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.0",
"private": true
}

292
poetry.lock generated
View File

@ -408,13 +408,13 @@ typeguard = ">=2.13.3,<5.0.0"
[[package]]
name = "aws-cdk-lib"
version = "2.172.0"
version = "2.173.2"
description = "Version 2 of the AWS Cloud Development Kit library"
optional = false
python-versions = "~=3.8"
files = [
{file = "aws_cdk_lib-2.172.0-py3-none-any.whl", hash = "sha256:960b64af8eb272d2bc80d42dab4748863c2021c39dbc543bb6e7bec0fdafa099"},
{file = "aws_cdk_lib-2.172.0.tar.gz", hash = "sha256:4e8cb368256024e2d35874d7ab2e68812177d7990a27b2ceb50c454e8a018533"},
{file = "aws_cdk_lib-2.173.2-py3-none-any.whl", hash = "sha256:1b76846669de83e6572e9c46f5708f6ac045d8e710bafb044230f24e722601ef"},
{file = "aws_cdk_lib-2.173.2.tar.gz", hash = "sha256:9eb355c4fd5c1aa56317549600baf88dd4d3b520e2081132119b51349ead8c03"},
]
[package.dependencies]
@ -1467,13 +1467,13 @@ django = ">=3"
[[package]]
name = "django-pglock"
version = "1.7.0"
version = "1.7.1"
description = "Postgres locking routines and lock table access."
optional = false
python-versions = "<4,>=3.9.0"
files = [
{file = "django_pglock-1.7.0-py3-none-any.whl", hash = "sha256:4e28fa19cae96f072f3b74a368519442c5413b1ce72f75f816b77dd567d456df"},
{file = "django_pglock-1.7.0.tar.gz", hash = "sha256:180da6d3067b1dcb46b5e37745ee32fe0d8d5976c53bc8912dcf2a46e5000b6a"},
{file = "django_pglock-1.7.1-py3-none-any.whl", hash = "sha256:15db418fb56bee37fc8707038495b5085af9b8c203ebfa300202572127bdb3f0"},
{file = "django_pglock-1.7.1.tar.gz", hash = "sha256:69050bdb522fd34585d49bb8a4798dbfbab9ec4754dd1927b1b9eef2ec0edadf"},
]
[package.dependencies]
@ -1922,13 +1922,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"]
[[package]]
name = "google-api-python-client"
version = "2.154.0"
version = "2.156.0"
description = "Google API Client Library for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "google_api_python_client-2.154.0-py2.py3-none-any.whl", hash = "sha256:a521bbbb2ec0ba9d6f307cdd64ed6e21eeac372d1bd7493a4ab5022941f784ad"},
{file = "google_api_python_client-2.154.0.tar.gz", hash = "sha256:1b420062e03bfcaa1c79e2e00a612d29a6a934151ceb3d272fe150a656dc8f17"},
{file = "google_api_python_client-2.156.0-py2.py3-none-any.whl", hash = "sha256:6352185c505e1f311f11b0b96c1b636dcb0fec82cd04b80ac5a671ac4dcab339"},
{file = "google_api_python_client-2.156.0.tar.gz", hash = "sha256:b809c111ded61716a9c1c7936e6899053f13bae3defcdfda904bd2ca68065b9c"},
]
[package.dependencies]
@ -3107,13 +3107,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"]
[[package]]
name = "msgraph-sdk"
version = "1.14.0"
version = "1.15.0"
description = "The Microsoft Graph Python SDK"
optional = false
python-versions = ">=3.8"
files = [
{file = "msgraph_sdk-1.14.0-py3-none-any.whl", hash = "sha256:1a2f327dc8fbe5a5e6d0d84cf71d605e7b118b3066b1e16f011ccd8fd927bb03"},
{file = "msgraph_sdk-1.14.0.tar.gz", hash = "sha256:5bbda80941c5d1794682753b8b291bd2ebed719a43d6de949fd0cd613b6dfbbd"},
{file = "msgraph_sdk-1.15.0-py3-none-any.whl", hash = "sha256:85332db7ee19eb3d65a2493de83994ce3f5e4d9a084b3643ff6dea797cda81a7"},
{file = "msgraph_sdk-1.15.0.tar.gz", hash = "sha256:c920e72cc9de2218f9f9f71682db22ea544d9b440a5f088892bfca686c546b91"},
]
[package.dependencies]
@ -3463,13 +3463,13 @@ files = [
[[package]]
name = "pdoc"
version = "15.0.0"
version = "15.0.1"
description = "API Documentation for Python Projects"
optional = false
python-versions = ">=3.9"
files = [
{file = "pdoc-15.0.0-py3-none-any.whl", hash = "sha256:151b0187a25eaf827099e981d6dbe3a4f68aeb18d0d637c24edcab788d5540f1"},
{file = "pdoc-15.0.0.tar.gz", hash = "sha256:b761220d3ba129cd87e6da1bb7b62c8e799973ab9c595de7ba1a514850d86da5"},
{file = "pdoc-15.0.1-py3-none-any.whl", hash = "sha256:fd437ab8eb55f9b942226af7865a3801e2fb731665199b74fd9a44737dbe20f9"},
{file = "pdoc-15.0.1.tar.gz", hash = "sha256:3b08382c9d312243ee6c2a1813d0ff517a6ab84d596fa2c6c6b5255b17c3d666"},
]
[package.dependencies]
@ -3881,19 +3881,19 @@ files = [
[[package]]
name = "pydantic"
version = "2.10.3"
version = "2.10.4"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
{file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"},
{file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""}
pydantic-core = "2.27.1"
pydantic-core = "2.27.2"
typing-extensions = ">=4.12.2"
[package.extras]
@ -3902,111 +3902,111 @@ timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
version = "2.27.1"
version = "2.27.2"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"},
{file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"},
{file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"},
{file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"},
{file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"},
{file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"},
{file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"},
{file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"},
{file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"},
{file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"},
{file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"},
{file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"},
{file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"},
{file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"},
{file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"},
{file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"},
{file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
{file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
{file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
{file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
{file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
{file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
{file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
{file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
]
[package.dependencies]
@ -4630,29 +4630,29 @@ pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.8.2"
version = "0.8.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"},
{file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"},
{file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"},
{file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"},
{file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"},
{file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"},
{file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"},
{file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"},
{file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"},
{file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"},
{file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"},
{file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"},
{file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"},
{file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"},
{file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"},
{file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"},
{file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"},
{file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"},
{file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"},
{file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"},
{file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"},
{file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"},
]
[[package]]
@ -5087,13 +5087,13 @@ wsproto = ">=0.14"
[[package]]
name = "twilio"
version = "9.3.8"
version = "9.4.1"
description = "Twilio API client and TwiML generator"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "twilio-9.3.8-py2.py3-none-any.whl", hash = "sha256:fce1f629295285d583dbe1d615f114a77aab25a654ba569bb18d304d31e9ca3b"},
{file = "twilio-9.3.8.tar.gz", hash = "sha256:93a80639db711e58915cfdf772da6274b005ef86f5d2f6092433cb3d53a25303"},
{file = "twilio-9.4.1-py2.py3-none-any.whl", hash = "sha256:2447e041cec11167d7765aaa62ab1dae3b82b712245ca9a966096acd8b9f426f"},
{file = "twilio-9.4.1.tar.gz", hash = "sha256:e24c640696ccc726bba14160951da3cfc6b4bcb772fdcb3e8c16dc3cc851ef12"},
]
[package.dependencies]
@ -5258,13 +5258,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
version = "0.32.1"
version = "0.34.0"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"},
{file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"},
{file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"},
{file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"},
]
[package.dependencies]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.10.5"
version = "2024.12.0"
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.0
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -3097,6 +3097,285 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/application_entitlements/:
get:
operationId: core_application_entitlements_list
description: ApplicationEntitlement Viewset
parameters:
- in: query
name: app
schema:
type: string
format: uuid
- in: query
name: name
schema:
type: string
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: page
required: false
in: query
description: A page number within the paginated result set.
schema:
type: integer
- name: page_size
required: false
in: query
description: Number of results to return per page.
schema:
type: integer
- in: query
name: pbm_uuid
schema:
type: string
format: uuid
- name: search
required: false
in: query
description: A search term.
schema:
type: string
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedApplicationEntitlementList'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
post:
operationId: core_application_entitlements_create
description: ApplicationEntitlement Viewset
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlementRequest'
required: true
security:
- authentik: []
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlement'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/application_entitlements/{pbm_uuid}/:
get:
operationId: core_application_entitlements_retrieve
description: ApplicationEntitlement Viewset
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlement'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
put:
operationId: core_application_entitlements_update
description: ApplicationEntitlement Viewset
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlementRequest'
required: true
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlement'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
patch:
operationId: core_application_entitlements_partial_update
description: ApplicationEntitlement Viewset
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedApplicationEntitlementRequest'
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/ApplicationEntitlement'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
delete:
operationId: core_application_entitlements_destroy
description: ApplicationEntitlement Viewset
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
security:
- authentik: []
responses:
'204':
description: No response body
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/application_entitlements/{pbm_uuid}/used_by/:
get:
operationId: core_application_entitlements_used_by_list
description: Get a list of all objects that use this object
parameters:
- in: path
name: pbm_uuid
schema:
type: string
format: uuid
description: A UUID string identifying this Application Entitlement.
required: true
tags:
- core
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/UsedBy'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/core/applications/:
get:
operationId: core_applications_list
@ -23305,6 +23584,7 @@ paths:
- authentik_blueprints.blueprintinstance
- authentik_brands.brand
- authentik_core.application
- authentik_core.applicationentitlement
- authentik_core.group
- authentik_core.token
- authentik_core.user
@ -23545,6 +23825,7 @@ paths:
- authentik_blueprints.blueprintinstance
- authentik_brands.brand
- authentik_core.application
- authentik_core.applicationentitlement
- authentik_core.group
- authentik_core.token
- authentik_core.user
@ -25008,6 +25289,12 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GroupKerberosSourceConnectionRequest'
required: true
security:
- authentik: []
responses:
@ -25042,6 +25329,11 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedGroupKerberosSourceConnectionRequest'
security:
- authentik: []
responses:
@ -25196,6 +25488,12 @@ paths:
description: Group-source connection Viewset
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GroupOAuthSourceConnectionRequest'
required: true
security:
- authentik: []
responses:
@ -25263,6 +25561,12 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GroupOAuthSourceConnectionRequest'
required: true
security:
- authentik: []
responses:
@ -25296,6 +25600,11 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedGroupOAuthSourceConnectionRequest'
security:
- authentik: []
responses:
@ -25448,6 +25757,12 @@ paths:
description: Group-source connection Viewset
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GroupPlexSourceConnectionRequest'
required: true
security:
- authentik: []
responses:
@ -25515,6 +25830,12 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GroupPlexSourceConnectionRequest'
required: true
security:
- authentik: []
responses:
@ -25548,6 +25869,11 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedGroupPlexSourceConnectionRequest'
security:
- authentik: []
responses:
@ -25741,6 +26067,12 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GroupSAMLSourceConnectionRequest'
required: true
security:
- authentik: []
responses:
@ -25774,6 +26106,11 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedGroupSAMLSourceConnectionRequest'
security:
- authentik: []
responses:
@ -28734,6 +29071,12 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/UserSourceConnectionRequest'
required: true
security:
- authentik: []
responses:
@ -28767,6 +29110,11 @@ paths:
required: true
tags:
- sources
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PatchedUserSourceConnectionRequest'
security:
- authentik: []
responses:
@ -38070,6 +38418,38 @@ components:
- pk
- provider_obj
- slug
ApplicationEntitlement:
type: object
description: ApplicationEntitlement Serializer
properties:
pbm_uuid:
type: string
format: uuid
readOnly: true
name:
type: string
app:
type: string
format: uuid
attributes: {}
required:
- app
- name
- pbm_uuid
ApplicationEntitlementRequest:
type: object
description: ApplicationEntitlement Serializer
properties:
name:
type: string
minLength: 1
app:
type: string
format: uuid
attributes: {}
required:
- app
- name
ApplicationRequest:
type: object
description: Application Serializer
@ -42555,14 +42935,15 @@ components:
group:
type: string
format: uuid
readOnly: true
source:
type: string
format: uuid
source_obj:
allOf:
- $ref: '#/components/schemas/Source'
readOnly: true
identifier:
type: string
readOnly: true
created:
type: string
format: date-time
@ -42573,6 +42954,24 @@ components:
- identifier
- pk
- source
- source_obj
GroupKerberosSourceConnectionRequest:
type: object
description: OAuth Group-Source connection Serializer
properties:
group:
type: string
format: uuid
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
required:
- group
- identifier
- source
GroupMatchingModeEnum:
enum:
- identifier
@ -42667,14 +43066,15 @@ components:
group:
type: string
format: uuid
readOnly: true
source:
type: string
format: uuid
source_obj:
allOf:
- $ref: '#/components/schemas/Source'
readOnly: true
identifier:
type: string
readOnly: true
created:
type: string
format: date-time
@ -42685,6 +43085,24 @@ components:
- identifier
- pk
- source
- source_obj
GroupOAuthSourceConnectionRequest:
type: object
description: OAuth Group-Source connection Serializer
properties:
group:
type: string
format: uuid
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
required:
- group
- identifier
- source
GroupPlexSourceConnection:
type: object
description: Plex Group-Source connection Serializer
@ -42696,14 +43114,15 @@ components:
group:
type: string
format: uuid
readOnly: true
source:
type: string
format: uuid
source_obj:
allOf:
- $ref: '#/components/schemas/Source'
readOnly: true
identifier:
type: string
readOnly: true
created:
type: string
format: date-time
@ -42714,6 +43133,24 @@ components:
- identifier
- pk
- source
- source_obj
GroupPlexSourceConnectionRequest:
type: object
description: Plex Group-Source connection Serializer
properties:
group:
type: string
format: uuid
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
required:
- group
- identifier
- source
GroupRequest:
type: object
description: Group Serializer
@ -42753,14 +43190,15 @@ components:
group:
type: string
format: uuid
readOnly: true
source:
type: string
format: uuid
source_obj:
allOf:
- $ref: '#/components/schemas/Source'
readOnly: true
identifier:
type: string
readOnly: true
created:
type: string
format: date-time
@ -42771,6 +43209,24 @@ components:
- identifier
- pk
- source
- source_obj
GroupSAMLSourceConnectionRequest:
type: object
description: OAuth Group-Source connection Serializer
properties:
group:
type: string
format: uuid
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
required:
- group
- identifier
- source
IdentificationChallenge:
type: object
description: Identification challenges with all UI elements
@ -42801,7 +43257,9 @@ components:
flow_designation:
$ref: '#/components/schemas/FlowDesignationEnum'
captcha_stage:
$ref: '#/components/schemas/CaptchaChallenge'
allOf:
- $ref: '#/components/schemas/CaptchaChallenge'
nullable: true
enroll_url:
type: string
recovery_url:
@ -44732,6 +45190,7 @@ components:
- authentik_core.group
- authentik_core.user
- authentik_core.application
- authentik_core.applicationentitlement
- authentik_core.token
- authentik_enterprise.license
- authentik_providers_google_workspace.googleworkspaceprovider
@ -45812,6 +46271,18 @@ components:
- radius
- rac
type: string
PaginatedApplicationEntitlementList:
type: object
properties:
pagination:
$ref: '#/components/schemas/Pagination'
results:
type: array
items:
$ref: '#/components/schemas/ApplicationEntitlement'
required:
- pagination
- results
PaginatedApplicationList:
type: object
properties:
@ -47714,6 +48185,17 @@ components:
required:
- backends
- name
PatchedApplicationEntitlementRequest:
type: object
description: ApplicationEntitlement Serializer
properties:
name:
type: string
minLength: 1
app:
type: string
format: uuid
attributes: {}
PatchedApplicationRequest:
type: object
description: Application Serializer
@ -48562,6 +49044,45 @@ components:
default_group_email_domain:
type: string
minLength: 1
PatchedGroupKerberosSourceConnectionRequest:
type: object
description: OAuth Group-Source connection Serializer
properties:
group:
type: string
format: uuid
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
PatchedGroupOAuthSourceConnectionRequest:
type: object
description: OAuth Group-Source connection Serializer
properties:
group:
type: string
format: uuid
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
PatchedGroupPlexSourceConnectionRequest:
type: object
description: Plex Group-Source connection Serializer
properties:
group:
type: string
format: uuid
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
PatchedGroupRequest:
type: object
description: Group Serializer
@ -48588,6 +49109,19 @@ components:
items:
type: string
format: uuid
PatchedGroupSAMLSourceConnectionRequest:
type: object
description: OAuth Group-Source connection Serializer
properties:
group:
type: string
format: uuid
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
PatchedIdentificationStageRequest:
type: object
description: IdentificationStage Serializer
@ -50510,6 +51044,9 @@ components:
properties:
user:
type: integer
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
@ -50562,6 +51099,11 @@ components:
type: object
description: OAuth Source Serializer
properties:
user:
type: integer
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
@ -50574,6 +51116,11 @@ components:
type: object
description: Plex Source connection Serializer
properties:
user:
type: integer
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
@ -50623,9 +51170,23 @@ components:
type: object
description: SAML Source Serializer
properties:
user:
type: integer
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
PatchedUserSourceConnectionRequest:
type: object
description: User source connection
properties:
user:
type: integer
source:
type: string
format: uuid
PatchedUserWriteStageRequest:
type: object
description: UserWriteStage Serializer
@ -55550,6 +56111,9 @@ components:
user:
type: integer
source:
type: string
format: uuid
source_obj:
allOf:
- $ref: '#/components/schemas/Source'
readOnly: true
@ -55564,6 +56128,7 @@ components:
- identifier
- pk
- source
- source_obj
- user
UserKerberosSourceConnectionRequest:
type: object
@ -55571,11 +56136,15 @@ components:
properties:
user:
type: integer
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
required:
- identifier
- source
- user
UserLoginChallenge:
type: object
@ -55798,8 +56367,10 @@ components:
title: ID
user:
type: integer
readOnly: true
source:
type: string
format: uuid
source_obj:
allOf:
- $ref: '#/components/schemas/Source'
readOnly: true
@ -55815,11 +56386,17 @@ components:
- identifier
- pk
- source
- source_obj
- user
UserOAuthSourceConnectionRequest:
type: object
description: OAuth Source Serializer
properties:
user:
type: integer
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
@ -55830,6 +56407,8 @@ components:
nullable: true
required:
- identifier
- source
- user
UserObjectPermission:
type: object
description: User-bound object level permission
@ -55887,8 +56466,10 @@ components:
title: ID
user:
type: integer
readOnly: true
source:
type: string
format: uuid
source_obj:
allOf:
- $ref: '#/components/schemas/Source'
readOnly: true
@ -55903,11 +56484,17 @@ components:
- identifier
- pk
- source
- source_obj
- user
UserPlexSourceConnectionRequest:
type: object
description: Plex Source connection Serializer
properties:
user:
type: integer
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
@ -55918,6 +56505,8 @@ components:
required:
- identifier
- plex_token
- source
- user
UserRequest:
type: object
description: User Serializer
@ -55969,8 +56558,10 @@ components:
title: ID
user:
type: integer
readOnly: true
source:
type: string
format: uuid
source_obj:
allOf:
- $ref: '#/components/schemas/Source'
readOnly: true
@ -55985,16 +56576,24 @@ components:
- identifier
- pk
- source
- source_obj
- user
UserSAMLSourceConnectionRequest:
type: object
description: SAML Source Serializer
properties:
user:
type: integer
source:
type: string
format: uuid
identifier:
type: string
minLength: 1
required:
- identifier
- source
- user
UserSelf:
type: object
description: User Serializer for information a user can retrieve about themselves
@ -56130,7 +56729,7 @@ components:
- title
UserSourceConnection:
type: object
description: OAuth Source Serializer
description: User source connection
properties:
pk:
type: integer
@ -56138,8 +56737,10 @@ components:
title: ID
user:
type: integer
readOnly: true
source:
type: string
format: uuid
source_obj:
allOf:
- $ref: '#/components/schemas/Source'
readOnly: true
@ -56151,6 +56752,19 @@ components:
- created
- pk
- source
- source_obj
- user
UserSourceConnectionRequest:
type: object
description: User source connection
properties:
user:
type: integer
source:
type: string
format: uuid
required:
- source
- user
UserTypeEnum:
enum:

View File

@ -9,7 +9,7 @@ http://localhost {
uri /outpost.goauthentik.io/auth/caddy
# capitalization of the headers is important, otherwise they will be empty
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Entitlements X-Authentik-Email X-Authentik-Name X-Authentik-Uid X-Authentik-Jwt X-Authentik-Meta-Jwks X-Authentik-Meta-Outpost X-Authentik-Meta-Provider X-Authentik-Meta-App X-Authentik-Meta-Version
# optional, in this config trust all private ranges, should probably be set to the outposts IP
trusted_proxies private_ranges

View File

@ -23,12 +23,14 @@ server {
# translate headers from the outposts back to the actual upstream
auth_request_set $authentik_username $upstream_http_x_authentik_username;
auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
auth_request_set $authentik_entitlements $upstream_http_x_authentik_entitlements;
auth_request_set $authentik_email $upstream_http_x_authentik_email;
auth_request_set $authentik_name $upstream_http_x_authentik_name;
auth_request_set $authentik_uid $upstream_http_x_authentik_uid;
proxy_set_header X-authentik-username $authentik_username;
proxy_set_header X-authentik-groups $authentik_groups;
proxy_set_header X-authentik-entitlements $authentik_entitlements;
proxy_set_header X-authentik-email $authentik_email;
proxy_set_header X-authentik-name $authentik_name;
proxy_set_header X-authentik-uid $authentik_uid;

View File

@ -26,6 +26,7 @@ http:
authResponseHeaders:
- X-authentik-username
- X-authentik-groups
- X-authentik-entitlements
- X-authentik-email
- X-authentik-name
- X-authentik-uid

View File

@ -192,5 +192,5 @@ class TestSourceOAuth2(SeleniumTestCase):
results = body_json["results"]
self.assertEqual(len(results), 1)
connection = results[0]
self.assertEqual(connection["source"]["slug"], self.slug)
self.assertEqual(connection["source_obj"]["slug"], self.slug)
self.assertEqual(connection["user"], self.user.pk)

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",

1361
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@
"@floating-ui/dom": "^1.6.11",
"@formatjs/intl-listformat": "^7.5.7",
"@fortawesome/fontawesome-free": "^6.6.0",
"@goauthentik/api": "^2024.10.5-1734022840",
"@goauthentik/api": "^2024.12.0-1734640050",
"@lit-labs/ssr": "^3.2.2",
"@lit/context": "^1.1.2",
"@lit/localize": "^0.12.2",
@ -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

@ -1,6 +1,7 @@
import "@goauthentik/admin/applications/ApplicationAuthorizeChart";
import "@goauthentik/admin/applications/ApplicationCheckAccessForm";
import "@goauthentik/admin/applications/ApplicationForm";
import "@goauthentik/admin/applications/entitlements/ApplicationEntitlementPage";
import "@goauthentik/admin/policies/BoundPoliciesList";
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
@ -301,6 +302,28 @@ export class ApplicationViewPage extends AKElement {
</div>
</div>
</section>
<section
slot="page-app-entitlements"
data-tab-title="${msg("Application entitlements")}"
>
<div slot="header" class="pf-c-banner pf-m-info">
${msg("Application entitlements are in preview.")}
<a href="mailto:hello+feature/app-ent@goauthentik.io"
>${msg("Send us feedback!")}</a
>
</div>
<div class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__title">
${msg(
"These entitlements can be used to configure user access in this application.",
)}
</div>
<ak-application-entitlements-list .app=${this.application.pk}>
</ak-application-entitlements-list>
</div>
</div>
</section>
<section
slot="page-policy-bindings"
data-tab-title="${msg("Policy / Group / User Bindings")}"

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

@ -0,0 +1,89 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import YAML from "yaml";
import { msg } from "@lit/localize";
import { CSSResult } from "lit";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import { ApplicationEntitlement, CoreApi } from "@goauthentik/api";
@customElement("ak-application-entitlement-form")
export class ApplicationEntitlementForm extends ModelForm<ApplicationEntitlement, string> {
async loadInstance(pk: string): Promise<ApplicationEntitlement> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsRetrieve({
pbmUuid: pk,
});
}
@property()
targetPk?: string;
getSuccessMessage(): string {
if (this.instance?.pbmUuid) {
return msg("Successfully updated entitlement.");
} else {
return msg("Successfully created entitlement.");
}
}
static get styles(): CSSResult[] {
return [...super.styles, PFContent];
}
send(data: ApplicationEntitlement): Promise<unknown> {
if (this.targetPk) {
data.app = this.targetPk;
}
if (this.instance?.pbmUuid) {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsUpdate({
pbmUuid: this.instance.pbmUuid || "",
applicationEntitlementRequest: data,
});
} else {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsCreate({
applicationEntitlementRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Attributes")}
?required=${false}
name="attributes"
>
<ak-codemirror
mode=${CodeMirrorMode.YAML}
value="${YAML.stringify(first(this.instance?.attributes, {}))}"
>
</ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Set custom attributes using YAML or JSON.")}
</p>
</ak-form-element-horizontal>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-entitlement-form": ApplicationEntitlementForm;
}
}

View File

@ -0,0 +1,152 @@
import "@goauthentik/admin/applications/entitlements/ApplicationEntitlementForm";
import "@goauthentik/admin/policies/BoundPoliciesList";
import { PolicyBindingCheckTarget } from "@goauthentik/admin/policies/utils";
import "@goauthentik/admin/rbac/ObjectPermissionModal";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { PFSize } from "@goauthentik/common/enums";
import "@goauthentik/components/ak-status-label";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
ApplicationEntitlement,
CoreApi,
RbacPermissionsAssignedByUsersListModelEnum,
} from "@goauthentik/api";
@customElement("ak-application-entitlements-list")
export class ApplicationEntitlementsPage extends Table<ApplicationEntitlement> {
@property()
app?: string;
checkbox = true;
clearOnRefresh = true;
expandable = true;
order = "order";
async apiEndpoint(): Promise<PaginatedResponse<ApplicationEntitlement>> {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsList({
...(await this.defaultEndpointConfig()),
app: this.app || "",
});
}
columns(): TableColumn[] {
return [new TableColumn(msg("Name"), "name"), new TableColumn(msg("Actions"))];
}
renderToolbarSelected(): TemplateResult {
const disabled = this.selectedElements.length < 1;
return html`<ak-forms-delete-bulk
objectLabel=${msg("Application entitlement(s)")}
.objects=${this.selectedElements}
.usedBy=${(item: ApplicationEntitlement) => {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsUsedByList({
pbmUuid: item.pbmUuid || "",
});
}}
.delete=${(item: ApplicationEntitlement) => {
return new CoreApi(DEFAULT_CONFIG).coreApplicationEntitlementsDestroy({
pbmUuid: item.pbmUuid || "",
});
}}
>
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
${msg("Delete")}
</button>
</ak-forms-delete-bulk>`;
}
row(item: ApplicationEntitlement): TemplateResult[] {
return [
html`${item.name}`,
html`<ak-forms-modal size=${PFSize.Medium}>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Entitlement")} </span>
<ak-application-entitlement-form
slot="form"
.instancePk=${item.pbmUuid}
targetPk=${ifDefined(this.app)}
>
</ak-application-entitlement-form>
<button slot="trigger" class="pf-c-button pf-m-plain">
<pf-tooltip position="top" content=${msg("Edit")}>
<i class="fas fa-edit"></i>
</pf-tooltip>
</button>
</ak-forms-modal>
<ak-rbac-object-permission-modal
model=${RbacPermissionsAssignedByUsersListModelEnum.CoreApplicationentitlement}
objectPk=${item.pbmUuid}
>
</ak-rbac-object-permission-modal>`,
];
}
renderExpanded(item: ApplicationEntitlement): TemplateResult {
return html` <td></td>
<td role="cell" colspan="4">
<div class="pf-c-table__expandable-row-content">
<div class="pf-c-content">
<p>
${msg(
"These bindings control which users have access to this entitlement.",
)}
</p>
<ak-bound-policies-list
.target=${item.pbmUuid}
.allowedTypes=${[
PolicyBindingCheckTarget.group,
PolicyBindingCheckTarget.user,
]}
>
</ak-bound-policies-list>
</div>
</div>
</td>`;
}
renderEmpty(): TemplateResult {
return super.renderEmpty(
html`<ak-empty-state
header=${msg("No app entitlements created.")}
icon="pf-icon-module"
>
<div slot="body">
${msg(
"This application does currently not have any application entitlement defined.",
)}
</div>
<div slot="primary"></div>
</ak-empty-state>`,
);
}
renderToolbar(): TemplateResult {
return html`<ak-forms-modal size=${PFSize.Medium}>
<span slot="submit"> ${msg("Create")} </span>
<span slot="header"> ${msg("Create Entitlement")} </span>
<ak-application-entitlement-form slot="form" targetPk=${ifDefined(this.app)}>
</ak-application-entitlement-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Create entitlement")}
</button>
</ak-forms-modal> `;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-application-roles-list": ApplicationEntitlementsPage;
}
}

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>`;
}

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