Compare commits

...

152 Commits

Author SHA1 Message Date
59401e07ad release: 2024.12.5 2025-04-08 14:54:17 -03:00
4008f0f28f Revert "core: fix non-exploitable open redirect (#13696)" (#13827) 2025-04-08 19:16:35 +02:00
210fbb0067 release: 2024.12.4 2025-03-28 14:48:01 +01:00
9c081d084d security: fix CVE-2025-29928 (cherry-pick #13695) (#13701)
security: fix CVE-2025-29928 (#13695)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2025-03-28 14:45:42 +01:00
c7c75dc195 core: fix non-exploitable open redirect (#13696)
discovered by @dominic-r

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	authentik/core/sources/flow_manager.py
2025-03-28 14:19:55 +01:00
5448dc93db flows: fix inspector permission check (#12907)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-02-01 01:54:55 +01:00
6d152bcc60 core: fix generic sources not being fetchable by pk (#12896)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-02-01 00:40:53 +01:00
f1b7a9f934 release: 2024.12.3 2025-01-29 21:47:30 +01:00
4af75d0979 ci: fix missing dockerhub login
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-29 21:47:23 +01:00
af0a314e0b ci: fix permissions for release-publish pipeline
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2025-01-29 19:24:17 +01:00
6c7f901220 ci: fix test_docker.sh (cherry-pick #12880) (#12881)
ci: fix test_docker.sh (#12880)

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

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

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

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

* move pre-release docker test to script



* set pipefail in ak



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



* fix image



* fix config error on startup



---------

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

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

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



* make flow executor invocation correct



* actually we can do this centrally



* make sure the state is really clean



---------

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

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

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

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

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

* ci: fix missing build args for dev and release



* fix?



---------

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

* lifecycle: make it work



* sigh



* I dont know why this works but it works



---------

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

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

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

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

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

* build duo_client from scratch, sigh

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

* deps for kadmin

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

* ok fine

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

* run on arm runner?

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

* fix yaml format

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

* rewrite release pipeline to use re-usable workflows

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

* fix typo

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

* re-usable multi-arch build?

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

* also add suffix for amd64

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

* parameterise image name

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

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

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

* fix missing checkout

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

* inherit secrets

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

* temp build directly

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

* get cache-to from python script

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

* better name?

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

* matrix for merging images?

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

* re-add build dep

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

* use multi-image tag

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

* include arch in buildcache

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

---------

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

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

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



* set fixed DPI



* automatically set resize method for RDP



---------

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

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



* validate against schema too



---------

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

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

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

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

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

* fix service connection health not updating fully

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

* fix logo alt not translated

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

---------

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

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

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

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

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

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

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

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

Translate web/xliff/en.xlf in zh_CN

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

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

* ci: dont run codeql on cherry-picked prs

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

---------

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

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

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

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

Translate django.po in zh-Hans

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

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

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

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

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

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

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

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

* web: test for flash of unstructured content

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

- To test for the FLOUC issue in SearchSelect.

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

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

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

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

* web: finalize testing for tables

Includes documentation updates and better tests for select-table.

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

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

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

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

* web: update

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

* web: fixed tests to complete.

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

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

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

* improve permission checks

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

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

* web: update api-less tables

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

* web: wizard for applications, now with bindings!

- Add policy bindings to the application wizard

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

Conceptually:

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

Details:

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

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

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

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

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

* web: rollback dependabot "upgrade" that broke testing

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

* web: small fixes for wdio and lint

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

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

* package-lock.json update

* web: small fixes for wdio and lint

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

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

```

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

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

* Forgot to run prettier.

* web: small fixes for elements and forms

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

* Prettier had opinions

* Fix the oauth2 provider test.

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

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

* web: Add InvalidationFlow to Radius Provider dialogues

## What

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

## Note

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

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

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

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

* Making progress...

* web/admin: provider formectomy complete

* fix minor issues

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

* custom ordering for provider types

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

* fix css

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

* fix missing PFBase causing wrong font

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

* fix missing card for type select

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

* fix padding on last page

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

* add card to bindings

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

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

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

* Debugging code is not needed.

* web: small visual fixes

As requested by reviewers:

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

* web: disable lint check for cAsEfUnNy AtTrIbUtE nAmEs.

* Apply suggestions from code review

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

* rework title

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

* format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-18 18:44:27 +01:00
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
28d8fcc115 root: fix missing entries in codeowners (#12369)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-18 00:40:20 +01:00
c436205e3d website/docs: miscellaneous fixes (#12351)
* website/docs: miscellaneous fixes

* restore trailing space in signature delimiter

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>

* add link to Brands

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>

* fix inaccuracies

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

---------

Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-12-18 00:31:07 +01:00
3f788e7abe website/integrations: vault: add external group documentation (#11994)
* Add external group documentation for vault

Signed-off-by: Mattis Haase <accounts@mattis-haase.de>

* Spelling changes, linting

---------

Signed-off-by: Mattis Haase <accounts@mattis-haase.de>
Co-authored-by: mzhaase <mattis@mattis-haase.de>
2024-12-17 13:15:08 -06:00
b7a1a9c107 providers/oauth2: make session deletion cascade to tokens (#12343)
* providers/oauth2: make session deletion cascade to tokens

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

* add note for why we're shadowing the field

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-17 18:23:42 +01:00
559ec290d0 website/integrations: added hoarder integration (#12161)
* add Hoarder integration

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

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

* hoarder sorted alphabetically

* more detailed way of using an application

* Update index.md

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

* Update index.md

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

* Update index.md

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

* Update index.md

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

* Update index.md

Co-authored-by: 4d62 <github-user@sdko.org>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>

* test

* ran make website

---------

Signed-off-by: Miguel Palau <mpalauzarza@gmail.com>
Co-authored-by: 4d62 <github-user@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
2024-12-17 10:46:35 -06:00
05279514f8 ci: setup action: remove unused dependencies on poetry install (#12365)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-17 15:22:13 +01:00
061275d243 translate: Updates for file web/xliff/en.xlf in zh_CN (#12337)
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-17 12:58:37 +01:00
30e7f7acbd translate: Updates for file web/xliff/en.xlf in zh-Hans (#12338)
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-17 12:58:33 +01:00
80ab39675c core: bump goauthentik.io/api/v3 from 3.2024105.1 to 3.2024105.3 (#12348)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024105.1 to 3.2024105.3.
- [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.1...v3.2024105.3)

---
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-17 12:58:19 +01:00
e63f13c9fe stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#12352)
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-17 12:58:07 +01:00
08b07aebb9 translate: Updates for file locale/en/LC_MESSAGES/django.po in es (#12354)
* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

* Translate locale/en/LC_MESSAGES/django.po in es

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

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-12-17 12:57:50 +01:00
83219ff2ca core: bump github.com/go-ldap/ldap/v3 from 3.4.8 to 3.4.9 (#12355)
Bumps [github.com/go-ldap/ldap/v3](https://github.com/go-ldap/ldap) from 3.4.8 to 3.4.9.
- [Release notes](https://github.com/go-ldap/ldap/releases)
- [Commits](https://github.com/go-ldap/ldap/compare/v3.4.8...v3.4.9)

---
updated-dependencies:
- dependency-name: github.com/go-ldap/ldap/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-17 12:57:23 +01:00
be3e01912d core: bump debugpy from 1.8.10 to 1.8.11 (#12356)
Bumps [debugpy](https://github.com/microsoft/debugpy) from 1.8.10 to 1.8.11.
- [Release notes](https://github.com/microsoft/debugpy/releases)
- [Commits](https://github.com/microsoft/debugpy/compare/v1.8.10...v1.8.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-17 12:56:15 +01:00
0e180ebd21 ci: bump helm/kind-action from 1.10.0 to 1.11.0 (#12361)
Bumps [helm/kind-action](https://github.com/helm/kind-action) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/helm/kind-action/releases)
- [Commits](https://github.com/helm/kind-action/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: helm/kind-action
  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-17 12:40:27 +01:00
190cb33f8e website/docs: reverse proxies: add backlink to trusted_proxy_cidrs config (#12364) 2024-12-17 12:21:04 +01:00
15061dab6d website/integrations: proxmox: cleanup guide (#11894)
* website/integrations: proxmox: clarify location of Providers tab

Updates the guide to explicitly state the path to the Providers tab.

Signed-off-by: 4d62 <github-user@sdko.org>

* website/integrations: proxmox: cleanup instructions

Cleans up instructions to make them more readable


Signed-off-by: 4d62 <github-user@sdko.org>

* website/integrations: proxmox: specify application tab location

Specifies the location of the Applications tab in the admin interface

Signed-off-by: 4d62 <github-user@sdko.org>

* website/integrations: proxmox remove duplicate instructions

Some instructions were copied over in my previous commit and were never removed. This commit removes redundant instructions

Signed-off-by: 4d62 <github-user@sdko.org>

* website/integrations: proxmox: try to adhere to style guide

Touch up a few things to try to make it match

Signed-off-by: 4d62 <github-user@sdko.org>

* website/integrations: proxmox: lint

Lint with prettier

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Signed-off-by: 4d62 <github-user@sdko.org>

* website/integrations: proxmox: address review comment

Address review comment https://github.com/goauthentik/authentik/pull/11894#discussion_r1837106698 - Merge lines 42 and 44

Signed-off-by: 4d62 <github-user@sdko.org>

* website/integrations: proxmox: lint

run `npx prettier --write website/integrations/services/proxmox-ve/index.md`

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

* Update website/integrations/services/proxmox-ve/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: 4d62 <github-user@sdko.org>

---------

Signed-off-by: 4d62 <github-user@sdko.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-12-16 13:24:38 -06:00
d5a7f0fc3a CODEOWNERS: add SECURITY.md (#12360) 2024-12-16 17:08:34 +00:00
c1525449cf website: bump aws-cdk from 2.173.0 to 2.173.1 in /website (#12357)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-16 17:48:30 +01:00
f23965a55e root: SECURITY.md: fix markdown for Security documentation link (#12353) 2024-12-16 17:44:40 +01:00
de5191be6c web: bump API Client version (#12342)
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-16 17:28:11 +01:00
0bc2d4a7b8 website/integrations: Update Home Assistant integration docs (#12233)
Update Home Assistant integration docs

Extra care should be taken to proxy only /auth endpoint through Authentik.

Signed-off-by: Sergii Bogomolov <sergii@bogomolov.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-12-16 14:54:14 +00:00
4b5e66f9eb website/docs: add 2024.12+ badge to redirect stage features (#12346)
add `2024.12+` badge to redirect stage features
2024-12-13 01:11:29 +00:00
9a5effae2c root: fix ssl settings for read replicas not being applied (#12341)
* root: fix ssl settings for read replicas not being applied

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

* fix

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

* slight refactor

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-12 18:53:52 +01:00
ff504a3b80 stages/redirect: create redirect stage (#12275)
* create redirect stage

* show "keep context" toggle in Flow mode only

* fix typos

* add docs

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>

* simplify property pass

* simplify toggle

* remove `print` statements

whoops

* fix typo

* remove default from `RedirectStage.mode`

* remove migration

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

* oops

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

* adjust docs

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-12-12 18:00:09 +01:00
587f2d74ac web/flows: update flow background (#12339)
* web/flows: update flow background

https://unsplash.com/photos/a-snowy-road-with-trees-and-mountains-in-the-background-sUeF3oM_UB8
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Optimised images with calibre/image-actions

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-12-12 17:51:30 +01:00
c3555c778c web: bump API Client version (#12336)
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-12 13:26:58 +00:00
1acf48ae1e core: bump golang.org/x/crypto from 0.25.0 to 0.31.0 (#12335)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 14:18:39 +01:00
a32d396cec website: bump prism-react-renderer from 2.4.0 to 2.4.1 in /website (#12329)
Bumps [prism-react-renderer](https://github.com/FormidableLabs/prism-react-renderer) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/FormidableLabs/prism-react-renderer/releases)
- [Commits](https://github.com/FormidableLabs/prism-react-renderer/compare/prism-react-renderer@2.4.0...prism-react-renderer@2.4.1)

---
updated-dependencies:
- dependency-name: prism-react-renderer
  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-12 13:57:21 +01:00
deacc17832 sources/kerberos: add kadmin type setting, provide additional context to property mappings (#12286) 2024-12-12 13:25:43 +01:00
96b3e2b3d9 website: bump aws-cdk from 2.172.0 to 2.173.0 in /website (#12330)
Bumps [aws-cdk](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk) from 2.172.0 to 2.173.0.
- [Release notes](https://github.com/aws/aws-cdk/releases)
- [Changelog](https://github.com/aws/aws-cdk/blob/v2.173.0/CHANGELOG.v2.md)
- [Commits](https://github.com/aws/aws-cdk/commits/v2.173.0/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  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-12 13:19:06 +01:00
ddd3b0557e website/docs: fix typo in device code auth example (#12331) 2024-12-12 09:47:57 +01:00
006766cd3c website: bump docusaurus-plugin-openapi-docs from 4.2.0 to 4.3.0 in /website (#12322)
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.2.0 to 4.3.0.
- [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.0/packages/docusaurus-plugin-openapi-docs)

---
updated-dependencies:
- dependency-name: docusaurus-plugin-openapi-docs
  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-11 15:01:50 +01:00
a79cda924b website: bump docusaurus-theme-openapi-docs from 4.2.0 to 4.3.0 in /website (#12323)
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.2.0 to 4.3.0.
- [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.0/packages/docusaurus-theme-openapi-docs)

---
updated-dependencies:
- dependency-name: docusaurus-theme-openapi-docs
  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-11 14:57:10 +01:00
9d0901effd core: bump goauthentik.io/api/v3 from 3.2024104.2 to 3.2024105.1 (#12324)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024104.2 to 3.2024105.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Changelog](https://github.com/goauthentik/client-go/blob/main/model_version_history.go)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024104.2...v3.2024105.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 14:57:01 +01:00
89b6a2a2f2 core: bump debugpy from 1.8.9 to 1.8.10 (#12325)
Bumps [debugpy](https://github.com/microsoft/debugpy) from 1.8.9 to 1.8.10.
- [Release notes](https://github.com/microsoft/debugpy/releases)
- [Commits](https://github.com/microsoft/debugpy/compare/v1.8.9...v1.8.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-11 14:56:46 +01:00
12d9966604 website/integrations: add espoCRM (#11622)
* Create index.md (espoCRM)

Signed-off-by: Masked-Kunsiquat <130736043+Masked-Kunsiquat@users.noreply.github.com>

* Update sidebarsIntegrations.js

added espoCRM integration

* Update index.md

* Update index.md

* Update website/sidebarsIntegrations.js

Signed-off-by: Masked-Kunsiquat <130736043+Masked-Kunsiquat@users.noreply.github.com>

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

fix integration capitalization

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Masked-Kunsiquat <130736043+Masked-Kunsiquat@users.noreply.github.com>

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

fix integration capitalization

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Masked-Kunsiquat <130736043+Masked-Kunsiquat@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Masked-Kunsiquat <130736043+Masked-Kunsiquat@users.noreply.github.com>

* Update index.md

Signed-off-by: Masked-Kunsiquat <130736043+Masked-Kunsiquat@users.noreply.github.com>

* applying new suggestions

* fix formatting

* fix formatting again

---------

Signed-off-by: Masked-Kunsiquat <130736043+Masked-Kunsiquat@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 22:21:48 +01:00
accc88fcdd web: bump API Client version (#12320)
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-10 19:44:21 +01:00
e5dd923333 release: 2024.10.5 (#12319)
* release: 2024.10.5

* manually bump aws version

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 19:20:01 +01:00
29f53fd3a4 website/docs: update and rearchitect the oauth2 provider docs (#11916)
* new topics

* update diagram

* more writing and sidebar entries

* fix file name

* more link fixes

* fix header level

* Optimised images with calibre/image-actions

* rewrite stuff

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

* reorganize more

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

* messed w/ diagram and added 3 categories

* fixed anchor

* removed whole line

* add link

* more rearrangements

* more content

* edits

* more polishes, rest of Jens' knowledge dump

* more content

* tweaks

* tweak

* argh

* synch with main

* tweaks to test merge

* cleanup

* offline_access

* polish

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 12:03:07 -06:00
cbe5a0d2c8 website/docs: update 2024.10.5 release notes (#12318)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 18:19:58 +01:00
81b3662046 flows: better test stage's challenge responses (#12316)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 17:46:03 +01:00
12ac058547 enterprise/stages/authenticator_endpoint_gdtc: don't set frame options globally (#12311)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 15:17:00 +01:00
69cd0ef565 stages/identification: fix invalid challenge warning when no captcha stage is set (#12312)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 15:16:44 +01:00
83edb0dcb8 website/docs: prepare 2024.10.5 release notes (#12309)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 14:15:24 +01:00
0c80b1b8c3 website: bump nanoid from 3.3.7 to 3.3.8 in /website (#12307)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-10 13:43:40 +01:00
47e330d08a flows: silent authz flow (#12213)
* flows: add FlowPlan .to_redirect helper to redirect to flow executor

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

* flows: add initial silent flow executor

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

* more cleanup

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

* refactor and add tests

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

* how'd that happen

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

* fix most tests

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

* don't set allowed_silent_types if we cant transmit data via URL

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

* fix stage not being set early enough

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

* fix OAuthDeviceCodeFinishStage being marked able-to-be-skipped-to when it is not

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

* fix more tests

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

* dont skip on rac for now

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

* add support for SAML redirect

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 13:43:22 +01:00
97676d28a7 root: use healthcheck in depends_on for postgres and redis (#12301)
Docker Compose Updates Healthcheck

The Healthcheck is not used for the depends_on option. This change ensures all dependencies are all ready to work before starting worker and server container.

Signed-off-by: Kaindl Network <82705244+kaindlnetwork@users.noreply.github.com>
2024-12-10 13:10:57 +01:00
b9435870c0 ci: ensure mark jobs always run and reflect correct status (#12288)
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-10 13:09:41 +01:00
81ae02e623 enterprise: allow deletion/modification of users when in read-only mode (#12289)
* enterprise: allow deletion/modification of users when in read-only mode

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

* actually 10.5+

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

* Apply suggestions from code review

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-12-10 13:07:50 +01:00
28a23110c2 web/flows: resize captcha iframes (#12260)
* 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: streamline CaptchaStage

# What

This commit:

1. Replaces the mass of `if () { if() { if() } }` with two state tables:
  - One for `render()`
  - One for `renderBody()`

2. Breaks each Captcha out into "interactive" and "executive" versions
3. Creates a handler table for each Captcha type
4. Replaces the `.forEach` expression with a `for` loop.
5. Move `updated` to the end of the class.
6. Make captchDocument and captchaFrame constructed-on-demand with a cache.
7. Remove a lot of `@state` handlers
8. Give IframeMessageEvent its own type.
9. Removed `this.scriptElement`
10. Replaced `window.removeEventListener` with an `AbortController()`
# Why

1. **Replacing `if` trees with a state table.** The logic of the original was really hard to follow.
   With the state table, we can clearly see now that for the `render()` function, we care about the
   Boolean flags `[embedded, challenged, interactive]` and have appropriate effects for each. With
   `renderBody()`, we can see that we care about the Boolean flags `[hasError, challenged]`, and can
   see the appropriate effects for each one.

2. (and 3.) **Breaking each Captcha clause into separate handlers.** Again, the logic was convoluted,
   when what we really care about is "Does this captcha have a corresponding handler attached to
   `window`, and, if so, should we run the interactive or executive version of it?" By putting all
   of that into a table of `[name, challenge, execute]`, we can clearly see what's being handled
   when.

4. **Replacing `foreach()` with `for()`**: [You cannot use a `forEach()` with async
   expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#:~:text=does%20not%20wait%20for%20promises).
   If you need asynchronous behavior in an ordered loop, `for()` is the safest way to handle it; if
   you need asynchronous behavior from multiple promises, `Promise.allSettled(handlers.map())` is
   the way to go.

   I tried to tell if this function *meant* to run every handler it found simultaneously, or if it
   meant to test them in order; I went with the second option, breaking and exiting the loop once a
   handler had run successfully.

5. **Reordered the code a bit**. We're trying to evolve a pattern in our source code: styles,
   properties, states, internal variables, constructor, getters & setters that are not `@property()`
   or `@state()`, DOM-related lifecycle handlers, event handlers, pre-render lifecycle handlers,
   renderers, and post-render lifecycle handlers. Helper methods (including subrenderers) go above
   the method(s) they help.

6. **Constructing Elements on demand with a cache**. It is not guaranteed that we will actually need
   either of those. Constructing them on demand with a cache is both performant and cleaner.
   Likewise, I removed these from the Lit object's `state()` table, since they're constructed once
   and never change over the lifetime of an instance of `ak-stage-captcha`.

9. **Remove `this.scriptElement`**: It was never referenced outside the one function where it was used.

10. **Remove `removeEventListener()`**: `AbortController()` is a bit more verbose for small event
    handler collections, but it's considered much more reliable and much cleaner.

* Didn't save the extracted ListenerController.
2024-12-09 09:11:04 -08:00
a117918cd6 website/docs: add page about the Cobalt pentest (#12249)
* draft for collab

* links

* added link to see all audits

* corrections and fix explanations

Signed-off-by: Fletcher Heisler <fheisler@users.noreply.github.com>

---------

Signed-off-by: Fletcher Heisler <fheisler@users.noreply.github.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Fletcher Heisler <fheisler@users.noreply.github.com>
2024-12-09 07:57:34 -06:00
f1a548f941 core: bump aws-cdk-lib from 2.171.1 to 2.172.0 (#12296)
Bumps [aws-cdk-lib](https://github.com/aws/aws-cdk) from 2.171.1 to 2.172.0.
- [Release notes](https://github.com/aws/aws-cdk/releases)
- [Changelog](https://github.com/aws/aws-cdk/blob/v2.172.0/CHANGELOG.v2.md)
- [Commits](https://github.com/aws/aws-cdk/compare/v2.171.1...v2.172.0)

---
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-09 14:39:51 +01:00
021c0f7cb9 website: bump aws-cdk from 2.171.1 to 2.172.0 in /website (#12295)
Bumps [aws-cdk](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk) from 2.171.1 to 2.172.0.
- [Release notes](https://github.com/aws/aws-cdk/releases)
- [Changelog](https://github.com/aws/aws-cdk/blob/v2.172.0/CHANGELOG.v2.md)
- [Commits](https://github.com/aws/aws-cdk/commits/v2.172.0/packages/aws-cdk)

---
updated-dependencies:
- dependency-name: aws-cdk
  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-09 14:39:40 +01:00
c09ce06812 core: bump sentry-sdk from 2.19.1 to 2.19.2 (#12297)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.19.1 to 2.19.2.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.19.1...2.19.2)

---
updated-dependencies:
- dependency-name: sentry-sdk
  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-09 14:39:14 +01:00
e0aa588e60 core: bump coverage from 7.6.8 to 7.6.9 (#12299)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.6.8 to 7.6.9.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.6.8...7.6.9)

---
updated-dependencies:
- dependency-name: coverage
  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-09 14:35:30 +01:00
e842a73f3b core, web: update translations (#12290)
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-07 21:13:41 +02:00
98c43447ff root: fix override locale only if it is not empty (#12283)
* update impersonate middleware

override language only if it's not empty

Signed-off-by: Zhang Jie <13131481+j-z10@users.noreply.github.com>

* Update brands locale scope

Signed-off-by: Zhang Jie <13131481+j-z10@users.noreply.github.com>

---------

Signed-off-by: Zhang Jie <13131481+j-z10@users.noreply.github.com>
2024-12-06 23:58:02 +02:00
eef1237297 translate: Updates for file web/xliff/en.xlf in fr (#12276)
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-06 22:13:08 +02:00
dea0a34dd9 core: bump twilio from 9.3.7 to 9.3.8 (#12282)
Bumps [twilio](https://github.com/twilio/twilio-python) from 9.3.7 to 9.3.8.
- [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.7...9.3.8)

---
updated-dependencies:
- dependency-name: twilio
  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-06 22:12:04 +02:00
f3359fb4d0 website: bump path-to-regexp and express in /website (#12279)
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `path-to-regexp` from 0.1.10 to 1.9.0
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v1.9.0)

Updates `express` from 4.21.1 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.1...4.21.2)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-06 22:11:51 +02:00
d344db9c2a core: bump sentry-sdk from 2.19.0 to 2.19.1 (#12280)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.19.0 to 2.19.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.19.0...2.19.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  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-06 22:11:41 +02:00
13b1811268 core: bump ruff from 0.8.1 to 0.8.2 (#12281)
* core: bump ruff from 0.8.1 to 0.8.2

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.1 to 0.8.2.
- [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.1...0.8.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

* format

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

* format v2

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

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-12-06 22:11:31 +02:00
84bc0b6fdd website/docs: fix lint (#12287) 2024-12-06 20:09:16 +00:00
5c2853bf73 website/integrations: netbird: fix redirect URI regex (#12284) 2024-12-06 15:03:52 +00:00
242546e621 web: simplify ?inline handler for Storybook (#12246)
* 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: simplify `?inline` handler for Storybook

# What

- Revise the `?inline` handler for Storybook
- Enable headless test runs of E2E
- Reduce headless testing to single instances

# Why

## `?inline` handling

Vite-for-Storybook-for-Web-Components has a requirement that all component CSS imports be
suffixed with an `?inline` argument so Vite knows to put the CSS into the component and
not inject it into the document head.

This `?inline` argument is an implementation detail of Storybook. It would be irrelevant clutter
added to our codebase. We were using `rollup-plugin-modify` to find every instance of an
import-to-component, but the implementation was clunky and involved scanning the source code
manually.

`rollup-plugin-modify` version 3 has regular expressions and takes a function as an argument. This
allows us to generate the CSS import maps on-the-fly when Storybook is run, eliminating a fragile
build step.  We can also remove the source code scanner for those imports.

## Changes to testing

It's just nice to be able to run the E2E tests headlessly, without them eating up your screen real
estate, flashing, or grabbing your mouse.

WebdriverIO's testing of Web Components is new and, as we've seen, a bit cranky. The WebdriverIO
team currently recommends not running the tests in parallel. We only have about 70 tests so far, and
they're fairly speedy, especially when you don't have to invoke a browser session for every test.
2024-12-05 10:16:10 -08:00
b75672ff0e website/docs: Update Traefik middleware example to reflect latest version of Traefik (#12267)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-05 18:09:13 +00:00
672ba72c8a website/docs: add . in https://netbird.company* (#12166)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-05 18:06:06 +00:00
d618f48bff core: bump goauthentik.io/api/v3 from 3.2024104.1 to 3.2024104.2 (#12263)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024104.1 to 3.2024104.2.
- [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.2024104.1...v3.2024104.2)

---
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-05 15:02:03 +02:00
716f2dff74 core: bump pydantic from 2.10.2 to 2.10.3 (#12262)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.2 to 2.10.3.
- [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.2...v2.10.3)

---
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-05 15:01:11 +02:00
fc8a0b5ce0 core: bump github.com/getsentry/sentry-go from 0.29.1 to 0.30.0 (#12264)
Bumps [github.com/getsentry/sentry-go](https://github.com/getsentry/sentry-go) from 0.29.1 to 0.30.0.
- [Release notes](https://github.com/getsentry/sentry-go/releases)
- [Changelog](https://github.com/getsentry/sentry-go/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-go/compare/v0.29.1...v0.30.0)

---
updated-dependencies:
- dependency-name: github.com/getsentry/sentry-go
  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-05 14:58:53 +02:00
2b079816bb core, web: update translations (#12268)
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-05 14:58:35 +02:00
eded745332 website: bump @types/react from 18.3.12 to 18.3.13 in /website (#12269)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.3.12 to 18.3.13.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  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-05 14:58:04 +02:00
1740ab938d website: bump prettier from 3.4.1 to 3.4.2 in /website (#12270)
Bumps [prettier](https://github.com/prettier/prettier) from 3.4.1 to 3.4.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.4.1...3.4.2)

---
updated-dependencies:
- dependency-name: prettier
  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-05 14:57:52 +02:00
480f305f29 ci: bump actions/attest-build-provenance from 1 to 2 (#12271)
Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 1 to 2.
- [Release notes](https://github.com/actions/attest-build-provenance/releases)
- [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md)
- [Commits](https://github.com/actions/attest-build-provenance/compare/v1...v2)

---
updated-dependencies:
- dependency-name: actions/attest-build-provenance
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 14:57:28 +02:00
90cc36fdee core: bump golang.org/x/sync from 0.9.0 to 0.10.0 (#12272)
Bumps [golang.org/x/sync](https://github.com/golang/sync) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/golang/sync/compare/v0.9.0...v0.10.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sync
  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-05 14:54:52 +02:00
cf5b951a8c core: bump django from 5.0.9 to 5.0.10 (#12273)
Bumps [django](https://github.com/django/django) from 5.0.9 to 5.0.10.
- [Commits](https://github.com/django/django/compare/5.0.9...5.0.10)

---
updated-dependencies:
- dependency-name: django
  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-05 14:54:39 +02:00
9a9d8e4cf7 core: bump webauthn from 2.3.0 to 2.4.0 (#12274)
Bumps [webauthn](https://github.com/duo-labs/py_webauthn) from 2.3.0 to 2.4.0.
- [Release notes](https://github.com/duo-labs/py_webauthn/releases)
- [Changelog](https://github.com/duo-labs/py_webauthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/duo-labs/py_webauthn/compare/v2.3.0...v2.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-05 14:53:20 +02:00
695de453ec website/integrations: add The Lounge (#11971)
* Add The Lounge Integration guide

Signed-off-by: Michael Brünen <34708235+OddMagnet@users.noreply.github.com>

* Add The Lounge to sidebar, fix Note section formatting

* Fix folder name (1/2)

* Fix folder name (2/2)

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Michael Brünen <34708235+OddMagnet@users.noreply.github.com>

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

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Michael Brünen <34708235+OddMagnet@users.noreply.github.com>

* Fix cutoff sentence

Signed-off-by: Michael Brünen <34708235+OddMagnet@users.noreply.github.com>

* Linter fixes

---------

Signed-off-by: Michael Brünen <34708235+OddMagnet@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-12-04 13:21:26 -06:00
d3a581b614 core: bump python-kadmin-rs from 0.3.0 to 0.4.0 (#12257) 2024-12-04 10:38:36 +02:00
334 changed files with 18885 additions and 8671 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,7 @@ runs:
run: |
export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/docker-compose.yml up -d
poetry install
poetry install --sync
cd web && npm ci
- name: Generate config
shell: poetry run python {0}

View File

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

View File

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

View File

@ -36,8 +36,11 @@ jobs:
poetry run make aws-cfn
git diff --exit-code
ci-aws-cfn-mark:
if: always()
needs:
- check-changes-applied
runs-on: ubuntu-latest
steps:
- run: echo mark
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

@ -134,7 +134,7 @@ jobs:
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.10.0
uses: helm/kind-action@v1.11.0
- name: run integration
run: |
poetry run coverage run manage.py test tests/integration
@ -209,6 +209,7 @@ jobs:
file: unittest.xml
token: ${{ secrets.CODECOV_TOKEN }}
ci-core-mark:
if: always()
needs:
- lint
- test-migrations
@ -218,70 +219,22 @@ jobs:
- test-e2e
runs-on: ubuntu-latest
steps:
- run: echo mark
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
build:
strategy:
fail-fast: false
matrix:
arch:
- amd64
- arm64
needs: ci-core-mark
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
attestations: write
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/dev-server
image-arch: ${{ matrix.arch }}
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: generate ts client
run: make gen-client-ts
- name: Build Docker Image
uses: docker/build-push-action@v6
id: push
with:
context: .
secrets: |
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
tags: ${{ steps.ev.outputs.imageTags }}
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max' || '' }}
platforms: linux/${{ matrix.arch }}
- uses: actions/attest-build-provenance@v1
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
needs: ci-core-mark
uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit
with:
image_name: ghcr.io/goauthentik/dev-server
release: false
pr-comment:
needs:
- build

View File

@ -49,12 +49,15 @@ jobs:
run: |
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./...
ci-outpost-mark:
if: always()
needs:
- lint-golint
- test-unittest
runs-on: ubuntu-latest
steps:
- run: echo mark
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
build-container:
timeout-minutes: 120
needs:
@ -69,7 +72,7 @@ jobs:
- rac
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
# Needed to upload container images to ghcr.io
packages: write
# Needed for attestation
id-token: write
@ -111,7 +114,7 @@ jobs:
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
- uses: actions/attest-build-provenance@v1
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:

View File

@ -61,12 +61,15 @@ jobs:
working-directory: web/
run: npm run build
ci-web-mark:
if: always()
needs:
- build
- lint
runs-on: ubuntu-latest
steps:
- run: echo mark
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
test:
needs:
- ci-web-mark

View File

@ -62,10 +62,13 @@ jobs:
working-directory: website/
run: npm run ${{ matrix.job }}
ci-website-mark:
if: always()
needs:
- lint
- test
- build
runs-on: ubuntu-latest
steps:
- run: echo mark
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

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

View File

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

View File

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

View File

@ -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

@ -19,10 +19,18 @@ Dockerfile @goauthentik/infrastructure
*Dockerfile @goauthentik/infrastructure
.dockerignore @goauthentik/infrastructure
docker-compose.yml @goauthentik/infrastructure
Makefile @goauthentik/infrastructure
.editorconfig @goauthentik/infrastructure
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
# Security
website/docs/security/ @goauthentik/security
SECURITY.md @goauthentik/security @goauthentik/docs
website/docs/security/ @goauthentik/security @goauthentik/docs

View File

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

View File

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

View File

@ -2,7 +2,7 @@ authentik takes security very seriously. We follow the rules of [responsible di
## Independent audits and pentests
In May/June of 2023 [Cure53](https://cure53.de) conducted an audit and pentest. The [results](https://cure53.de/pentest-report_authentik.pdf) are published on the [Cure53 website](https://cure53.de/#publications-2023). For more details about authentik's response to the findings of the audit refer to [2023-06 Cure53 Code audit](https://goauthentik.io/docs/security/2023-06-cure53).
We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specfic audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).
## What authentik classifies as a CVE
@ -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.4"
__version__ = "2024.12.5"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

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

@ -25,5 +25,7 @@ class BrandMiddleware:
locale = brand.default_locale
if locale != "":
locale_to_set = locale
with override(locale_to_set):
return self.get_response(request)
if locale_to_set:
with override(locale_to_set):
return self.get_response(request)
return self.get_response(request)

View File

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

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

View File

@ -21,6 +21,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
from authentik.core.models import GroupSourceConnection, Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer
from authentik.lib.api import MultipleFieldLookupMixin
from authentik.lib.utils.file import (
FilePathSerializer,
FileUploadSerializer,
@ -75,6 +76,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
class SourceViewSet(
MultipleFieldLookupMixin,
TypesMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
@ -87,6 +89,7 @@ class SourceViewSet(
queryset = Source.objects.none()
serializer_class = SourceSerializer
lookup_field = "slug"
lookup_fields = ["slug", "pbm_uuid"]
search_fields = ["slug", "name"]
filterset_fields = ["slug", "name", "managed"]
@ -159,9 +162,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 +172,10 @@ class UserSourceConnectionSerializer(SourceSerializer):
"pk",
"user",
"source",
"source_obj",
"created",
]
extra_kwargs = {
"user": {"read_only": True},
"created": {"read_only": True},
}
@ -197,9 +200,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 +210,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

@ -1,13 +1,14 @@
"""User API Views"""
from datetime import timedelta
from importlib import import_module
from json import loads
from typing import Any
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.models import Permission
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
from django.contrib.sessions.backends.base import SessionBase
from django.db.models.functions import ExtractHour
from django.db.transaction import atomic
from django.db.utils import IntegrityError
@ -91,6 +92,7 @@ from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
class UserGroupSerializer(ModelSerializer):
@ -767,7 +769,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if not instance.is_active:
sessions = AuthenticatedSession.objects.filter(user=instance)
session_ids = sessions.values_list("session_key", flat=True)
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
for session in session_ids:
SessionStore(session).delete()
sessions.delete()
LOGGER.debug("Deleted user's sessions", user=instance.username)
return response

View File

@ -42,8 +42,10 @@ class ImpersonateMiddleware:
# Ensure that the user is active, otherwise nothing will work
request.user.is_active = True
with override(locale_to_set):
return self.get_response(request)
if locale_to_set:
with override(locale_to_set):
return self.get_response(request)
return self.get_response(request)
class RequestIDMiddleware:

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

@ -1,7 +1,10 @@
"""authentik core signals"""
from importlib import import_module
from django.conf import settings
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.contrib.sessions.backends.base import SessionBase
from django.core.cache import cache
from django.core.signals import Signal
from django.db.models import Model
@ -25,6 +28,7 @@ password_changed = Signal()
login_failed = Signal()
LOGGER = get_logger()
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
@receiver(post_save, sender=Application)
@ -60,8 +64,7 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
@receiver(pre_delete, sender=AuthenticatedSession)
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
"""Delete session when authenticated session is deleted"""
cache_key = f"{KEY_PREFIX}{instance.session_key}"
cache.delete(cache_key)
SessionStore(instance.session_key).delete()
@receiver(pre_save)

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(
@ -265,12 +259,7 @@ class SourceFlowManager:
if stages:
for stage in stages:
plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=flow.slug,
)
return plan.to_redirect(self.request, flow)
def handle_auth(
self,

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

@ -17,10 +17,8 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import (
SESSION_KEY_APPLICATION_PRE,
SESSION_KEY_PLAN,
ToDefaultFlow,
)
from authentik.lib.utils.urls import redirect_with_qs
from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_PERMISSIONS,
@ -58,8 +56,7 @@ class RedirectToAppLaunch(View):
except FlowNonApplicableException:
raise Http404 from None
plan.insert_stage(in_memory_stage(RedirectToAppStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
return plan.to_redirect(request, flow)
class RedirectToAppStage(ChallengeStageView):

View File

@ -6,6 +6,7 @@ from django.http import HttpRequest, HttpResponse, JsonResponse
from django.urls import resolve
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.api.users import UserViewSet
from authentik.enterprise.api import LicenseViewSet
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import LicenseUsageStatus
@ -59,6 +60,9 @@ class EnterpriseMiddleware:
# Flow executor is mounted as an API path but explicitly allowed
if request.resolver_match._func_path == class_to_path(FlowExecutorView):
return True
# Always allow making changes to users, even in case the license has ben exceeded
if request.resolver_match._func_path == class_to_path(UserViewSet):
return True
# Only apply these restrictions to the API
if "authentik_api" not in request.resolver_match.app_names:
return True

View File

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

View File

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

View File

@ -18,9 +18,7 @@ from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import RedirectStage
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.engine import PolicyEngine
@ -56,12 +54,7 @@ class RACStartView(EnterprisePolicyAccessView):
provider=self.provider,
)
)
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=self.provider.authorization_flow.slug,
)
return plan.to_redirect(request, self.provider.authorization_flow)
class RACInterface(InterfaceView):

View File

@ -4,7 +4,9 @@ from typing import Any
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.clickjacking import xframe_options_sameorigin
from googleapiclient.discovery import build
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
@ -26,6 +28,7 @@ HEADER_ACCESS_CHALLENGE_RESPONSE = "X-Verified-Access-Challenge-Response"
DEVICE_TRUST_VERIFIED_ACCESS = "VerifiedAccess"
@method_decorator(xframe_options_sameorigin, name="dispatch")
class GoogleChromeDeviceTrustConnector(View):
"""Google Chrome Device-trust connector based endpoint authenticator"""

View File

@ -215,3 +215,49 @@ class TestReadOnly(FlowTestCase):
{"detail": "Request denied due to expired/invalid license.", "code": "denied_license"},
)
self.assertEqual(response.status_code, 400)
@patch(
"authentik.enterprise.license.LicenseKey.validate",
MagicMock(
return_value=LicenseKey(
aud="",
exp=expiry_valid,
name=generate_id(),
internal_users=100,
external_users=100,
)
),
)
@patch(
"authentik.enterprise.license.LicenseKey.get_internal_user_count",
MagicMock(return_value=1000),
)
@patch(
"authentik.enterprise.license.LicenseKey.get_external_user_count",
MagicMock(return_value=1000),
)
@patch(
"authentik.enterprise.license.LicenseKey.record_usage",
MagicMock(),
)
def test_manage_users(self):
"""Test that managing users is still possible"""
License.objects.create(key=generate_id())
usage = LicenseUsage.objects.create(
internal_user_count=100,
external_user_count=100,
status=LicenseUsageStatus.VALID,
)
usage.record_date = now() - timedelta(weeks=THRESHOLD_READ_ONLY_WEEKS + 1)
usage.save(update_fields=["record_date"])
admin = create_test_admin_user()
self.client.force_login(admin)
# Reading is always allowed
response = self.client.get(reverse("authentik_api:user-list"))
self.assertEqual(response.status_code, 200)
# Writing should also be allowed
response = self.client.patch(reverse("authentik_api:user-detail", kwargs={"pk": admin.pk}))
self.assertEqual(response.status_code, 200)

View File

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

View File

@ -40,6 +40,7 @@ class Migration(migrations.Migration):
("require_authenticated", "Require Authenticated"),
("require_unauthenticated", "Require Unauthenticated"),
("require_superuser", "Require Superuser"),
("require_redirect", "Require Redirect"),
("require_outpost", "Require Outpost"),
],
default="none",

View File

@ -33,6 +33,7 @@ class FlowAuthenticationRequirement(models.TextChoices):
REQUIRE_AUTHENTICATED = "require_authenticated"
REQUIRE_UNAUTHENTICATED = "require_unauthenticated"
REQUIRE_SUPERUSER = "require_superuser"
REQUIRE_REDIRECT = "require_redirect"
REQUIRE_OUTPOST = "require_outpost"

View File

@ -1,10 +1,10 @@
"""Flows Planner"""
from dataclasses import dataclass, field
from typing import Any
from typing import TYPE_CHECKING, Any
from django.core.cache import cache
from django.http import HttpRequest
from django.http import HttpRequest, HttpResponse
from sentry_sdk import start_span
from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger
@ -23,10 +23,15 @@ from authentik.flows.models import (
in_memory_stage,
)
from authentik.lib.config import CONFIG
from authentik.lib.utils.urls import redirect_with_qs
from authentik.outposts.models import Outpost
from authentik.policies.engine import PolicyEngine
from authentik.root.middleware import ClientIPMiddleware
if TYPE_CHECKING:
from authentik.flows.stage import StageView
LOGGER = get_logger()
PLAN_CONTEXT_PENDING_USER = "pending_user"
PLAN_CONTEXT_SSO = "is_sso"
@ -37,6 +42,8 @@ PLAN_CONTEXT_OUTPOST = "outpost"
# Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan
# was restored.
PLAN_CONTEXT_IS_RESTORED = "is_restored"
PLAN_CONTEXT_IS_REDIRECTED = "is_redirected"
PLAN_CONTEXT_REDIRECT_STAGE_TARGET = "redirect_stage_target"
CACHE_TIMEOUT = CONFIG.get_int("cache.timeout_flows")
CACHE_PREFIX = "goauthentik.io/flows/planner/"
@ -102,6 +109,8 @@ class FlowPlan:
def pop(self):
"""Pop next pending stage from bottom of list"""
if not self.markers and not self.bindings:
return
self.markers.pop(0)
self.bindings.pop(0)
@ -110,6 +119,59 @@ class FlowPlan:
"""Check if there are any stages left in this plan"""
return len(self.markers) + len(self.bindings) > 0
def requires_flow_executor(
self,
allowed_silent_types: list["StageView"] | None = None,
):
# Check if we actually need to show the Flow executor, or if we can jump straight to the end
found_unskippable = True
if allowed_silent_types:
LOGGER.debug("Checking if we can skip the flow executor...")
# Policies applied to the flow have already been evaluated, so we're checking for stages
# allow-listed or bindings that require a policy re-eval
found_unskippable = False
for binding, marker in zip(self.bindings, self.markers, strict=True):
if binding.stage.view not in allowed_silent_types:
found_unskippable = True
if marker and isinstance(marker, ReevaluateMarker):
found_unskippable = True
LOGGER.debug("Required flow executor status", status=found_unskippable)
return found_unskippable
def to_redirect(
self,
request: HttpRequest,
flow: Flow,
allowed_silent_types: list["StageView"] | None = None,
) -> HttpResponse:
"""Redirect to the flow executor for this flow plan"""
from authentik.flows.views.executor import (
SESSION_KEY_PLAN,
FlowExecutorView,
)
request.session[SESSION_KEY_PLAN] = self
requires_flow_executor = self.requires_flow_executor(allowed_silent_types)
if not requires_flow_executor:
# No unskippable stages found, so we can directly return the response of the last stage
final_stage: type[StageView] = self.bindings[-1].stage.view
temp_exec = FlowExecutorView(flow=flow, request=request, plan=self)
temp_exec.current_stage = self.bindings[-1].stage
temp_exec.current_stage_view = final_stage
temp_exec.setup(request, flow.slug)
stage = final_stage(request=request, executor=temp_exec)
response = stage.dispatch(request)
# Ensure we clean the flow state we have in the session before we redirect away
temp_exec.stage_ok()
return response
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=flow.slug,
)
class FlowPlanner:
"""Execute all policies to plan out a flat list of all Stages
@ -128,7 +190,7 @@ class FlowPlanner:
self.flow = flow
self._logger = get_logger().bind(flow_slug=flow.slug)
def _check_authentication(self, request: HttpRequest):
def _check_authentication(self, request: HttpRequest, context: dict[str, Any]):
"""Check the flow's authentication level is matched by `request`"""
if (
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED
@ -145,6 +207,11 @@ class FlowPlanner:
and not request.user.is_superuser
):
raise FlowNonApplicableException()
if (
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_REDIRECT
and context.get(PLAN_CONTEXT_IS_REDIRECTED) is None
):
raise FlowNonApplicableException()
outpost_user = ClientIPMiddleware.get_outpost_user(request)
if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST:
if not outpost_user:
@ -176,18 +243,13 @@ class FlowPlanner:
)
context = default_context or {}
# Bit of a workaround here, if there is a pending user set in the default context
# we use that user for our cache key
# to make sure they don't get the generic response
# we use that user for our cache key to make sure they don't get the generic response
if context and PLAN_CONTEXT_PENDING_USER in context:
user = context[PLAN_CONTEXT_PENDING_USER]
else:
user = request.user
# We only need to check the flow authentication if it's planned without a user
# in the context, as a user in the context can only be set via the explicit code API
# or if a flow is restarted due to `invalid_response_action` being set to
# `restart_with_context`, which can only happen if the user was already authorized
# to use the flow
context.update(self._check_authentication(request))
context.update(self._check_authentication(request, context))
# First off, check the flow's direct policy bindings
# to make sure the user even has access to the flow
engine = PolicyEngine(self.flow, user, request)

View File

@ -2,6 +2,7 @@
from typing import TYPE_CHECKING
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.http import HttpRequest
from django.http.request import QueryDict
@ -92,7 +93,11 @@ class ChallengeStageView(StageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Return a challenge for the frontend to solve"""
challenge = self._get_challenge(*args, **kwargs)
try:
challenge = self._get_challenge(*args, **kwargs)
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
if not challenge.is_valid():
self.logger.warning(
"f(ch): Invalid challenge",
@ -168,11 +173,7 @@ class ChallengeStageView(StageView):
stage_type=self.__class__.__name__, method="get_challenge"
).time(),
):
try:
challenge = self.get_challenge(*args, **kwargs)
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
challenge = self.get_challenge(*args, **kwargs)
with start_span(
op="authentik.flow.stage._get_challenge",
name=self.__class__.__name__,
@ -224,6 +225,14 @@ class ChallengeStageView(StageView):
full_errors[field].append(field_error)
challenge_response.initial_data["response_errors"] = full_errors
if not challenge_response.is_valid():
if settings.TEST:
raise StageInvalidException(
(
f"Invalid challenge response: \n\t{challenge_response.errors}"
f"\n\nValidated data:\n\t {challenge_response.data}"
f"\n\nInitial data:\n\t {challenge_response.initial_data}"
),
)
self.logger.error(
"f(ch): invalid challenge response",
errors=challenge_response.errors,

View File

@ -5,6 +5,8 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.cache import cache
from django.http import HttpRequest
from django.shortcuts import redirect
from django.test import RequestFactory, TestCase
from django.urls import reverse
from guardian.shortcuts import get_anonymous_user
@ -14,8 +16,19 @@ from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation, FlowStageBinding
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.flows.models import (
FlowAuthenticationRequirement,
FlowDesignation,
FlowStageBinding,
in_memory_stage,
)
from authentik.flows.planner import (
PLAN_CONTEXT_IS_REDIRECTED,
PLAN_CONTEXT_PENDING_USER,
FlowPlanner,
cache_key,
)
from authentik.flows.stage import StageView
from authentik.lib.tests.utils import dummy_get_response
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost
@ -73,6 +86,24 @@ class TestFlowPlanner(TestCase):
planner.allow_empty_flows = True
planner.plan(request)
def test_authentication_redirect_required(self):
"""Test flow authentication (redirect required)"""
flow = create_test_flow()
flow.authentication = FlowAuthenticationRequirement.REQUIRE_REDIRECT
request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
request.user = AnonymousUser()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
with self.assertRaises(FlowNonApplicableException):
planner.plan(request)
context = {}
context[PLAN_CONTEXT_IS_REDIRECTED] = create_test_flow()
planner.plan(request, context)
@reconcile_app("authentik_outposts")
def test_authentication_outpost(self):
"""Test flow authentication (outpost)"""
@ -211,3 +242,99 @@ class TestFlowPlanner(TestCase):
self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
def test_to_redirect(self):
"""Test to_redirect and skipping the flow executor"""
flow = create_test_flow()
flow.authentication = FlowAuthenticationRequirement.NONE
request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
request.user = AnonymousUser()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
plan = planner.plan(request)
self.assertTrue(plan.requires_flow_executor())
self.assertEqual(
plan.to_redirect(request, flow).url,
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug}),
)
def test_to_redirect_skip_simple(self):
"""Test to_redirect and skipping the flow executor"""
flow = create_test_flow()
flow.authentication = FlowAuthenticationRequirement.NONE
request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session.save()
request.user = AnonymousUser()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
plan = planner.plan(request)
class TStageView(StageView):
def dispatch(self, request: HttpRequest, *args, **kwargs):
return redirect("https://authentik.company")
plan.append_stage(in_memory_stage(TStageView))
self.assertFalse(plan.requires_flow_executor(allowed_silent_types=[TStageView]))
self.assertEqual(
plan.to_redirect(request, flow, allowed_silent_types=[TStageView]).url,
"https://authentik.company",
)
def test_to_redirect_skip_stage(self):
"""Test to_redirect and skipping the flow executor
(with a stage bound that cannot be skipped)"""
flow = create_test_flow()
flow.authentication = FlowAuthenticationRequirement.NONE
FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
)
request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
request.user = AnonymousUser()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
plan = planner.plan(request)
class TStageView(StageView):
def dispatch(self, request: HttpRequest, *args, **kwargs):
return redirect("https://authentik.company")
plan.append_stage(in_memory_stage(TStageView))
self.assertTrue(plan.requires_flow_executor(allowed_silent_types=[TStageView]))
def test_to_redirect_skip_policies(self):
"""Test to_redirect and skipping the flow executor
(with a marker on the stage view type that can be skipped)
Note that this is not actually used anywhere in the code, all stages that are dynamically
added are statically added"""
flow = create_test_flow()
flow.authentication = FlowAuthenticationRequirement.NONE
request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
request.user = AnonymousUser()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True
plan = planner.plan(request)
class TStageView(StageView):
def dispatch(self, request: HttpRequest, *args, **kwargs):
return redirect("https://authentik.company")
plan.append_stage(in_memory_stage(TStageView), ReevaluateMarker(None))
self.assertTrue(plan.requires_flow_executor(allowed_silent_types=[TStageView]))

View File

@ -103,7 +103,7 @@ class FlowExecutorView(APIView):
permission_classes = [AllowAny]
flow: Flow
flow: Flow = None
plan: FlowPlan | None = None
current_binding: FlowStageBinding | None = None
@ -114,7 +114,8 @@ class FlowExecutorView(APIView):
def setup(self, request: HttpRequest, flow_slug: str):
super().setup(request, flow_slug=flow_slug)
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
if not self.flow:
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
self._logger = get_logger().bind(flow_slug=flow_slug)
set_tag("authentik.flow", self.flow.slug)
@ -171,7 +172,8 @@ class FlowExecutorView(APIView):
# Existing plan is deleted from session and instance
self.plan = None
self.cancel()
self._logger.debug("f(exec): Continuing existing plan")
else:
self._logger.debug("f(exec): Continuing existing plan")
# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params
@ -597,9 +599,4 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
raise Http404 from None
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=stage.configure_flow.slug,
)
return plan.to_redirect(request, stage.configure_flow)

View File

@ -78,7 +78,9 @@ class FlowInspectorView(APIView):
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
if settings.DEBUG:
return
if request.user.has_perm("authentik_flow.inspect_flow", self.flow):
if request.user.has_perm("authentik_flows.inspect_flow") or request.user.has_perm(
"authentik_flows.inspect_flow", self.flow
):
return
raise Http404

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

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

View File

@ -5,6 +5,7 @@ import json
import os
from collections.abc import Mapping
from contextlib import contextmanager
from copy import deepcopy
from dataclasses import dataclass, field
from enum import Enum
from glob import glob
@ -279,9 +280,25 @@ class ConfigLoader:
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
return default
def get_optional_int(self, path: str, default=None) -> int | None:
"""Wrapper for get that converts value into int or None if set"""
value = self.get(path, default)
if value is UNSET:
return default
try:
return int(value)
except (ValueError, TypeError) as exc:
if value is None or (isinstance(value, str) and value.lower() == "null"):
return None
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
return default
def get_bool(self, path: str, default=False) -> bool:
"""Wrapper for get that converts value into boolean"""
return str(self.get(path, default)).lower() == "true"
value = self.get(path, UNSET)
if value is UNSET:
return default
return str(self.get(path)).lower() == "true"
def get_keys(self, path: str, sep=".") -> list[str]:
"""List attribute keys by using yaml path"""
@ -336,6 +353,71 @@ def redis_url(db: int) -> str:
return _redis_url
def django_db_config(config: ConfigLoader | None = None) -> dict:
if not config:
config = CONFIG
db = {
"default": {
"ENGINE": "authentik.root.db",
"HOST": config.get("postgresql.host"),
"NAME": config.get("postgresql.name"),
"USER": config.get("postgresql.user"),
"PASSWORD": config.get("postgresql.password"),
"PORT": config.get("postgresql.port"),
"OPTIONS": {
"sslmode": config.get("postgresql.sslmode"),
"sslrootcert": config.get("postgresql.sslrootcert"),
"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"])
for setting, current_value in db["default"].items():
if isinstance(current_value, dict):
continue
override = config.get(
f"postgresql.read_replicas.{replica}.{setting.lower()}", default=UNSET
)
if override is not UNSET:
_database[setting] = override
for setting in db["default"]["OPTIONS"].keys():
override = config.get(
f"postgresql.read_replicas.{replica}.{setting.lower()}", default=UNSET
)
if override is not UNSET:
_database["OPTIONS"][setting] = override
db[f"replica_{replica}"] = _database
return db
if __name__ == "__main__":
if len(argv) < 2: # noqa: PLR2004
print(dumps(CONFIG.raw, indent=4, cls=AttrEncoder))

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

@ -9,7 +9,14 @@ from unittest import mock
from django.conf import ImproperlyConfigured
from django.test import TestCase
from authentik.lib.config import ENV_PREFIX, UNSET, Attr, AttrEncoder, ConfigLoader
from authentik.lib.config import (
ENV_PREFIX,
UNSET,
Attr,
AttrEncoder,
ConfigLoader,
django_db_config,
)
class TestConfig(TestCase):
@ -175,3 +182,283 @@ class TestConfig(TestCase):
config = ConfigLoader()
config.set("foo.bar", "baz")
self.assertEqual(list(config.get_keys("foo")), ["bar"])
def test_db_default(self):
"""Test default DB Config"""
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")
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"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",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
}
},
)
def test_db_read_replicas(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")
# Read replica
config.set("postgresql.read_replicas.0.host", "bar")
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"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",
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
"DISABLE_SERVER_SIDE_CURSORS": False,
},
"replica_0": {
"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",
"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",
},
},
)
def test_db_read_replicas_pgpool(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_pgpool", True)
# Read replica
config.set("postgresql.read_replicas.0.host", "bar")
# This isn't supported
config.set("postgresql.read_replicas.0.use_pgpool", False)
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"DISABLE_SERVER_SIDE_CURSORS": True,
"CONN_MAX_AGE": 0,
"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": 0,
"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",
},
},
)
def test_db_read_replicas_diff_ssl(self):
"""Test read replicas (with different SSL Settings)"""
"""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")
# Read replica
config.set("postgresql.read_replicas.0.host", "bar")
config.set("postgresql.read_replicas.0.sslcert", "bar")
conf = django_db_config(config)
self.assertEqual(
conf,
{
"default": {
"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",
"DISABLE_SERVER_SIDE_CURSORS": False,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
},
"replica_0": {
"ENGINE": "authentik.root.db",
"HOST": "bar",
"NAME": "foo",
"OPTIONS": {
"sslcert": "bar",
"sslkey": "foo",
"sslmode": "foo",
"sslrootcert": "foo",
},
"PASSWORD": "foo",
"PORT": "foo",
"TEST": {"NAME": "foo"},
"USER": "foo",
"DISABLE_SERVER_SIDE_CURSORS": False,
"CONN_MAX_AGE": 0,
"CONN_HEALTH_CHECKS": False,
},
},
)

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

@ -0,0 +1,38 @@
# Generated by Django 5.0.10 on 2024-12-12 17:16
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0040_provider_invalidation_flow"),
(
"authentik_providers_oauth2",
"0025_rename_jwks_sources_oauth2provider_jwt_federation_sources_and_more",
),
]
operations = [
migrations.AlterField(
model_name="accesstoken",
name="session",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="authentik_core.authenticatedsession",
),
),
migrations.AlterField(
model_name="authorizationcode",
name="session",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="authentik_core.authenticatedsession",
),
),
]

View File

@ -396,7 +396,7 @@ class BaseGrantModel(models.Model):
_scope = models.TextField(default="", verbose_name=_("Scopes"))
auth_time = models.DateTimeField(verbose_name="Authentication time")
session = models.ForeignKey(
AuthenticatedSession, null=True, on_delete=models.SET_DEFAULT, default=None
AuthenticatedSession, null=True, on_delete=models.CASCADE, default=None
)
class Meta:
@ -497,6 +497,11 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
token = models.TextField(default=generate_client_secret)
_id_token = models.TextField(verbose_name=_("ID Token"))
# Shadow the `session` field from `BaseGrantModel` as we want refresh tokens to persist even
# when the session is terminated.
session = models.ForeignKey(
AuthenticatedSession, null=True, on_delete=models.SET_DEFAULT, default=None
)
class Meta:
indexes = [

View File

@ -311,7 +311,7 @@ class TestAuthorize(OAuthTestCase):
user = create_test_admin_user()
self.client.force_login(user)
# Step 1, initiate params and get redirect to flow
self.client.get(
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
@ -320,16 +320,10 @@ class TestAuthorize(OAuthTestCase):
"redirect_uri": "foo://localhost",
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"to": f"foo://localhost?code={code.code}&state={state}",
},
self.assertEqual(
response.url,
f"foo://localhost?code={code.code}&state={state}",
)
self.assertAlmostEqual(
code.expires.timestamp() - now().timestamp(),
@ -377,7 +371,7 @@ class TestAuthorize(OAuthTestCase):
),
):
# Step 1, initiate params and get redirect to flow
self.client.get(
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "id_token",
@ -388,22 +382,16 @@ class TestAuthorize(OAuthTestCase):
"nonce": generate_id(),
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
token: AccessToken = AccessToken.objects.filter(user=user).first()
expires = timedelta_from_string(provider.access_token_validity).total_seconds()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"to": (
f"http://localhost#access_token={token.token}"
f"&id_token={provider.encode(token.id_token.to_dict())}"
f"&token_type={TOKEN_TYPE}"
f"&expires_in={int(expires)}&state={state}"
),
},
self.assertEqual(
response.url,
(
f"http://localhost#access_token={token.token}"
f"&id_token={provider.encode(token.id_token.to_dict())}"
f"&token_type={TOKEN_TYPE}"
f"&expires_in={int(expires)}&state={state}"
),
)
jwt = self.validate_jwt(token, provider)
self.assertEqual(jwt["amr"], ["pwd"])
@ -455,7 +443,7 @@ class TestAuthorize(OAuthTestCase):
),
):
# Step 1, initiate params and get redirect to flow
self.client.get(
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "id_token",
@ -466,10 +454,7 @@ class TestAuthorize(OAuthTestCase):
"nonce": generate_id(),
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_code, 302)
token: AccessToken = AccessToken.objects.filter(user=user).first()
expires = timedelta_from_string(provider.access_token_validity).total_seconds()
jwt = self.validate_jwe(token, provider)
@ -506,7 +491,7 @@ class TestAuthorize(OAuthTestCase):
),
):
# Step 1, initiate params and get redirect to flow
self.client.get(
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
@ -518,16 +503,10 @@ class TestAuthorize(OAuthTestCase):
"nonce": generate_id(),
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"to": (f"http://localhost#code={code.code}" f"&state={state}"),
},
self.assertEqual(
response.url,
f"http://localhost#code={code.code}&state={state}",
)
self.assertAlmostEqual(
code.expires.timestamp() - now().timestamp(),

View File

@ -45,7 +45,7 @@ class TestTokenPKCE(OAuthTestCase):
challenge = generate_id()
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
# Step 1, initiate params and get redirect to flow
self.client.get(
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
@ -56,16 +56,10 @@ class TestTokenPKCE(OAuthTestCase):
"code_challenge_method": "S256",
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"to": f"foo://localhost?code={code.code}&state={state}",
},
self.assertEqual(
response.url,
f"foo://localhost?code={code.code}&state={state}",
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
@ -107,7 +101,7 @@ class TestTokenPKCE(OAuthTestCase):
self.client.force_login(user)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
# Step 1, initiate params and get redirect to flow
self.client.get(
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
@ -118,16 +112,10 @@ class TestTokenPKCE(OAuthTestCase):
# "code_challenge_method": "S256",
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"to": f"foo://localhost?code={code.code}&state={state}",
},
self.assertEqual(
response.url,
f"foo://localhost?code={code.code}&state={state}",
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
@ -174,7 +162,7 @@ class TestTokenPKCE(OAuthTestCase):
)
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
# Step 1, initiate params and get redirect to flow
self.client.get(
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
@ -185,16 +173,10 @@ class TestTokenPKCE(OAuthTestCase):
"code_challenge_method": "S256",
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"to": f"foo://localhost?code={code.code}&state={state}",
},
self.assertEqual(
response.url,
f"foo://localhost?code={code.code}&state={state}",
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
@ -225,7 +207,7 @@ class TestTokenPKCE(OAuthTestCase):
verifier = generate_id()
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
# Step 1, initiate params and get redirect to flow
self.client.get(
response = self.client.get(
reverse("authentik_providers_oauth2:authorize"),
data={
"response_type": "code",
@ -235,16 +217,10 @@ class TestTokenPKCE(OAuthTestCase):
"code_challenge": verifier,
},
)
response = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
self.assertJSONEqual(
response.content.decode(),
{
"component": "xak-flow-redirect",
"to": f"foo://localhost?code={code.code}&state={state}",
},
self.assertEqual(
response.url,
f"foo://localhost?code={code.code}&state={state}",
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),

View File

@ -27,9 +27,7 @@ from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import StageView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.policies.types import PolicyRequest
from authentik.policies.views import PolicyAccessView, RequestValidationError
@ -454,11 +452,16 @@ class AuthorizationFlowInitView(PolicyAccessView):
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=self.provider.authorization_flow.slug,
return plan.to_redirect(
self.request,
self.provider.authorization_flow,
# We can only skip the flow executor and directly go to the final redirect URL if
# we can submit the data to the RP via URL
allowed_silent_types=(
[OAuthFulfillmentStage]
if self.params.response_mode in [ResponseMode.QUERY, ResponseMode.FRAGMENT]
else []
),
)
@ -496,11 +499,11 @@ class OAuthFulfillmentStage(StageView):
)
challenge.is_valid()
self.executor.stage_ok()
return HttpChallengeResponse(
challenge=challenge,
)
self.executor.stage_ok()
return HttpResponseRedirectScheme(uri, allowed_schemes=[parsed.scheme])
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:

View File

@ -16,7 +16,6 @@ from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.views import PolicyAccessView
from authentik.providers.oauth2.models import DeviceToken
from authentik.providers.oauth2.views.device_finish import (
@ -73,12 +72,7 @@ class CodeValidatorView(PolicyAccessView):
LOGGER.warning("Flow not applicable to user")
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=self.token.provider.authorization_flow.slug,
)
return plan.to_redirect(self.request, self.token.provider.authorization_flow)
class DeviceEntryView(PolicyAccessView):
@ -109,11 +103,7 @@ class DeviceEntryView(PolicyAccessView):
plan.append_stage(in_memory_stage(OAuthDeviceCodeStage))
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=device_flow.slug,
)
return plan.to_redirect(self.request, device_flow)
class OAuthDeviceCodeChallenge(Challenge):

View File

@ -7,8 +7,6 @@ from authentik.core.models import Application
from authentik.flows.models import Flow, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import SessionEndStage
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.views import PolicyAccessView
@ -37,9 +35,4 @@ class EndSessionView(PolicyAccessView):
},
)
plan.insert_stage(in_memory_stage(SessionEndStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=self.flow.slug,
)
return plan.to_redirect(self.request, self.flow)

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

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

View File

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

View File

@ -13,8 +13,6 @@ from authentik.events.models import Event, EventAction
from authentik.flows.models import Flow, in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
from authentik.flows.stage import SessionEndStage
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion
@ -64,12 +62,7 @@ class SAMLSLOView(PolicyAccessView):
},
)
plan.insert_stage(in_memory_stage(SessionEndStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=self.flow.slug,
)
return plan.to_redirect(self.request, self.flow)
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:
"""GET and POST use the same handler, but we can't

View File

@ -13,12 +13,11 @@ from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import in_memory_stage
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
from authentik.lib.utils.urls import redirect_with_qs
from authentik.flows.views.executor import SESSION_KEY_POST
from authentik.lib.views import bad_request_message
from authentik.policies.views import PolicyAccessView
from authentik.providers.saml.exceptions import CannotHandleAssertion
from authentik.providers.saml.models import SAMLProvider
from authentik.providers.saml.models import SAMLBindings, SAMLProvider
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
from authentik.providers.saml.views.flows import (
REQUEST_KEY_RELAY_STATE,
@ -74,11 +73,12 @@ class SAMLSSOView(PolicyAccessView):
except FlowNonApplicableException:
raise Http404 from None
plan.append_stage(in_memory_stage(SAMLFlowFinalView))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=self.provider.authorization_flow.slug,
return plan.to_redirect(
request,
self.provider.authorization_flow,
allowed_silent_types=(
[SAMLFlowFinalView] if self.provider.sp_binding in [SAMLBindings.REDIRECT] else []
),
)
def post(self, request: HttpRequest, application_slug: str) -> HttpResponse:

View File

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

View File

@ -12,7 +12,7 @@ from sentry_sdk import set_tag
from xmlsec import enable_debug_trace
from authentik import __version__
from authentik.lib.config import CONFIG, redis_url
from authentik.lib.config import CONFIG, django_db_config, redis_url
from authentik.lib.logging import get_logger_config, structlog_configure
from authentik.lib.sentry import sentry_init
from authentik.lib.utils.reflection import get_env
@ -40,7 +40,6 @@ LANGUAGE_COOKIE_NAME = "authentik_language"
SESSION_COOKIE_NAME = "authentik_session"
SESSION_COOKIE_DOMAIN = CONFIG.get("cookie_domain", None)
APPEND_SLASH = False
X_FRAME_OPTIONS = "SAMEORIGIN"
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
@ -115,6 +114,7 @@ TENANT_APPS = [
"authentik.stages.invitation",
"authentik.stages.password",
"authentik.stages.prompt",
"authentik.stages.redirect",
"authentik.stages.user_delete",
"authentik.stages.user_login",
"authentik.stages.user_logout",
@ -298,47 +298,7 @@ CHANNEL_LAYERS = {
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
ORIGINAL_BACKEND = "django_prometheus.db.backends.postgresql"
DATABASES = {
"default": {
"ENGINE": "authentik.root.db",
"HOST": CONFIG.get("postgresql.host"),
"NAME": CONFIG.get("postgresql.name"),
"USER": CONFIG.get("postgresql.user"),
"PASSWORD": CONFIG.get("postgresql.password"),
"PORT": CONFIG.get("postgresql.port"),
"OPTIONS": {
"sslmode": CONFIG.get("postgresql.sslmode"),
"sslrootcert": CONFIG.get("postgresql.sslrootcert"),
"sslcert": CONFIG.get("postgresql.sslcert"),
"sslkey": CONFIG.get("postgresql.sslkey"),
},
"TEST": {
"NAME": CONFIG.get("postgresql.test.name"),
},
}
}
if CONFIG.get_bool("postgresql.use_pgpool", False):
DATABASES["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
if CONFIG.get_bool("postgresql.use_pgbouncer", False):
# https://docs.djangoproject.com/en/4.0/ref/databases/#transaction-pooling-server-side-cursors
DATABASES["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
# https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections
DATABASES["default"]["CONN_MAX_AGE"] = None # persistent
for replica in CONFIG.get_keys("postgresql.read_replicas"):
_database = DATABASES["default"].copy()
for setting in DATABASES["default"].keys():
default = object()
if setting in ("TEST",):
continue
override = CONFIG.get(
f"postgresql.read_replicas.{replica}.{setting.lower()}", default=default
)
if override is not default:
_database[setting] = override
DATABASES[f"replica_{replica}"] = _database
DATABASES = django_db_config()
DATABASE_ROUTERS = (
"authentik.tenants.db.FailoverRouter",

View File

@ -13,6 +13,7 @@ from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.events.api.tasks import SystemTaskSerializer
from authentik.lib.api import MultipleFieldLookupMixin
from authentik.sources.kerberos.models import KerberosSource
from authentik.sources.kerberos.tasks import CACHE_KEY_STATUS
@ -32,6 +33,7 @@ class KerberosSourceSerializer(SourceSerializer):
"group_matching_mode",
"realm",
"krb5_conf",
"kadmin_type",
"sync_users",
"sync_users_password",
"sync_principal",
@ -58,17 +60,19 @@ class KerberosSyncStatusSerializer(PassiveSerializer):
tasks = SystemTaskSerializer(many=True, read_only=True)
class KerberosSourceViewSet(UsedByMixin, ModelViewSet):
class KerberosSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
"""Kerberos Source Viewset"""
queryset = KerberosSource.objects.all()
serializer_class = KerberosSourceSerializer
lookup_field = "slug"
lookup_fields = ["slug", "pbm_uuid"]
filterset_fields = [
"name",
"slug",
"enabled",
"realm",
"kadmin_type",
"sync_users",
"sync_users_password",
"sync_principal",

View File

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

View File

@ -0,0 +1,22 @@
# Generated by Django 5.0.10 on 2024-12-06 19:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_kerberos", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="kerberossource",
name="kadmin_type",
field=models.TextField(
choices=[("MIT", "Mit"), ("Heimdal", "Heimdal"), ("other", "Other")],
default="other",
help_text="KAdmin server type",
),
),
]

View File

@ -13,7 +13,7 @@ from django.http import HttpRequest
from django.shortcuts import reverse
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from kadmin import KAdmin
from kadmin import KAdmin, KAdminApiVersion
from kadmin.exceptions import PyKAdminException
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
@ -36,6 +36,12 @@ LOGGER = get_logger()
_kadmin_connections: dict[str, Any] = {}
class KAdminType(models.TextChoices):
MIT = "MIT"
HEIMDAL = "Heimdal"
OTHER = "other"
class KerberosSource(Source):
"""Federate Kerberos realm with authentik"""
@ -44,6 +50,9 @@ class KerberosSource(Source):
blank=True,
help_text=_("Custom krb5.conf to use. Uses the system one by default"),
)
kadmin_type = models.TextField(
choices=KAdminType.choices, default=KAdminType.OTHER, help_text=_("KAdmin server type")
)
sync_users = models.BooleanField(
default=False, help_text=_("Sync users from Kerberos into authentik"), db_index=True
@ -199,6 +208,14 @@ class KerberosSource(Source):
return str(conf_path)
def _kadmin_init(self) -> KAdmin | None:
api_version = None
match self.kadmin_type:
case KAdminType.MIT:
api_version = KAdminApiVersion.Version4
case KAdminType.HEIMDAL:
api_version = KAdminApiVersion.Version2
case KAdminType.OTHER:
api_version = KAdminApiVersion.Version2
# kadmin doesn't use a ccache for its connection
# as such, we don't need to create a separate ccache for each source
if not self.sync_principal:
@ -207,6 +224,7 @@ class KerberosSource(Source):
return KAdmin.with_password(
self.sync_principal,
self.sync_password,
api_version=api_version,
)
if self.sync_keytab:
keytab = self.sync_keytab
@ -218,11 +236,13 @@ class KerberosSource(Source):
return KAdmin.with_keytab(
self.sync_principal,
keytab,
api_version=api_version,
)
if self.sync_ccache:
return KAdmin.with_ccache(
self.sync_principal,
self.sync_ccache,
api_version=api_version,
)
return None

View File

@ -45,8 +45,10 @@ def kerberos_sync_password(sender, user: User, password: str, **_):
continue
with Krb5ConfContext(source):
try:
source.connection().getprinc(user_source_connection.identifier).change_password(
password
kadm = source.connection()
kadm.get_principal(user_source_connection.identifier).change_password(
kadm,
password,
)
except PyKAdminException as exc:
LOGGER.warning("failed to set Kerberos password", exc=exc, source=source)

View File

@ -43,8 +43,10 @@ class KerberosSync:
self._messages = []
self._logger = get_logger().bind(source=self._source, syncer=self.__class__.__name__)
self.mapper = SourceMapper(self._source)
self.user_manager = self.mapper.get_manager(User, ["principal"])
self.group_manager = self.mapper.get_manager(Group, ["group_id", "principal"])
self.user_manager = self.mapper.get_manager(User, ["principal", "principal_obj"])
self.group_manager = self.mapper.get_manager(
Group, ["group_id", "principal", "principal_obj"]
)
self.matcher = SourceMatcher(
self._source, UserKerberosSourceConnection, GroupKerberosSourceConnection
)
@ -67,12 +69,16 @@ class KerberosSync:
def _handle_principal(self, principal: str) -> bool:
try:
# TODO: handle permission error
principal_obj = self._connection.get_principal(principal)
defaults = self.mapper.build_object_properties(
object_type=User,
manager=self.user_manager,
user=None,
request=None,
principal=principal,
principal_obj=principal_obj,
)
self._logger.debug("Writing user with attributes", **defaults)
if "username" not in defaults:
@ -91,6 +97,7 @@ class KerberosSync:
request=None,
group_id=group_id,
principal=principal,
principal_obj=principal_obj,
)
for group_id in defaults.pop("groups", [])
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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,11 +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, SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.lib.views import bad_request_message
from authentik.providers.saml.utils.encoding import nice64
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
@ -89,12 +89,7 @@ class InitiateView(View):
raise Http404 from None
for stage in stages_to_append:
plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=source.pre_authentication_flow.slug,
)
return plan.to_redirect(self.request, source.pre_authentication_flow)
def get(self, request: HttpRequest, source_slug: str) -> HttpResponse:
"""Replies with an XHTML SSO Request."""
@ -154,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

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

View File

@ -332,7 +332,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
serializer = SelectableStageSerializer(
data={
"pk": stage.pk,
"name": getattr(stage, "friendly_name", stage.name),
"name": getattr(stage, "friendly_name", stage.name) or stage.name,
"verbose_name": str(stage._meta.verbose_name)
.replace("Setup Stage", "")
.strip(),

View File

@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch
from django.test.client import RequestFactory
from django.urls.base import reverse
from django.utils.timezone import now
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
@ -13,6 +14,7 @@ from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id, generate_key
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_static.models import AuthenticatorStaticStage
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage, TOTPDigits
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
@ -76,8 +78,8 @@ class AuthenticatorValidateStageTests(FlowTestCase):
conf_stage = AuthenticatorStaticStage.objects.create(
name=generate_id(),
)
conf_stage2 = AuthenticatorStaticStage.objects.create(
name=generate_id(),
conf_stage2 = AuthenticatorTOTPStage.objects.create(
name=generate_id(), digits=TOTPDigits.SIX
)
stage = AuthenticatorValidateStage.objects.create(
name=generate_id(),
@ -153,10 +155,14 @@ class AuthenticatorValidateStageTests(FlowTestCase):
{
"device_class": "static",
"device_uid": "1",
"challenge": {},
"last_used": now(),
},
{
"device_class": "totp",
"device_uid": "2",
"challenge": {},
"last_used": now(),
},
]
session[SESSION_KEY_PLAN] = plan

View File

@ -141,5 +141,10 @@
"name": "Devolutions",
"icon_dark": "data:image/svg+xml;base64,CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNzJweCIgaGVpZ2h0PSI3MnB4IiB2aWV3Qm94PSIwIDAgNzIgNzIiPgk8ZGVmcz4KICAgICAgICA8ZmlsdGVyIGlkPSJhIiB3aWR0aD0iMjAwJSIgaGVpZ2h0PSIyMDAlIj4KICAgICAgICAgICAgPGZlT2Zmc2V0IHJlc3VsdD0ib2ZmT3V0IiBpbj0iU291cmNlQWxwaGEiIGR5PSIyLjIiLz4KICAgICAgICAgICAgPGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iYmx1ck91dCIgaW49Im9mZk91dCIgc3RkRGV2aWF0aW9uPSIxLjUiLz4KICAgICAgICAgICAgPGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjQgMCIvPgogICAgICAgICAgICA8ZmVNZXJnZT4KICAgICAgICAgICAgICAgIDxmZU1lcmdlTm9kZS8+CiAgICAgICAgICAgICAgICA8ZmVNZXJnZU5vZGUgaW49IlNvdXJjZUdyYXBoaWMiLz4KICAgICAgICAgICAgPC9mZU1lcmdlPgogICAgICAgIDwvZmlsdGVyPgogICAgPC9kZWZzPgo8cGF0aCBmaWxsPSIjZmZmZmZmIiBmaWx0ZXI9InVybCgjYSkiIGQ9Ik0zMS4wNTksNS4zOTVjMTYuODktMi43MjcsMzIuODE3LDguNzcyLDM1LjU0NSwyNS42NjJjMi43MjcsMTYuODktOC43NzIsMzIuODE3LTI1LjY2MiwzNS41NDUKCUMyNC4wNTEsNjkuMzI5LDguMTI0LDU3LjgzLDUuMzk3LDQwLjk0QzIuNjcsMjQuMDQ5LDE0LjE2OSw4LjEyMiwzMS4wNTksNS4zOTV6Ii8+CjxwYXRoIGZpbGw9IiMwMDY4YzMiIGQ9Ik01NS4zNjQsMTcuMjAyYy01LjA0Ny01LjE5Ny0xMS44MDItOC4xMDktMTkuMDItOC4yYy03LjE5MS0wLjA5LTEzLjk4NSwyLjY2OS0xOS4xNDksNy43NjgKCUMxMS45MSwyMS45ODksOSwyOC45NTUsOSwzNi4zOTJsMC4wNDQsMS4yNmMwLDEuMTgxLDAuOTYxLDIuMTQyLDIuMTQyLDIuMTQyczIuMTQxLTAuOTYsMi4xNDItMi4xNGwwLjAxLTAuOTEzCgljMC05Ljk0NSw4LjQ1My0yMC41OTMsMjEuMDM1LTIwLjU5M2MxMy4xMzIsMCwyMS4yNjEsMTAuNzEyLDIxLjI2MSwyMC42MzdjMCw1LjE3My0yLjA2Myw5LjkxOS01LjgwOCwxMy4zNjMKCUM0Ni4xNyw1My41MDksNDEuMjYsNTUuMzYsMzYsNTUuMzZjLTMuMTMsMC02LjIxOS0wLjc1NS04Ljk1OC0yLjE4NmwxOC44Ny04LjY4NmMxLjI2Ny0wLjU4MywyLjExNS0xLjgxLDIuMjEzLTMuMjAxCglzLTAuNTY5LTIuNzI1LTEuNzU0LTMuNDg3bC0xNS43MDYtOC40MTVjLTAuNTU0LTAuMzU3LTEuMjEzLTAuNDc3LTEuODU4LTAuMzM3Yy0wLjY0NCwwLjEzOS0xLjE5NSwwLjUyMS0xLjU1MiwxLjA3NQoJYy0wLjczNywxLjE0NC0wLjQwNiwyLjY3MywwLjcyMSwzLjM5N2w4LjQ1NCw2LjkyM2wtMTguNDE5LDguNDc4Yy0xLjQyNywwLjY1Ny0yLjI5OCwyLjEtMi4yMTgsMy42NzUKCWMwLjA0OCwwLjk0OSwwLjQ5MywxLjg4NCwxLjI1MywyLjYzNEMyMi4xNTgsNjAuMjY5LDI4Ljg0LDYzLDM1Ljk4NSw2M2MwLjQ0NSwwLDAuODkyLTAuMDExLDEuMzQxLTAuMDMyCgljMTQuMTU2LTAuNjczLDI1LjQzMi0xMi4zMTUsMjUuNjcxLTI2LjUwNUM2My4xMTgsMjkuMjM2LDYwLjQwNywyMi4zOTYsNTUuMzY0LDE3LjIwMnoiLz4KPC9zdmc+Cg==",
"icon_light": "data:image/svg+xml;base64,CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iNzJweCIgaGVpZ2h0PSI3MnB4IiB2aWV3Qm94PSIwIDAgNzIgNzIiPgk8ZGVmcz4KICAgICAgICA8ZmlsdGVyIGlkPSJhIiB3aWR0aD0iMjAwJSIgaGVpZ2h0PSIyMDAlIj4KICAgICAgICAgICAgPGZlT2Zmc2V0IHJlc3VsdD0ib2ZmT3V0IiBpbj0iU291cmNlQWxwaGEiIGR5PSIyLjIiLz4KICAgICAgICAgICAgPGZlR2F1c3NpYW5CbHVyIHJlc3VsdD0iYmx1ck91dCIgaW49Im9mZk91dCIgc3RkRGV2aWF0aW9uPSIxLjUiLz4KICAgICAgICAgICAgPGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjQgMCIvPgogICAgICAgICAgICA8ZmVNZXJnZT4KICAgICAgICAgICAgICAgIDxmZU1lcmdlTm9kZS8+CiAgICAgICAgICAgICAgICA8ZmVNZXJnZU5vZGUgaW49IlNvdXJjZUdyYXBoaWMiLz4KICAgICAgICAgICAgPC9mZU1lcmdlPgogICAgICAgIDwvZmlsdGVyPgogICAgPC9kZWZzPgo8cGF0aCBmaWxsPSIjZmZmZmZmIiBmaWx0ZXI9InVybCgjYSkiIGQ9Ik0zMS4wNTksNS4zOTVjMTYuODktMi43MjcsMzIuODE3LDguNzcyLDM1LjU0NSwyNS42NjJjMi43MjcsMTYuODktOC43NzIsMzIuODE3LTI1LjY2MiwzNS41NDUKCUMyNC4wNTEsNjkuMzI5LDguMTI0LDU3LjgzLDUuMzk3LDQwLjk0QzIuNjcsMjQuMDQ5LDE0LjE2OSw4LjEyMiwzMS4wNTksNS4zOTV6Ii8+CjxwYXRoIGZpbGw9IiMwMDY4YzMiIGQ9Ik01NS4zNjQsMTcuMjAyYy01LjA0Ny01LjE5Ny0xMS44MDItOC4xMDktMTkuMDItOC4yYy03LjE5MS0wLjA5LTEzLjk4NSwyLjY2OS0xOS4xNDksNy43NjgKCUMxMS45MSwyMS45ODksOSwyOC45NTUsOSwzNi4zOTJsMC4wNDQsMS4yNmMwLDEuMTgxLDAuOTYxLDIuMTQyLDIuMTQyLDIuMTQyczIuMTQxLTAuOTYsMi4xNDItMi4xNGwwLjAxLTAuOTEzCgljMC05Ljk0NSw4LjQ1My0yMC41OTMsMjEuMDM1LTIwLjU5M2MxMy4xMzIsMCwyMS4yNjEsMTAuNzEyLDIxLjI2MSwyMC42MzdjMCw1LjE3My0yLjA2Myw5LjkxOS01LjgwOCwxMy4zNjMKCUM0Ni4xNyw1My41MDksNDEuMjYsNTUuMzYsMzYsNTUuMzZjLTMuMTMsMC02LjIxOS0wLjc1NS04Ljk1OC0yLjE4NmwxOC44Ny04LjY4NmMxLjI2Ny0wLjU4MywyLjExNS0xLjgxLDIuMjEzLTMuMjAxCglzLTAuNTY5LTIuNzI1LTEuNzU0LTMuNDg3bC0xNS43MDYtOC40MTVjLTAuNTU0LTAuMzU3LTEuMjEzLTAuNDc3LTEuODU4LTAuMzM3Yy0wLjY0NCwwLjEzOS0xLjE5NSwwLjUyMS0xLjU1MiwxLjA3NQoJYy0wLjczNywxLjE0NC0wLjQwNiwyLjY3MywwLjcyMSwzLjM5N2w4LjQ1NCw2LjkyM2wtMTguNDE5LDguNDc4Yy0xLjQyNywwLjY1Ny0yLjI5OCwyLjEtMi4yMTgsMy42NzUKCWMwLjA0OCwwLjk0OSwwLjQ5MywxLjg4NCwxLjI1MywyLjYzNEMyMi4xNTgsNjAuMjY5LDI4Ljg0LDYzLDM1Ljk4NSw2M2MwLjQ0NSwwLDAuODkyLTAuMDExLDEuMzQxLTAuMDMyCgljMTQuMTU2LTAuNjczLDI1LjQzMi0xMi4zMTUsMjUuNjcxLTI2LjUwNUM2My4xMTgsMjkuMjM2LDYwLjQwNywyMi4zOTYsNTUuMzY0LDE3LjIwMnoiLz4KPC9zdmc+Cg=="
}
}
},
"22248c4c-7a12-46e2-9a41-44291b373a4d": {
"name": "LogMeOnce",
"icon_dark": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgMjA0OCAyMDQ4IiB3aWR0aD0iMTI4IiBoZWlnaHQ9IjEyOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTSAwIDAgTCAtNSAxIEwgLTE1IDQgTCAtMzggMTEgTCAtNTcgMTggTCAtNzYgMjkgTCAtOTAgMzggTCAtMTA5IDUyIEwgLTExNiA1OSBMIC0xMjcgNjggTCAtMTQwIDgxIEwgLTE0OCA4OCBMIC0xNDkgOTEgTCAtMTUxIDkxIEwgLTE2MSAxMDEgTCAtMTY5IDEwOCBMIC0xNjkgMTEwIEwgLTE3MSAxMTAgTCAtMTgwIDExOSBMIC0xODggMTI2IEwgLTIwMSAxMzcgTCAtMjEyIDE0NiBMIC0yMjYgMTU3IEwgLTIzOSAxNjcgTCAtMjUwIDE3NSBMIC0yNzAgMTg4IEwgLTI5MSAyMDIgTCAtMzA5IDIxMiBMIC0zMzAgMjI0IEwgLTM1MyAyMzUgTCAtMzg2IDI1MCBMIC00MDcgMjU5IEwgLTQ0OSAyNzMgTCAtNDcyIDI4MSBMIC01MDIgMjg5IEwgLTU1MSAyOTggTCAtNjE1IDMwOSBMIC02NDcgMzE2IEwgLTY4MSAzMjQgTCAtNjk4IDMzMCBMIC03MTQgMzM4IEwgLTczNSAzNTEgTCAtNzQ1IDM1OCBMIC03NTYgMzY3IEwgLTc2NCAzNzUgTCAtNzY2IDM3OSBMIC03NjggMzc5IEwgLTc3MyAzODUgTCAtNzgxIDM5NSBMIC03OTEgNDEwIEwgLTgwMSA0MjggTCAtODEwIDQ0NyBMIC04MTYgNDY1IEwgLTgyMiA1MDAgTCAtODI3IDU0NSBMIC04MzAgNTkyIEwgLTgzMiA2NTMgTCAtODMyIDcxMiBMIC04MzEgNzQyIEwgLTgyOCA3OTQgTCAtODIyIDg2NiBMIC04MTYgOTI4IEwgLTgxMiA5NTcgTCAtODAwIDEwMjAgTCAtNzg5IDEwNzIgTCAtNzgzIDEwOTggTCAtNzY1IDExNjIgTCAtNzUxIDEyMDcgTCAtNzQ0IDEyMjUgTCAtNzM0IDEyNTQgTCAtNzIwIDEyOTAgTCAtNzA0IDEzMjggTCAtNjg5IDEzNjEgTCAtNjY4IDE0MDMgTCAtNjUzIDE0MzEgTCAtNjM5IDE0NTYgTCAtNjIwIDE0ODggTCAtNjA3IDE1MDkgTCAtNTk0IDE1MjkgTCAtNTg0IDE1NDQgTCAtNTcxIDE1NjMgTCAtNTU4IDE1ODEgTCAtNTQ0IDE2MDAgTCAtNTMzIDE2MTUgTCAtNTIwIDE2MzIgTCAtNTA3IDE2NDggTCAtNDc5IDE2ODIgTCAtNDcwIDE2OTMgTCAtNDYwIDE3MDQgTCAtNDQ5IDE3MTYgTCAtNDQyIDE3MjQgTCAtNDMyIDE3MzQgTCAtNDI1IDE3NDIgTCAtMzY4IDE3OTkgTCAtMzUxIDE4MTUgTCAtMzM1IDE4MzAgTCAtMzI3IDE4MzcgTCAtMzE0IDE4NDkgTCAtMjg4IDE4NzEgTCAtMjcxIDE4ODUgTCAtMjYxIDE4OTMgTCAtMjQ0IDE5MDcgTCAtMjI4IDE5MTkgTCAtMjE1IDE5MjkgTCAtMTk2IDE5NDMgTCAtMTg0IDE5NTIgTCAtMTY4IDE5NjMgTCAtMTQ4IDE5NzcgTCAtMTMzIDE5ODcgTCAtMTEyIDIwMDAgTCAtODggMjAxMyBMIC03MiAyMDIxIEwgLTUyIDIwMzAgTCAtMzIgMjAzNyBMIDEyIDIwNDggTCA2MCAyMDQ4IEwgNjEgMjA0NyBMIDczIDIwNDQgTCAxMDAgMjAzNyBMIDEyMSAyMDI5IEwgMTQ5IDIwMTUgTCAxNjcgMjAwNSBMIDE4NiAxOTk0IEwgMjEwIDE5NzkgTCAyMzEgMTk2NSBMIDI2NiAxOTQxIEwgMjg0IDE5MjggTCAyOTUgMTkyMCBMIDMwNiAxOTExIEwgMzI0IDE4OTggTCAzMzggMTg4NyBMIDM0OSAxODc3IEwgMzU4IDE4NzAgTCAzNjkgMTg2MCBMIDM4MCAxODUxIEwgMzgwIDE4NDkgTCAzODIgMTg0OSBMIDM4NSAxODQ2IEwgMzk2IDE4MzcgTCA0MDMgMTgzMCBMIDQxMSAxODIzIEwgNDE4IDE4MTYgTCA0MjYgMTgwOSBMIDQ3NSAxNzYwIEwgNDc3IDE3NTYgTCA0NzkgMTc1NiBMIDQ4NyAxNzQ3IEwgNTAzIDE3MzAgTCA1MTAgMTcyMiBMIDUxOSAxNzEyIEwgNTI2IDE3MDQgTCA1MzggMTY5MSBMIDU0NyAxNjgwIEwgNTU4IDE2NjcgTCA1NjYgMTY1NiBMIDU3OSAxNjQwIEwgNTkwIDE2MjYgTCA2MDYgMTYwNSBMIDYyMSAxNTgzIEwgNjMxIDE1NjkgTCA2NDUgMTU0OSBMIDY1NSAxNTMzIEwgNjcwIDE1MTAgTCA2ODUgMTQ4NSBMIDY5NiAxNDY3IEwgNzA2IDE0NDkgTCA3MzAgMTQwNSBMIDc0NSAxMzc0IEwgNzYyIDEzMzggTCA3NzQgMTMxMCBMIDc5MiAxMjY3IEwgODExIDEyMTYgTCA4MjcgMTE2NiBMIDg0MiAxMTE3IEwgODU4IDEwNTEgTCA4NzAgOTk2IEwgODc1IDk2NyBMIDg4MSA5MTcgTCA4ODYgODgyIEwgODkxIDg0NiBMIDg5NCA4MTIgTCA4OTYgNzc0IEwgODk2IDY1NSBMIDg5NSA2MzAgTCA4OTIgNTkzIEwgODg4IDU1OSBMIDg3NyA0NzAgTCA4NzIgNDQ1IEwgODY1IDQyNiBMIDg1NyA0MTEgTCA4NDcgMzk1IEwgODM0IDM3OCBMIDgxNCAzNTggTCA4MDAgMzQ3IEwgNzg1IDMzNyBMIDc2NiAzMjcgTCA3NTAgMzIxIEwgNzI3IDMxNSBMIDY5MyAzMDggTCA2NTAgMzAxIEwgNTk3IDI5MiBMIDU3MCAyODYgTCA1NDggMjgwIEwgNTE1IDI3MCBMIDQ4NyAyNjEgTCA0NTkgMjUwIEwgNDM4IDI0MSBMIDQxNCAyMzAgTCAzODIgMjEzIEwgMzU4IDE5OSBMIDM0NyAxOTIgTCAzMzAgMTgwIEwgMzEyIDE2NyBMIDMwMCAxNTcgTCAyODYgMTQ2IEwgMjcxIDEzMyBMIDI2MyAxMjYgTCAyNTEgMTE1IEwgMjQwIDEwNiBMIDIyOCA5NSBMIDIwMCA3MSBMIDE4OSA2MiBMIDE3NCA1MCBMIDE1OCAzOCBMIDE0MiAyNyBMIDEyNyAxOSBMIDExNSAxNCBMIDk1IDggTCA3MSAwIEwgMCAwIHogTSAzMCA2MjIgTCA1MSA2MjMgTCA3OSA2MjcgTCAxMDAgNjMzIEwgMTIxIDY0MSBMIDE0MSA2NTIgTCAxNTcgNjYzIEwgMTcyIDY3NiBMIDE4NiA2OTAgTCAxODYgNjkyIEwgMTg4IDY5MiBMIDIwNiA3MTYgTCAyMTYgNzM0IEwgMjIxIDc0NSBMIDIyNiA3NTggTCAyMzMgNzg0IEwgMjM2IDc5OCBMIDIzNyA4MDggTCAyMzcgODQwIEwgMjMzIDg2NSBMIDIyNiA4OTEgTCAyMTkgOTA5IEwgMjA5IDkyOCBMIDE5NSA5NDkgTCAxODYgOTU5IEwgMTc5IDk2NyBMIDE2NiA5ODAgTCAxNTUgOTg5IEwgMTQyIDk5OCBMIDEyNiAxMDA3IEwgMTEzIDEwMTIgTCAxMTQgMTAyNCBMIDEyMiAxMDYwIEwgMTM3IDExMjMgTCAxNTYgMTIwOCBMIDE3MCAxMjcxIEwgMTc0IDEyOTYgTCAxNzYgMTMxMCBMIDE3NiAxMzM1IEwgMTczIDEzNDkgTCAxNjYgMTM1OSBMIDE1NyAxMzY3IEwgMTQ2IDEzNzIgTCAxNDAgMTM3NCBMIDExOSAxMzc2IEwgNTEgMTM3NiBMIC0zMiAxMzc3IEwgLTY0IDEzNzcgTCAtNzggMTM3NCBMIC04NSAxMzcwIEwgLTk4IDEzNTkgTCAtMTA2IDEzNDkgTCAtMTEwIDEzNDEgTCAtMTEyIDEzMzMgTCAtMTEyIDEzMTggTCAtMTA3IDEyODggTCAtMTAwIDEyNTYgTCAtODUgMTE5NiBMIC02OSAxMTI0IEwgLTU3IDEwNzIgTCAtNTAgMTAzNiBMIC00NyAxMDExIEwgLTUzIDEwMDkgTCAtNjkgMTAwMSBMIC04MSA5OTQgTCAtOTMgOTg1IEwgLTEwMyA5NzYgTCAtMTExIDk2OSBMIC0xMjAgOTYwIEwgLTEzMSA5NDYgTCAtMTQyIDkyOSBMIC0xNTIgOTEwIEwgLTE2MCA4OTAgTCAtMTY2IDg3MCBMIC0xNjkgODUzIEwgLTE3MCA4NDQgTCAtMTcwIDgxNCBMIC0xNjcgNzk0IEwgLTE2MSA3NjcgTCAtMTU0IDc0NiBMIC0xMzcgNzEzIEwgLTEyNiA2OTggTCAtMTE5IDY5MCBMIC0xMDggNjc4IEwgLTkzIDY2NSBMIC03NyA2NTMgTCAtNTYgNjQxIEwgLTM4IDYzMyBMIC0xNiA2MjcgTCAzIDYyNCBMIDMwIDYyMiB6ICIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoOTkxKSIgc3R5bGU9ImZpbGw6I2ZmZmZmZiIgLz4KPC9zdmc+Cg==",
"icon_light": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyMDQ4IDIwNDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSg5OTEpIiBkPSJtMCAwaDcxbDI0IDggMjAgNiAxMiA1IDE1IDggMTYgMTEgMTYgMTIgMTUgMTIgMTEgOSAyOCAyNCAxMiAxMSAxMSA5IDEyIDExIDggNyAxNSAxMyAxNCAxMSAxMiAxMCAxOCAxMyAxNyAxMiAxMSA3IDI0IDE0IDMyIDE3IDI0IDExIDIxIDkgMjggMTEgMjggOSAzMyAxMCAyMiA2IDI3IDYgNTMgOSA0MyA3IDM0IDcgMjMgNiAxNiA2IDE5IDEwIDE1IDEwIDE0IDExIDIwIDIwIDEzIDE3IDEwIDE2IDggMTUgNyAxOSA1IDI1IDExIDg5IDQgMzQgMyAzNyAxIDI1djExOWwtMiAzOC0zIDM0LTUgMzYtNSAzNS02IDUwLTUgMjktMTIgNTUtMTYgNjYtMTUgNDktMTYgNTAtMTkgNTEtMTggNDMtMTIgMjgtMTcgMzYtMTUgMzEtMjQgNDQtMTAgMTgtMTEgMTgtMTUgMjUtMTUgMjMtMTAgMTYtMTQgMjAtMTAgMTQtMTUgMjItMTYgMjEtMTEgMTQtMTMgMTYtOCAxMS0xMSAxMy05IDExLTEyIDEzLTcgOC05IDEwLTcgOC0xNiAxNy04IDloLTJsLTIgNC00OSA0OS04IDctNyA3LTggNy03IDctMTEgOS0zIDNoLTJ2MmwtMTEgOS0xMSAxMC05IDctMTEgMTAtMTQgMTEtMTggMTMtMTEgOS0xMSA4LTE4IDEzLTM1IDI0LTIxIDE0LTI0IDE1LTE5IDExLTE4IDEwLTI4IDE0LTIxIDgtMjcgNy0xMiAzLTEgMWgtNDhsLTQ0LTExLTIwLTctMjAtOS0xNi04LTI0LTEzLTIxLTEzLTE1LTEwLTIwLTE0LTE2LTExLTEyLTktMTktMTQtMTMtMTAtMTYtMTItMTctMTQtMTAtOC0xNy0xNC0yNi0yMi0xMy0xMi04LTctMTYtMTUtMTctMTYtNTctNTctNy04LTEwLTEwLTctOC0xMS0xMi0xMC0xMS05LTExLTI4LTM0LTEzLTE2LTEzLTE3LTExLTE1LTE0LTE5LTEzLTE4LTEzLTE5LTEwLTE1LTEzLTIwLTEzLTIxLTE5LTMyLTE0LTI1LTE1LTI4LTIxLTQyLTE1LTMzLTE2LTM4LTE0LTM2LTEwLTI5LTctMTgtMTQtNDUtMTgtNjQtNi0yNi0xMS01Mi0xMi02My00LTI5LTYtNjItNi03Mi0zLTUyLTEtMzB2LTU5bDItNjEgMy00NyA1LTQ1IDYtMzUgNi0xOCA5LTE5IDEwLTE4IDEwLTE1IDgtMTAgNS02aDJsMi00IDgtOCAxMS05IDEwLTcgMjEtMTMgMTYtOCAxNy02IDM0LTggMzItNyA2NC0xMSA0OS05IDMwLTggMjMtOCA0Mi0xNCAyMS05IDMzLTE1IDIzLTExIDIxLTEyIDE4LTEwIDIxLTE0IDIwLTEzIDExLTggMTMtMTAgMTQtMTEgMTEtOSAxMy0xMSA4LTcgOS05aDJ2LTJsOC03IDEwLTEwaDJsMS0zIDgtNyAxMy0xMyAxMS05IDctNyAxOS0xNCAxNC05IDE5LTExIDE5LTcgMjMtNyAxMC0zeiIgZmlsbD0iI0YxODQyOSIvPgo8cGF0aCB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMDIxLDYyMikiIGQ9Im0wIDAgMjEgMSAyOCA0IDIxIDYgMjEgOCAyMCAxMSAxNiAxMSAxNSAxMyAxNCAxNHYyaDJsMTggMjQgMTAgMTggNSAxMSA1IDEzIDcgMjYgMyAxNCAxIDEwdjMybC00IDI1LTcgMjYtNyAxOC0xMCAxOS0xNCAyMS05IDEwLTcgOC0xMyAxMy0xMSA5LTEzIDktMTYgOS0xMyA1IDEgMTIgOCAzNiAxNSA2MyAxOSA4NSAxNCA2MyA0IDI1IDIgMTR2MjVsLTMgMTQtNyAxMC05IDgtMTEgNS02IDItMjEgMmgtNjhsLTgzIDFoLTMybC0xNC0zLTctNC0xMy0xMS04LTEwLTQtOC0yLTh2LTE1bDUtMzAgNy0zMiAxNS02MCAxNi03MiAxMi01MiA3LTM2IDMtMjUtNi0yLTE2LTgtMTItNy0xMi05LTEwLTktOC03LTktOS0xMS0xNC0xMS0xNy0xMC0xOS04LTIwLTYtMjAtMy0xNy0xLTl2LTMwbDMtMjAgNi0yNyA3LTIxIDE3LTMzIDExLTE1IDctOCAxMS0xMiAxNS0xMyAxNi0xMiAyMS0xMiAxOC04IDIyLTYgMTktM3oiIGZpbGw9IiNGRUZGRkUiLz4KPC9zdmc+Cg=="
}
}

View File

@ -37,7 +37,7 @@
<tr>
<td style="padding: 20px; font-size: 12px; color: #212124;" align="center">
{% blocktrans with expires=expires|naturaltime %}
If you did not request a password change, please ignore this Email. The link above is valid for {{ expires }}.
If you did not request a password change, please ignore this email. The link above is valid for {{ expires }}.
{% endblocktrans %}
</td>
</tr>

View File

@ -5,7 +5,7 @@ You recently requested to change your password for your authentik account. Use t
{% endblocktrans %}
{{ url }}
{% blocktrans with expires=expires|naturaltime %}
If you did not request a password change, please ignore this Email. The link above is valid for {{ expires }}.
If you did not request a password change, please ignore this email. The link above is valid for {{ expires }}.
{% endblocktrans %}
--

View File

@ -26,6 +26,7 @@ from authentik.flows.models import FlowDesignation
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_GET
from authentik.lib.avatars import DEFAULT_AVATAR
from authentik.lib.utils.reflection import all_subclasses
from authentik.lib.utils.urls import reverse_with_qs
from authentik.root.middleware import ClientIPMiddleware
@ -76,7 +77,7 @@ class IdentificationChallenge(Challenge):
allow_show_password = BooleanField(default=False)
application_pre = CharField(required=False)
flow_designation = ChoiceField(FlowDesignation.choices)
captcha_stage = CaptchaChallenge(required=False)
captcha_stage = CaptchaChallenge(required=False, allow_null=True)
enroll_url = CharField(required=False)
recovery_url = CharField(required=False)
@ -224,6 +225,8 @@ class IdentificationStageView(ChallengeStageView):
"js_url": current_stage.captcha_stage.js_url,
"site_key": current_stage.captcha_stage.public_key,
"interactive": current_stage.captcha_stage.interactive,
"pending_user": "",
"pending_user_avatar": DEFAULT_AVATAR,
}
if current_stage.captcha_stage
else None

View File

View File

@ -0,0 +1,42 @@
"""RedirectStage API Views"""
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.stages.redirect.models import RedirectMode, RedirectStage
class RedirectStageSerializer(StageSerializer):
"""RedirectStage Serializer"""
def validate(self, attrs):
mode = attrs.get("mode")
target_static = attrs.get("target_static")
target_flow = attrs.get("target_flow")
if mode == RedirectMode.STATIC and not target_static:
raise ValidationError(_("Target URL should be present when mode is Static."))
if mode == RedirectMode.FLOW and not target_flow:
raise ValidationError(_("Target Flow should be present when mode is Flow."))
return attrs
class Meta:
model = RedirectStage
fields = StageSerializer.Meta.fields + [
"keep_context",
"mode",
"target_static",
"target_flow",
]
class RedirectStageViewSet(UsedByMixin, ModelViewSet):
"""RedirectStage Viewset"""
queryset = RedirectStage.objects.all()
serializer_class = RedirectStageSerializer
filterset_fields = ["name"]
search_fields = ["name"]
ordering = ["name"]

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