Compare commits

..

66 Commits

Author SHA1 Message Date
a36f788e60 ci: setup action: remove unused dependencies on poetry install (cherry-pick #12365) (#12371)
ci: setup action: remove unused dependencies on poetry install (#12365)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-12-18 00:53:16 +01:00
50ad69bdad root: fix ssl settings for read replicas not being applied (cherry-pick #12341) (#12345)
root: fix ssl settings for read replicas not being applied (#12341)

* root: fix ssl settings for read replicas not being applied



* fix



* slight refactor



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-12-12 18:58:14 +01:00
0edd7531a1 release: 2024.10.5 2024-12-10 17:48:12 +01:00
5a2c914d19 flows: better test stage's challenge responses (cherry-pick #12316) (#12317)
flows: better test stage's challenge responses (#12316)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-12-10 17:47:19 +01:00
f21062581a ci: build release test image with no cache
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 17:45:48 +01:00
676e7885e8 root: lock setuptools to prevent docker install issue
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-12-10 17:12:52 +01:00
80441d2277 enterprise/stages/authenticator_endpoint_gdtc: don't set frame options globally (cherry-pick #12311) (#12315)
enterprise/stages/authenticator_endpoint_gdtc: don't set frame options globally (#12311)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-12-10 15:22:47 +01:00
e760f73518 stages/identification: fix invalid challenge warning when no captcha stage is set (cherry-pick #12312) (#12314)
stages/identification: fix invalid challenge warning when no captcha stage is set (#12312)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-12-10 15:22:38 +01:00
948f80d7ae 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>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	website/docs/enterprise/manage-enterprise.md
2024-12-10 13:08:59 +01:00
0e4b153e7f web/flows: resize captcha iframes (cherry-pick #12260) (#12304)
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.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2024-12-10 00:28:51 +01:00
efac5ce7bd web: backport fix impersonate api (#12184)
web/admin: fix impersonate API call

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-11-25 17:04:56 +01:00
d9fbe1d467 root: fix database ssl options not set correctly (cherry-pick #12180) (#12183)
root: fix database ssl options not set correctly (#12180)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-25 14:56:30 +01:00
527e584699 release: 2024.10.4 2024-11-21 19:21:40 +01:00
80dfe371e6 providers/oauth2: fix migration (cherry-pick #12138) (#12139)
providers/oauth2: fix migration (#12138)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-21 18:55:26 +01:00
a3d1491aee providers/oauth2: fix migration dependencies (cherry-pick #12123) (#12132)
providers/oauth2: fix migration dependencies (#12123)

we had to change these dependencies for 2024.8.x since that doesn't have invalidation flows

they also need to be changed for 2024.10 when upgrading, and these migrations don't need the invalidation flow migration at all

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-21 17:25:33 +01:00
1b98792637 web: bump API Client version (cherry-pick #12129) (#12130)
web: bump API Client version (#12129)

Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-11-21 17:25:04 +01:00
111e120220 providers/oauth2: fix redirect uri input (cherry-pick #12122) (#12127)
providers/oauth2: fix redirect uri input (#12122)

* fix elements disappearing



* fix incorrect field input



* fix wizard form and display



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-21 17:22:00 +01:00
20642d49c3 providers/proxy: fix redirect_uri (cherry-pick #12121) (#12125)
providers/proxy: fix redirect_uri (#12121)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-21 17:21:39 +01:00
a9776a83d3 release: 2024.10.3 2024-11-21 15:17:46 +01:00
b9faae83b4 web/admin: better footer links (#12004)
* 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.

* First things first: save the blueprint that initializes the test runner.

* Committing to having the PKs be a string, and streamlining an event handler.  Type solidity needed for the footer control.

* web/admin/better-footer-links

# What

- A data control that takes two string fields and returns the JSON object for a FooterLink
- A data control that takes a control like the one above and assists the user in entering a
  collection of such objects.

# Why

We're trying to move away from CodeMirror for the simple things, like tables of what is essentially
data entry. Jens proposed this ArrayInput thing, and I've simplified it so you define what "a row"
is as a small, lightweight custom Component that returns and validates the datatype for that row,
and ArrayInput creates a table of rows, and that's that.

We're still working out the details, but the demo is to replace the "Name & URL" table in
AdminSettingsForm with this, since it was silly to ask the customer to hand-write JSON or YAML,
getting the keys right every time, for an `Array<Record<{ name: string, href: string }>>`. And some
client-side validation can't hurt.

Storybook included.  Tests to come.

* Not ready for prime time.

* One lint.  Other lints are still in progress.

* web: lots of 'as unknown as Foo'

I know this is considered bad practice, but we use Lit and Lit.spread
to send initialization arguments to functions that create DOM
objects, and Lit's prefix convention of '.' for object, '?' for
boolean, and '@' for event handler doesn't map at all to the Interface
declarations of Typescript.  So we have to cast these types when
sending them via functions to constructors.

* web/admin/better-footer-links

# What

- Remove the "JSON or YAML" language from the AdminSettings page for describing FooterLinks inputs.
- Add unit tests for ArrayInput and AdminSettingsFooterLinks.
- Provide a property for accessing a component's value

# Why

Providing a property by which the JSONified version of the value can be accessed enhances the
ability of tests to independently check that the value is in a state we desire, since properties can
easily be accessed across the wire protocol used by browser-based testing environments.

* Ensure the UI is built from _current_ before running tests.
2024-11-21 14:53:32 +01:00
afc2998697 web: bump API Client version (#12118)
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>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/package-lock.json
#	web/package.json
2024-11-21 14:53:22 +01:00
fabacc56c4 security: fix CVE 2024 52289 (#12113)
* initial migration

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

* migrate tests

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

* fix loading

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

* fix

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

* start dynamic ui

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

* initial ui

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

* add serialize

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

* add error message handling

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

* fix/add tests

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

* prepare docs

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

* migrate to new input

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

* fix tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	authentik/core/tests/test_transactional_applications_api.py
2024-11-21 14:48:09 +01:00
11b013d3b8 security: fix CVE 2024 52307 (#12115)
* security: fix CVE-2024-52307

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

* add docs

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

* fix tests

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-11-21 14:28:03 +01:00
e10c47d8b8 security: fix CVE 2024 52287 (cherry-pick #12114) (#12117)
security: fix CVE 2024 52287 (#12114)

* security: CVE-2024-52287



* add tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-21 14:24:31 +01:00
d2b194f6b7 website/docs: add CSP to hardening (cherry-pick #11970) (#12116)
website/docs: add CSP to hardening (#11970)

* add CSP to hardening

* re-word docs




* fix typo

* use the correct term "location" instead of "origin" in CSP docs

* reword docs

* add comments to permissive CSP directives

* add warning about overwriting existing CSP headers

---------

Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-11-21 14:21:11 +01:00
780a59c908 internal: add CSP header to files in /media (cherry-pick #12092) (#12108)
internal: add CSP header to files in `/media` (#12092)

add CSP header to files in `/media`

This fixes a security issue of stored cross-site scripting via embedding
JavaScript in SVG files by a malicious user with `can_save_media`
capability.

This can be exploited if:
- the uploaded file is served from the same origin as authentik, and
- the user opens the uploaded file directly in their browser

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-21 09:58:42 +01:00
f8015fccd8 website/docs: group CVEs by year (cherry-pick #12099) (#12100)
website/docs: group CVEs by year (#12099)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-20 23:05:37 +01:00
05f4e738a1 root: check remote IP for proxy protocol same as HTTP/etc (cherry-pick #12094) (#12097)
root: check remote IP for proxy protocol same as HTTP/etc (#12094)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-20 21:49:53 +01:00
f535a23c03 root: fix activation of locale not being scoped (cherry-pick #12091) (#12096)
root: fix activation of locale not being scoped (#12091)

closes #12088

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-20 21:33:08 +01:00
91905530c7 providers/scim: accept string and int for SCIM IDs (cherry-pick #12093) (#12095)
providers/scim: accept string and int for SCIM IDs (#12093)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-20 18:36:50 +01:00
40a970e321 core: fix source_flow_manager throwing error when authenticated user attempts to re-authenticate with existing link (cherry-pick #12080) (#12081)
core: fix source_flow_manager throwing error when authenticated user attempts to re-authenticate with existing link (#12080)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-19 18:33:43 +01:00
b51d8d0ba3 web/flows: fix invisible captcha call (cherry-pick #12048) (#12049)
web/flows: fix invisible captcha call (#12048)

* fix invisible captcha call

* fix invisible captcha DOM removal

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2024-11-15 18:50:57 +01:00
7e8891338f rbac: fix incorrect object_description for object-level permissions (cherry-pick #12029) (#12043)
rbac: fix incorrect object_description for object-level permissions (#12029)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-15 14:22:22 +01:00
3ae0001bb5 providers/ldap: fix global search_full_directory permission not being sufficient (cherry-pick #12028) (#12030)
providers/ldap: fix global search_full_directory permission not being sufficient (#12028)

* providers/ldap: fix global search_full_directory permission not being sufficient



* use full name of permission



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-15 13:52:39 +01:00
66a4970014 release: 2024.10.2 2024-11-14 17:05:40 +01:00
7ab9300761 website/docs: 2024.10.2 release notes (cherry-pick #12025) (#12026)
website/docs: 2024.10.2 release notes (#12025)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-14 17:00:32 +01:00
a2eccd5022 core: use versioned_script for path only (cherry-pick #12003) (#12023)
core: use versioned_script for path only (#12003)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-14 14:03:03 +01:00
31aeaa247f providers/oauth2: fix manual device code entry (cherry-pick #12017) (#12019)
providers/oauth2: fix manual device code entry (#12017)

* providers/oauth2: fix manual device code entry



* make code input a char field to prevent leading 0s from being cut off



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-13 21:59:10 +01:00
f49008bbb6 crypto: validate that generated certificate's name is unique (cherry-pick #12015) (#12016)
crypto: validate that generated certificate's name is unique (#12015)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-13 17:05:59 +01:00
feb13c8ee5 providers/proxy: fix Issuer when AUTHENTIK_HOST_BROWSER is set (cherry-pick #11968) (#12005)
providers/proxy: fix Issuer when AUTHENTIK_HOST_BROWSER is set (#11968)

correctly use host_browser's hostname as host header for token requests to ensure Issuer is identical

Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-13 00:59:10 +01:00
d5ef831718 web: bump API Client version (#11992)
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>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/package-lock.json
#	web/package.json
2024-11-11 14:30:24 +01:00
64676819ec blueprints: add default Password policy (cherry-pick #11793) (#11993)
blueprints: add default Password policy (#11793)

* add password policy to default password change flow

This change complies with the minimal compositional requirements by
NIST SP 800-63 Digital Identity Guidelines. See
https://pages.nist.gov/800-63-4/sp800-63b.html#password

More work is needed to comply with other parts of the Guidelines,
specifically

> If the chosen password is found on the blocklist, the CSP or verifier
> [...] SHALL provide the reason for rejection.

and

> Verifiers SHALL offer guidance to the subscriber to assist the user in
> choosing a strong password. This is particularly important following
> the rejection of a password on the blocklist as it discourages trivial
> modification of listed weak passwords.

* add docs for default Password policy

* remove HIBP from default Password policy

* add zxcvbn to default Password policy

* add fallback password error message to password policy, fix validation policy



* reword docs




* add HIBP caveat




* separate policy into separate blueprint



* use password policy for oobe flow



* kiss



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-11-11 13:54:44 +01:00
7ed268fef4 stages/captcha: Run interactive captcha in Frame (cherry-pick #11857) (#11991)
stages/captcha: Run interactive captcha in Frame (#11857)

* initial turnstile frame



* add interactive flag



* add interactive support for all



* fix missing migration



* don't hide in identification stage if interactive



* fixup



* require less hacky css



* update docs



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-11 13:31:01 +01:00
f6526d1be9 stages/password: use recovery flow from brand (cherry-pick #11953) (#11969)
stages/password: use recovery flow from brand (#11953)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-08 16:56:16 +01:00
12f8b4566b website/docs: fix slug matching redirect URI causing broken refresh (cherry-pick #11950) (#11954)
website/docs: fix slug matching redirect URI causing broken refresh (#11950)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-07 19:26:49 +01:00
665de8ef22 release: 2024.10.1 2024-11-05 18:06:32 +01:00
9eaa723bf8 website/docs: 2024.10.1 Release Notes (cherry-pick #11926) (#11928)
website/docs: `2024.10.1` Release Notes (#11926)

* fix API Changes in `2024.10` changelog

* add `2024.10.1` API Changes to changelog

* add changes in `2024.10.1` to changelog

* change `details` to `h3` in changelog

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2024-11-05 18:05:00 +01:00
b2ca9c8cbc website: remove RC disclaimer for version 2024.10 (cherry-pick #11871) (#11920)
website: remove RC disclaimer for version 2024.10 (#11871)

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2024-11-05 12:13:11 +01:00
7927392100 website/docs: add info about invalidation flow, default flows in general (cherry-pick #11800) (#11921)
website/docs: add info about invalidation flow, default flows in general (#11800)

* restructure

* tweak

* fix header

* added more definitions

* jens excellent idea

* restructure the Layouts content

* tweaks

* links fix

* links still

* fighting links and cache

* argh links

* ditto

* remove link

* anothe link

* Jens' edit

* listed default flows set by brand

* add links back

* tweaks

* used import for list

* tweak

* rewrite some stuff



* format



* mangled rebase, fixed

* bump

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-11-05 12:12:59 +01:00
d8d07e32cb website: fix docs redirect (cherry-pick #11873) (#11922)
website: fix docs redirect (#11873)

fix docs redirect

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2024-11-05 12:12:46 +01:00
f7c5d329eb website/docs: fix release notes to say Federation (cherry-pick #11889) (#11923)
website/docs: fix release notes to say Federation (#11889)

* fix Federation

* typo

* added back should

* slooooow down

---------

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
2024-11-05 12:12:27 +01:00
92dec32547 enterprise/rac: fix API Schema for invalidation_flow (cherry-pick #11907) (#11908)
enterprise/rac: fix API Schema for invalidation_flow (#11907)

* enterprise/rac: fix API Schema for invalidation_flow



* fix tests



* add tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-11-04 20:42:41 +01:00
510feccd31 core: add None check to a device's extra_description (cherry-pick #11904) (#11906)
core: add `None` check to a device's `extra_description` (#11904)

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2024-11-04 18:12:34 +01:00
364a9a1f02 web: fix missing status code on failed build (#11903)
* fix missing status code on failed build

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

* fix locale

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

* fix format

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	web/xliff/tr.xlf
2024-11-04 18:06:22 +01:00
40cbb7567b providers/oauth2: fix size limited index for tokens (cherry-pick #11879) (#11905)
providers/oauth2: fix size limited index for tokens (#11879)

* providers/oauth2: fix size limited index for tokens

I preserved the migrations as comments so the index IDs and migration
IDs remain searchable without accessing git history.

* rename migration file to more descriptive

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2024-11-04 18:05:55 +01:00
8ad0f63994 website: update supported versions (cherry-pick #11841) (#11872)
website: update supported versions (#11841)

update supported versions

Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
2024-10-31 12:59:54 +01:00
6ce33ab912 root: bumpversion 2024.10 (#11865)
release: 2024.10.0
2024-10-30 22:45:48 +01:00
d96b577abd web/admin: fix code-based MFA toggle not working in wizard (cherry-pick #11854) (#11855)
web/admin: fix code-based MFA toggle not working in wizard (#11854)

closes #11834

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-10-29 20:19:54 +01:00
8c547589f6 sources/kerberos: add kiprop to ignored system principals (cherry-pick #11852) (#11853)
sources/kerberos: add kiprop to ignored system principals (#11852)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-10-29 17:31:32 +01:00
3775e5b84f website: 2024.10 Release Notes (cherry-pick #11839) (#11840)
website: 2024.10 Release Notes (#11839)

* generate diffs and changelog

* add 2024.10 release notes

* reorder release note highlights

* lint website

* reorder release note new features

* reword Kerberos




* extend JWE description




---------

Signed-off-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Simonyi Gergő <28359278+gergosimonyi@users.noreply.github.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-10-28 17:27:59 +01:00
fa30339f65 website/docs: remove � (cherry-pick #11823) (#11835)
website/docs: remove � (#11823)

remove 

Signed-off-by: Tobias <5702338+T0biii@users.noreply.github.com>
Co-authored-by: Tobias <5702338+T0biii@users.noreply.github.com>
2024-10-28 13:21:02 +01:00
e825eda106 website/docs: Update social-logins github (cherry-pick #11822) (#11836)
website/docs: Update social-logins github (#11822)

Update index.md

Signed-off-by: Tobias <5702338+T0biii@users.noreply.github.com>
Co-authored-by: Tobias <5702338+T0biii@users.noreply.github.com>
2024-10-28 13:20:38 +01:00
246cae3dfa lifecycle: fix kdc5-config missing (cherry-pick #11826) (#11829)
lifecycle: fix kdc5-config missing (#11826)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-10-28 01:15:50 +01:00
6cfd2bd1af website/docs: update preview status of different features (cherry-pick #11817) (#11818)
website/docs: update preview status of different features (#11817)

* remove preview from RAC



* add preview page instead of info box



* remove preview from rbac



* add preview to gdtc



* add preview to kerberos source



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-10-25 21:42:45 +02:00
f0e4f93fe6 lifecycle: fix missing krb5 deps for full testing in image (cherry-pick #11815) (#11816)
lifecycle: fix missing krb5 deps for full testing in image (#11815)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-10-25 18:46:55 +02:00
434aa57ba7 release: 2024.10.0-rc1 2024-10-25 17:26:39 +02:00
800 changed files with 39959 additions and 84235 deletions

View File

@ -1,16 +1,16 @@
[bumpversion] [bumpversion]
current_version = 2024.12.2 current_version = 2024.10.5
tag = True tag = True
commit = 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*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
serialize = serialize =
{major}.{minor}.{patch}-{rc_t}{rc_n} {major}.{minor}.{patch}-{rc_t}{rc_n}
{major}.{minor}.{patch} {major}.{minor}.{patch}
message = release: {new_version} message = release: {new_version}
tag_name = version/{new_version} tag_name = version/{new_version}
[bumpversion:part:rc_t] [bumpversion:part:rc_t]
values = values =
rc rc
final final
optional_value = final optional_value = final
@ -30,5 +30,3 @@ optional_value = final
[bumpversion:file:internal/constants/constants.go] [bumpversion:file:internal/constants/constants.go]
[bumpversion:file:web/src/common/constants.ts] [bumpversion:file:web/src/common/constants.ts]
[bumpversion:file:website/docs/install-config/install/aws/template.yaml]

View File

@ -11,9 +11,9 @@ inputs:
description: "Docker image arch" description: "Docker image arch"
outputs: outputs:
shouldPush: shouldBuild:
description: "Whether to push the image or not" description: "Whether to build image or not"
value: ${{ steps.ev.outputs.shouldPush }} value: ${{ steps.ev.outputs.shouldBuild }}
sha: sha:
description: "sha" description: "sha"

View File

@ -7,14 +7,7 @@ from time import time
parser = configparser.ConfigParser() parser = configparser.ConfigParser()
parser.read(".bumpversion.cfg") parser.read(".bumpversion.cfg")
# Decide if we should push the image or not should_build = str(len(os.environ.get("DOCKER_USERNAME", "")) > 0).lower()
should_push = True
if len(os.environ.get("DOCKER_USERNAME", "")) < 1:
# Don't push if we don't have DOCKER_USERNAME, i.e. no secrets are available
should_push = False
if os.environ.get("GITHUB_REPOSITORY").lower() == "goauthentik/authentik-internal":
# Don't push on the internal repo
should_push = False
branch_name = os.environ["GITHUB_REF"] branch_name = os.environ["GITHUB_REF"]
if os.environ.get("GITHUB_HEAD_REF", "") != "": if os.environ.get("GITHUB_HEAD_REF", "") != "":
@ -71,7 +64,7 @@ def get_attest_image_names(image_with_tags: list[str]):
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output: with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"shouldPush={str(should_push).lower()}", file=_output) print(f"shouldBuild={should_build}", file=_output)
print(f"sha={sha}", file=_output) print(f"sha={sha}", file=_output)
print(f"version={version}", file=_output) print(f"version={version}", file=_output)
print(f"prerelease={prerelease}", file=_output) print(f"prerelease={prerelease}", file=_output)

View File

@ -7,7 +7,6 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: build:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
id-token: write id-token: write

View File

@ -7,7 +7,6 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: build:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token - id: generate_token

View File

@ -1,46 +0,0 @@
name: authentik-ci-aws-cfn
on:
push:
branches:
- main
- next
- version-*
pull_request:
branches:
- main
- version-*
env:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
jobs:
check-changes-applied:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup authentik env
uses: ./.github/actions/setup
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: |
npm ci
- name: Check changes have been applied
run: |
poetry run make aws-cfn
git diff --exit-code
ci-aws-cfn-mark:
if: always()
needs:
- check-changes-applied
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

@ -116,7 +116,7 @@ jobs:
poetry run make test poetry run make test
poetry run coverage xml poetry run coverage xml
- if: ${{ always() }} - if: ${{ always() }}
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
flags: unit flags: unit
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@ -134,13 +134,13 @@ jobs:
- name: Setup authentik env - name: Setup authentik env
uses: ./.github/actions/setup uses: ./.github/actions/setup
- name: Create k8s Kind Cluster - name: Create k8s Kind Cluster
uses: helm/kind-action@v1.12.0 uses: helm/kind-action@v1.10.0
- name: run integration - name: run integration
run: | run: |
poetry run coverage run manage.py test tests/integration poetry run coverage run manage.py test tests/integration
poetry run coverage xml poetry run coverage xml
- if: ${{ always() }} - if: ${{ always() }}
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
flags: integration flags: integration
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@ -198,7 +198,7 @@ jobs:
poetry run coverage run manage.py test ${{ matrix.job.glob }} poetry run coverage run manage.py test ${{ matrix.job.glob }}
poetry run coverage xml poetry run coverage xml
- if: ${{ always() }} - if: ${{ always() }}
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@v4
with: with:
flags: e2e flags: e2e
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@ -209,7 +209,6 @@ jobs:
file: unittest.xml file: unittest.xml
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
ci-core-mark: ci-core-mark:
if: always()
needs: needs:
- lint - lint
- test-migrations - test-migrations
@ -219,9 +218,7 @@ jobs:
- test-e2e - test-e2e
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: re-actors/alls-green@release/v1 - run: echo mark
with:
jobs: ${{ toJSON(needs) }}
build: build:
strategy: strategy:
fail-fast: false fail-fast: false
@ -243,7 +240,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -255,7 +252,7 @@ jobs:
image-name: ghcr.io/goauthentik/dev-server image-name: ghcr.io/goauthentik/dev-server
image-arch: ${{ matrix.arch }} image-arch: ${{ matrix.arch }}
- name: Login to Container Registry - name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }} if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
@ -272,15 +269,15 @@ jobs:
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }} GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }} GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
tags: ${{ steps.ev.outputs.imageTags }} tags: ${{ steps.ev.outputs.imageTags }}
push: ${{ steps.ev.outputs.shouldPush == 'true' }} push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
build-args: | build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache 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' || '' }} cache-to: ${{ steps.ev.outputs.shouldBuild == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max' || '' }}
platforms: linux/${{ matrix.arch }} platforms: linux/${{ matrix.arch }}
- uses: actions/attest-build-provenance@v2 - uses: actions/attest-build-provenance@v1
id: attest id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }} if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with: with:
subject-name: ${{ steps.ev.outputs.attestImageNames }} subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}
@ -306,7 +303,7 @@ jobs:
with: with:
image-name: ghcr.io/goauthentik/dev-server image-name: ghcr.io/goauthentik/dev-server
- name: Comment on PR - name: Comment on PR
if: ${{ steps.ev.outputs.shouldPush == 'true' }} if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
uses: ./.github/actions/comment-pr-instructions uses: ./.github/actions/comment-pr-instructions
with: with:
tag: ${{ steps.ev.outputs.imageMainTag }} tag: ${{ steps.ev.outputs.imageMainTag }}

View File

@ -49,15 +49,12 @@ jobs:
run: | run: |
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./... go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./...
ci-outpost-mark: ci-outpost-mark:
if: always()
needs: needs:
- lint-golint - lint-golint
- test-unittest - test-unittest
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: re-actors/alls-green@release/v1 - run: echo mark
with:
jobs: ${{ toJSON(needs) }}
build-container: build-container:
timeout-minutes: 120 timeout-minutes: 120
needs: needs:
@ -82,7 +79,7 @@ jobs:
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -93,7 +90,7 @@ jobs:
with: with:
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }} image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
- name: Login to Container Registry - name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }} if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
@ -107,16 +104,16 @@ jobs:
with: with:
tags: ${{ steps.ev.outputs.imageTags }} tags: ${{ steps.ev.outputs.imageTags }}
file: ${{ matrix.type }}.Dockerfile file: ${{ matrix.type }}.Dockerfile
push: ${{ steps.ev.outputs.shouldPush == 'true' }} push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
build-args: | build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
context: . context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache 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) || '' }} cache-to: ${{ steps.ev.outputs.shouldBuild == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
- uses: actions/attest-build-provenance@v2 - uses: actions/attest-build-provenance@v1
id: attest id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }} if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
with: with:
subject-name: ${{ steps.ev.outputs.attestImageNames }} subject-name: ${{ steps.ev.outputs.attestImageNames }}
subject-digest: ${{ steps.push.outputs.digest }} subject-digest: ${{ steps.push.outputs.digest }}

View File

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

View File

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

View File

@ -11,7 +11,6 @@ env:
jobs: jobs:
build: build:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token - id: generate_token

View File

@ -7,7 +7,6 @@ on:
jobs: jobs:
clean-ghcr: clean-ghcr:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
name: Delete old unused container images name: Delete old unused container images
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View File

@ -12,7 +12,6 @@ env:
jobs: jobs:
publish-source-docs: publish-source-docs:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 120 timeout-minutes: 120
steps: steps:

View File

@ -11,7 +11,6 @@ permissions:
jobs: jobs:
update-next: update-next:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: internal-production environment: internal-production
steps: steps:

View File

@ -17,7 +17,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -55,7 +55,7 @@ jobs:
VERSION=${{ github.ref }} VERSION=${{ github.ref }}
tags: ${{ steps.ev.outputs.imageTags }} tags: ${{ steps.ev.outputs.imageTags }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
- uses: actions/attest-build-provenance@v2 - uses: actions/attest-build-provenance@v1
id: attest id: attest
with: with:
subject-name: ${{ steps.ev.outputs.attestImageNames }} subject-name: ${{ steps.ev.outputs.attestImageNames }}
@ -83,7 +83,7 @@ jobs:
with: with:
go-version-file: "go.mod" go-version-file: "go.mod"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.3.0 uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: prepare variables - name: prepare variables
@ -119,7 +119,7 @@ jobs:
file: ${{ matrix.type }}.Dockerfile file: ${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
context: . context: .
- uses: actions/attest-build-provenance@v2 - uses: actions/attest-build-provenance@v1
id: attest id: attest
with: with:
subject-name: ${{ steps.ev.outputs.attestImageNames }} subject-name: ${{ steps.ev.outputs.attestImageNames }}
@ -169,27 +169,6 @@ jobs:
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
upload-aws-cfn-template:
permissions:
# Needed for AWS login
id-token: write
contents: read
needs:
- build-server
- build-outpost
env:
AWS_REGION: eu-central-1
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
aws-region: ${{ env.AWS_REGION }}
- name: Upload template
run: |
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: test-release:
needs: needs:
- build-server - build-server

View File

@ -18,7 +18,7 @@ jobs:
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
docker buildx install docker buildx install
mkdir -p ./gen-ts-api mkdir -p ./gen-ts-api
docker build -t testing:latest . docker build --no-cache -t testing:latest .
echo "AUTHENTIK_IMAGE=testing" >> .env echo "AUTHENTIK_IMAGE=testing" >> .env
echo "AUTHENTIK_TAG=latest" >> .env echo "AUTHENTIK_TAG=latest" >> .env
docker compose up --no-start docker compose up --no-start

View File

@ -1,21 +0,0 @@
name: "authentik-repo-mirror"
on: [push, delete]
jobs:
to_internal:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- if: ${{ env.MIRROR_KEY != '' }}
uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@github.com:goauthentik/authentik-internal.git
ssh_private_key:
${{ secrets.GH_MIRROR_KEY }}
env:
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}

View File

@ -11,7 +11,6 @@ permissions:
jobs: jobs:
stale: stale:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- id: generate_token - id: generate_token

View File

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

View File

@ -19,18 +19,10 @@ Dockerfile @goauthentik/infrastructure
*Dockerfile @goauthentik/infrastructure *Dockerfile @goauthentik/infrastructure
.dockerignore @goauthentik/infrastructure .dockerignore @goauthentik/infrastructure
docker-compose.yml @goauthentik/infrastructure docker-compose.yml @goauthentik/infrastructure
Makefile @goauthentik/infrastructure
.editorconfig @goauthentik/infrastructure
CODEOWNERS @goauthentik/infrastructure
# Web # Web
web/ @goauthentik/frontend web/ @goauthentik/frontend
tests/wdio/ @goauthentik/frontend tests/wdio/ @goauthentik/frontend
# Locale
locale/ @goauthentik/backend @goauthentik/frontend
web/xliff/ @goauthentik/backend @goauthentik/frontend
# Docs & Website # Docs & Website
website/ @goauthentik/docs website/ @goauthentik/docs
CODE_OF_CONDUCT.md @goauthentik/docs
# Security # Security
SECURITY.md @goauthentik/security @goauthentik/docs website/docs/security/ @goauthentik/security
website/docs/security/ @goauthentik/security @goauthentik/docs

View File

@ -1 +1 @@
website/docs/developer-docs/index.md website/developer-docs/index.md

View File

@ -80,7 +80,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
go build -o /go/authentik ./cmd/server go build -o /go/authentik ./cmd/server
# Stage 4: MaxMind GeoIP # Stage 4: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.0.1 AS geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN" ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
ENV GEOIPUPDATE_VERBOSE="1" ENV GEOIPUPDATE_VERBOSE="1"

View File

@ -5,7 +5,7 @@ PWD = $(shell pwd)
UID = $(shell id -u) UID = $(shell id -u)
GID = $(shell id -g) GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.npm_version) NPM_VERSION = $(shell python -m scripts.npm_version)
PY_SOURCES = authentik tests scripts lifecycle .github website/docs/install-config/install/aws PY_SOURCES = authentik tests scripts lifecycle .github
DOCKER_IMAGE ?= "authentik:test" DOCKER_IMAGE ?= "authentik:test"
GEN_API_TS = "gen-ts-api" GEN_API_TS = "gen-ts-api"
@ -252,9 +252,6 @@ website-build:
website-watch: ## Build and watch the documentation website, updating automatically website-watch: ## Build and watch the documentation website, updating automatically
cd website && npm run watch cd website && npm run watch
aws-cfn:
cd website && npm run aws-cfn
######################### #########################
## Docker ## Docker
######################### #########################

View File

@ -2,7 +2,7 @@ authentik takes security very seriously. We follow the rules of [responsible di
## Independent audits and pentests ## Independent audits and pentests
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). 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).
## What authentik classifies as a CVE ## 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 | | Version | Supported |
| --------- | --------- | | --------- | --------- |
| 2024.8.x | ✅ |
| 2024.10.x | ✅ | | 2024.10.x | ✅ |
| 2024.12.x | ✅ |
## Reporting a Vulnerability ## Reporting a Vulnerability

View File

@ -2,7 +2,7 @@
from os import environ from os import environ
__version__ = "2024.12.2" __version__ = "2024.10.5"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
@ -16,5 +16,5 @@ def get_full_version() -> str:
"""Get full version, with build hash appended""" """Get full version, with build hash appended"""
version = __version__ version = __version__
if (build_hash := get_build_hash()) != "": if (build_hash := get_build_hash()) != "":
return f"{version}+{build_hash}" version += "." + build_hash
return version return version

View File

@ -7,9 +7,7 @@ from sys import version as python_version
from typing import TypedDict from typing import TypedDict
from cryptography.hazmat.backends.openssl.backend import backend from cryptography.hazmat.backends.openssl.backend import backend
from django.conf import settings
from django.utils.timezone import now from django.utils.timezone import now
from django.views.debug import SafeExceptionReporterFilter
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request from rest_framework.request import Request
@ -54,16 +52,10 @@ class SystemInfoSerializer(PassiveSerializer):
def get_http_headers(self, request: Request) -> dict[str, str]: def get_http_headers(self, request: Request) -> dict[str, str]:
"""Get HTTP Request headers""" """Get HTTP Request headers"""
headers = {} headers = {}
raw_session = request._request.COOKIES.get(settings.SESSION_COOKIE_NAME)
for key, value in request.META.items(): for key, value in request.META.items():
if not isinstance(value, str): if not isinstance(value, str):
continue continue
actual_value = value headers[key] = value
if raw_session in actual_value:
actual_value = actual_value.replace(
raw_session, SafeExceptionReporterFilter.cleansed_substitute
)
headers[key] = actual_value
return headers return headers
def get_http_host(self, request: Request) -> str: def get_http_host(self, request: Request) -> str:

View File

@ -1,16 +1,12 @@
"""authentik administration overview""" """authentik administration overview"""
from socket import gethostname
from django.conf import settings from django.conf import settings
from drf_spectacular.utils import extend_schema, inline_serializer from drf_spectacular.utils import extend_schema, inline_serializer
from packaging.version import parse from rest_framework.fields import IntegerField
from rest_framework.fields import BooleanField, CharField
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from authentik import get_full_version
from authentik.rbac.permissions import HasPermission from authentik.rbac.permissions import HasPermission
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
@ -20,38 +16,11 @@ class WorkerView(APIView):
permission_classes = [HasPermission("authentik_rbac.view_system_info")] permission_classes = [HasPermission("authentik_rbac.view_system_info")]
@extend_schema( @extend_schema(responses=inline_serializer("Workers", fields={"count": IntegerField()}))
responses=inline_serializer(
"Worker",
fields={
"worker_id": CharField(),
"version": CharField(),
"version_matching": BooleanField(),
},
many=True,
)
)
def get(self, request: Request) -> Response: def get(self, request: Request) -> Response:
"""Get currently connected worker count.""" """Get currently connected worker count."""
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5) count = len(CELERY_APP.control.ping(timeout=0.5))
our_version = parse(get_full_version())
response = []
for worker in raw:
key = list(worker.keys())[0]
version = worker[key].get("version")
version_matching = False
if version:
version_matching = parse(version) == our_version
response.append(
{"worker_id": key, "version": version, "version_matching": version_matching}
)
# In debug we run with `task_always_eager`, so tasks are ran on the main process # In debug we run with `task_always_eager`, so tasks are ran on the main process
if settings.DEBUG: # pragma: no cover if settings.DEBUG: # pragma: no cover
response.append( count += 1
{ return Response({"count": count})
"worker_id": f"authentik-debug@{gethostname()}",
"version": get_full_version(),
"version_matching": True,
}
)
return Response(response)

View File

@ -1,10 +1,11 @@
"""authentik admin app config""" """authentik admin app config"""
from prometheus_client import Info from prometheus_client import Gauge, Info
from authentik.blueprints.apps import ManagedAppConfig from authentik.blueprints.apps import ManagedAppConfig
PROM_INFO = Info("authentik_version", "Currently running authentik version") PROM_INFO = Info("authentik_version", "Currently running authentik version")
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
class AuthentikAdminConfig(ManagedAppConfig): class AuthentikAdminConfig(ManagedAppConfig):

View File

@ -1,35 +1,14 @@
"""admin signals""" """admin signals"""
from django.dispatch import receiver from django.dispatch import receiver
from packaging.version import parse
from prometheus_client import Gauge
from authentik import get_full_version from authentik.admin.apps import GAUGE_WORKERS
from authentik.root.celery import CELERY_APP from authentik.root.celery import CELERY_APP
from authentik.root.monitoring import monitoring_set from authentik.root.monitoring import monitoring_set
GAUGE_WORKERS = Gauge(
"authentik_admin_workers",
"Currently connected workers, their versions and if they are the same version as authentik",
["version", "version_matched"],
)
_version = parse(get_full_version())
@receiver(monitoring_set) @receiver(monitoring_set)
def monitoring_set_workers(sender, **kwargs): def monitoring_set_workers(sender, **kwargs):
"""Set worker gauge""" """Set worker gauge"""
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5) count = len(CELERY_APP.control.ping(timeout=0.5))
worker_version_count = {} GAUGE_WORKERS.set(count)
for worker in raw:
key = list(worker.keys())[0]
version = worker[key].get("version")
version_matching = False
if version:
version_matching = parse(version) == _version
worker_version_count.setdefault(version, {"count": 0, "matching": version_matching})
worker_version_count[version]["count"] += 1
for version, stats in worker_version_count.items():
GAUGE_WORKERS.labels(version, stats["matching"]).set(stats["count"])

View File

@ -34,7 +34,7 @@ class TestAdminAPI(TestCase):
response = self.client.get(reverse("authentik_api:admin_workers")) response = self.client.get(reverse("authentik_api:admin_workers"))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
body = loads(response.content) body = loads(response.content)
self.assertEqual(len(body), 0) self.assertEqual(body["count"], 0)
def test_metrics(self): def test_metrics(self):
"""Test metrics API""" """Test metrics API"""

View File

@ -0,0 +1,67 @@
"""API Authorization"""
from django.conf import settings
from django.db.models import Model
from django.db.models.query import QuerySet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.authentication import get_authorization_header
from rest_framework.filters import BaseFilterBackend
from rest_framework.permissions import BasePermission
from rest_framework.request import Request
from authentik.api.authentication import validate_auth
from authentik.rbac.filters import ObjectFilter
class OwnerFilter(BaseFilterBackend):
"""Filter objects by their owner"""
owner_key = "user"
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
if request.user.is_superuser:
return queryset
return queryset.filter(**{self.owner_key: request.user})
class SecretKeyFilter(DjangoFilterBackend):
"""Allow access to all objects when authenticated with secret key as token.
Replaces both DjangoFilterBackend and ObjectFilter"""
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
auth_header = get_authorization_header(request)
token = validate_auth(auth_header)
if token and token == settings.SECRET_KEY:
return queryset
queryset = ObjectFilter().filter_queryset(request, queryset, view)
return super().filter_queryset(request, queryset, view)
class OwnerPermissions(BasePermission):
"""Authorize requests by an object's owner matching the requesting user"""
owner_key = "user"
def has_permission(self, request: Request, view) -> bool:
"""If the user is authenticated, we allow all requests here. For listing, the
object-level permissions are done by the filter backend"""
return request.user.is_authenticated
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
"""Check if the object's owner matches the currently logged in user"""
if not hasattr(obj, self.owner_key):
return False
owner = getattr(obj, self.owner_key)
if owner != request.user:
return False
return True
class OwnerSuperuserPermissions(OwnerPermissions):
"""Similar to OwnerPermissions, except always allow access for superusers"""
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
if request.user.is_superuser:
return True
return super().has_object_permission(request, view, obj)

View File

@ -1,68 +0,0 @@
"""Test and debug Blueprints"""
import atexit
import readline
from pathlib import Path
from pprint import pformat
from sys import exit as sysexit
from textwrap import indent
from django.core.management.base import BaseCommand, no_translations
from structlog.stdlib import get_logger
from yaml import load
from authentik.blueprints.v1.common import BlueprintLoader, EntryInvalidError
from authentik.core.management.commands.shell import get_banner_text
from authentik.lib.utils.errors import exception_to_string
LOGGER = get_logger()
class Command(BaseCommand):
"""Test and debug Blueprints"""
lines = []
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
histfolder = Path("~").expanduser() / Path(".local/share/authentik")
histfolder.mkdir(parents=True, exist_ok=True)
histfile = histfolder / Path("blueprint_shell_history")
readline.parse_and_bind("tab: complete")
readline.parse_and_bind("set editing-mode vi")
try:
readline.read_history_file(str(histfile))
except FileNotFoundError:
pass
atexit.register(readline.write_history_file, str(histfile))
@no_translations
def handle(self, *args, **options):
"""Interactively debug blueprint files"""
self.stdout.write(get_banner_text("Blueprint shell"))
self.stdout.write("Type '.eval' to evaluate previously entered statement(s).")
def do_eval():
yaml_input = "\n".join([line for line in self.lines if line])
data = load(yaml_input, BlueprintLoader)
self.stdout.write(pformat(data))
self.lines = []
while True:
try:
line = input("> ")
if line == ".eval":
do_eval()
else:
self.lines.append(line)
except EntryInvalidError as exc:
self.stdout.write("Failed to evaluate expression:")
self.stdout.write(indent(exception_to_string(exc), prefix=" "))
except EOFError:
break
except KeyboardInterrupt:
self.stdout.write()
sysexit(0)
self.stdout.write()

View File

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

View File

@ -146,10 +146,6 @@ entries:
] ]
] ]
nested_context: !Context context2 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: identifiers:
name: test name: test
conditions: conditions:

View File

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

View File

@ -24,10 +24,6 @@ from authentik.lib.sentry import SentryIgnoredException
from authentik.policies.models import PolicyBindingModel 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]: def get_attrs(obj: SerializerModel) -> dict[str, Any]:
"""Get object's attributes via their serializer, and convert it to a normal dict""" """Get object's attributes via their serializer, and convert it to a normal dict"""
serializer: Serializer = obj.serializer(obj) serializer: Serializer = obj.serializer(obj)
@ -202,9 +198,6 @@ class Blueprint:
class YAMLTag: class YAMLTag:
"""Base class for all YAML Tags""" """Base class for all YAML Tags"""
def __repr__(self) -> str:
return str(self.resolve(BlueprintEntry(""), Blueprint()))
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any: def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
"""Implement yaml tag logic""" """Implement yaml tag logic"""
raise NotImplementedError raise NotImplementedError
@ -563,53 +556,6 @@ class Value(EnumeratedItem):
raise EntryInvalidError.from_entry(f"Empty/invalid context: {context}", entry) from exc 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): class BlueprintDumper(SafeDumper):
"""Dump dataclasses to yaml""" """Dump dataclasses to yaml"""
@ -660,7 +606,6 @@ class BlueprintLoader(SafeLoader):
self.add_constructor("!Enumerate", Enumerate) self.add_constructor("!Enumerate", Enumerate)
self.add_constructor("!Value", Value) self.add_constructor("!Value", Value)
self.add_constructor("!Index", Index) self.add_constructor("!Index", Index)
self.add_constructor("!AtIndex", AtIndex)
class EntryInvalidError(SentryIgnoredException): class EntryInvalidError(SentryIgnoredException):

View File

@ -65,12 +65,7 @@ from authentik.lib.utils.reflection import get_apps
from authentik.outposts.models import OutpostServiceConnection from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel from authentik.policies.models import Policy, PolicyBindingModel
from authentik.policies.reputation.models import Reputation from authentik.policies.reputation.models import Reputation
from authentik.providers.oauth2.models import ( from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
AccessToken,
AuthorizationCode,
DeviceToken,
RefreshToken,
)
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
from authentik.rbac.models import Role from authentik.rbac.models import Role
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
@ -130,7 +125,6 @@ def excluded_models() -> list[type[Model]]:
MicrosoftEntraProviderGroup, MicrosoftEntraProviderGroup,
EndpointDevice, EndpointDevice,
EndpointDeviceConnection, EndpointDeviceConnection,
DeviceToken,
) )
@ -299,11 +293,7 @@ class Importer:
serializer_kwargs = {} serializer_kwargs = {}
model_instance = existing_models.first() model_instance = existing_models.first()
if ( if not isinstance(model(), BaseMetaModel) and model_instance:
not isinstance(model(), BaseMetaModel)
and model_instance
and entry.state != BlueprintEntryDesiredState.MUST_CREATED
):
self.logger.debug( self.logger.debug(
"Initialise serializer with instance", "Initialise serializer with instance",
model=model, model=model,
@ -313,12 +303,11 @@ class Importer:
serializer_kwargs["instance"] = model_instance serializer_kwargs["instance"] = model_instance
serializer_kwargs["partial"] = True serializer_kwargs["partial"] = True
elif model_instance and entry.state == BlueprintEntryDesiredState.MUST_CREATED: elif model_instance and entry.state == BlueprintEntryDesiredState.MUST_CREATED:
msg = (
f"State is set to {BlueprintEntryDesiredState.MUST_CREATED.value} "
"and object exists already",
)
raise EntryInvalidError.from_entry( raise EntryInvalidError.from_entry(
ValidationError({k: msg for k in entry.identifiers.keys()}, "unique"), (
f"State is set to {BlueprintEntryDesiredState.MUST_CREATED} "
"and object exists already",
),
entry, entry,
) )
else: else:

View File

@ -159,7 +159,7 @@ def blueprints_discovery(self: SystemTask, path: str | None = None):
check_blueprint_v1_file(blueprint) check_blueprint_v1_file(blueprint)
count += 1 count += 1
self.set_status( self.set_status(
TaskStatus.SUCCESSFUL, _("Successfully imported {count} files.".format(count=count)) TaskStatus.SUCCESSFUL, _("Successfully imported %(count)d files." % {"count": count})
) )

View File

@ -14,10 +14,10 @@ from rest_framework.response import Response
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import SecretKeyFilter
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.rbac.filters import SecretKeyFilter
from authentik.tenants.utils import get_current_tenant from authentik.tenants.utils import get_current_tenant
@ -84,8 +84,8 @@ class CurrentBrandSerializer(PassiveSerializer):
matched_domain = CharField(source="domain") matched_domain = CharField(source="domain")
branding_title = CharField() branding_title = CharField()
branding_logo = CharField(source="branding_logo_url") branding_logo = CharField()
branding_favicon = CharField(source="branding_favicon_url") branding_favicon = CharField()
ui_footer_links = ListField( ui_footer_links = ListField(
child=FooterLinkSerializer(), child=FooterLinkSerializer(),
read_only=True, read_only=True,

View File

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

View File

@ -10,7 +10,6 @@ from structlog.stdlib import get_logger
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow from authentik.flows.models import Flow
from authentik.lib.config import CONFIG
from authentik.lib.models import SerializerModel from authentik.lib.models import SerializerModel
LOGGER = get_logger() LOGGER = get_logger()
@ -72,18 +71,6 @@ class Brand(SerializerModel):
) )
attributes = models.JSONField(default=dict, blank=True) attributes = models.JSONField(default=dict, blank=True)
def branding_logo_url(self) -> str:
"""Get branding_logo with the correct prefix"""
if self.branding_logo.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_logo
return self.branding_logo
def branding_favicon_url(self) -> str:
"""Get branding_favicon with the correct prefix"""
if self.branding_favicon.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
return self.branding_favicon
@property @property
def serializer(self) -> Serializer: def serializer(self) -> Serializer:
from authentik.brands.api import BrandSerializer from authentik.brands.api import BrandSerializer

View File

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

@ -209,7 +209,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
@extend_schema( @extend_schema(
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
name="list_rbac", name="superuser_full_list",
location=OpenApiParameter.QUERY, location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL, type=OpenApiTypes.BOOL,
), ),
@ -229,8 +229,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Custom list method that checks Policy based access instead of guardian""" """Custom list method that checks Policy based access instead of guardian"""
should_cache = request.query_params.get("search", "") == "" should_cache = request.query_params.get("search", "") == ""
list_rbac = str(request.query_params.get("list_rbac", "false")).lower() == "true" superuser_full_list = (
if list_rbac: str(request.query_params.get("superuser_full_list", "false")).lower() == "true"
)
if superuser_full_list and request.user.is_superuser:
return super().list(request) return super().list(request)
only_with_launch_url = str( only_with_launch_url = str(

View File

@ -2,12 +2,16 @@
from typing import TypedDict from typing import TypedDict
from django_filters.rest_framework import DjangoFilterBackend
from guardian.utils import get_anonymous_user
from rest_framework import mixins from rest_framework import mixins
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from ua_parser import user_agent_parser from ua_parser import user_agent_parser
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.core.models import AuthenticatedSession from authentik.core.models import AuthenticatedSession
@ -106,4 +110,11 @@ class AuthenticatedSessionViewSet(
search_fields = ["user__username", "last_ip", "last_user_agent"] search_fields = ["user__username", "last_ip", "last_user_agent"]
filterset_fields = ["user__username", "last_ip", "last_user_agent"] filterset_fields = ["user__username", "last_ip", "last_user_agent"]
ordering = ["user__username"] ordering = ["user__username"]
owner_field = "user" permission_classes = [OwnerSuperuserPermissions]
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)

View File

@ -2,16 +2,19 @@
from collections.abc import Iterable from collections.abc import Iterable
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.parsers import MultiPartParser from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.object_types import TypesMixin from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
@ -156,9 +159,9 @@ class SourceViewSet(
class UserSourceConnectionSerializer(SourceSerializer): class UserSourceConnectionSerializer(SourceSerializer):
"""User source connection""" """OAuth Source Serializer"""
source_obj = SourceSerializer(read_only=True, source="source") source = SourceSerializer(read_only=True)
class Meta: class Meta:
model = UserSourceConnection model = UserSourceConnection
@ -166,10 +169,10 @@ class UserSourceConnectionSerializer(SourceSerializer):
"pk", "pk",
"user", "user",
"source", "source",
"source_obj",
"created", "created",
] ]
extra_kwargs = { extra_kwargs = {
"user": {"read_only": True},
"created": {"read_only": True}, "created": {"read_only": True},
} }
@ -186,16 +189,17 @@ class UserSourceConnectionViewSet(
queryset = UserSourceConnection.objects.all() queryset = UserSourceConnection.objects.all()
serializer_class = UserSourceConnectionSerializer serializer_class = UserSourceConnectionSerializer
permission_classes = [OwnerSuperuserPermissions]
filterset_fields = ["user", "source__slug"] filterset_fields = ["user", "source__slug"]
search_fields = ["source__slug"] search_fields = ["source__slug"]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug", "pk"] ordering = ["source__slug", "pk"]
owner_field = "user"
class GroupSourceConnectionSerializer(SourceSerializer): class GroupSourceConnectionSerializer(SourceSerializer):
"""Group Source Connection""" """Group Source Connection Serializer"""
source_obj = SourceSerializer(read_only=True) source = SourceSerializer(read_only=True)
class Meta: class Meta:
model = GroupSourceConnection model = GroupSourceConnection
@ -203,11 +207,12 @@ class GroupSourceConnectionSerializer(SourceSerializer):
"pk", "pk",
"group", "group",
"source", "source",
"source_obj",
"identifier", "identifier",
"created", "created",
] ]
extra_kwargs = { extra_kwargs = {
"group": {"read_only": True},
"identifier": {"read_only": True},
"created": {"read_only": True}, "created": {"read_only": True},
} }
@ -224,7 +229,8 @@ class GroupSourceConnectionViewSet(
queryset = GroupSourceConnection.objects.all() queryset = GroupSourceConnection.objects.all()
serializer_class = GroupSourceConnectionSerializer serializer_class = GroupSourceConnectionSerializer
permission_classes = [OwnerSuperuserPermissions]
filterset_fields = ["group", "source__slug"] filterset_fields = ["group", "source__slug"]
search_fields = ["source__slug"] search_fields = ["source__slug"]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug", "pk"] ordering = ["source__slug", "pk"]
owner_field = "user"

View File

@ -3,15 +3,18 @@
from typing import Any from typing import Any
from django.utils.timezone import now from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm, get_anonymous_user
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField from rest_framework.fields import CharField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.blueprints.api import ManagedSerializer from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
@ -135,11 +138,16 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
"managed", "managed",
] ]
ordering = ["identifier", "expires"] ordering = ["identifier", "expires"]
owner_field = "user" permission_classes = [OwnerSuperuserPermissions]
rbac_allow_create_without_perm = True filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
def get_queryset(self):
user = self.request.user if self.request else get_anonymous_user()
if user.is_superuser:
return super().get_queryset()
return super().get_queryset().filter(user=user.pk)
def perform_create(self, serializer: TokenSerializer): def perform_create(self, serializer: TokenSerializer):
# TODO: better permission check
if not self.request.user.is_superuser: if not self.request.user.is_superuser:
instance = serializer.save( instance = serializer.save(
user=self.request.user, user=self.request.user,

View File

@ -1,12 +1,10 @@
"""transactional application and provider creation""" """transactional application and provider creation"""
from django.apps import apps from django.apps import apps
from django.db.models import Model
from django.utils.translation import gettext as _
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_field from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_field
from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField, ListField from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField, ListField
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
@ -22,9 +20,8 @@ from authentik.blueprints.v1.common import (
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.core.api.applications import ApplicationSerializer from authentik.core.api.applications import ApplicationSerializer
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Application, Provider from authentik.core.models import Provider
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.bindings import PolicyBindingSerializer
def get_provider_serializer_mapping(): def get_provider_serializer_mapping():
@ -48,20 +45,6 @@ class TransactionProviderField(DictField):
"""Dictionary field which can hold provider creation data""" """Dictionary field which can hold provider creation data"""
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"]
class TransactionApplicationSerializer(PassiveSerializer): class TransactionApplicationSerializer(PassiveSerializer):
"""Serializer for creating a provider and an application in one transaction""" """Serializer for creating a provider and an application in one transaction"""
@ -69,8 +52,6 @@ class TransactionApplicationSerializer(PassiveSerializer):
provider_model = ChoiceField(choices=list(get_provider_serializer_mapping().keys())) provider_model = ChoiceField(choices=list(get_provider_serializer_mapping().keys()))
provider = TransactionProviderField() provider = TransactionProviderField()
policy_bindings = TransactionPolicyBindingSerializer(many=True, required=False)
_provider_model: type[Provider] = None _provider_model: type[Provider] = None
def validate_provider_model(self, fq_model_name: str) -> str: def validate_provider_model(self, fq_model_name: str) -> str:
@ -115,19 +96,6 @@ class TransactionApplicationSerializer(PassiveSerializer):
id="app", id="app",
) )
) )
for binding in attrs.get("policy_bindings", []):
binding["target"] = KeyOf(None, ScalarNode(tag="", value="app"))
for key, value in binding.items():
if not isinstance(value, Model):
continue
binding[key] = value.pk
blueprint.entries.append(
BlueprintEntry(
model="authentik_policies.policybinding",
state=BlueprintEntryDesiredState.MUST_CREATED,
identifiers=binding,
)
)
importer = Importer(blueprint, {}) importer = Importer(blueprint, {})
try: try:
valid, _ = importer.validate(raise_validation_errors=True) valid, _ = importer.validate(raise_validation_errors=True)
@ -152,7 +120,8 @@ class TransactionApplicationResponseSerializer(PassiveSerializer):
class TransactionalApplicationView(APIView): class TransactionalApplicationView(APIView):
"""Create provider and application and attach them in a single transaction""" """Create provider and application and attach them in a single transaction"""
permission_classes = [IsAuthenticated] # TODO: Migrate to a more specific permission
permission_classes = [IsAdminUser]
@extend_schema( @extend_schema(
request=TransactionApplicationSerializer(), request=TransactionApplicationSerializer(),
@ -164,23 +133,8 @@ class TransactionalApplicationView(APIView):
"""Convert data into a blueprint, validate it and apply it""" """Convert data into a blueprint, validate it and apply it"""
data = TransactionApplicationSerializer(data=request.data) data = TransactionApplicationSerializer(data=request.data)
data.is_valid(raise_exception=True) data.is_valid(raise_exception=True)
blueprint: Blueprint = data.validated_data
for entry in blueprint.entries: importer = Importer(data.validated_data, {})
full_model = entry.get_model(blueprint)
app, __, model = full_model.partition(".")
if not request.user.has_perm(f"{app}.add_{model}"):
raise PermissionDenied(
{
entry.id: _(
"User lacks permission to create {model}".format_map(
{
"model": full_model,
}
)
)
}
)
importer = Importer(blueprint, {})
applied = importer.apply() applied = importer.apply()
response = {"applied": False, "logs": []} response = {"applied": False, "logs": []}
response["applied"] = applied response["applied"] = applied

View File

@ -585,7 +585,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"""Set password for user""" """Set password for user"""
user: User = self.get_object() user: User = self.get_object()
try: try:
user.set_password(request.data.get("password"), request=request) user.set_password(request.data.get("password"))
user.save() user.save()
except (ValidationError, IntegrityError) as exc: except (ValidationError, IntegrityError) as exc:
LOGGER.debug("Failed to set password", exc=exc) LOGGER.debug("Failed to set password", exc=exc)
@ -666,12 +666,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@permission_required("authentik_core.impersonate") @permission_required("authentik_core.impersonate")
@extend_schema( @extend_schema(
request=inline_serializer( request=OpenApiTypes.NONE,
"ImpersonationSerializer",
{
"reason": CharField(required=True),
},
),
responses={ responses={
"204": OpenApiResponse(description="Successfully started impersonation"), "204": OpenApiResponse(description="Successfully started impersonation"),
"401": OpenApiResponse(description="Access denied"), "401": OpenApiResponse(description="Access denied"),
@ -684,7 +679,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
LOGGER.debug("User attempted to impersonate", user=request.user) LOGGER.debug("User attempted to impersonate", user=request.user)
return Response(status=401) return Response(status=401)
user_to_be = self.get_object() user_to_be = self.get_object()
reason = request.data.get("reason", "")
# Check both object-level perms and global perms # Check both object-level perms and global perms
if not request.user.has_perm( if not request.user.has_perm(
"authentik_core.impersonate", user_to_be "authentik_core.impersonate", user_to_be
@ -694,16 +688,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
if user_to_be.pk == self.request.user.pk: if user_to_be.pk == self.request.user.pk:
LOGGER.debug("User attempted to impersonate themselves", user=request.user) LOGGER.debug("User attempted to impersonate themselves", user=request.user)
return Response(status=401) return Response(status=401)
if not reason and request.tenant.impersonation_require_reason:
LOGGER.debug(
"User attempted to impersonate without providing a reason", user=request.user
)
return Response(status=401)
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED, reason=reason).from_http(request, user_to_be) Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return Response(status=201) return Response(status=201)

View File

@ -44,12 +44,13 @@ class TokenBackend(InbuiltBackend):
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
) -> User | None: ) -> User | None:
try: try:
user = User._default_manager.get_by_natural_key(username) user = User._default_manager.get_by_natural_key(username)
except User.DoesNotExist: except User.DoesNotExist:
# Run the default password hasher once to reduce the timing # Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760). # difference between an existing and a nonexistent user (#20760).
User().set_password(password, request=request) User().set_password(password)
return None return None
tokens = Token.filter_not_expired( tokens = Token.filter_not_expired(

View File

@ -58,7 +58,6 @@ class PropertyMappingEvaluator(BaseEvaluator):
self._context["user"] = user self._context["user"] = user
if request: if request:
req.http_request = request req.http_request = request
self._context["http_request"] = request
req.context.update(**kwargs) req.context.update(**kwargs)
self._context["request"] = req self._context["request"] = req
self._context.update(**kwargs) self._context.update(**kwargs)

View File

@ -17,9 +17,7 @@ from authentik.events.middleware import should_log_model
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict from authentik.events.utils import model_to_dict
BANNER_TEXT = f"""### authentik shell ({get_full_version()})
def get_banner_text(shell_type="shell") -> str:
return f"""### authentik {shell_type} ({get_full_version()})
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """ ### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
@ -116,4 +114,4 @@ class Command(BaseCommand):
readline.parse_and_bind("tab: complete") readline.parse_and_bind("tab: complete")
# Run interactive shell # Run interactive shell
code.interact(banner=get_banner_text(), local=namespace) code.interact(banner=BANNER_TEXT, local=namespace)

View File

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

View File

@ -1,45 +0,0 @@
# 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

@ -1,45 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0041_applicationentitlement"),
]
operations = [
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(fields=["expires"], name="authentik_c_expires_08251d_idx"),
),
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(fields=["expiring"], name="authentik_c_expirin_9cd839_idx"),
),
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(
fields=["expiring", "expires"], name="authentik_c_expirin_195a84_idx"
),
),
migrations.AddIndex(
model_name="authenticatedsession",
index=models.Index(fields=["session_key"], name="authentik_c_session_d0f005_idx"),
),
migrations.AddIndex(
model_name="token",
index=models.Index(fields=["expires"], name="authentik_c_expires_a62b4b_idx"),
),
migrations.AddIndex(
model_name="token",
index=models.Index(fields=["expiring"], name="authentik_c_expirin_a1b838_idx"),
),
migrations.AddIndex(
model_name="token",
index=models.Index(
fields=["expiring", "expires"], name="authentik_c_expirin_ba04d9_idx"
),
),
]

View File

@ -1,57 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-08 17:39
from django.db import migrations
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_user_debug_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from django.apps import apps as real_apps
from django.contrib.auth.management import create_permissions
db_alias = schema_editor.connection.alias
User = apps.get_model("authentik_core", "User")
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
# Permissions are only created _after_ migrations are run
# - https://github.com/django/django/blob/43cdfa8b20e567a801b7d0a09ec67ddd062d5ea4/django/contrib/auth/apps.py#L19
# - https://stackoverflow.com/a/72029063/1870445
create_permissions(real_apps.get_app_config("authentik_core"), using=db_alias)
Permission = apps.get_model("auth", "Permission")
new_prem = Permission.objects.using(db_alias).get(codename="user_view_debug")
db_alias = schema_editor.connection.alias
for user in User.objects.using(db_alias).filter(
**{f"attributes__{USER_ATTRIBUTE_DEBUG}": True}
):
user.permissions.add(new_prem)
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="user",
options={
"permissions": [
("reset_user_password", "Reset Password"),
("impersonate", "Can impersonate other users"),
("assign_user_permissions", "Can assign permissions to users"),
("unassign_user_permissions", "Can unassign permissions from users"),
("preview_user", "Can preview user data sent to providers"),
("view_user_applications", "View applications the user has access to"),
("user_view_debug", "User receives additional details for error messages"),
],
"verbose_name": "User",
"verbose_name_plural": "Users",
},
),
migrations.RunPython(migrate_user_debug_attribute),
]

View File

@ -41,6 +41,7 @@ from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGT
from authentik.tenants.utils import get_current_tenant, get_unique_identifier from authentik.tenants.utils import get_current_tenant, get_unique_identifier
LOGGER = get_logger() LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated" USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires" USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout" USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
@ -281,7 +282,6 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
("unassign_user_permissions", _("Can unassign permissions from users")), ("unassign_user_permissions", _("Can unassign permissions from users")),
("preview_user", _("Can preview user data sent to providers")), ("preview_user", _("Can preview user data sent to providers")),
("view_user_applications", _("View applications the user has access to")), ("view_user_applications", _("View applications the user has access to")),
("user_view_debug", _("User receives additional details for error messages")),
] ]
indexes = [ indexes = [
models.Index(fields=["last_login"]), models.Index(fields=["last_login"]),
@ -314,32 +314,6 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
always_merger.merge(final_attributes, self.attributes) always_merger.merge(final_attributes, self.attributes)
return final_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 @property
def serializer(self) -> Serializer: def serializer(self) -> Serializer:
from authentik.core.api.users import UserSerializer from authentik.core.api.users import UserSerializer
@ -356,13 +330,13 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
"""superuser == staff user""" """superuser == staff user"""
return self.is_superuser # type: ignore return self.is_superuser # type: ignore
def set_password(self, raw_password, signal=True, sender=None, request=None): def set_password(self, raw_password, signal=True, sender=None):
if self.pk and signal: if self.pk and signal:
from authentik.core.signals import password_changed from authentik.core.signals import password_changed
if not sender: if not sender:
sender = self sender = self
password_changed.send(sender=sender, user=self, password=raw_password, request=request) password_changed.send(sender=sender, user=self, password=raw_password)
self.password_change_date = now() self.password_change_date = now()
return super().set_password(raw_password) return super().set_password(raw_password)
@ -607,31 +581,6 @@ class Application(SerializerModel, PolicyBindingModel):
verbose_name_plural = _("Applications") 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): class SourceUserMatchingModes(models.TextChoices):
"""Different modes a source can handle new/returning users""" """Different modes a source can handle new/returning users"""
@ -846,11 +795,6 @@ class ExpiringModel(models.Model):
class Meta: class Meta:
abstract = True abstract = True
indexes = [
models.Index(fields=["expires"]),
models.Index(fields=["expiring"]),
models.Index(fields=["expiring", "expires"]),
]
def expire_action(self, *args, **kwargs): def expire_action(self, *args, **kwargs):
"""Handler which is called when this object is expired. By """Handler which is called when this object is expired. By
@ -906,7 +850,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
class Meta: class Meta:
verbose_name = _("Token") verbose_name = _("Token")
verbose_name_plural = _("Tokens") verbose_name_plural = _("Tokens")
indexes = ExpiringModel.Meta.indexes + [ indexes = [
models.Index(fields=["identifier"]), models.Index(fields=["identifier"]),
models.Index(fields=["key"]), models.Index(fields=["key"]),
] ]
@ -1006,9 +950,6 @@ class AuthenticatedSession(ExpiringModel):
class Meta: class Meta:
verbose_name = _("Authenticated Session") verbose_name = _("Authenticated Session")
verbose_name_plural = _("Authenticated Sessions") verbose_name_plural = _("Authenticated Sessions")
indexes = ExpiringModel.Meta.indexes + [
models.Index(fields=["session_key"]),
]
def __str__(self) -> str: def __str__(self) -> str:
return f"Authenticated Session {self.session_key[:10]}" return f"Authenticated Session {self.session_key[:10]}"

View File

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

View File

@ -9,9 +9,6 @@
versionFamily: "{{ version_family }}", versionFamily: "{{ version_family }}",
versionSubdomain: "{{ version_subdomain }}", versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}", build: "{{ build }}",
api: {
base: "{{ base_url }}",
},
}; };
window.addEventListener("DOMContentLoaded", function () { window.addEventListener("DOMContentLoaded", function () {
{% for message in messages %} {% for message in messages %}

View File

@ -9,8 +9,8 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title> <title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}"> <link rel="icon" href="{{ brand.branding_favicon }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}"> <link rel="shortcut icon" href="{{ brand.branding_favicon }}">
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">

View File

@ -4,7 +4,7 @@
{% load i18n %} {% load i18n %}
{% block head_before %} {% block head_before %}
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" /> <link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)"> <link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
@ -13,7 +13,7 @@
{% block head %} {% block head %}
<style> <style>
:root { :root {
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}"); --ak-flow-background: url("/static/dist/assets/images/flow_background.jpg");
--pf-c-background-image--BackgroundImage: var(--ak-flow-background); --pf-c-background-image--BackgroundImage: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background); --pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background); --pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
@ -50,7 +50,7 @@
<div class="ak-login-container"> <div class="ak-login-container">
<main class="pf-c-login__main"> <main class="pf-c-login__main">
<div class="pf-c-login__main-header pf-c-brand ak-brand"> <div class="pf-c-login__main-header pf-c-brand ak-brand">
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" /> <img src="{{ brand.branding_logo }}" alt="authentik Logo" />
</div> </div>
<header class="pf-c-login__main-header"> <header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl"> <h1 class="pf-c-title pf-m-3xl">

View File

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

@ -29,8 +29,7 @@ class TestImpersonation(APITestCase):
reverse( reverse(
"authentik_api:user-impersonate", "authentik_api:user-impersonate",
kwargs={"pk": self.other_user.pk}, kwargs={"pk": self.other_user.pk},
), )
data={"reason": "some reason"},
) )
response = self.client.get(reverse("authentik_api:user-me")) response = self.client.get(reverse("authentik_api:user-me"))
@ -56,8 +55,7 @@ class TestImpersonation(APITestCase):
reverse( reverse(
"authentik_api:user-impersonate", "authentik_api:user-impersonate",
kwargs={"pk": self.other_user.pk}, kwargs={"pk": self.other_user.pk},
), )
data={"reason": "some reason"},
) )
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
@ -77,8 +75,7 @@ class TestImpersonation(APITestCase):
reverse( reverse(
"authentik_api:user-impersonate", "authentik_api:user-impersonate",
kwargs={"pk": self.other_user.pk}, kwargs={"pk": self.other_user.pk},
), )
data={"reason": "some reason"},
) )
self.assertEqual(response.status_code, 201) self.assertEqual(response.status_code, 201)
@ -92,8 +89,7 @@ class TestImpersonation(APITestCase):
self.client.force_login(self.other_user) self.client.force_login(self.other_user)
response = self.client.post( response = self.client.post(
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}), reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})
data={"reason": "some reason"},
) )
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
@ -109,8 +105,7 @@ class TestImpersonation(APITestCase):
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.post( response = self.client.post(
reverse("authentik_api:user-impersonate", kwargs={"pk": self.other_user.pk}), reverse("authentik_api:user-impersonate", kwargs={"pk": self.other_user.pk})
data={"reason": "some reason"},
) )
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
@ -123,22 +118,7 @@ class TestImpersonation(APITestCase):
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.post( response = self.client.post(
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}), reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})
data={"reason": "some reason"},
)
self.assertEqual(response.status_code, 401)
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
self.assertEqual(response_body["user"]["username"], self.user.username)
def test_impersonate_reason_required(self):
"""test impersonation that user must provide reason"""
self.client.force_login(self.user)
response = self.client.post(
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}),
data={"reason": ""},
) )
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)

View File

@ -1,13 +1,11 @@
"""Test Transactional API""" """Test Transactional API"""
from django.urls import reverse from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from authentik.core.models import Application, Group from authentik.core.models import Application
from authentik.core.tests.utils import create_test_flow, create_test_user from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.lib.generators import generate_id from authentik.lib.generators import generate_id
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import OAuth2Provider from authentik.providers.oauth2.models import OAuth2Provider
@ -15,9 +13,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
"""Test Transactional API""" """Test Transactional API"""
def setUp(self) -> None: def setUp(self) -> None:
self.user = create_test_user() self.user = create_test_admin_user()
assign_perm("authentik_core.add_application", self.user)
assign_perm("authentik_providers_oauth2.add_oauth2provider", self.user)
def test_create_transactional(self): def test_create_transactional(self):
"""Test transactional Application + provider creation""" """Test transactional Application + provider creation"""
@ -46,66 +42,6 @@ class TestTransactionalApplicationsAPI(APITestCase):
self.assertIsNotNone(app) self.assertIsNotNone(app)
self.assertEqual(app.provider.pk, provider.pk) self.assertEqual(app.provider.pk, provider.pk)
def test_create_transactional_permission_denied(self):
"""Test transactional Application + provider creation (missing permissions)"""
self.client.force_login(self.user)
uid = generate_id()
response = self.client.put(
reverse("authentik_api:core-transactional-application"),
data={
"app": {
"name": uid,
"slug": uid,
},
"provider_model": "authentik_providers_saml.samlprovider",
"provider": {
"name": uid,
"authorization_flow": str(create_test_flow().pk),
"invalidation_flow": str(create_test_flow().pk),
"acs_url": "https://goauthentik.io",
},
},
)
self.assertJSONEqual(
response.content.decode(),
{"provider": "User lacks permission to create authentik_providers_saml.samlprovider"},
)
def test_create_transactional_bindings(self):
"""Test transactional Application + provider creation"""
assign_perm("authentik_policies.add_policybinding", self.user)
self.client.force_login(self.user)
uid = generate_id()
group = Group.objects.create(name=generate_id())
authorization_flow = create_test_flow()
response = self.client.put(
reverse("authentik_api:core-transactional-application"),
data={
"app": {
"name": uid,
"slug": uid,
},
"provider_model": "authentik_providers_oauth2.oauth2provider",
"provider": {
"name": uid,
"authorization_flow": str(authorization_flow.pk),
"invalidation_flow": str(authorization_flow.pk),
"redirect_uris": [],
},
"policy_bindings": [{"group": group.pk, "order": 0}],
},
)
self.assertJSONEqual(response.content.decode(), {"applied": True, "logs": []})
provider = OAuth2Provider.objects.filter(name=uid).first()
self.assertIsNotNone(provider)
app = Application.objects.filter(slug=uid).first()
self.assertIsNotNone(app)
self.assertEqual(app.provider.pk, provider.pk)
binding = PolicyBinding.objects.filter(target=app).first()
self.assertIsNotNone(binding)
self.assertEqual(binding.target, app)
self.assertEqual(binding.group, group)
def test_create_transactional_invalid(self): def test_create_transactional_invalid(self):
"""Test transactional Application + provider creation""" """Test transactional Application + provider creation"""
self.client.force_login(self.user) self.client.force_login(self.user)
@ -135,32 +71,3 @@ class TestTransactionalApplicationsAPI(APITestCase):
} }
}, },
) )
def test_create_transactional_duplicate_name_provider(self):
"""Test transactional Application + provider creation"""
self.client.force_login(self.user)
uid = generate_id()
OAuth2Provider.objects.create(
name=uid,
authorization_flow=create_test_flow(),
invalidation_flow=create_test_flow(),
)
response = self.client.put(
reverse("authentik_api:core-transactional-application"),
data={
"app": {
"name": uid,
"slug": uid,
},
"provider_model": "authentik_providers_oauth2.oauth2provider",
"provider": {
"name": uid,
"authorization_flow": str(create_test_flow().pk),
"invalidation_flow": str(create_test_flow().pk),
},
},
)
self.assertJSONEqual(
response.content.decode(),
{"provider": {"name": ["State is set to must_created and object exists already"]}},
)

View File

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

View File

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

View File

@ -16,7 +16,6 @@ from authentik.api.v3.config import ConfigView
from authentik.brands.api import CurrentBrandSerializer from authentik.brands.api import CurrentBrandSerializer
from authentik.brands.models import Brand from authentik.brands.models import Brand
from authentik.core.models import UserTypes from authentik.core.models import UserTypes
from authentik.lib.config import CONFIG
from authentik.policies.denied import AccessDeniedResponse from authentik.policies.denied import AccessDeniedResponse
@ -52,7 +51,6 @@ class InterfaceView(TemplateView):
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}" kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
kwargs["build"] = get_build_hash() kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs kwargs["url_kwargs"] = self.kwargs
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)

View File

@ -28,6 +28,7 @@ from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.authorization import SecretKeyFilter
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY from authentik.crypto.apps import MANAGED_KEY
@ -35,7 +36,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter, SecretKeyFilter from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -85,5 +85,5 @@ def certificate_discovery(self: SystemTask):
if dirty: if dirty:
cert.save() cert.save()
self.set_status( self.set_status(
TaskStatus.SUCCESSFUL, _("Successfully imported {count} files.".format(count=discovered)) TaskStatus.SUCCESSFUL, _("Successfully imported %(count)d files." % {"count": discovered})
) )

View File

@ -1,27 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_enterprise", "0003_remove_licenseusage_within_limits_and_more"),
]
operations = [
migrations.AddIndex(
model_name="licenseusage",
index=models.Index(fields=["expires"], name="authentik_e_expires_3f2956_idx"),
),
migrations.AddIndex(
model_name="licenseusage",
index=models.Index(fields=["expiring"], name="authentik_e_expirin_11d3d7_idx"),
),
migrations.AddIndex(
model_name="licenseusage",
index=models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_4d558f_idx"
),
),
]

View File

@ -93,4 +93,3 @@ class LicenseUsage(ExpiringModel):
class Meta: class Meta:
verbose_name = _("License Usage") verbose_name = _("License Usage")
verbose_name_plural = _("License Usage Records") verbose_name_plural = _("License Usage Records")
indexes = ExpiringModel.Meta.indexes

View File

@ -1,8 +1,11 @@
"""RAC Provider API Views""" """RAC Provider API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.groups import GroupMemberSerializer from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
@ -31,6 +34,12 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
] ]
class ConnectionTokenOwnerFilter(OwnerFilter):
"""Owner filter for connection tokens (checks session's user)"""
owner_key = "session__user"
class ConnectionTokenViewSet( class ConnectionTokenViewSet(
mixins.RetrieveModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
@ -46,4 +55,10 @@ class ConnectionTokenViewSet(
filterset_fields = ["endpoint", "session__user", "provider"] filterset_fields = ["endpoint", "session__user", "provider"]
search_fields = ["endpoint__name", "provider__name"] search_fields = ["endpoint__name", "provider__name"]
ordering = ["endpoint__name", "provider__name"] ordering = ["endpoint__name", "provider__name"]
owner_field = "session__user" permission_classes = [OwnerSuperuserPermissions]
filter_backends = [
ConnectionTokenOwnerFilter,
DjangoFilterBackend,
OrderingFilter,
SearchFilter,
]

View File

@ -96,7 +96,7 @@ class EndpointViewSet(UsedByMixin, ModelViewSet):
OpenApiTypes.STR, OpenApiTypes.STR,
), ),
OpenApiParameter( OpenApiParameter(
name="list_rbac", name="superuser_full_list",
location=OpenApiParameter.QUERY, location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL, type=OpenApiTypes.BOOL,
), ),
@ -110,8 +110,8 @@ class EndpointViewSet(UsedByMixin, ModelViewSet):
"""List accessible endpoints""" """List accessible endpoints"""
should_cache = request.GET.get("search", "") == "" should_cache = request.GET.get("search", "") == ""
list_rbac = str(request.GET.get("list_rbac", "false")).lower() == "true" superuser_full_list = str(request.GET.get("superuser_full_list", "false")).lower() == "true"
if list_rbac: if superuser_full_list and request.user.is_superuser:
return super().list(request) return super().list(request)
queryset = self._filter_queryset_for_list(self.get_queryset()) queryset = self._filter_queryset_for_list(self.get_queryset())

View File

@ -1,28 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
("authentik_providers_rac", "0005_alter_racpropertymapping_options"),
]
operations = [
migrations.AddIndex(
model_name="connectiontoken",
index=models.Index(fields=["expires"], name="authentik_p_expires_91f148_idx"),
),
migrations.AddIndex(
model_name="connectiontoken",
index=models.Index(fields=["expiring"], name="authentik_p_expirin_59a5a7_idx"),
),
migrations.AddIndex(
model_name="connectiontoken",
index=models.Index(
fields=["expiring", "expires"], name="authentik_p_expirin_aed3ca_idx"
),
),
]

View File

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

View File

@ -6,8 +6,8 @@
<script src="{% versioned_script 'dist/enterprise/rac/index-%v.js' %}" type="module"></script> <script src="{% versioned_script 'dist/enterprise/rac/index-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)"> <meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon_url }}"> <link rel="icon" href="{{ tenant.branding_favicon }}">
<link rel="shortcut icon" href="{{ tenant.branding_favicon_url }}"> <link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
{% include "base/header_js.html" %} {% include "base/header_js.html" %}
{% endblock %} {% endblock %}

View File

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

View File

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

View File

@ -1,11 +1,14 @@
"""AuthenticatorEndpointGDTCStage API Views""" """AuthenticatorEndpointGDTCStage API Views"""
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import ( from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
@ -64,7 +67,8 @@ class EndpointDeviceViewSet(
search_fields = ["name"] search_fields = ["name"]
filterset_fields = ["name"] filterset_fields = ["name"]
ordering = ["name"] ordering = ["name"]
owner_field = "user" permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
class EndpointAdminDeviceViewSet(ModelViewSet): class EndpointAdminDeviceViewSet(ModelViewSet):

View File

@ -1,15 +1,17 @@
"""Notification API Views""" """Notification API Views"""
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import mixins from rest_framework import mixins
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField from rest_framework.fields import ReadOnlyField
from rest_framework.permissions import IsAuthenticated from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer from authentik.core.api.utils import ModelSerializer
from authentik.events.api.events import EventSerializer from authentik.events.api.events import EventSerializer
@ -55,7 +57,8 @@ class NotificationViewSet(
"seen", "seen",
"user", "user",
] ]
owner_field = "user" permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
@extend_schema( @extend_schema(
request=OpenApiTypes.NONE, request=OpenApiTypes.NONE,
@ -63,7 +66,7 @@ class NotificationViewSet(
204: OpenApiResponse(description="Marked tasks as read successfully."), 204: OpenApiResponse(description="Marked tasks as read successfully."),
}, },
) )
@action(detail=False, methods=["post"], permission_classes=[IsAuthenticated]) @action(detail=False, methods=["post"])
def mark_all_seen(self, request: Request) -> Response: def mark_all_seen(self, request: Request) -> Response:
"""Mark all the user's notifications as seen""" """Mark all the user's notifications as seen"""
Notification.objects.filter(user=request.user, seen=False).update(seen=True) Notification.objects.filter(user=request.user, seen=False).update(seen=True)

View File

@ -1,41 +0,0 @@
# Generated by Django 5.0.10 on 2025-01-13 18:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_events", "0007_event_authentik_e_action_9a9dd9_idx_and_more"),
]
operations = [
migrations.AddIndex(
model_name="event",
index=models.Index(fields=["expires"], name="authentik_e_expires_8c73a8_idx"),
),
migrations.AddIndex(
model_name="event",
index=models.Index(fields=["expiring"], name="authentik_e_expirin_b5cb5e_idx"),
),
migrations.AddIndex(
model_name="event",
index=models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_e37180_idx"
),
),
migrations.AddIndex(
model_name="systemtask",
index=models.Index(fields=["expires"], name="authentik_e_expires_4d3985_idx"),
),
migrations.AddIndex(
model_name="systemtask",
index=models.Index(fields=["expiring"], name="authentik_e_expirin_81d649_idx"),
),
migrations.AddIndex(
model_name="systemtask",
index=models.Index(
fields=["expiring", "expires"], name="authentik_e_expirin_eb3598_idx"
),
),
]

View File

@ -60,7 +60,7 @@ def default_event_duration():
"""Default duration an Event is saved. """Default duration an Event is saved.
This is used as a fallback when no brand is available""" This is used as a fallback when no brand is available"""
try: try:
tenant = get_current_tenant(only=["event_retention"]) tenant = get_current_tenant()
return now() + timedelta_from_string(tenant.event_retention) return now() + timedelta_from_string(tenant.event_retention)
except Tenant.DoesNotExist: except Tenant.DoesNotExist:
return now() + timedelta(days=365) return now() + timedelta(days=365)
@ -306,7 +306,7 @@ class Event(SerializerModel, ExpiringModel):
class Meta: class Meta:
verbose_name = _("Event") verbose_name = _("Event")
verbose_name_plural = _("Events") verbose_name_plural = _("Events")
indexes = ExpiringModel.Meta.indexes + [ indexes = [
models.Index(fields=["action"]), models.Index(fields=["action"]),
models.Index(fields=["user"]), models.Index(fields=["user"]),
models.Index(fields=["app"]), models.Index(fields=["app"]),
@ -694,4 +694,3 @@ class SystemTask(SerializerModel, ExpiringModel):
permissions = [("run_task", _("Run task"))] permissions = [("run_task", _("Run task"))]
verbose_name = _("System Task") verbose_name = _("System Task")
verbose_name_plural = _("System Tasks") verbose_name_plural = _("System Tasks")
indexes = ExpiringModel.Meta.indexes

View File

@ -106,9 +106,9 @@ def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_
@receiver(password_changed) @receiver(password_changed)
def on_password_changed(sender, user: User, password: str, request: HttpRequest | None, **_): def on_password_changed(sender, user: User, password: str, **_):
"""Log password change""" """Log password change"""
Event.new(EventAction.PASSWORD_SET).from_http(request, user=user) Event.new(EventAction.PASSWORD_SET).from_http(None, user=user)
@receiver(post_save, sender=Event) @receiver(post_save, sender=Event)

View File

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

View File

@ -1,7 +1,5 @@
"""Flow Stage API Views""" """Flow Stage API Views"""
from uuid import uuid4
from django.urls.base import reverse from django.urls.base import reverse
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework import mixins from rest_framework import mixins
@ -29,11 +27,6 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
component = SerializerMethodField() component = SerializerMethodField()
flow_set = FlowSetSerializer(many=True, required=False) flow_set = FlowSetSerializer(many=True, required=False)
def to_representation(self, instance: Stage):
if isinstance(instance, Stage) and instance.is_in_memory:
instance.stage_uuid = uuid4()
return super().to_representation(instance)
def get_component(self, obj: Stage) -> str: def get_component(self, obj: Stage) -> str:
"""Get object type so that we know how to edit the object""" """Get object type so that we know how to edit the object"""
if obj.__class__ == Stage: if obj.__class__ == Stage:

View File

@ -97,9 +97,12 @@ class FlowErrorChallenge(Challenge):
if not request or not error: if not request or not error:
return return
self.initial_data["request_id"] = request.request_id self.initial_data["request_id"] = request.request_id
from authentik.core.models import USER_ATTRIBUTE_DEBUG
if request.user and request.user.is_authenticated: if request.user and request.user.is_authenticated:
if request.user.has_perm("authentik_core.user_view_debug"): if request.user.is_superuser or request.user.group_attributes(request).get(
USER_ATTRIBUTE_DEBUG, False
):
self.initial_data["error"] = str(error) self.initial_data["error"] = str(error)
self.initial_data["traceback"] = exception_to_string(error) self.initial_data["traceback"] = exception_to_string(error)

View File

@ -88,8 +88,7 @@ class Migration(migrations.Migration):
model_name="flowstagebinding", model_name="flowstagebinding",
name="re_evaluate_policies", name="re_evaluate_policies",
field=models.BooleanField( field=models.BooleanField(
default=False, default=False, help_text="Evaluate policies when the Stage is present to the user."
help_text="Evaluate policies when the Stage is presented to the user.",
), ),
), ),
migrations.AddField( migrations.AddField(

View File

@ -20,7 +20,7 @@ class Migration(migrations.Migration):
model_name="flowstagebinding", model_name="flowstagebinding",
name="re_evaluate_policies", name="re_evaluate_policies",
field=models.BooleanField( field=models.BooleanField(
default=True, help_text="Evaluate policies when the Stage is presented to the user." default=True, help_text="Evaluate policies when the Stage is present to the user."
), ),
), ),
] ]

View File

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

View File

@ -14,7 +14,6 @@ from structlog.stdlib import get_logger
from authentik.core.models import Token from authentik.core.models import Token
from authentik.core.types import UserSettingSerializer from authentik.core.types import UserSettingSerializer
from authentik.flows.challenge import FlowLayout from authentik.flows.challenge import FlowLayout
from authentik.lib.config import CONFIG
from authentik.lib.models import InheritanceForeignKey, SerializerModel from authentik.lib.models import InheritanceForeignKey, SerializerModel
from authentik.lib.utils.reflection import class_to_path from authentik.lib.utils.reflection import class_to_path
from authentik.policies.models import PolicyBindingModel from authentik.policies.models import PolicyBindingModel
@ -33,7 +32,6 @@ class FlowAuthenticationRequirement(models.TextChoices):
REQUIRE_AUTHENTICATED = "require_authenticated" REQUIRE_AUTHENTICATED = "require_authenticated"
REQUIRE_UNAUTHENTICATED = "require_unauthenticated" REQUIRE_UNAUTHENTICATED = "require_unauthenticated"
REQUIRE_SUPERUSER = "require_superuser" REQUIRE_SUPERUSER = "require_superuser"
REQUIRE_REDIRECT = "require_redirect"
REQUIRE_OUTPOST = "require_outpost" REQUIRE_OUTPOST = "require_outpost"
@ -102,12 +100,8 @@ class Stage(SerializerModel):
user settings are available, or a challenge.""" user settings are available, or a challenge."""
return None return None
@property
def is_in_memory(self):
return hasattr(self, "__in_memory_type")
def __str__(self): def __str__(self):
if self.is_in_memory: if hasattr(self, "__in_memory_type"):
return f"In-memory Stage {getattr(self, '__in_memory_type')}" return f"In-memory Stage {getattr(self, '__in_memory_type')}"
return f"Stage {self.name}" return f"Stage {self.name}"
@ -183,13 +177,9 @@ class Flow(SerializerModel, PolicyBindingModel):
"""Get the URL to the background image. If the name is /static or starts with http """Get the URL to the background image. If the name is /static or starts with http
it is returned as-is""" it is returned as-is"""
if not self.background: if not self.background:
return ( return "/static/dist/assets/images/flow_background.jpg"
CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg" if self.background.name.startswith("http") or self.background.name.startswith("/static"):
)
if self.background.name.startswith("http"):
return self.background.name return self.background.name
if self.background.name.startswith("/static"):
return CONFIG.get("web.path", "/")[:-1] + self.background.name
return self.background.url return self.background.url
stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True) stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True)
@ -231,7 +221,7 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
) )
re_evaluate_policies = models.BooleanField( re_evaluate_policies = models.BooleanField(
default=True, default=True,
help_text=_("Evaluate policies when the Stage is presented to the user."), help_text=_("Evaluate policies when the Stage is present to the user."),
) )
invalid_response_action = models.TextField( invalid_response_action = models.TextField(

View File

@ -1,10 +1,10 @@
"""Flows Planner""" """Flows Planner"""
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any from typing import Any
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest
from sentry_sdk import start_span from sentry_sdk import start_span
from sentry_sdk.tracing import Span from sentry_sdk.tracing import Span
from structlog.stdlib import BoundLogger, get_logger from structlog.stdlib import BoundLogger, get_logger
@ -23,15 +23,10 @@ from authentik.flows.models import (
in_memory_stage, in_memory_stage,
) )
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.lib.utils.urls import redirect_with_qs
from authentik.outposts.models import Outpost from authentik.outposts.models import Outpost
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.root.middleware import ClientIPMiddleware from authentik.root.middleware import ClientIPMiddleware
if TYPE_CHECKING:
from authentik.flows.stage import StageView
LOGGER = get_logger() LOGGER = get_logger()
PLAN_CONTEXT_PENDING_USER = "pending_user" PLAN_CONTEXT_PENDING_USER = "pending_user"
PLAN_CONTEXT_SSO = "is_sso" PLAN_CONTEXT_SSO = "is_sso"
@ -42,8 +37,6 @@ PLAN_CONTEXT_OUTPOST = "outpost"
# Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan # Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan
# was restored. # was restored.
PLAN_CONTEXT_IS_RESTORED = "is_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_TIMEOUT = CONFIG.get_int("cache.timeout_flows")
CACHE_PREFIX = "goauthentik.io/flows/planner/" CACHE_PREFIX = "goauthentik.io/flows/planner/"
@ -117,62 +110,6 @@ class FlowPlan:
"""Check if there are any stages left in this plan""" """Check if there are any stages left in this plan"""
return len(self.markers) + len(self.bindings) > 0 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
stage = final_stage(request=request, executor=temp_exec)
return stage.dispatch(request)
get_qs = request.GET.copy()
if request.user.is_authenticated and (
# Object-scoped permission or global permission
request.user.has_perm("authentik_flows.inspect_flow", flow)
or request.user.has_perm("authentik_flows.inspect_flow")
):
get_qs["inspector"] = "available"
return redirect_with_qs(
"authentik_core:if-flow",
get_qs,
flow_slug=flow.slug,
)
class FlowPlanner: class FlowPlanner:
"""Execute all policies to plan out a flat list of all Stages """Execute all policies to plan out a flat list of all Stages
@ -191,7 +128,7 @@ class FlowPlanner:
self.flow = flow self.flow = flow
self._logger = get_logger().bind(flow_slug=flow.slug) self._logger = get_logger().bind(flow_slug=flow.slug)
def _check_authentication(self, request: HttpRequest, context: dict[str, Any]): def _check_authentication(self, request: HttpRequest):
"""Check the flow's authentication level is matched by `request`""" """Check the flow's authentication level is matched by `request`"""
if ( if (
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED
@ -208,11 +145,6 @@ class FlowPlanner:
and not request.user.is_superuser and not request.user.is_superuser
): ):
raise FlowNonApplicableException() 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) outpost_user = ClientIPMiddleware.get_outpost_user(request)
if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST: if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST:
if not outpost_user: if not outpost_user:
@ -244,13 +176,18 @@ class FlowPlanner:
) )
context = default_context or {} context = default_context or {}
# Bit of a workaround here, if there is a pending user set in the default context # 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: if context and PLAN_CONTEXT_PENDING_USER in context:
user = context[PLAN_CONTEXT_PENDING_USER] user = context[PLAN_CONTEXT_PENDING_USER]
else: else:
user = request.user user = request.user
# We only need to check the flow authentication if it's planned without a user
context.update(self._check_authentication(request, context)) # 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))
# First off, check the flow's direct policy bindings # First off, check the flow's direct policy bindings
# to make sure the user even has access to the flow # to make sure the user even has access to the flow
engine = PolicyEngine(self.flow, user, request) engine = PolicyEngine(self.flow, user, request)

View File

@ -93,11 +93,7 @@ class ChallengeStageView(StageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Return a challenge for the frontend to solve""" """Return a challenge for the frontend to solve"""
try: challenge = self._get_challenge(*args, **kwargs)
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(): if not challenge.is_valid():
self.logger.warning( self.logger.warning(
"f(ch): Invalid challenge", "f(ch): Invalid challenge",
@ -173,7 +169,11 @@ class ChallengeStageView(StageView):
stage_type=self.__class__.__name__, method="get_challenge" stage_type=self.__class__.__name__, method="get_challenge"
).time(), ).time(),
): ):
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()
with start_span( with start_span(
op="authentik.flow.stage._get_challenge", op="authentik.flow.stage._get_challenge",
name=self.__class__.__name__, name=self.__class__.__name__,

View File

@ -9,8 +9,8 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title> <title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon_url }}"> <link rel="icon" href="{{ brand.branding_favicon }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}"> <link rel="shortcut icon" href="{{ brand.branding_favicon }}">
{% block head_before %} {% block head_before %}
{% endblock %} {% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">

View File

@ -7,8 +7,8 @@ from django.http import HttpRequest, HttpResponse
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.urls import reverse from django.urls import reverse
from authentik.core.models import Group, User from authentik.core.models import User
from authentik.core.tests.utils import create_test_flow, create_test_user from authentik.core.tests.utils import create_test_flow
from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import ( from authentik.flows.models import (
FlowDeniedAction, FlowDeniedAction,
@ -255,11 +255,7 @@ class TestFlowExecutor(FlowTestCase):
) )
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -282,8 +278,8 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding) self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
# Second request, this passes the first dummy stage # Second request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -305,11 +301,7 @@ class TestFlowExecutor(FlowTestCase):
) )
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -318,11 +310,7 @@ class TestFlowExecutor(FlowTestCase):
re_evaluate_policies=True, re_evaluate_policies=True,
) )
binding3 = FlowStageBinding.objects.create( binding3 = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
stage=DummyStage.objects.create(name=generate_id()),
order=2,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
@ -340,9 +328,9 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.bindings[2], binding3) self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertEqual(plan.markers[2].__class__, StageMarker) self.assertIsInstance(plan.markers[2], StageMarker)
# Second request, this passes the first dummy stage # Second request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -353,8 +341,8 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding2) self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.bindings[1], binding3) self.assertEqual(plan.bindings[1], binding3)
self.assertEqual(plan.markers[0].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, StageMarker) self.assertIsInstance(plan.markers[1], StageMarker)
# third request, this should trigger the re-evaluate # third request, this should trigger the re-evaluate
# We do this request without the patch, so the policy results in false # We do this request without the patch, so the policy results in false
@ -372,11 +360,7 @@ class TestFlowExecutor(FlowTestCase):
) )
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -385,11 +369,7 @@ class TestFlowExecutor(FlowTestCase):
re_evaluate_policies=True, re_evaluate_policies=True,
) )
binding3 = FlowStageBinding.objects.create( binding3 = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
stage=DummyStage.objects.create(name=generate_id()),
order=2,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0) PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0)
@ -407,9 +387,9 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.bindings[2], binding3) self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertEqual(plan.markers[2].__class__, StageMarker) self.assertIsInstance(plan.markers[2], StageMarker)
# Second request, this passes the first dummy stage # Second request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -420,8 +400,8 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding2) self.assertEqual(plan.bindings[0], binding2)
self.assertEqual(plan.bindings[1], binding3) self.assertEqual(plan.bindings[1], binding3)
self.assertEqual(plan.markers[0].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, StageMarker) self.assertIsInstance(plan.markers[1], StageMarker)
# Third request, this passes the first dummy stage # Third request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -431,7 +411,7 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[0], binding3) self.assertEqual(plan.bindings[0], binding3)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
# third request, this should trigger the re-evaluate # third request, this should trigger the re-evaluate
# We do this request without the patch, so the policy results in false # We do this request without the patch, so the policy results in false
@ -449,11 +429,7 @@ class TestFlowExecutor(FlowTestCase):
) )
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -468,11 +444,7 @@ class TestFlowExecutor(FlowTestCase):
re_evaluate_policies=True, re_evaluate_policies=True,
) )
binding4 = FlowStageBinding.objects.create( binding4 = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name=generate_id()), order=2
stage=DummyStage.objects.create(name=generate_id()),
order=2,
evaluate_on_plan=True,
re_evaluate_policies=False,
) )
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
@ -493,10 +465,10 @@ class TestFlowExecutor(FlowTestCase):
self.assertEqual(plan.bindings[2], binding3) self.assertEqual(plan.bindings[2], binding3)
self.assertEqual(plan.bindings[3], binding4) self.assertEqual(plan.bindings[3], binding4)
self.assertEqual(plan.markers[0].__class__, StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[1], ReevaluateMarker)
self.assertEqual(plan.markers[2].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[2], ReevaluateMarker)
self.assertEqual(plan.markers[3].__class__, StageMarker) self.assertIsInstance(plan.markers[3], StageMarker)
# Second request, this passes the first dummy stage # Second request, this passes the first dummy stage
response = self.client.post(exec_url) response = self.client.post(exec_url)
@ -547,9 +519,9 @@ class TestFlowExecutor(FlowTestCase):
) )
# Stage 0 is a deny stage that is added dynamically # Stage 0 is a deny stage that is added dynamically
# when the reputation policy says so # when the reputation policy says so
deny_stage = DenyStage.objects.create(name=generate_id()) deny_stage = DenyStage.objects.create(name="deny")
reputation_policy = ReputationPolicy.objects.create( reputation_policy = ReputationPolicy.objects.create(
name=generate_id(), threshold=-1, check_ip=False name="reputation", threshold=-1, check_ip=False
) )
deny_binding = FlowStageBinding.objects.create( deny_binding = FlowStageBinding.objects.create(
target=flow, target=flow,
@ -562,7 +534,7 @@ class TestFlowExecutor(FlowTestCase):
# Stage 1 is an identification stage # Stage 1 is an identification stage
ident_stage = IdentificationStage.objects.create( ident_stage = IdentificationStage.objects.create(
name=generate_id(), name="ident",
user_fields=[UserFields.E_MAIL], user_fields=[UserFields.E_MAIL],
pretend_user_exists=False, pretend_user_exists=False,
) )
@ -587,64 +559,3 @@ class TestFlowExecutor(FlowTestCase):
) )
response = self.client.post(exec_url, {"uid_field": "invalid-string"}, follow=True) response = self.client.post(exec_url, {"uid_field": "invalid-string"}, follow=True)
self.assertStageResponse(response, flow, component="ak-stage-access-denied") self.assertStageResponse(response, flow, component="ak-stage-access-denied")
def test_re_evaluate_group_binding(self):
"""Test re-evaluate stage binding that has a policy binding to a group"""
flow = create_test_flow()
user_group_membership = create_test_user()
user_direct_binding = create_test_user()
user_other = create_test_user()
group_a = Group.objects.create(name=generate_id())
user_group_membership.ak_groups.add(group_a)
# Stage 0 is an identification stage
ident_stage = IdentificationStage.objects.create(
name=generate_id(),
user_fields=[UserFields.USERNAME],
pretend_user_exists=False,
)
FlowStageBinding.objects.create(
target=flow,
stage=ident_stage,
order=0,
)
# Stage 1 is a dummy stage that is only shown for users in group_a
dummy_stage = DummyStage.objects.create(name=generate_id())
dummy_binding = FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1)
PolicyBinding.objects.create(group=group_a, target=dummy_binding, order=0)
PolicyBinding.objects.create(user=user_direct_binding, target=dummy_binding, order=0)
# Stage 2 is a deny stage that (in this case) only user_b will see
deny_stage = DenyStage.objects.create(name=generate_id())
FlowStageBinding.objects.create(target=flow, stage=deny_stage, order=2)
exec_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
with self.subTest(f"Test user access through group: {user_group_membership}"):
self.client.logout()
# First request, run the planner
response = self.client.get(exec_url)
self.assertStageResponse(response, flow, component="ak-stage-identification")
response = self.client.post(
exec_url, {"uid_field": user_group_membership.username}, follow=True
)
self.assertStageResponse(response, flow, component="ak-stage-dummy")
with self.subTest(f"Test user access through user: {user_direct_binding}"):
self.client.logout()
# First request, run the planner
response = self.client.get(exec_url)
self.assertStageResponse(response, flow, component="ak-stage-identification")
response = self.client.post(
exec_url, {"uid_field": user_direct_binding.username}, follow=True
)
self.assertStageResponse(response, flow, component="ak-stage-dummy")
with self.subTest(f"Test user has no access: {user_other}"):
self.client.logout()
# First request, run the planner
response = self.client.get(exec_url)
self.assertStageResponse(response, flow, component="ak-stage-identification")
response = self.client.post(exec_url, {"uid_field": user_other.username}, follow=True)
self.assertStageResponse(response, flow, component="ak-stage-access-denied")

View File

@ -8,7 +8,6 @@ from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction from authentik.flows.models import FlowDesignation, FlowStageBinding, InvalidResponseAction
from authentik.lib.generators import generate_id
from authentik.stages.dummy.models import DummyStage from authentik.stages.dummy.models import DummyStage
from authentik.stages.identification.models import IdentificationStage, UserFields from authentik.stages.identification.models import IdentificationStage, UserFields
@ -27,7 +26,7 @@ class TestFlowInspector(APITestCase):
# Stage 1 is an identification stage # Stage 1 is an identification stage
ident_stage = IdentificationStage.objects.create( ident_stage = IdentificationStage.objects.create(
name=generate_id(), name="ident",
user_fields=[UserFields.USERNAME], user_fields=[UserFields.USERNAME],
) )
FlowStageBinding.objects.create( FlowStageBinding.objects.create(
@ -36,8 +35,9 @@ class TestFlowInspector(APITestCase):
order=1, order=1,
invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT, invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
) )
dummy_stage = DummyStage.objects.create(name=generate_id()) FlowStageBinding.objects.create(
FlowStageBinding.objects.create(target=flow, stage=dummy_stage, order=1) target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
)
res = self.client.get( res = self.client.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
@ -68,11 +68,9 @@ class TestFlowInspector(APITestCase):
) )
content = loads(ins.content) content = loads(ins.content)
self.assertEqual(content["is_completed"], False) self.assertEqual(content["is_completed"], False)
self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "ident")
self.assertEqual( self.assertEqual(
content["current_plan"]["current_stage"]["stage_obj"]["name"], ident_stage.name content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], "dummy2"
)
self.assertEqual(
content["current_plan"]["next_planned_stage"]["stage_obj"]["name"], dummy_stage.name
) )
self.client.post( self.client.post(
@ -86,12 +84,8 @@ class TestFlowInspector(APITestCase):
) )
content = loads(ins.content) content = loads(ins.content)
self.assertEqual(content["is_completed"], False) self.assertEqual(content["is_completed"], False)
self.assertEqual( self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident")
content["plans"][0]["current_stage"]["stage_obj"]["name"], ident_stage.name self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2")
)
self.assertEqual(
content["current_plan"]["current_stage"]["stage_obj"]["name"], dummy_stage.name
)
self.assertEqual( self.assertEqual(
content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username
) )

View File

@ -5,8 +5,6 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.core.cache import cache from django.core.cache import cache
from django.http import HttpRequest
from django.shortcuts import redirect
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
from django.urls import reverse from django.urls import reverse
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
@ -16,20 +14,8 @@ from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import ( from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation, FlowStageBinding
FlowAuthenticationRequirement, from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
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.generators import generate_id
from authentik.lib.tests.utils import dummy_get_response from authentik.lib.tests.utils import dummy_get_response
from authentik.outposts.apps import MANAGED_OUTPOST from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost from authentik.outposts.models import Outpost
@ -87,24 +73,6 @@ class TestFlowPlanner(TestCase):
planner.allow_empty_flows = True planner.allow_empty_flows = True
planner.plan(request) 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") @reconcile_app("authentik_outposts")
def test_authentication_outpost(self): def test_authentication_outpost(self):
"""Test flow authentication (outpost)""" """Test flow authentication (outpost)"""
@ -154,7 +122,7 @@ class TestFlowPlanner(TestCase):
"""Test planner cache""" """Test planner cache"""
flow = create_test_flow(FlowDesignation.AUTHENTICATION) flow = create_test_flow(FlowDesignation.AUTHENTICATION)
FlowStageBinding.objects.create( FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
) )
request = self.request_factory.get( request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}), reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
@ -173,7 +141,7 @@ class TestFlowPlanner(TestCase):
"""Test planner with default_context""" """Test planner with default_context"""
flow = create_test_flow() flow = create_test_flow()
FlowStageBinding.objects.create( FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0 target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
) )
user = User.objects.create(username="test-user") user = User.objects.create(username="test-user")
@ -192,7 +160,7 @@ class TestFlowPlanner(TestCase):
FlowStageBinding.objects.create( FlowStageBinding.objects.create(
target=flow, target=flow,
stage=DummyStage.objects.create(name=generate_id()), stage=DummyStage.objects.create(name="dummy1"),
order=0, order=0,
re_evaluate_policies=True, re_evaluate_policies=True,
) )
@ -205,7 +173,7 @@ class TestFlowPlanner(TestCase):
planner = FlowPlanner(flow) planner = FlowPlanner(flow)
plan = planner.plan(request) plan = planner.plan(request)
self.assertEqual(plan.markers[0].__class__, ReevaluateMarker) self.assertIsInstance(plan.markers[0], ReevaluateMarker)
def test_planner_reevaluate_actual(self): def test_planner_reevaluate_actual(self):
"""Test planner with re-evaluate""" """Test planner with re-evaluate"""
@ -213,14 +181,11 @@ class TestFlowPlanner(TestCase):
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create( binding = FlowStageBinding.objects.create(
target=flow, target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
stage=DummyStage.objects.create(name=generate_id()),
order=0,
re_evaluate_policies=False,
) )
binding2 = FlowStageBinding.objects.create( binding2 = FlowStageBinding.objects.create(
target=flow, target=flow,
stage=DummyStage.objects.create(name=generate_id()), stage=DummyStage.objects.create(name="dummy2"),
order=1, order=1,
re_evaluate_policies=True, re_evaluate_policies=True,
) )
@ -244,103 +209,5 @@ class TestFlowPlanner(TestCase):
self.assertEqual(plan.bindings[0], binding) self.assertEqual(plan.bindings[0], binding)
self.assertEqual(plan.bindings[1], binding2) self.assertEqual(plan.bindings[1], binding2)
self.assertEqual(plan.markers[0].__class__, StageMarker)
self.assertEqual(plan.markers[1].__class__, ReevaluateMarker)
self.assertIsInstance(plan.markers[0], StageMarker) self.assertIsInstance(plan.markers[0], StageMarker)
self.assertIsInstance(plan.markers[1], ReevaluateMarker) 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

@ -171,8 +171,7 @@ class FlowExecutorView(APIView):
# Existing plan is deleted from session and instance # Existing plan is deleted from session and instance
self.plan = None self.plan = None
self.cancel() self.cancel()
else: self._logger.debug("f(exec): Continuing existing plan")
self._logger.debug("f(exec): Continuing existing plan")
# Initial flow request, check if we have an upstream query string passed in # Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params request.session[SESSION_KEY_GET] = get_params
@ -598,4 +597,9 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
except FlowNonApplicableException: except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user") LOGGER.warning("Flow not applicable to user")
raise Http404 from None raise Http404 from None
return plan.to_redirect(request, stage.configure_flow) request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=stage.configure_flow.slug,
)

View File

@ -78,9 +78,7 @@ class FlowInspectorView(APIView):
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug) self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
if settings.DEBUG: if settings.DEBUG:
return return
if request.user.has_perm( if request.user.has_perm("authentik_flow.inspect_flow", self.flow):
"authentik_flows.inspect_flow", self.flow
) or request.user.has_perm("authentik_flows.inspect_flow"):
return return
raise Http404 raise Http404
@ -96,9 +94,6 @@ class FlowInspectorView(APIView):
"""Get current flow state and record it""" """Get current flow state and record it"""
plans = [] plans = []
for plan in request.session.get(SESSION_KEY_HISTORY, []): for plan in request.session.get(SESSION_KEY_HISTORY, []):
plan: FlowPlan
if plan.flow_pk != self.flow.pk.hex:
continue
plan_serializer = FlowInspectorPlanSerializer( plan_serializer = FlowInspectorPlanSerializer(
instance=plan, context={"request": request} instance=plan, context={"request": request}
) )

View File

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

View File

@ -6,6 +6,8 @@ postgresql:
user: authentik user: authentik
port: 5432 port: 5432
password: "env://POSTGRES_PASSWORD" password: "env://POSTGRES_PASSWORD"
use_pgbouncer: false
use_pgpool: false
test: test:
name: test_authentik name: test_authentik
read_replicas: {} read_replicas: {}
@ -133,7 +135,6 @@ web:
# No default here as it's set dynamically # No default here as it's set dynamically
# workers: 2 # workers: 2
threads: 4 threads: 4
path: /
worker: worker:
concurrency: 2 concurrency: 2

View File

@ -9,25 +9,20 @@ from typing import Any
from cachetools import TLRUCache, cached from cachetools import TLRUCache, cached
from django.core.exceptions import FieldError from django.core.exceptions import FieldError
from django.http import HttpRequest
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
from sentry_sdk import start_span from sentry_sdk import start_span
from sentry_sdk.tracing import Span from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import AuthenticatedSession, User from authentik.core.models import User
from authentik.events.models import Event from authentik.events.models import Event
from authentik.lib.expression.exceptions import ControlFlowException from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.utils.http import get_http_session from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.time import timedelta_from_string
from authentik.policies.models import Policy, PolicyBinding from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest, PolicyResult from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider
from authentik.stages.authenticator import devices_for_user from authentik.stages.authenticator import devices_for_user
LOGGER = get_logger() LOGGER = get_logger()
@ -61,7 +56,6 @@ class BaseEvaluator:
"ak_logger": get_logger(self._filename).bind(), "ak_logger": get_logger(self._filename).bind(),
"ak_user_by": BaseEvaluator.expr_user_by, "ak_user_by": BaseEvaluator.expr_user_by,
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator, "ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
"ak_create_jwt": self.expr_create_jwt,
"ip_address": ip_address, "ip_address": ip_address,
"ip_network": ip_network, "ip_network": ip_network,
"list_flatten": BaseEvaluator.expr_flatten, "list_flatten": BaseEvaluator.expr_flatten,
@ -188,36 +182,6 @@ class BaseEvaluator:
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None) proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
return proc.profiling_wrapper() return proc.profiling_wrapper()
def expr_create_jwt(
self,
user: User,
provider: OAuth2Provider | str,
scopes: list[str],
validity: str = "seconds=60",
) -> str | None:
"""Issue a JWT for a given provider"""
request: HttpRequest = self._context.get("http_request")
if not request:
return None
if not isinstance(provider, OAuth2Provider):
provider = OAuth2Provider.objects.get(name=provider)
session = None
if hasattr(request, "session") and request.session.session_key:
session = AuthenticatedSession.objects.filter(
session_key=request.session.session_key
).first()
access_token = AccessToken(
provider=provider,
user=user,
expires=now() + timedelta_from_string(validity),
scope=scopes,
auth_time=now(),
session=session,
)
access_token.id_token = IDToken.new(provider, access_token, request)
access_token.save()
return access_token.token
def wrap_expression(self, expression: str) -> str: def wrap_expression(self, expression: str) -> str:
"""Wrap expression in a function, call it, and save the result as `result`""" """Wrap expression in a function, call it, and save the result as `result`"""
handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys()) handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys())

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