Compare commits

..

50 Commits

Author SHA1 Message Date
1a21479b0d release: 2024.4.4 2024-08-22 17:38:56 +02:00
38154f72e0 rbac: check user type correctly
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-08-22 17:38:52 +02:00
19318d4c00 security: fix CVE-2024-42490 (cherry-pick #11022) (#11024)
security: fix CVE-2024-42490 (#11022)

CVE-2024-42490

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-08-22 17:18:55 +02:00
be3d7c0666 website/docs: update 2024.4 release notes with latest changes (cherry-pick #10231) (#10244)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-06-26 16:56:28 +00:00
5afceaa55f release: 2024.4.3 2024-06-26 19:36:51 +09:00
72dc27f1c9 security: fix CVE-2024-37905 (cherry-pick #10230) (#10236)
Co-authored-by: Jens L <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix CVE-2024-37905 (#10230)
2024-06-26 10:24:15 +00:00
b5ffd16861 security: fix CVE-2024-38371 (cherry-pick #10229) (#10233)
Co-authored-by: Jens L <jens@goauthentik.io>
fix CVE-2024-38371 (#10229)
2024-06-26 09:42:57 +00:00
8af754e88c sources/saml: fix FlowPlanner error due to pickle (cherry-pick #9708) (#9709)
sources/saml: fix FlowPlanner error due to pickle (#9708)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-13 21:04:12 +02:00
ade1f08c89 web: fix value handling inside controlled components (cherry-pick #9648) (#9685)
web: fix value handling inside controlled components (#9648)

* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: fix value handling inside controlled components

This is one of those stupid bugs that drive web developers crazy. The basics are straightforward:
when you cause a higher-level component to have a "big enough re-render," for some unknown
definition of "big enough," it will re-render the sub-components. In traditional web interaction,
those components should never be re-rendered while the user is interacting with the form, but in
frameworks where there's dynamic re-arrangement, part or all of the form could get re-rendered at
any mmoment. Since neither the form nor any of its intermediaries is tracking the values as they're
changed, it's up to the components themselves to keep the user's input-- and to be hardened against
property changes coming from the outside world.

So static memoization of the initial value passed in, and aggressively walling off the values the
customer generates from that field, are needed to protect the user's work from any framework's
dynamic DOM management. I remember struggling with this in React; I had hoped Lit was better, but in
this case, not better enough.

The protocol for "is it an ak-data-control" is "it has a `json()` method that returns the data ready
to be sent to the authentik server."  I missed that in one place, so that's on me.

* Eslint had opinions.

* Added comments to explain something.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2024-05-13 12:26:20 +02:00
9240fa1037 core: fix source flow_manager not always appending save stage (cherry-pick #9659) (#9662)
core: fix source flow_manager not always appending save stage (#9659)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-09 20:13:53 +02:00
1f5953b5b7 release: 2024.4.2 2024-05-07 16:39:33 +02:00
5befccc1fd website/docs: prepare 2024.4.2 release notes (cherry-pick #9555) (#9624)
Co-authored-by: Jens L <jens@goauthentik.io>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-05-07 14:39:02 +00:00
ff193d809a sources/scim: fix duplicate groups and invalid schema (cherry-pick #9466) (#9606)
sources/scim: fix duplicate groups and invalid schema (#9466)

* sources/scim: fix duplicate groups



* fix missing schema in response



* fix members missing in returned group



* optimise queries



* fix



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-06 15:41:54 +02:00
23bbb6e5ef core: fix condition in task clean_expiring_models (cherry-pick #9603) (#9604)
core: fix condition in task clean_expiring_models (#9603)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-05-06 14:36:42 +02:00
225d02d02d core: fix task clean_expiring_models removing valid sessions with using database storage (cherry-pick #9598) (#9601) 2024-05-06 14:09:25 +02:00
90fe1eda66 tenants: fix scheduled tasks not running on default tenant (cherry-pick #9583) (#9586)
tenants: fix scheduled tasks not running on default tenant (#9583)

* tenants: fix scheduled tasks not running on default tenant



* add some extra time to keep system task around



* make sure we actually send it to all tenants



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-06 12:30:08 +02:00
35ba88a203 providers/scim: fix SCIM ID incorrectly used as primary key (cherry-pick #9557) (#9579)
providers/scim: fix SCIM ID incorrectly used as primary key (#9557)

* providers/scim: fix SCIM ID incorrectly used as primary key



* fix unique together



* add test



* add check for empty scim ID



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-06 02:23:33 +02:00
8414a9dcad core: fix source_flow_manager saving user-source connection too early (cherry-pick #9559) (#9578)
core: fix source_flow_manager saving user-source connection too early (#9559)

* core: fix source_flow_manager saving user-source connection too early



* ci: fix branch name



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-05 17:32:38 +02:00
1d626f5b57 enterprise/audit: fix audit logging with m2m relations (cherry-pick #9571) (#9572)
enterprise/audit: fix audit logging with m2m relations (#9571)

Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-05 16:13:45 +02:00
508dd0ac64 providers/scim: fix time_limit not set correctly (cherry-pick #9546) (#9553)
providers/scim: fix time_limit not set correctly (#9546)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-03 12:29:30 +02:00
f4b82a8b09 web/flows: fix error when enrolling multiple WebAuthn devices consecutively (cherry-pick #9545) (#9547)
web/flows: fix error when enrolling multiple WebAuthn devices consecutively (#9545)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-02 20:09:05 +02:00
2900f01976 providers/saml: fix ecdsa support (cherry-pick #9537) (#9544)
* providers/saml: fix ecdsa support (#9537)

* crypto: add option to select which alg to use to generate

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

* fix missing ecdsa options for XML signing

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

* bump xml libraries and remove disclaimer

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

* lock djangoframework

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

---------

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

* bump api client

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-02 16:08:33 +02:00
0f6ece5eb7 lifecycle: close database connection after migrating (cherry-pick #9516) (#9531)
lifecycle: close database connection after migrating (#9516)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-05-01 13:57:21 +02:00
b9936fe532 web: Add missing integrity hashes to package-lock.json (#9527)
web: Add missing integrity hashes to package-lock.json
2024-04-30 21:05:45 +02:00
d0b3cc5916 core: only prefetch related objects when required (cherry-pick #9476) (#9510)
core: only prefetch related objects when required (#9476)

* core: only prefetch related objects when required



* add tests



* add tests to assert query count



* "optimize" another query away



* prefetch parent and roles



* whops that needs to be pre-fetched



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-29 13:10:28 +02:00
e034f5e5dc web/flows: fix missing fallback for flow logo (cherry-pick #9487) (#9492)
web/flows: fix missing fallback for flow logo (#9487)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-28 16:37:29 +02:00
9d6816bbc8 events: ensure all models' __str__ can be called without any further lookups (cherry-pick #9480) (#9485)
events: ensure all models' __str__ can be called without any further lookups (#9480)

* events: ensure all models' __str__ can be called without any further lookups



* allow for additional queries for models using default_token_key



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-28 00:33:56 +02:00
82d4ea9e8a flows: fix execute API endpoint (cherry-pick #9478) (#9481)
flows: fix execute API endpoint (#9478)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-27 01:57:26 +02:00
c8a804f2a7 sources/oauth: fix OAuth Client sending token request incorrectly (cherry-pick #9474) (#9475)
sources/oauth: fix OAuth Client sending token request incorrectly (#9474)

closes #9289

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-26 20:57:01 +02:00
ca70c963e5 release: 2024.4.1 2024-04-26 17:39:46 +02:00
4c89d4a4a4 website/docs: update release notes for 2024.4.1 again (cherry-pick #9471) (#9472)
website/docs: update release notes for 2024.4.1 again (#9471)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-26 17:39:25 +02:00
8a47acac3a sources/scim: fix service account user path (cherry-pick #9463) (#9470)
sources/scim: fix service account user path (#9463)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-26 17:19:30 +02:00
4a3b22491c web/admin: fix disabled button color with dark theme (cherry-pick #9465) (#9468)
web/admin: fix disabled button color with dark theme (#9465)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-26 17:02:41 +02:00
f991d656c7 web/admin: show user internal service account as disabled (cherry-pick #9464) (#9467)
web/admin: show user internal service account as disabled (#9464)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-26 17:02:26 +02:00
e86aa11131 website/docs: prepare 2024.4.1 (cherry-pick #9459) (#9461)
website/docs: prepare 2024.4.1 (#9459)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-26 14:39:44 +02:00
03725ae086 lifecycle: always try custom redis URL (cherry-pick #9441) (#9458)
lifecycle: always try custom redis URL (#9441)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-26 13:26:52 +02:00
f2a37e8c7c web/common: fix locale detection for user-set locale (cherry-pick #9436) (#9439)
web/common: fix locale detection for user-set locale (#9436)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-25 22:37:56 +02:00
e935690b1b ci: fix ci pipeline (cherry-pick #9427) (#9429)
ci: fix ci pipeline (#9427)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-25 16:06:12 +02:00
02709e4ede core: fix logic for token expiration (cherry-pick #9426) (#9428)
core: fix logic for token expiration (#9426)

* core: fix logic for token expiration



* bump default token expiration



* fix frontend



* fix



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-25 16:05:30 +02:00
f78adab9d1 web: Add resolved and integrity fields back to package-lock.json (cherry-pick #9419) (#9421)
web: Add resolved and integrity fields back to package-lock.json (#9419)

* web: Fix missing resolved and integrity fields in package-lock.json

* web,website: Add lockfile lint to CI

Co-authored-by: Jan van Brügge <supermanitu@gmail.com>
2024-04-25 12:41:31 +02:00
61f3a72fd9 stages/identification: don't check source component (cherry-pick #9410) (#9420)
stages/identification: don't check source component (#9410)

* Do not include the built-in source in this check



* Update authentik/stages/identification/stage.py



---------

Signed-off-by: PythonCoderAS <13932583+PythonCoderAS@users.noreply.github.com>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: PythonCoderAS <13932583+PythonCoderAS@users.noreply.github.com>
Co-authored-by: Jens L <jens@beryju.org>
2024-04-25 11:55:49 +02:00
541becfe30 sources/oauth: ensure all UI sources return a valid source (cherry-pick #9401) (#9406)
sources/oauth: ensure all UI sources return a valid source (#9401)

* web/admin: prevent selection of inbuilt source in identification stage



* fix apple source



* also fix plex challenge



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-24 23:00:24 +02:00
11ff7955f7 web: markdown: display markdown even when frontmatter is missing (cherry-pick #9404) (#9405)
web: markdown: display markdown even when frontmatter is missing (#9404)

* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: markdown: display markdown even when frontmatter is missing

Make the check for the document title comprehensive across the
entire demeter.  If there is no front matter, `data` will be missing,
not just `data.title`.

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2024-04-24 22:54:51 +02:00
afa4234036 release: 2024.4.0 2024-04-24 17:42:10 +02:00
ca22a4deaf website/docs: finalize 2024.4 release notes (cherry-pick #9396) (#9398)
website/docs: finalize 2024.4 release notes (#9396)

* website/docs: finalize 2024.4 release notes



* escape curly braces manually



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-24 17:41:29 +02:00
7b7a3d34ec web/admin: fix document title for admin interface (cherry-pick #9362) (#9365)
web/admin: fix document title for admin interface (#9362)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-04-20 23:05:23 +02:00
b1ca579397 website/docs: release notes 2024.4: add performance improvements values (cherry-pick #9356) (#9357)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-04-19 16:53:15 +00:00
c8072579c8 release: 2024.4.0-rc1 2024-04-19 16:05:20 +02:00
378a701fb9 root: bump blueprint schema version
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-19 16:05:15 +02:00
bba793d94c lifecycle: fix ak test-all command
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-19 16:04:39 +02:00
129 changed files with 4146 additions and 6575 deletions

View File

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

View File

@ -12,7 +12,7 @@ should_build = str(os.environ.get("DOCKER_USERNAME", None) is not None).lower()
branch_name = os.environ["GITHUB_REF"]
if os.environ.get("GITHUB_HEAD_REF", "") != "":
branch_name = os.environ["GITHUB_HEAD_REF"]
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-").replace("'", "-")
image_names = os.getenv("IMAGE_NAME").split(",")
image_arch = os.getenv("IMAGE_ARCH") or None
@ -54,9 +54,9 @@ image_main_tag = image_tags[0]
image_tags_rendered = ",".join(image_tags)
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"shouldBuild={should_build}", file=_output)
print(f"sha={sha}", file=_output)
print(f"version={version}", file=_output)
print(f"prerelease={prerelease}", file=_output)
print(f"imageTags={image_tags_rendered}", file=_output)
print(f"imageMainTag={image_main_tag}", file=_output)
print("shouldBuild=%s" % should_build, file=_output)
print("sha=%s" % sha, file=_output)
print("version=%s" % version, file=_output)
print("prerelease=%s" % prerelease, file=_output)
print("imageTags=%s" % image_tags_rendered, file=_output)
print("imageMainTag=%s" % image_main_tag, file=_output)

View File

@ -130,7 +130,7 @@ jobs:
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.10.0
uses: helm/kind-action@v1.9.0
- name: run integration
run: |
poetry run coverage run manage.py test tests/integration

View File

@ -29,7 +29,7 @@ jobs:
- name: Generate API
run: make gen-client-go
- name: golangci-lint
uses: golangci/golangci-lint-action@v5
uses: golangci/golangci-lint-action@v4
with:
version: v1.54.2
args: --timeout 5000s --verbose

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.4.1"
__version__ = "2024.4.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -73,11 +73,6 @@ def auth_user_lookup(raw_header: bytes) -> User | None:
if user:
CTX_AUTH_VIA.set("secret_key")
return user
# then try to auth via expression JWT
user = token_expression_jwt(auth_credentials)
if user:
CTX_AUTH_VIA.set("expression_jwt")
return user
raise AuthenticationFailed("Token invalid/expired")
@ -95,13 +90,6 @@ def token_secret_key(value: str) -> User | None:
return outpost.user
def token_expression_jwt(value: str) -> User | None:
"""Authenticate API call made by Expressions"""
from authentik.lib.expression.evaluator import authenticate_token
return authenticate_token(value)
class TokenAuthentication(BaseAuthentication):
"""Token-based authentication using HTTP Bearer authentication"""

View File

@ -45,6 +45,13 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False)
def validate_user(self, user: User):
"""Ensure user of token cannot be changed"""
if self.instance and self.instance.user_id:
if user.pk != self.instance.user_id:
raise ValidationError("User cannot be changed")
return user
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created."""
request: Request = self.context.get("request")

View File

@ -14,6 +14,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.api.utils import PassiveSerializer
from authentik.rbac.filters import ObjectFilter
class DeleteAction(Enum):
@ -53,7 +54,7 @@ class UsedByMixin:
@extend_schema(
responses={200: UsedBySerializer(many=True)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def used_by(self, request: Request, *args, **kwargs) -> Response:
"""Get a list of all objects that use this object"""
model: Model = self.get_object()

View File

@ -36,7 +36,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
_filename = model.name
else:
_filename = str(model)
super().__init__(None, filename=_filename)
super().__init__(filename=_filename)
req = PolicyRequest(user=User())
req.obj = model
if user:

View File

@ -13,7 +13,7 @@ from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
from authentik.core.models import Source, SourceUserMatchingModes, User, UserSourceConnection
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostUserEnrollmentStage
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostSourceStage
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow, FlowToken, Stage, in_memory_stage
@ -100,8 +100,6 @@ class SourceFlowManager:
if self.request.user.is_authenticated:
new_connection.user = self.request.user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
existing_connections = self.connection_type.objects.filter(
@ -148,7 +146,6 @@ class SourceFlowManager:
]:
new_connection.user = user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_DENY,
@ -209,13 +206,9 @@ class SourceFlowManager:
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
"""Hook to override stages which are appended to the flow"""
if not self.source.enrollment_flow:
return []
if flow.slug == self.source.enrollment_flow.slug:
return [
in_memory_stage(PostUserEnrollmentStage),
]
return []
return [
in_memory_stage(PostSourceStage),
]
def _prepare_flow(
self,
@ -269,6 +262,9 @@ class SourceFlowManager:
)
# We run the Flow planner here so we can pass the Pending user in the context
planner = FlowPlanner(flow)
# We append some stages so the initial flow we get might be empty
planner.allow_empty_flows = True
planner.use_cache = False
plan = planner.plan(self.request, kwargs)
for stage in self.get_stages_to_append(flow):
plan.append_stage(stage)
@ -327,7 +323,7 @@ class SourceFlowManager:
reverse(
"authentik_core:if-user",
)
+ f"#/settings;page-{self.source.slug}"
+ "#/settings;page-sources"
)
def handle_enroll(

View File

@ -10,7 +10,7 @@ from authentik.flows.stage import StageView
PLAN_CONTEXT_SOURCES_CONNECTION = "goauthentik.io/sources/connection"
class PostUserEnrollmentStage(StageView):
class PostSourceStage(StageView):
"""Dynamically injected stage which saves the Connection after
the user has been enrolled."""
@ -21,10 +21,12 @@ class PostUserEnrollmentStage(StageView):
]
user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
connection.user = user
linked = connection.pk is None
connection.save()
Event.new(
EventAction.SOURCE_LINKED,
message="Linked Source",
source=connection.source,
).from_http(self.request)
if linked:
Event.new(
EventAction.SOURCE_LINKED,
message="Linked Source",
source=connection.source,
).from_http(self.request)
return self.executor.stage_ok()

View File

@ -2,7 +2,9 @@
from datetime import datetime, timedelta
from django.conf import ImproperlyConfigured
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.contrib.sessions.backends.db import SessionStore as DBSessionStore
from django.core.cache import cache
from django.utils.timezone import now
from structlog.stdlib import get_logger
@ -15,6 +17,7 @@ from authentik.core.models import (
User,
)
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@ -39,16 +42,31 @@ def clean_expired_models(self: SystemTask):
amount = 0
for session in AuthenticatedSession.objects.all():
cache_key = f"{KEY_PREFIX}{session.session_key}"
value = None
try:
value = cache.get(cache_key)
match CONFIG.get("session_storage", "cache"):
case "cache":
cache_key = f"{KEY_PREFIX}{session.session_key}"
value = None
try:
value = cache.get(cache_key)
except Exception as exc:
LOGGER.debug("Failed to get session from cache", exc=exc)
if not value:
session.delete()
amount += 1
except Exception as exc:
LOGGER.debug("Failed to get session from cache", exc=exc)
if not value:
session.delete()
amount += 1
case "db":
if not (
DBSessionStore.get_model_class()
.objects.filter(session_key=session.session_key, expire_date__gt=now())
.exists()
):
session.delete()
amount += 1
case _:
# Should never happen, as we check for other values in authentik/root/settings.py
raise ImproperlyConfigured(
"Invalid session_storage setting, allowed values are db and cache"
)
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")

View File

@ -66,11 +66,14 @@ class TestPropertyMappings(TestCase):
expression="return request.http_request.path",
)
http_request = self.factory.get("/")
tmpl = f"""
res = ak_call_policy('{expr.name}')
tmpl = (
"""
res = ak_call_policy('%s')
result = [request.http_request.path, res.raw_result]
return result
"""
% expr.name
)
evaluator = PropertyMapping(expression=tmpl, name=generate_id())
res = evaluator.evaluate(self.user, http_request)
self.assertEqual(res, ["/", "/"])

View File

@ -2,11 +2,15 @@
from django.contrib.auth.models import AnonymousUser
from django.test import TestCase
from django.urls import reverse
from guardian.utils import get_anonymous_user
from authentik.core.models import SourceUserMatchingModes, User
from authentik.core.sources.flow_manager import Action
from authentik.core.sources.stage import PostSourceStage
from authentik.core.tests.utils import create_test_flow
from authentik.flows.planner import FlowPlan
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import get_request
from authentik.policies.denied import AccessDeniedResponse
@ -21,42 +25,62 @@ class TestSourceFlowManager(TestCase):
def setUp(self) -> None:
super().setUp()
self.source: OAuthSource = OAuthSource.objects.create(name="test")
self.authentication_flow = create_test_flow()
self.enrollment_flow = create_test_flow()
self.source: OAuthSource = OAuthSource.objects.create(
name=generate_id(),
slug=generate_id(),
authentication_flow=self.authentication_flow,
enrollment_flow=self.enrollment_flow,
)
self.identifier = generate_id()
def test_unauthenticated_enroll(self):
"""Test un-authenticated user enrolling"""
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
request = get_request("/", user=AnonymousUser())
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.ENROLL)
flow_manager.get_flow()
response = flow_manager.get_flow()
self.assertEqual(response.status_code, 302)
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
self.assertEqual(flow_plan.bindings[0].stage.view, PostSourceStage)
def test_unauthenticated_auth(self):
"""Test un-authenticated user authenticating"""
UserOAuthSourceConnection.objects.create(
user=get_anonymous_user(), source=self.source, identifier=self.identifier
)
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
)
request = get_request("/", user=AnonymousUser())
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
action, _ = flow_manager.get_action()
self.assertEqual(action, Action.AUTH)
flow_manager.get_flow()
response = flow_manager.get_flow()
self.assertEqual(response.status_code, 302)
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
self.assertEqual(flow_plan.bindings[0].stage.view, PostSourceStage)
def test_authenticated_link(self):
"""Test authenticated user linking"""
UserOAuthSourceConnection.objects.create(
user=get_anonymous_user(), source=self.source, identifier=self.identifier
)
user = User.objects.create(username="foo", email="foo@bar.baz")
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=user), self.identifier, {}
)
action, _ = flow_manager.get_action()
request = get_request("/", user=user)
flow_manager = OAuthSourceFlowManager(self.source, request, self.identifier, {})
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
self.assertIsNone(connection.pk)
response = flow_manager.get_flow()
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url,
reverse("authentik_core:if-user") + "#/settings;page-sources",
)
def test_unauthenticated_link(self):
"""Test un-authenticated user linking"""
flow_manager = OAuthSourceFlowManager(self.source, get_request("/"), self.identifier, {})
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
self.assertIsNone(connection.pk)
flow_manager.get_flow()
def test_unauthenticated_enroll_email(self):

View File

@ -13,9 +13,8 @@ from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
Token,
TokenIntents,
User,
)
from authentik.core.tests.utils import create_test_admin_user
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id
@ -24,7 +23,7 @@ class TestTokenAPI(APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create(username="testuser")
self.user = create_test_user()
self.admin = create_test_admin_user()
self.client.force_login(self.user)
@ -154,6 +153,24 @@ class TestTokenAPI(APITestCase):
self.assertEqual(token.expiring, True)
self.assertNotEqual(token.expires.timestamp(), expires.timestamp())
def test_token_change_user(self):
"""Test creating a token and then changing the user"""
ident = generate_id()
response = self.client.post(reverse("authentik_api:token-list"), {"identifier": ident})
self.assertEqual(response.status_code, 201)
token = Token.objects.get(identifier=ident)
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
response = self.client.put(
reverse("authentik_api:token-detail", kwargs={"identifier": ident}),
data={"identifier": "user_token_poc_v3", "intent": "api", "user": self.admin.pk},
)
self.assertEqual(response.status_code, 400)
token.refresh_from_db()
self.assertEqual(token.user, self.user)
def test_list(self):
"""Test Token List (Test normal authentication)"""
Token.objects.all().delete()

View File

@ -4,7 +4,7 @@ from django.utils.text import slugify
from authentik.brands.models import Brand
from authentik.core.models import Group, User
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow, FlowDesignation
from authentik.lib.generators import generate_id
@ -50,12 +50,10 @@ def create_test_brand(**kwargs) -> Brand:
return Brand.objects.create(domain=uid, default=True, **kwargs)
def create_test_cert(use_ec_private_key=False) -> CertificateKeyPair:
def create_test_cert(alg=PrivateKeyAlg.RSA) -> CertificateKeyPair:
"""Generate a certificate for testing"""
builder = CertificateBuilder(
name=f"{generate_id()}.self-signed.goauthentik.io",
use_ec_private_key=use_ec_private_key,
)
builder = CertificateBuilder(f"{generate_id()}.self-signed.goauthentik.io")
builder.alg = alg
builder.build(
subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"],
validity_days=360,

View File

@ -14,7 +14,13 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, DateTimeField, IntegerField, SerializerMethodField
from rest_framework.fields import (
CharField,
ChoiceField,
DateTimeField,
IntegerField,
SerializerMethodField,
)
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.response import Response
@ -26,10 +32,11 @@ from authentik.api.authorization import SecretKeyFilter
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()
@ -178,6 +185,7 @@ class CertificateGenerationSerializer(PassiveSerializer):
common_name = CharField()
subject_alt_name = CharField(required=False, allow_blank=True, label=_("Subject-alt name"))
validity_days = IntegerField(initial=365)
alg = ChoiceField(default=PrivateKeyAlg.RSA, choices=PrivateKeyAlg.choices)
class CertificateKeyPairFilter(FilterSet):
@ -240,6 +248,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
raw_san = data.validated_data.get("subject_alt_name", "")
sans = raw_san.split(",") if raw_san != "" else []
builder = CertificateBuilder(data.validated_data["common_name"])
builder.alg = data.validated_data["alg"]
builder.build(
subject_alt_names=sans,
validity_days=int(data.validated_data["validity_days"]),
@ -258,7 +267,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
],
responses={200: CertificateDataSerializer(many=False)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def view_certificate(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs certificate and log access"""
certificate: CertificateKeyPair = self.get_object()
@ -288,7 +297,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
],
responses={200: CertificateDataSerializer(many=False)},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def view_private_key(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs private key and log access"""
certificate: CertificateKeyPair = self.get_object()

View File

@ -9,20 +9,28 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from cryptography.x509.oid import NameOID
from django.db import models
from django.utils.translation import gettext_lazy as _
from authentik import __version__
from authentik.crypto.models import CertificateKeyPair
class PrivateKeyAlg(models.TextChoices):
"""Algorithm to create private key with"""
RSA = "rsa", _("rsa")
ECDSA = "ecdsa", _("ecdsa")
class CertificateBuilder:
"""Build self-signed certificates"""
common_name: str
alg: PrivateKeyAlg
_use_ec_private_key: bool
def __init__(self, name: str, use_ec_private_key=False):
self._use_ec_private_key = use_ec_private_key
def __init__(self, name: str):
self.alg = PrivateKeyAlg.RSA
self.__public_key = None
self.__private_key = None
self.__builder = None
@ -42,11 +50,13 @@ class CertificateBuilder:
def generate_private_key(self) -> PrivateKeyTypes:
"""Generate private key"""
if self._use_ec_private_key:
if self.alg == PrivateKeyAlg.ECDSA:
return ec.generate_private_key(curve=ec.SECP256R1())
return rsa.generate_private_key(
public_exponent=65537, key_size=4096, backend=default_backend()
)
if self.alg == PrivateKeyAlg.RSA:
return rsa.generate_private_key(
public_exponent=65537, key_size=4096, backend=default_backend()
)
raise ValueError(f"Invalid alg: {self.alg}")
def build(
self,

View File

@ -214,6 +214,46 @@ class TestCrypto(APITestCase):
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
def test_certificate_download_denied(self):
"""Test certificate export (download)"""
self.client.logout()
keypair = create_test_cert()
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": keypair.pk},
),
data={"download": True},
)
self.assertEqual(403, response.status_code)
def test_private_key_download_denied(self):
"""Test private_key export (download)"""
self.client.logout()
keypair = create_test_cert()
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": keypair.pk},
),
data={"download": True},
)
self.assertEqual(403, response.status_code)
def test_used_by(self):
"""Test used_by endpoint"""
self.client.force_login(create_test_admin_user())
@ -246,6 +286,26 @@ class TestCrypto(APITestCase):
],
)
def test_used_by_denied(self):
"""Test used_by endpoint"""
self.client.logout()
keypair = create_test_cert()
OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://localhost",
signing_key=keypair,
)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-used-by",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
def test_discovery(self):
"""Test certificate discovery"""
name = generate_id()

View File

@ -2,11 +2,12 @@
from copy import deepcopy
from functools import partial
from typing import Any
from django.apps.registry import apps
from django.core.files import File
from django.db import connection
from django.db.models import Model
from django.db.models import ManyToManyRel, Model
from django.db.models.expressions import BaseExpression, Combinable
from django.db.models.signals import post_init
from django.http import HttpRequest
@ -44,7 +45,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
post_init.disconnect(dispatch_uid=request.request_id)
def serialize_simple(self, model: Model) -> dict:
"""Serialize a model in a very simple way. No ForeginKeys or other relationships are
"""Serialize a model in a very simple way. No ForeignKeys or other relationships are
resolved"""
data = {}
deferred_fields = model.get_deferred_fields()
@ -70,6 +71,9 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
for key, value in before.items():
if after.get(key) != value:
diff[key] = {"previous_value": value, "new_value": after.get(key)}
for key, value in after.items():
if key not in before and key not in diff and before.get(key) != value:
diff[key] = {"previous_value": before.get(key), "new_value": value}
return sanitize_item(diff)
def post_init_handler(self, request: HttpRequest, sender, instance: Model, **_):
@ -98,8 +102,37 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
thread_kwargs = {}
if hasattr(instance, "_previous_state") or created:
prev_state = getattr(instance, "_previous_state", {})
if created:
prev_state = {}
# Get current state
new_state = self.serialize_simple(instance)
diff = self.diff(prev_state, new_state)
thread_kwargs["diff"] = diff
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)
def m2m_changed_handler( # noqa: PLR0913
self,
request: HttpRequest,
sender,
instance: Model,
action: str,
pk_set: set[Any],
thread_kwargs: dict | None = None,
**_,
):
thread_kwargs = {}
m2m_field = None
# For the audit log we don't care about `pre_` or `post_` so we trim that part off
_, _, action_direction = action.partition("_")
# resolve the "through" model to an actual field
for field in instance._meta.get_fields():
if not isinstance(field, ManyToManyRel):
continue
if field.through == sender:
m2m_field = field
if m2m_field:
# If we're clearing we just set the "flag" to True
if action_direction == "clear":
pk_set = True
thread_kwargs["diff"] = {m2m_field.related_name: {action_direction: pk_set}}
return super().m2m_changed_handler(request, sender, instance, action, thread_kwargs)

View File

@ -1,9 +1,22 @@
from unittest.mock import PropertyMock, patch
from django.apps import apps
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
from authentik.events.utils import sanitize_item
from authentik.lib.generators import generate_id
class TestEnterpriseAudit(TestCase):
class TestEnterpriseAudit(APITestCase):
"""Test audit middleware"""
def setUp(self) -> None:
self.user = create_test_admin_user()
def test_import(self):
"""Ensure middleware is imported when app.ready is called"""
@ -16,3 +29,182 @@ class TestEnterpriseAudit(TestCase):
self.assertIn(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware", settings.MIDDLEWARE
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_create(self):
"""Test create audit log"""
self.client.force_login(self.user)
username = generate_id()
response = self.client.post(
reverse("authentik_api:user-list"),
data={"name": generate_id(), "username": username, "groups": [], "path": "foo"},
)
user = User.objects.get(username=username)
self.assertEqual(response.status_code, 201)
events = Event.objects.filter(
action=EventAction.MODEL_CREATED,
context__model__model_name="user",
context__model__app="authentik_core",
context__model__pk=user.pk,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{
"name": {
"new_value": user.name,
"previous_value": None,
},
"path": {"new_value": "foo", "previous_value": None},
"type": {"new_value": "internal", "previous_value": None},
"uuid": {
"new_value": user.uuid.hex,
"previous_value": None,
},
"email": {"new_value": "", "previous_value": None},
"username": {
"new_value": user.username,
"previous_value": None,
},
"is_active": {"new_value": True, "previous_value": None},
"attributes": {"new_value": {}, "previous_value": None},
"date_joined": {
"new_value": sanitize_item(user.date_joined),
"previous_value": None,
},
"first_name": {"new_value": "", "previous_value": None},
"id": {"new_value": user.pk, "previous_value": None},
"last_name": {"new_value": "", "previous_value": None},
"password": {"new_value": "********************", "previous_value": None},
"password_change_date": {
"new_value": sanitize_item(user.password_change_date),
"previous_value": None,
},
},
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_update(self):
"""Test update audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
current_name = user.name
new_name = generate_id()
response = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": user.id}),
data={"name": new_name},
)
user.refresh_from_db()
self.assertEqual(response.status_code, 200)
events = Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model__model_name="user",
context__model__app="authentik_core",
context__model__pk=user.pk,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{
"name": {
"new_value": new_name,
"previous_value": current_name,
},
},
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_delete(self):
"""Test delete audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
response = self.client.delete(
reverse("authentik_api:user-detail", kwargs={"pk": user.id}),
)
self.assertEqual(response.status_code, 204)
events = Event.objects.filter(
action=EventAction.MODEL_DELETED,
context__model__model_name="user",
context__model__app="authentik_core",
context__model__pk=user.pk,
)
event = events.first()
self.assertIsNotNone(event)
self.assertNotIn("diff", event.context)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_m2m_add(self):
"""Test m2m add audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
group = Group.objects.create(name=generate_id())
response = self.client.post(
reverse("authentik_api:group-add-user", kwargs={"pk": group.group_uuid}),
data={
"pk": user.pk,
},
)
self.assertEqual(response.status_code, 204)
events = Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model__model_name="group",
context__model__app="authentik_core",
context__model__pk=group.pk.hex,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{"users": {"add": [user.pk]}},
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_m2m_remove(self):
"""Test m2m remove audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
group = Group.objects.create(name=generate_id())
response = self.client.post(
reverse("authentik_api:group-remove-user", kwargs={"pk": group.group_uuid}),
data={
"pk": user.pk,
},
)
self.assertEqual(response.status_code, 204)
events = Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model__model_name="group",
context__model__app="authentik_core",
context__model__pk=group.pk.hex,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{"users": {"remove": [user.pk]}},
)

View File

@ -214,7 +214,15 @@ class AuditMiddleware:
model=model_to_dict(instance),
).run()
def m2m_changed_handler(self, request: HttpRequest, sender, instance: Model, action: str, **_):
def m2m_changed_handler(
self,
request: HttpRequest,
sender,
instance: Model,
action: str,
thread_kwargs: dict | None = None,
**_,
):
"""Signal handler for all object's m2m_changed"""
if action not in ["pre_add", "pre_remove", "post_clear"]:
return
@ -229,4 +237,5 @@ class AuditMiddleware:
request,
user=user,
model=model_to_dict(instance),
**thread_kwargs,
).run()

View File

@ -119,7 +119,7 @@ class SystemTask(TenantTask):
"task_call_kwargs": sanitize_item(kwargs),
"status": self._status,
"messages": sanitize_item(self._messages),
"expires": now() + timedelta(hours=self.result_timeout_hours),
"expires": now() + timedelta(hours=self.result_timeout_hours + 3),
"expiring": True,
},
)

View File

@ -33,6 +33,7 @@ from authentik.lib.utils.file import (
)
from authentik.lib.views import bad_request_message
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()
@ -277,7 +278,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
400: OpenApiResponse(description="Flow not applicable"),
},
)
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def execute(self, request: Request, slug: str):
"""Execute flow for current user"""
# Because we pre-plan the flow here, and not in the planner, we need to manually clear

View File

@ -203,7 +203,8 @@ class FlowPlanner:
"f(plan): building plan",
)
plan = self._build_plan(user, request, default_context)
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
if self.use_cache:
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
if not plan.bindings and not self.allow_empty_flows:
raise EmptyFlowException()
return plan

View File

@ -53,6 +53,7 @@ cache:
# result_backend:
# url: ""
# transport_options: ""
debug: false
remote_debug: false
@ -95,9 +96,6 @@ outposts:
discover: true
disable_embedded_outpost: false
expressions:
global_runtime: python # or python_restricted
ldap:
task_timeout_hours: 2
page_size: 50

View File

@ -3,117 +3,27 @@
import re
import socket
from collections.abc import Iterable
from datetime import timedelta
from functools import lru_cache
from ipaddress import ip_address, ip_network
from pathlib import Path
from tempfile import gettempdir
from textwrap import indent
from typing import Any
from authentik_client.api.admin_api import AdminApi
from authentik_client.api.authenticators_api import AuthenticatorsApi
from authentik_client.api.core_api import CoreApi
from authentik_client.api.crypto_api import CryptoApi
from authentik_client.api.enterprise_api import EnterpriseApi
from authentik_client.api.events_api import EventsApi
from authentik_client.api.flows_api import FlowsApi
from authentik_client.api.managed_api import ManagedApi
from authentik_client.api.oauth2_api import Oauth2Api
from authentik_client.api.outposts_api import OutpostsApi
from authentik_client.api.policies_api import PoliciesApi
from authentik_client.api.propertymappings_api import PropertymappingsApi
from authentik_client.api.providers_api import ProvidersApi
from authentik_client.api.rac_api import RacApi
from authentik_client.api.rbac_api import RbacApi
from authentik_client.api.root_api import RootApi
from authentik_client.api.schema_api import SchemaApi
from authentik_client.api.sources_api import SourcesApi
from authentik_client.api.stages_api import StagesApi
from authentik_client.api.tenants_api import TenantsApi
from authentik_client.api_client import ApiClient
from authentik_client.configuration import Configuration
from cachetools import TLRUCache, cached
from django.conf import settings
from django.core.exceptions import FieldError
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
from jwt import PyJWTError, decode, encode
from rest_framework.serializers import ValidationError
from RestrictedPython import compile_restricted, limited_builtins, safe_builtins, utility_builtins
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.events.models import Event
from authentik.lib.config import CONFIG
from authentik.lib.generators import generate_key
from authentik.lib.utils.errors import exception_to_string
from authentik.lib.utils.http import authentik_user_agent, get_http_session
from authentik.lib.utils.reflection import get_apps
from authentik.lib.utils.http import get_http_session
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
from authentik.policies.types import PolicyRequest, PolicyResult
from authentik.stages.authenticator import devices_for_user
LOGGER = get_logger()
_tmp = Path(gettempdir())
token_path = _tmp / "authentik-evaluator-token"
API_CLIENTS = {
"AdminApi": AdminApi,
"AuthenticatorsApi": AuthenticatorsApi,
"CoreApi": CoreApi,
"CryptoApi": CryptoApi,
"EnterpriseApi": EnterpriseApi,
"EventsApi": EventsApi,
"FlowsApi": FlowsApi,
"ManagedApi": ManagedApi,
"Oauth2Api": Oauth2Api,
"OutpostsApi": OutpostsApi,
"PoliciesApi": PoliciesApi,
"PropertymappingsApi": PropertymappingsApi,
"ProvidersApi": ProvidersApi,
"RacApi": RacApi,
"RbacApi": RbacApi,
"RootApi": RootApi,
"SchemaApi": SchemaApi,
"SourcesApi": SourcesApi,
"StagesApi": StagesApi,
"TenantsApi": TenantsApi,
}
JWT_AUD = "goauthentik.io/api/expression"
_SAFE_MODULES = frozenset(("authentik_client",))
def _safe_import(name, *args, **kwargs):
if name not in _SAFE_MODULES:
raise Exception(f"Don't you even think about {name!r}")
return __import__(name, *args, **kwargs)
@lru_cache
def get_api_token_secret():
if token_path.exists():
with open(token_path) as _token_file:
return _token_file.read()
key = generate_key()
with open(_tmp / "authentik-evaluator-token", "w") as _token_file:
_token_file.write(key)
return key
def authenticate_token(raw_value: str):
"""Authenticate API call from evaluator token"""
try:
jwt = decode(raw_value, get_api_token_secret(), ["HS256"], audience=JWT_AUD)
return User.objects.filter(pk=jwt["sub"]).first()
except PyJWTError as exc:
LOGGER.debug("failed to auth", exc=exc)
return None
class BaseEvaluator:
@ -127,14 +37,8 @@ class BaseEvaluator:
# Filename used for exec
_filename: str
_user: User
# Timeout in seconds, used for the expiration of the API key
timeout = 30
def __init__(self, user: User, filename: str | None = None):
def __init__(self, filename: str | None = None):
self._filename = filename if filename else "BaseEvaluator"
self._user = user
# update website/docs/expressions/_objects.md
# update website/docs/expressions/_functions.md
self._globals = {
@ -153,44 +57,8 @@ class BaseEvaluator:
"resolve_dns": BaseEvaluator.expr_resolve_dns,
"reverse_dns": BaseEvaluator.expr_reverse_dns,
}
for app in get_apps():
# Load models from each app
for model in app.get_models():
self._globals[model.__name__] = model
self._globals.update(API_CLIENTS)
self._context = {}
def get_token(self) -> str:
"""Generate API token to be used by the API Client"""
_now = now()
if not self._user:
self._user = get_anonymous_user()
return encode(
{
"aud": JWT_AUD,
"iss": f"goauthentik.io/expression/{self._filename}",
"sub": str(self._user.pk),
"iat": int(_now.timestamp()),
"exp": int((_now + timedelta(seconds=self.timeout)).timestamp()),
},
get_api_token_secret(),
)
def get_api_client(self):
token = self.get_token()
config = Configuration(
f"unix://{str(_tmp.joinpath('authentik-core.sock'))}/api/v3",
api_key={
"authentik": token,
},
api_key_prefix={"authentik": "Bearer"},
)
if settings.DEBUG:
config.host = "http://localhost:8000/api/v3"
client = ApiClient(config)
client.user_agent = authentik_user_agent()
return client
@cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180))
@staticmethod
def expr_resolve_dns(host: str, ip_version: int | None = None) -> list[str]:
@ -317,16 +185,7 @@ class BaseEvaluator:
def compile(self, expression: str) -> Any:
"""Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect."""
param_keys = self._context.keys()
compiler = (
compile_restricted
if CONFIG.get("expressions.global_runtime") == "python_restricted"
else compile
)
return compiler(
self.wrap_expression(expression, param_keys),
self._filename,
"exec",
)
return compile(self.wrap_expression(expression, param_keys), self._filename, "exec")
def evaluate(self, expression_source: str) -> Any:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
@ -342,17 +201,7 @@ class BaseEvaluator:
self.handle_error(exc, expression_source)
raise exc
try:
if CONFIG.get("expressions.global_runtime") == "python_restricted":
self._globals["__builtins__"] = {
**safe_builtins,
**limited_builtins,
**utility_builtins,
"__import__": _safe_import,
}
_locals = self._context
# We need to create the API Client later so that the token is valid
# from when the execution starts
self._globals["api"] = self.get_api_client()
# Yes this is an exec, yes it is potentially bad. Since we limit what variables are
# available here, and these policies can only be edited by admins, this is a risk
# we're willing to take.
@ -360,7 +209,6 @@ class BaseEvaluator:
exec(ast_obj, self._globals, _locals) # nosec # noqa
result = _locals["result"]
except Exception as exc:
print(exception_to_string(exc))
# So, this is a bit questionable. Essentially, we are edit the stacktrace
# so the user only sees information relevant to them
# and none of our surrounding error handling

View File

@ -1,7 +1,6 @@
"""Test Evaluator base functions"""
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event
@ -34,7 +33,7 @@ class TestEvaluator(TestCase):
def test_expr_event_create(self):
"""Test expr_event_create"""
evaluator = BaseEvaluator(get_anonymous_user(), generate_id())
evaluator = BaseEvaluator(generate_id())
evaluator._context = {
"foo": "bar",
}

View File

@ -23,6 +23,7 @@ from authentik.outposts.models import (
KubernetesServiceConnection,
OutpostServiceConnection,
)
from authentik.rbac.filters import ObjectFilter
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
@ -88,7 +89,7 @@ class ServiceConnectionViewSet(
return Response(TypeCreateSerializer(data, many=True).data)
@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[])
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def state(self, request: Request, pk: str) -> Response:
"""Get the service connection's state"""
connection = self.get_object()

View File

@ -14,13 +14,12 @@ class ExpressionPolicySerializer(PolicySerializer):
def validate_expression(self, expr: str) -> str:
"""validate the syntax of the expression"""
name = "temp-policy" if not self.instance else self.instance.name
request = self.context.get("request")
PolicyEvaluator(request.user if request else None, name).validate(expr)
PolicyEvaluator(name).validate(expr)
return expr
class Meta:
model = ExpressionPolicy
fields = PolicySerializer.Meta.fields + ["expression", "execution_user"]
fields = PolicySerializer.Meta.fields + ["expression"]
class ExpressionPolicyViewSet(UsedByMixin, ModelViewSet):

View File

@ -1,12 +1,11 @@
"""Authentik policy_expression app config"""
from authentik.blueprints.apps import ManagedAppConfig
from django.apps import AppConfig
class AuthentikPolicyExpressionConfig(ManagedAppConfig):
class AuthentikPolicyExpressionConfig(AppConfig):
"""Authentik policy_expression app config"""
name = "authentik.policies.expression"
label = "authentik_policies_expression"
verbose_name = "authentik Policies.Expression"
default = True

View File

@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Optional
from django.http import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.flows.planner import PLAN_CONTEXT_SSO
from authentik.lib.expression.evaluator import BaseEvaluator
from authentik.policies.exceptions import PolicyException
@ -25,8 +24,8 @@ class PolicyEvaluator(BaseEvaluator):
policy: Optional["ExpressionPolicy"] = None
def __init__(self, user: User, policy_name: str | None = None):
super().__init__(user, policy_name or "PolicyEvaluator")
def __init__(self, policy_name: str | None = None):
super().__init__(policy_name or "PolicyEvaluator")
self._messages = []
# update website/docs/expressions/_objects.md
# update website/docs/expressions/_functions.md
@ -45,8 +44,6 @@ class PolicyEvaluator(BaseEvaluator):
if request.http_request:
self.set_http_request(request.http_request)
self._context["request"] = request
if not self._user:
self._user = request.user
self._context["context"] = request.context
def set_http_request(self, request: HttpRequest):

View File

@ -1,26 +0,0 @@
# Generated by Django 5.0.3 on 2024-03-20 12:14
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_expression", "0004_expressionpolicy_authentik_p_policy__fb6feb_idx"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="expressionpolicy",
name="execution_user",
field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@ -12,9 +12,6 @@ from authentik.policies.types import PolicyRequest, PolicyResult
class ExpressionPolicy(Policy):
"""Execute arbitrary Python code to implement custom checks and validation."""
execution_user = models.ForeignKey(
"authentik_core.User", default=None, null=True, on_delete=models.SET_DEFAULT
)
expression = models.TextField()
@property
@ -29,11 +26,17 @@ class ExpressionPolicy(Policy):
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
evaluator = PolicyEvaluator(self.execution_user, self.name)
evaluator = PolicyEvaluator(self.name)
evaluator.policy = self
evaluator.set_policy_request(request)
return evaluator.evaluate(self.expression)
def save(self, *args, **kwargs):
evaluator = PolicyEvaluator(self.name)
evaluator.policy = self
evaluator.validate(self.expression)
return super().save(*args, **kwargs)
class Meta(Policy.PolicyMeta):
verbose_name = _("Expression Policy")
verbose_name_plural = _("Expression Policies")

View File

@ -1,13 +0,0 @@
from django.db.models.signals import pre_save
from django.dispatch import receiver
from authentik.policies.expression.evaluator import PolicyEvaluator
from authentik.policies.expression.models import ExpressionPolicy
@receiver(pre_save, sender=ExpressionPolicy)
def pre_save_expression_policy(sender: type[ExpressionPolicy], instance: ExpressionPolicy, **_):
"""Ensure policy is valid before saving"""
evaluator = PolicyEvaluator(instance.execution_user, instance.name)
evaluator.policy = instance
evaluator.validate(instance.expression)

View File

@ -41,14 +41,14 @@ class TestEvaluator(TestCase):
def test_valid(self):
"""test simple value expression"""
template = "return True"
evaluator = PolicyEvaluator(self.request.user, "test")
evaluator = PolicyEvaluator("test")
evaluator.set_policy_request(self.request)
self.assertEqual(evaluator.evaluate(template).passing, True)
def test_messages(self):
"""test expression with message return"""
template = 'ak_message("some message");return False'
evaluator = PolicyEvaluator(self.request.user, "test")
evaluator = PolicyEvaluator("test")
evaluator.set_policy_request(self.request)
result = evaluator.evaluate(template)
self.assertEqual(result.passing, False)
@ -57,7 +57,7 @@ class TestEvaluator(TestCase):
def test_invalid_syntax(self):
"""test invalid syntax"""
template = ";"
evaluator = PolicyEvaluator(self.request.user, "test")
evaluator = PolicyEvaluator("test")
evaluator.set_policy_request(self.request)
with self.assertRaises(PolicyException):
evaluator.evaluate(template)
@ -65,14 +65,14 @@ class TestEvaluator(TestCase):
def test_validate(self):
"""test validate"""
template = "True"
evaluator = PolicyEvaluator(self.request.user, "test")
evaluator = PolicyEvaluator("test")
result = evaluator.validate(template)
self.assertEqual(result, True)
def test_validate_invalid(self):
"""test validate"""
template = ";"
evaluator = PolicyEvaluator(self.request.user, "test")
evaluator = PolicyEvaluator("test")
with self.assertRaises(ValidationError):
evaluator.validate(template)
@ -83,7 +83,7 @@ class TestEvaluator(TestCase):
execution_logging=True,
expression="ak_message(request.http_request.path)\nreturn True",
)
evaluator = PolicyEvaluator(self.request.user, "test")
evaluator = PolicyEvaluator("test")
evaluator.set_policy_request(self.request)
proc = PolicyProcess(PolicyBinding(policy=expr), request=self.request, connection=None)
res = proc.profiling_wrapper()
@ -96,13 +96,16 @@ class TestEvaluator(TestCase):
execution_logging=True,
expression="ak_message(request.http_request.path)\nreturn True",
)
tmpl = f"""
tmpl = (
"""
ak_message(request.http_request.path)
res = ak_call_policy('{expr.name}')
res = ak_call_policy('%s')
ak_message(request.http_request.path)
for msg in res.messages:
ak_message(msg)
"""
% expr.name
)
evaluator = PolicyEvaluator("test")
evaluator.set_policy_request(self.request)
res = evaluator.evaluate(tmpl)

View File

@ -4,9 +4,10 @@ from urllib.parse import urlencode
from django.urls import reverse
from authentik.core.models import Application
from authentik.core.models import Application, Group
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.lib.generators import generate_id
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
@ -77,3 +78,23 @@ class TesOAuth2DeviceInit(OAuthTestCase):
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code}),
)
def test_device_init_denied(self):
"""Test device init"""
group = Group.objects.create(name="foo")
PolicyBinding.objects.create(
group=group,
target=self.application,
order=0,
)
token = DeviceToken.objects.create(
user_code="foo",
provider=self.provider,
)
res = self.client.get(
reverse("authentik_providers_oauth2_root:device-login")
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code})
)
self.assertEqual(res.status_code, 200)
self.assertIn(b"Permission denied", res.content)

View File

@ -10,6 +10,7 @@ from jwt import PyJWKSet
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.crypto.builder import PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider
@ -82,7 +83,7 @@ class TestJWKS(OAuthTestCase):
client_id="test",
authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid",
signing_key=create_test_cert(use_ec_private_key=True),
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
)
app = Application.objects.create(name="test", slug="test", provider=provider)
response = self.client.get(

View File

@ -11,10 +11,11 @@ from django.views.decorators.csrf import csrf_exempt
from rest_framework.throttling import AnonRateThrottle
from structlog.stdlib import get_logger
from authentik.core.models import Application
from authentik.lib.config import CONFIG
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE, get_application
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
LOGGER = get_logger()
@ -37,7 +38,9 @@ class DeviceView(View):
).first()
if not provider:
return HttpResponseBadRequest()
if not get_application(provider):
try:
_ = provider.application
except Application.DoesNotExist:
return HttpResponseBadRequest()
self.provider = provider
self.client_id = client_id

View File

@ -1,8 +1,9 @@
"""Device flow views"""
from typing import Any
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from django.views import View
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, IntegerField
from structlog.stdlib import get_logger
@ -16,7 +17,8 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO,
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.policies.views import PolicyAccessView
from authentik.providers.oauth2.models import DeviceToken
from authentik.providers.oauth2.views.device_finish import (
PLAN_CONTEXT_DEVICE,
OAuthDeviceCodeFinishStage,
@ -31,60 +33,52 @@ LOGGER = get_logger()
QS_KEY_CODE = "code" # nosec
def get_application(provider: OAuth2Provider) -> Application | None:
"""Get application from provider"""
try:
app = provider.application
if not app:
class CodeValidatorView(PolicyAccessView):
"""Helper to validate frontside token"""
def __init__(self, code: str, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.code = code
def resolve_provider_application(self):
self.token = DeviceToken.objects.filter(user_code=self.code).first()
if not self.token:
raise Application.DoesNotExist
self.provider = self.token.provider
self.application = self.token.provider.application
def get(self, request: HttpRequest, *args, **kwargs):
scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True
planner.use_cache = False
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: self.token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": self.application.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
return app
except Application.DoesNotExist:
return None
def validate_code(code: int, request: HttpRequest) -> HttpResponse | None:
"""Validate user token"""
token = DeviceToken.objects.filter(
user_code=code,
).first()
if not token:
return None
app = get_application(token.provider)
if not app:
return None
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider)
planner = FlowPlanner(token.provider.authorization_flow)
planner.allow_empty_flows = True
planner.use_cache = False
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: app,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": app.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=self.token.provider.authorization_flow.slug,
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=token.provider.authorization_flow.slug,
)
class DeviceEntryView(View):
class DeviceEntryView(PolicyAccessView):
"""View used to initiate the device-code flow, url entered by endusers"""
def dispatch(self, request: HttpRequest) -> HttpResponse:
@ -94,7 +88,9 @@ class DeviceEntryView(View):
LOGGER.info("Brand has no device code flow configured", brand=brand)
return HttpResponse(status=404)
if QS_KEY_CODE in request.GET:
validation = validate_code(request.GET[QS_KEY_CODE], request)
validation = CodeValidatorView(request.GET[QS_KEY_CODE], request=request).dispatch(
request
)
if validation:
return validation
LOGGER.info("Got code from query parameter but no matching token found")
@ -131,7 +127,7 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
def validate_code(self, code: int) -> HttpResponse | None:
"""Validate code and save the returned http response"""
response = validate_code(code, self.stage.request)
response = CodeValidatorView(code, request=self.stage.request).dispatch(self.stage.request)
if not response:
raise ValidationError(_("Invalid code"), "invalid")
return response

View File

@ -0,0 +1,44 @@
# Generated by Django 5.0.4 on 2024-05-01 15:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0013_samlprovider_default_relay_state"),
]
operations = [
migrations.AlterField(
model_name="samlprovider",
name="digest_algorithm",
field=models.TextField(
choices=[
("http://www.w3.org/2000/09/xmldsig#sha1", "SHA1"),
("http://www.w3.org/2001/04/xmlenc#sha256", "SHA256"),
("http://www.w3.org/2001/04/xmldsig-more#sha384", "SHA384"),
("http://www.w3.org/2001/04/xmlenc#sha512", "SHA512"),
],
default="http://www.w3.org/2001/04/xmlenc#sha256",
),
),
migrations.AlterField(
model_name="samlprovider",
name="signature_algorithm",
field=models.TextField(
choices=[
("http://www.w3.org/2000/09/xmldsig#rsa-sha1", "RSA-SHA1"),
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "RSA-SHA256"),
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "RSA-SHA384"),
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "RSA-SHA512"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1", "ECDSA-SHA1"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", "ECDSA-SHA256"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384", "ECDSA-SHA384"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512", "ECDSA-SHA512"),
("http://www.w3.org/2000/09/xmldsig#dsa-sha1", "DSA-SHA1"),
],
default="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
),
),
]

View File

@ -11,6 +11,10 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import (
DSA_SHA1,
ECDSA_SHA1,
ECDSA_SHA256,
ECDSA_SHA384,
ECDSA_SHA512,
RSA_SHA1,
RSA_SHA256,
RSA_SHA384,
@ -92,8 +96,7 @@ class SAMLProvider(Provider):
),
)
digest_algorithm = models.CharField(
max_length=50,
digest_algorithm = models.TextField(
choices=(
(SHA1, _("SHA1")),
(SHA256, _("SHA256")),
@ -102,13 +105,16 @@ class SAMLProvider(Provider):
),
default=SHA256,
)
signature_algorithm = models.CharField(
max_length=50,
signature_algorithm = models.TextField(
choices=(
(RSA_SHA1, _("RSA-SHA1")),
(RSA_SHA256, _("RSA-SHA256")),
(RSA_SHA384, _("RSA-SHA384")),
(RSA_SHA512, _("RSA-SHA512")),
(ECDSA_SHA1, _("ECDSA-SHA1")),
(ECDSA_SHA256, _("ECDSA-SHA256")),
(ECDSA_SHA384, _("ECDSA-SHA384")),
(ECDSA_SHA512, _("ECDSA-SHA512")),
(DSA_SHA1, _("DSA-SHA1")),
),
default=RSA_SHA256,

View File

@ -7,13 +7,14 @@ from lxml import etree # nosec
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.crypto.builder import PrivateKeyAlg
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.lib.xml import lxml_from_string
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.metadata import MetadataProcessor
from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
from authentik.sources.saml.processors.constants import NS_MAP, NS_SAML_METADATA
from authentik.sources.saml.processors.constants import ECDSA_SHA256, NS_MAP, NS_SAML_METADATA
class TestServiceProviderMetadataParser(TestCase):
@ -107,12 +108,41 @@ class TestServiceProviderMetadataParser(TestCase):
load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
)
def test_signature(self):
"""Test signature validation"""
def test_signature_rsa(self):
"""Test signature validation (RSA)"""
provider = SAMLProvider.objects.create(
name=generate_id(),
authorization_flow=self.flow,
signing_kp=create_test_cert(),
signing_kp=create_test_cert(PrivateKeyAlg.RSA),
)
Application.objects.create(
name=generate_id(),
slug=generate_id(),
provider=provider,
)
request = self.factory.get("/")
metadata = MetadataProcessor(provider, request).build_entity_descriptor()
root = fromstring(metadata.encode())
xmlsec.tree.add_ids(root, ["ID"])
signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
signature_node = signature_nodes[0]
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(
provider.signing_kp.certificate_data,
xmlsec.constants.KeyDataFormatCertPem,
None,
)
ctx.key = key
ctx.verify(signature_node)
def test_signature_ecdsa(self):
"""Test signature validation (ECDSA)"""
provider = SAMLProvider.objects.create(
name=generate_id(),
authorization_flow=self.flow,
signing_kp=create_test_cert(PrivateKeyAlg.ECDSA),
signature_algorithm=ECDSA_SHA256,
)
Application.objects.create(
name=generate_id(),

View File

@ -41,7 +41,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
if not scim_group:
self.logger.debug("Group does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Groups/{scim_group.id}")
response = self._request("DELETE", f"/Groups/{scim_group.scim_id}")
scim_group.delete()
return response
@ -89,7 +89,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
for user in connections:
members.append(
GroupMember(
value=user.id,
value=user.scim_id,
)
)
if members:
@ -107,16 +107,19 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
exclude_unset=True,
),
)
SCIMGroup.objects.create(provider=self.provider, group=group, id=response["id"])
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
SCIMGroup.objects.create(provider=self.provider, group=group, scim_id=scim_id)
def _update(self, group: Group, connection: SCIMGroup):
"""Update existing group"""
scim_group = self.to_scim(group)
scim_group.id = connection.id
scim_group.id = connection.scim_id
try:
return self._request(
"PUT",
f"/Groups/{scim_group.id}",
f"/Groups/{connection.scim_id}",
json=scim_group.model_dump(
mode="json",
exclude_unset=True,
@ -185,13 +188,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"id", flat=True
"scim_id", flat=True
)
)
if len(user_ids) < 1:
return
self._patch(
scim_group.id,
scim_group.scim_id,
PatchOperation(
op=PatchOp.add,
path="members",
@ -211,13 +214,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"id", flat=True
"scim_id", flat=True
)
)
if len(user_ids) < 1:
return
self._patch(
scim_group.id,
scim_group.scim_id,
PatchOperation(
op=PatchOp.remove,
path="members",

View File

@ -9,13 +9,14 @@ from pydanticscim.service_provider import (
)
from pydanticscim.user import User as BaseUser
SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
class User(BaseUser):
"""Modified User schema with added externalId field"""
schemas: list[str] = [
"urn:ietf:params:scim:schemas:core:2.0:User",
]
schemas: list[str] = [SCIM_USER_SCHEMA]
externalId: str | None = None
meta: dict | None = None
@ -23,9 +24,7 @@ class User(BaseUser):
class Group(BaseGroup):
"""Modified Group schema with added externalId field"""
schemas: list[str] = [
"urn:ietf:params:scim:schemas:core:2.0:Group",
]
schemas: list[str] = [SCIM_GROUP_SCHEMA]
externalId: str | None = None
meta: dict | None = None

View File

@ -34,7 +34,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
if not scim_user:
self.logger.debug("User does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Users/{scim_user.id}")
response = self._request("DELETE", f"/Users/{scim_user.scim_id}")
scim_user.delete()
return response
@ -85,15 +85,18 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
exclude_unset=True,
),
)
SCIMUser.objects.create(provider=self.provider, user=user, id=response["id"])
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
SCIMUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
def _update(self, user: User, connection: SCIMUser):
"""Update existing user"""
scim_user = self.to_scim(user)
scim_user.id = connection.id
scim_user.id = connection.scim_id
self._request(
"PUT",
f"/Users/{connection.id}",
f"/Users/{connection.scim_id}",
json=scim_user.model_dump(
mode="json",
exclude_unset=True,

View File

@ -3,7 +3,7 @@
from structlog.stdlib import get_logger
from authentik.providers.scim.models import SCIMProvider
from authentik.providers.scim.tasks import scim_sync
from authentik.providers.scim.tasks import scim_task_wrapper
from authentik.tenants.management import TenantCommand
LOGGER = get_logger()
@ -21,4 +21,4 @@ class Command(TenantCommand):
if not provider:
LOGGER.warning("Provider does not exist", name=provider_name)
continue
scim_sync.delay(provider.pk).get()
scim_task_wrapper(provider.pk).get()

View File

@ -0,0 +1,76 @@
# Generated by Django 5.0.4 on 2024-05-03 12:38
import uuid
from django.db import migrations, models
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.lib.migrations import progress_bar
def fix_scim_user_group_pk(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
SCIMUser = apps.get_model("authentik_providers_scim", "SCIMUser")
SCIMGroup = apps.get_model("authentik_providers_scim", "SCIMGroup")
db_alias = schema_editor.connection.alias
print("\nFixing primary key for SCIM users, this might take a couple of minutes...")
for user in progress_bar(SCIMUser.objects.using(db_alias).all()):
SCIMUser.objects.using(db_alias).filter(
pk=user.pk, user=user.user_id, provider=user.provider_id
).update(scim_id=user.pk, id=uuid.uuid4())
print("\nFixing primary key for SCIM groups, this might take a couple of minutes...")
for group in progress_bar(SCIMGroup.objects.using(db_alias).all()):
SCIMGroup.objects.using(db_alias).filter(
pk=group.pk, group=group.group_id, provider=group.provider_id
).update(scim_id=group.pk, id=uuid.uuid4())
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_scim",
"0001_squashed_0006_rename_parent_group_scimprovider_filter_group",
),
]
operations = [
migrations.AddField(
model_name="scimgroup",
name="scim_id",
field=models.TextField(default="temp"),
preserve_default=False,
),
migrations.AddField(
model_name="scimuser",
name="scim_id",
field=models.TextField(default="temp"),
preserve_default=False,
),
migrations.RunPython(fix_scim_user_group_pk),
migrations.AlterField(
model_name="scimgroup",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="scimuser",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
migrations.AlterField(model_name="scimuser", name="scim_id", field=models.TextField()),
migrations.AlterField(model_name="scimgroup", name="scim_id", field=models.TextField()),
migrations.AlterUniqueTogether(
name="scimgroup",
unique_together={("scim_id", "group", "provider")},
),
migrations.AlterUniqueTogether(
name="scimuser",
unique_together={("scim_id", "user", "provider")},
),
]

View File

@ -1,5 +1,7 @@
"""SCIM Provider models"""
from uuid import uuid4
from django.core.cache import cache
from django.db import models
from django.db.models import QuerySet
@ -97,12 +99,13 @@ class SCIMMapping(PropertyMapping):
class SCIMUser(models.Model):
"""Mapping of a user and provider to a SCIM user ID"""
id = models.TextField(primary_key=True)
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
scim_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("id", "user", "provider"),)
unique_together = (("scim_id", "user", "provider"),)
def __str__(self) -> str:
return f"SCIM User {self.user_id} to {self.provider_id}"
@ -111,12 +114,13 @@ class SCIMUser(models.Model):
class SCIMGroup(models.Model):
"""Mapping of a group and provider to a SCIM user ID"""
id = models.TextField(primary_key=True)
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
scim_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("id", "group", "provider"),)
unique_together = (("scim_id", "group", "provider"),)
def __str__(self) -> str:
return f"SCIM Group {self.group_id} to {self.provider_id}"

View File

@ -9,7 +9,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import Group, User
from authentik.lib.utils.reflection import class_to_path
from authentik.providers.scim.models import SCIMProvider
from authentik.providers.scim.tasks import scim_signal_direct, scim_signal_m2m, scim_sync
from authentik.providers.scim.tasks import scim_signal_direct, scim_signal_m2m, scim_task_wrapper
LOGGER = get_logger()
@ -17,7 +17,7 @@ LOGGER = get_logger()
@receiver(post_save, sender=SCIMProvider)
def post_save_provider(sender: type[Model], instance, created: bool, **_):
"""Trigger sync when SCIM provider is saved"""
scim_sync.delay(instance.pk)
scim_task_wrapper(instance.pk)
@receiver(post_save, sender=User)

View File

@ -38,7 +38,23 @@ def client_for_model(provider: SCIMProvider, model: Model) -> SCIMClient:
def scim_sync_all():
"""Run sync for all providers"""
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
scim_sync.delay(provider.pk)
scim_task_wrapper(provider.pk)
def scim_task_wrapper(provider_pk: int):
"""Wrap scim_sync to set the correct timeouts"""
provider: SCIMProvider = SCIMProvider.objects.filter(
pk=provider_pk, backchannel_application__isnull=False
).first()
if not provider:
return
users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
soft_time_limit = (users_paginator.num_pages + groups_paginator.num_pages) * PAGE_TIMEOUT
time_limit = soft_time_limit * 1.5
return scim_sync.apply_async(
(provider.pk,), time_limit=int(time_limit), soft_time_limit=int(soft_time_limit)
)
@CELERY_APP.task(bind=True, base=SystemTask)
@ -60,7 +76,7 @@ def scim_sync(self: SystemTask, provider_pk: int) -> None:
users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
self.soft_time_limit = self.time_limit = (
users_paginator.count + groups_paginator.count
users_paginator.num_pages + groups_paginator.num_pages
) * PAGE_TIMEOUT
with allow_join_result():
try:

View File

@ -8,7 +8,7 @@ from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
from authentik.providers.scim.tasks import scim_sync
from authentik.providers.scim.tasks import scim_task_wrapper
from authentik.tenants.models import Tenant
@ -79,7 +79,7 @@ class SCIMMembershipTests(TestCase):
)
self.configure()
scim_sync.delay(self.provider.pk).get()
scim_task_wrapper(self.provider.pk).get()
self.assertEqual(mocker.call_count, 6)
self.assertEqual(mocker.request_history[0].method, "GET")
@ -169,7 +169,7 @@ class SCIMMembershipTests(TestCase):
)
self.configure()
scim_sync.delay(self.provider.pk).get()
scim_task_wrapper(self.provider.pk).get()
self.assertEqual(mocker.call_count, 6)
self.assertEqual(mocker.request_history[0].method, "GET")

View File

@ -10,7 +10,7 @@ from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
from authentik.providers.scim.tasks import scim_sync
from authentik.providers.scim.tasks import scim_task_wrapper
from authentik.tenants.models import Tenant
@ -88,6 +88,72 @@ class SCIMUserTests(TestCase):
},
)
@Mocker()
def test_user_create_different_provider_same_id(self, mock: Mocker):
"""Test user creation with multiple providers that happen
to return the same object ID"""
# Create duplicate provider
provider: SCIMProvider = SCIMProvider.objects.create(
name=generate_id(),
url="https://localhost",
token=generate_id(),
exclude_users_service_account=True,
)
app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
)
app.backchannel_providers.add(provider)
provider.property_mappings.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
)
provider.property_mappings_group.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
)
scim_id = generate_id()
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
)
mock.post(
"https://localhost/Users",
json={
"id": scim_id,
},
)
uid = generate_id()
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
self.assertEqual(mock.call_count, 4)
self.assertEqual(mock.request_history[0].method, "GET")
self.assertEqual(mock.request_history[1].method, "POST")
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
"primary": True,
"type": "other",
"value": f"{uid}@goauthentik.io",
}
],
"externalId": user.uid,
"name": {
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"displayName": f"{uid} {uid}",
"userName": uid,
},
)
@Mocker()
def test_user_create_update(self, mock: Mocker):
"""Test user creation and update"""
@ -236,7 +302,7 @@ class SCIMUserTests(TestCase):
email=f"{uid}@goauthentik.io",
)
scim_sync.delay(self.provider.pk).get()
scim_task_wrapper(self.provider.pk).get()
self.assertEqual(mock.call_count, 5)
self.assertEqual(mock.request_history[0].method, "GET")

View File

@ -25,7 +25,7 @@ class ObjectFilter(ObjectPermissionsFilter):
# Outposts (which are the only objects using internal service accounts)
# except requests to return an empty list when they have no objects
# assigned
if request.user.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
if getattr(request.user, "type", UserTypes.INTERNAL) == UserTypes.INTERNAL_SERVICE_ACCOUNT:
return queryset
if not queryset.exists():
# User doesn't have direct permission to all objects

View File

@ -376,7 +376,13 @@ CELERY = {
"task_default_queue": "authentik",
"broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")),
"result_backend": CONFIG.get("result_backend.url") or redis_url(CONFIG.get("redis.db")),
"broker_transport_options": CONFIG.get_dict_from_b64_json("broker.transport_options"),
"broker_transport_options": CONFIG.get_dict_from_b64_json(
"broker.transport_options", {"retry_policy": {"timeout": 5.0}}
),
"result_backend_transport_options": CONFIG.get_dict_from_b64_json(
"result_backend.transport_options", {"retry_policy": {"timeout": 5.0}}
),
"redis_retry_on_timeout": True,
}
# Sentry integration

View File

@ -76,7 +76,7 @@ class S3Storage(BaseS3Storage):
return safe_join(self.location, connection.schema_name, name)
except ValueError:
raise SuspiciousOperation(f"Attempted access to '{name}' denied.") from None
raise SuspiciousOperation("Attempted access to '%s' denied." % name) from None
# This is a fix for https://github.com/jschneier/django-storages/pull/839
def url(self, name, parameters=None, expire=None, http_method=None):

View File

@ -0,0 +1,44 @@
# Generated by Django 5.0.4 on 2024-05-01 15:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_sources_saml", "0013_samlsource_verification_kp_and_more"),
]
operations = [
migrations.AlterField(
model_name="samlsource",
name="digest_algorithm",
field=models.TextField(
choices=[
("http://www.w3.org/2000/09/xmldsig#sha1", "SHA1"),
("http://www.w3.org/2001/04/xmlenc#sha256", "SHA256"),
("http://www.w3.org/2001/04/xmldsig-more#sha384", "SHA384"),
("http://www.w3.org/2001/04/xmlenc#sha512", "SHA512"),
],
default="http://www.w3.org/2001/04/xmlenc#sha256",
),
),
migrations.AlterField(
model_name="samlsource",
name="signature_algorithm",
field=models.TextField(
choices=[
("http://www.w3.org/2000/09/xmldsig#rsa-sha1", "RSA-SHA1"),
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "RSA-SHA256"),
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "RSA-SHA384"),
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "RSA-SHA512"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1", "ECDSA-SHA1"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", "ECDSA-SHA256"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384", "ECDSA-SHA384"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512", "ECDSA-SHA512"),
("http://www.w3.org/2000/09/xmldsig#dsa-sha1", "DSA-SHA1"),
],
default="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
),
),
]

View File

@ -15,6 +15,10 @@ from authentik.flows.models import Flow
from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import (
DSA_SHA1,
ECDSA_SHA1,
ECDSA_SHA256,
ECDSA_SHA384,
ECDSA_SHA512,
RSA_SHA1,
RSA_SHA256,
RSA_SHA384,
@ -143,8 +147,7 @@ class SAMLSource(Source):
verbose_name=_("Signing Keypair"),
)
digest_algorithm = models.CharField(
max_length=50,
digest_algorithm = models.TextField(
choices=(
(SHA1, _("SHA1")),
(SHA256, _("SHA256")),
@ -153,13 +156,16 @@ class SAMLSource(Source):
),
default=SHA256,
)
signature_algorithm = models.CharField(
max_length=50,
signature_algorithm = models.TextField(
choices=(
(RSA_SHA1, _("RSA-SHA1")),
(RSA_SHA256, _("RSA-SHA256")),
(RSA_SHA384, _("RSA-SHA384")),
(RSA_SHA512, _("RSA-SHA512")),
(ECDSA_SHA1, _("ECDSA-SHA1")),
(ECDSA_SHA256, _("ECDSA-SHA256")),
(ECDSA_SHA384, _("ECDSA-SHA384")),
(ECDSA_SHA512, _("ECDSA-SHA512")),
(DSA_SHA1, _("DSA-SHA1")),
),
default=RSA_SHA256,

View File

@ -26,9 +26,16 @@ SAML_BINDING_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
# https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.2
RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
# https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.6
ECDSA_SHA1 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1"
ECDSA_SHA224 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224"
ECDSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"
ECDSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384"
ECDSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"
SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
@ -41,6 +48,11 @@ SIGN_ALGORITHM_TRANSFORM_MAP = {
RSA_SHA256: xmlsec.constants.TransformRsaSha256,
RSA_SHA384: xmlsec.constants.TransformRsaSha384,
RSA_SHA512: xmlsec.constants.TransformRsaSha512,
ECDSA_SHA1: xmlsec.constants.TransformEcdsaSha1,
ECDSA_SHA224: xmlsec.constants.TransformEcdsaSha224,
ECDSA_SHA256: xmlsec.constants.TransformEcdsaSha256,
ECDSA_SHA384: xmlsec.constants.TransformEcdsaSha384,
ECDSA_SHA512: xmlsec.constants.TransformEcdsaSha512,
}
DIGEST_ALGORITHM_TRANSLATION_MAP = {

View File

@ -13,6 +13,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.models import Group, User
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
from authentik.providers.scim.clients.schema import Group as SCIMGroupModel
from authentik.sources.scim.models import SCIMSourceGroup
from authentik.sources.scim.views.v2.base import SCIMView
@ -26,9 +27,11 @@ class GroupsView(SCIMView):
def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict:
"""Convert Group to SCIM data"""
payload = SCIMGroupModel(
schemas=[SCIM_USER_SCHEMA],
id=str(scim_group.group.pk),
externalId=scim_group.id,
displayName=scim_group.group.name,
members=[],
meta={
"resourceType": "Group",
"location": self.request.build_absolute_uri(
@ -42,28 +45,24 @@ class GroupsView(SCIMView):
),
},
)
return payload.model_dump(
mode="json",
exclude_unset=True,
)
for member in scim_group.group.users.order_by("pk"):
member: User
payload.members.append(GroupMember(value=str(member.uuid)))
return payload.model_dump(mode="json", exclude_unset=True)
def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response:
"""List Group handler"""
base_query = SCIMSourceGroup.objects.select_related("group").prefetch_related(
"group__users"
)
if group_id:
connection = (
SCIMSourceGroup.objects.filter(source=self.source, group__group_uuid=group_id)
.select_related("group")
.first()
)
connection = base_query.filter(source=self.source, group__group_uuid=group_id).first()
if not connection:
raise Http404
return Response(self.group_to_scim(connection))
connections = (
SCIMSourceGroup.objects.filter(source=self.source)
.select_related("group")
.order_by("pk")
base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request))
)
connections = connections.filter(self.filter_parse(request))
page = self.paginate_query(connections)
return Response(
{
@ -79,6 +78,8 @@ class GroupsView(SCIMView):
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict):
"""Partial update a group"""
group = connection.group if connection else Group()
if _group := Group.objects.filter(name=data.get("displayName")).first():
group = _group
if "displayName" in data:
group.name = data.get("displayName")
if group.name == "":

View File

@ -11,6 +11,7 @@ from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.models import User
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
from authentik.providers.scim.clients.schema import User as SCIMUserModel
from authentik.sources.scim.models import SCIMSourceUser
from authentik.sources.scim.views.v2.base import SCIMView
@ -33,6 +34,7 @@ class UsersView(SCIMView):
def user_to_scim(self, scim_user: SCIMSourceUser) -> dict:
"""Convert User to SCIM data"""
payload = SCIMUserModel(
schemas=[SCIM_USER_SCHEMA],
id=str(scim_user.user.uuid),
externalId=scim_user.id,
userName=scim_user.user.username,
@ -62,10 +64,7 @@ class UsersView(SCIMView):
),
},
)
final_payload = payload.model_dump(
mode="json",
exclude_unset=True,
)
final_payload = payload.model_dump(mode="json", exclude_unset=True)
final_payload.update(scim_user.attributes)
return final_payload
@ -99,6 +98,8 @@ class UsersView(SCIMView):
def update_user(self, connection: SCIMSourceUser | None, data: QueryDict):
"""Partial update a user"""
user = connection.user if connection else User()
if _user := User.objects.filter(username=data.get("userName")).first():
user = _user
user.path = self.source.get_user_path()
if "userName" in data:
user.username = data.get("userName")

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.4 on 2024-05-01 15:32
import authentik.lib.utils.time
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_tenants", "0002_tenant_default_token_duration_and_more"),
]
operations = [
migrations.AlterField(
model_name="tenant",
name="default_token_duration",
field=models.TextField(
default="days=1",
help_text="Default token duration",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
]

View File

@ -3,6 +3,7 @@
from tenant_schemas_celery.scheduler import (
TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler,
)
from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry
class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
@ -11,3 +12,11 @@ class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
@classmethod
def get_queryset(cls):
return super().get_queryset().filter(ready=True)
def apply_entry(self, entry: TenantAwareScheduleEntry, producer=None):
# https://github.com/maciej-gol/tenant-schemas-celery/blob/master/tenant_schemas_celery/scheduler.py#L85
# When (as by default) no tenant schemas are set, the public schema is excluded
# so we need to explicitly include it here, otherwise the task is not executed
if entry.tenant_schemas is None:
entry.tenant_schemas = self.get_queryset().values_list("schema_name", flat=True)
return super().apply_entry(entry, producer)

View File

@ -85,19 +85,30 @@ entries:
model: authentik_stages_prompt.prompt
- attrs:
expression: |
from authentik.core.models import (
USER_ATTRIBUTE_CHANGE_EMAIL,
USER_ATTRIBUTE_CHANGE_NAME,
USER_ATTRIBUTE_CHANGE_USERNAME
)
prompt_data = request.context.get("prompt_data")
user_group_attributes = request.user.group_attributes(request.http_request)
if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email):
if not request.user.group_attributes(request.http_request).get(
USER_ATTRIBUTE_CHANGE_EMAIL, request.http_request.tenant.default_user_change_email
):
if prompt_data.get("email") != request.user.email:
ak_message("Not allowed to change email address.")
return False
if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name):
if not request.user.group_attributes(request.http_request).get(
USER_ATTRIBUTE_CHANGE_NAME, request.http_request.tenant.default_user_change_name
):
if prompt_data.get("name") != request.user.name:
ak_message("Not allowed to change name.")
return False
if not user_group_attributes.get(USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username):
if not request.user.group_attributes(request.http_request).get(
USER_ATTRIBUTE_CHANGE_USERNAME, request.http_request.tenant.default_user_change_username
):
if prompt_data.get("username") != request.user.username:
ak_message("Not allowed to change username.")
return False

View File

@ -89,9 +89,10 @@ entries:
expression: |
# This policy ensures that the setup flow can only be
# used one time
FlowsApi(api).flows_instances_partial_update("initial-setup", {
"authentication": "REQUIRE_SUPERUSER"
})
from authentik.flows.models import Flow, FlowAuthenticationRequirement
Flow.objects.filter(slug="initial-setup").update(
authentication=FlowAuthenticationRequirement.REQUIRE_SUPERUSER,
)
return True
id: policy-default-oobe-flow-set-authentication
identifiers:

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object",
"title": "authentik 2024.4.1 Blueprint schema",
"title": "authentik 2024.4.4 Blueprint schema",
"required": [
"version",
"entries"
@ -3477,10 +3477,6 @@
"type": "string",
"minLength": 1,
"title": "Expression"
},
"execution_user": {
"type": "integer",
"title": "Execution user"
}
},
"required": []
@ -4135,6 +4131,10 @@
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
"http://www.w3.org/2000/09/xmldsig#dsa-sha1"
],
"title": "Signature algorithm"
@ -4939,6 +4939,10 @@
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
"http://www.w3.org/2000/09/xmldsig#dsa-sha1"
],
"title": "Signature algorithm"

View File

@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.4}
restart: unless-stopped
command: server
environment:
@ -53,7 +53,7 @@ services:
- postgresql
- redis
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.4}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024041.1
goauthentik.io/api/v3 v3.2024023.2
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.19.0
golang.org/x/sync v0.7.0

4
go.sum
View File

@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024041.1 h1:oYj6DYqmZJd6/wyknBZLnLa+4+ShT4ry7HQn0W8VXxY=
goauthentik.io/api/v3 v3.2024041.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
goauthentik.io/api/v3 v3.2024023.2 h1:lSVaZAKTpsDhtw11wnkGjPalkDzv9H2VKEJllBi2aXs=
goauthentik.io/api/v3 v3.2024023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

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

View File

@ -117,6 +117,8 @@ def run_migrations():
)
finally:
release_lock(curr)
curr.close()
conn.close()
if __name__ == "__main__":

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

2340
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "authentik"
version = "2024.4.1"
version = "2024.4.4"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]
@ -83,13 +83,13 @@ filterwarnings = [
]
[tool.poetry.dependencies]
authentik_client = "2024.4.1.post1714149882"
argon2-cffi = "*"
celery = "*"
channels = { version = "*", extras = ["daphne"] }
channels-redis = "*"
codespell = "*"
colorama = "*"
cryptography = "*"
dacite = "*"
deepmerge = "*"
defusedxml = "*"
@ -102,7 +102,7 @@ django-redis = "*"
django-storages = { extras = ["s3"], version = "*" }
# See https://github.com/django-tenants/django-tenants/pull/997
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch="authentik-fixes" }
djangorestframework = "*"
djangorestframework = "3.14.0"
djangorestframework-guardian = "*"
docker = "*"
drf-spectacular = "*"
@ -116,24 +116,17 @@ gunicorn = "*"
jsonpatch = "*"
kubernetes = "*"
ldap3 = "*"
lxml = [
# 5.0.0 works with libxml2 2.11.x, which is standard on brew
{ version = "5.0.0", platform = "darwin" },
# 4.9.x works with previous libxml2 versions, which is what we get on linux
{ version = "4.9.4", platform = "linux" },
]
lxml = "*"
opencontainers = { extras = ["reggie"], version = "*" }
packaging = "*"
paramiko = "*"
psycopg = { extras = ["c"], version = "*" }
pycryptodome = "*"
pydantic = "*"
pydantic-scim = "*"
pyjwt = "*"
python = "~3.12"
pyyaml = "*"
requests-oauthlib = "*"
restrictedpython = "*"
scim2-filter-parser = "*"
sentry-sdk = "*"
service_identity = "*"

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2024.4.1
version: 2024.4.4
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -11852,10 +11852,6 @@ paths:
name: execution_logging
schema:
type: boolean
- in: query
name: execution_user
schema:
type: integer
- in: query
name: expression
schema:
@ -17055,6 +17051,10 @@ paths:
enum:
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
@ -20914,6 +20914,10 @@ paths:
enum:
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
@ -30454,6 +30458,11 @@ components:
- pending_user
- pending_user_avatar
- type
AlgEnum:
enum:
- rsa
- ecdsa
type: string
App:
type: object
description: Serialize Application info
@ -32111,6 +32120,10 @@ components:
type: string
validity_days:
type: integer
alg:
allOf:
- $ref: '#/components/schemas/AlgEnum'
default: rsa
required:
- common_name
- validity_days
@ -33569,9 +33582,6 @@ components:
readOnly: true
expression:
type: string
execution_user:
type: integer
nullable: true
required:
- bound_to
- component
@ -33595,9 +33605,6 @@ components:
expression:
type: string
minLength: 1
execution_user:
type: integer
nullable: true
required:
- expression
- name
@ -38851,9 +38858,6 @@ components:
expression:
type: string
minLength: 1
execution_user:
type: integer
nullable: true
PatchedFlowRequest:
type: object
description: Flow Serializer
@ -43671,6 +43675,10 @@ components:
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
type: string
Source:

View File

@ -6,16 +6,16 @@
"": {
"name": "@goauthentik/web-tests",
"dependencies": {
"chromedriver": "^124.0.1"
"chromedriver": "^123.0.4"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@wdio/cli": "^8.36.1",
"@wdio/local-runner": "^8.36.1",
"@wdio/mocha-framework": "^8.36.1",
"@wdio/spec-reporter": "^8.36.1",
"@wdio/cli": "^8.36.0",
"@wdio/local-runner": "^8.36.0",
"@wdio/mocha-framework": "^8.36.0",
"@wdio/spec-reporter": "^8.36.0",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.25.1",
@ -1189,19 +1189,19 @@
}
},
"node_modules/@wdio/cli": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.36.1.tgz",
"integrity": "sha512-LZBZiwcvvv5P0HuRXt8IV09UiFT5dnDr1Ag5u2roJL2D7l8wDHHa70PXw9MmlbrnyFCUN3hO7FQVUi9MAsDbDQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.36.0.tgz",
"integrity": "sha512-B8iEwz9DRzHquPihT74nKUzN9s+rCd1TkBp+JGmdgm7pJqiWTe4FORrzaxWjdiCO78jbYK9LgaMORpCcAzjwIA==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.1",
"@vitest/snapshot": "^1.2.1",
"@wdio/config": "8.36.1",
"@wdio/globals": "8.36.1",
"@wdio/config": "8.36.0",
"@wdio/globals": "8.36.0",
"@wdio/logger": "8.28.0",
"@wdio/protocols": "8.32.0",
"@wdio/types": "8.36.1",
"@wdio/utils": "8.36.1",
"@wdio/types": "8.36.0",
"@wdio/utils": "8.36.0",
"async-exit-hook": "^2.0.1",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
@ -1216,7 +1216,7 @@
"lodash.union": "^4.6.0",
"read-pkg-up": "10.0.0",
"recursive-readdir": "^2.2.3",
"webdriverio": "8.36.1",
"webdriverio": "8.36.0",
"yargs": "^17.7.2"
},
"bin": {
@ -1239,14 +1239,14 @@
}
},
"node_modules/@wdio/config": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.36.1.tgz",
"integrity": "sha512-yCENnym0CrYuLKMJ3fv00WkjCR8QpPqVohGBkq5FvZOZpVJEpoG86Q8l4HtyRnd6ggMTKCA1vTQ/myhbPmZmaQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.36.0.tgz",
"integrity": "sha512-sAbqnx/G+OsrMquIncFXjM4U0/E0ULMP0jDHZND75r0e1DYYCHmyacrvIHu3Jyxinl9f6+4XQdev6vqdTqPdNg==",
"dev": true,
"dependencies": {
"@wdio/logger": "8.28.0",
"@wdio/types": "8.36.1",
"@wdio/utils": "8.36.1",
"@wdio/types": "8.36.0",
"@wdio/utils": "8.36.0",
"decamelize": "^6.0.0",
"deepmerge-ts": "^5.0.0",
"glob": "^10.2.2",
@ -1257,29 +1257,29 @@
}
},
"node_modules/@wdio/globals": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.36.1.tgz",
"integrity": "sha512-Qpj6gZCRNxqdVkTwYyi4JdeYO4tLSUj3Ti6yxO0v9A4IRaKW1tS29KUcGgjL9CFSBKAOi2zRY8vvFz1u6ewxtQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.36.0.tgz",
"integrity": "sha512-vqMq1hR+iF0lqMNJpk9z+QB9l/QfL1DbvOfNhPtQ13NgctfNg42ffuhEObbzTLQN0MftcnPBu6O3pai79y8bUA==",
"dev": true,
"engines": {
"node": "^16.13 || >=18"
},
"optionalDependencies": {
"expect-webdriverio": "^4.11.2",
"webdriverio": "8.36.1"
"webdriverio": "8.36.0"
}
},
"node_modules/@wdio/local-runner": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.36.1.tgz",
"integrity": "sha512-FYsTzbNGRnrniOsLWrZO7+DLecAS9W75AIzFZQVQxruiDFkGmKY5OV6gsuvMlasaqAQXW1s+w29bqrLY4DxdEw==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.36.0.tgz",
"integrity": "sha512-MIzbWcXgRQGQQK4H5N39/JFoikOg5cu34l1U6rgw74D6hO79L4RwBg2Oo4TJJYgHUL/4RbVwyeLdb5WDTdluTQ==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/logger": "8.28.0",
"@wdio/repl": "8.24.12",
"@wdio/runner": "8.36.1",
"@wdio/types": "8.36.1",
"@wdio/runner": "8.36.0",
"@wdio/types": "8.36.0",
"async-exit-hook": "^2.0.1",
"split2": "^4.1.0",
"stream-buffers": "^3.0.2"
@ -1316,16 +1316,16 @@
}
},
"node_modules/@wdio/mocha-framework": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.36.1.tgz",
"integrity": "sha512-G0h5AeneMNtoh9CcVQ82OCKj0axxUOEotEcInDu8V6UJbUywNJVL/bdTMKdaq5i84Hnc+s1LUKmLvN95F+lHGA==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.36.0.tgz",
"integrity": "sha512-5wZgh1apbSKTtgGwvd//L4kxdaXe30AQ3y9YeJD+OuAJUTYFRjTpMS13bO3pX518imQeB8HCm4aUc2kxs7J81Q==",
"dev": true,
"dependencies": {
"@types/mocha": "^10.0.0",
"@types/node": "^20.1.0",
"@wdio/logger": "8.28.0",
"@wdio/types": "8.36.1",
"@wdio/utils": "8.36.1",
"@wdio/types": "8.36.0",
"@wdio/utils": "8.36.0",
"mocha": "^10.0.0"
},
"engines": {
@ -1351,14 +1351,14 @@
}
},
"node_modules/@wdio/reporter": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.36.1.tgz",
"integrity": "sha512-HcXr9XKq/6kPC9nexMRXIc/ft3Lvp0yCaW5tps01Axus9wbi5ysLHi2z5sB84F2YdpM+aRf7Lac56xkc4Jldeg==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.36.0.tgz",
"integrity": "sha512-pkAxqiMC+ljmksOKlK9g6y2NRvrdQiKtxD11rsMwJ6CH4kVDSGIvENw7u3kxg7Qwp0j1rCKf5Hp51npqKQgeDQ==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/logger": "8.28.0",
"@wdio/types": "8.36.1",
"@wdio/types": "8.36.0",
"diff": "^5.0.0",
"object-inspect": "^1.12.0"
},
@ -1367,35 +1367,35 @@
}
},
"node_modules/@wdio/runner": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.36.1.tgz",
"integrity": "sha512-bLkxQ46MLEbzIf30adl2nyz8kxED/V0IjcQASm0VKfNmsG8LOf7iOIz+udOF4GkMoF++5JuONA5abUsyLvwatg==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.36.0.tgz",
"integrity": "sha512-M2ZDL0gmR2VvVMchi3Pkonva6Gn6eFh6IwVCpT0np7zioaqOksy3IM7Aki8kPKKS88Osip5dAfoKIrY7JpHovA==",
"dev": true,
"dependencies": {
"@types/node": "^20.11.28",
"@wdio/config": "8.36.1",
"@wdio/globals": "8.36.1",
"@wdio/config": "8.36.0",
"@wdio/globals": "8.36.0",
"@wdio/logger": "8.28.0",
"@wdio/types": "8.36.1",
"@wdio/utils": "8.36.1",
"@wdio/types": "8.36.0",
"@wdio/utils": "8.36.0",
"deepmerge-ts": "^5.1.0",
"expect-webdriverio": "^4.12.0",
"gaze": "^1.1.3",
"webdriver": "8.36.1",
"webdriverio": "8.36.1"
"webdriver": "8.36.0",
"webdriverio": "8.36.0"
},
"engines": {
"node": "^16.13 || >=18"
}
},
"node_modules/@wdio/spec-reporter": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.36.1.tgz",
"integrity": "sha512-VgAd8VQCfwKYz4A3BPDUYNIQxXhRSTaVNbmDzSlYfo5Jekygk7fz0LRFYBpJ69l7eQH0P5nzEyF92oW/rvE3VA==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.36.0.tgz",
"integrity": "sha512-GVOiWqVYvzoAo4/4hNVxvyVWVoHyEmAywYhkykyJGL05YpO0oDOZY2kINPePEX5Z+nIsXsiKPmtsGGqWsfQwTw==",
"dev": true,
"dependencies": {
"@wdio/reporter": "8.36.1",
"@wdio/types": "8.36.1",
"@wdio/reporter": "8.36.0",
"@wdio/types": "8.36.0",
"chalk": "^5.1.2",
"easy-table": "^1.2.0",
"pretty-ms": "^7.0.0"
@ -1417,9 +1417,9 @@
}
},
"node_modules/@wdio/types": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.36.1.tgz",
"integrity": "sha512-kKtyJbypasKo/VQuJ6dTQQwFtHE9qoygjoCZjrQCLGraRSjOEiqZHPR0497wbeCvcgHIYyImbmcylqZNGUE0CQ==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.36.0.tgz",
"integrity": "sha512-0hw/PaJHqDrbIMvU08w3oMDGg89udSkqWF2hFlGAjOc20quRrhn0F1L+NhFpYdezeRKz5gpgTDIqaQs9RWKq1A==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0"
@ -1429,14 +1429,14 @@
}
},
"node_modules/@wdio/utils": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.36.1.tgz",
"integrity": "sha512-xmgPHU11/o9n2FeRmDFkPRC0okiwA1i2xOcR2c3aSpuk99XkAm9RaMn/6u9LFaqsCpgaVxazcYEGSceO7U4hZA==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.36.0.tgz",
"integrity": "sha512-3VAbavN206qkvm6lITtOtTgscFChax7shzqHjUNln+QWMRyELtT81iw32ux2ld+Bg3F60LAmhbGodu0lJH7k2w==",
"dev": true,
"dependencies": {
"@puppeteer/browsers": "^1.6.0",
"@wdio/logger": "8.28.0",
"@wdio/types": "8.36.1",
"@wdio/types": "8.36.0",
"decamelize": "^6.0.0",
"deepmerge-ts": "^5.1.0",
"edgedriver": "^5.3.5",
@ -2084,9 +2084,9 @@
}
},
"node_modules/chromedriver": {
"version": "124.0.1",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-124.0.1.tgz",
"integrity": "sha512-hxd1tpAUhgMFBZd1h3W7KyMckxofOYCuKAMtcvBDAU0YKKorZcWuq6zP06+Ph0Z1ynPjtgAj0hP9VphCwesjZw==",
"version": "123.0.4",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-123.0.4.tgz",
"integrity": "sha512-3Yi7y7q35kkSAOTbRisiww/SL2w+DqafDPAaUShpSuLMmPaOvHQR0i3bm2/33QBiQ8fUb1J/MzppzVL6IDqvhA==",
"hasInstallScript": true,
"dependencies": {
"@testim/chrome-version": "^1.1.4",
@ -8886,18 +8886,18 @@
}
},
"node_modules/webdriver": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.36.1.tgz",
"integrity": "sha512-547RivYCHStVqtiGQBBcABAkzJbPnAWsxpXGzmj5KL+TOM2JF41N2iQRtUxXqr0jme1Nzzye7WS7Y7iSnK6i1g==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.36.0.tgz",
"integrity": "sha512-6fmZI1+OCGbhuGMLBLvA7m9TJvHU1Cyzxqd8rGzIyb8hocR53yh/olfOL1BPcjU1NXmKuU1BePSGF+yiKajiEA==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@types/ws": "^8.5.3",
"@wdio/config": "8.36.1",
"@wdio/config": "8.36.0",
"@wdio/logger": "8.28.0",
"@wdio/protocols": "8.32.0",
"@wdio/types": "8.36.1",
"@wdio/utils": "8.36.1",
"@wdio/types": "8.36.0",
"@wdio/utils": "8.36.0",
"deepmerge-ts": "^5.1.0",
"got": "^12.6.1",
"ky": "^0.33.0",
@ -8908,18 +8908,18 @@
}
},
"node_modules/webdriverio": {
"version": "8.36.1",
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.36.1.tgz",
"integrity": "sha512-vzE09oFQeMbOYJ/75jZ13sDIljzC3HH7uoUJKAMAEtyrn/bu1F9Sg/4IDEsvQaRD3pz3ae6SkRld33lcQk6HJA==",
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.36.0.tgz",
"integrity": "sha512-4WnEI+OxslHpfSnDXuADaR6bL1M7QxBUEF1mTN56AroOCJelyPvt94yRhszwQnLcJJB2OLn49eUz8M4yBCB51w==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/config": "8.36.1",
"@wdio/config": "8.36.0",
"@wdio/logger": "8.28.0",
"@wdio/protocols": "8.32.0",
"@wdio/repl": "8.24.12",
"@wdio/types": "8.36.1",
"@wdio/utils": "8.36.1",
"@wdio/types": "8.36.0",
"@wdio/utils": "8.36.0",
"archiver": "^7.0.0",
"aria-query": "^5.0.0",
"css-shorthand-properties": "^1.1.1",
@ -8936,7 +8936,7 @@
"resq": "^1.9.1",
"rgb2hex": "0.2.5",
"serialize-error": "^11.0.1",
"webdriver": "8.36.1"
"webdriver": "8.36.0"
},
"engines": {
"node": "^16.13 || >=18"

View File

@ -6,10 +6,10 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@wdio/cli": "^8.36.1",
"@wdio/local-runner": "^8.36.1",
"@wdio/mocha-framework": "^8.36.1",
"@wdio/spec-reporter": "^8.36.1",
"@wdio/cli": "^8.36.0",
"@wdio/local-runner": "^8.36.0",
"@wdio/mocha-framework": "^8.36.0",
"@wdio/spec-reporter": "^8.36.0",
"eslint": "^8.57.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.25.1",
@ -32,6 +32,6 @@
"node": ">=20"
},
"dependencies": {
"chromedriver": "^124.0.1"
"chromedriver": "^123.0.4"
}
}

2444
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.5",
"@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.4.1-1714149838",
"@goauthentik/api": "^2024.4.1-1714655911",
"@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.1",
"@lit/localize": "^0.12.1",
@ -46,7 +46,7 @@
"@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^3.0.1",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.112.2",
"@sentry/browser": "^7.111.0",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.4.2",
@ -65,7 +65,7 @@
"style-mod": "^4.1.2",
"ts-pattern": "^5.1.1",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.4.2"
"yaml": "^2.4.1"
},
"devDependencies": {
"@babel/core": "^7.24.4",
@ -81,13 +81,13 @@
"@lit/localize-tools": "^0.7.2",
"@rollup/plugin-replace": "^5.0.5",
"@spotlightjs/spotlight": "^1.2.17",
"@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-links": "^8.0.9",
"@storybook/addon-essentials": "^8.0.8",
"@storybook/addon-links": "^8.0.8",
"@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.0.8",
"@storybook/manager-api": "^8.0.9",
"@storybook/web-components": "^8.0.9",
"@storybook/web-components-vite": "^8.0.9",
"@storybook/manager-api": "^8.0.8",
"@storybook/web-components": "^8.0.8",
"@storybook/web-components-vite": "^8.0.8",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "5.60.15",
@ -114,10 +114,10 @@
"prettier": "^3.2.5",
"pseudolocale": "^2.0.0",
"react": "^18.2.0",
"react-dom": "^18.3.1",
"react-dom": "^18.2.0",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.0.9",
"storybook": "^8.0.8",
"storybook-addon-mock": "^5.0.0",
"ts-lit-plugin": "^2.0.2",
"tslib": "^2.6.2",
@ -129,9 +129,9 @@
"@esbuild/darwin-arm64": "^0.20.1",
"@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.20.1",
"@rollup/rollup-darwin-arm64": "4.17.0",
"@rollup/rollup-linux-arm64-gnu": "4.17.0",
"@rollup/rollup-linux-x64-gnu": "4.17.0"
"@rollup/rollup-darwin-arm64": "4.14.3",
"@rollup/rollup-linux-arm64-gnu": "4.14.3",
"@rollup/rollup-linux-x64-gnu": "4.14.3"
},
"engines": {
"node": ">=20"

View File

@ -29,5 +29,9 @@ export const signatureAlgorithmOptions = toOptions([
["RSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMorersaSha256, true],
["RSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMorersaSha384],
["RSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMorersaSha512],
["ECDSA-SHA1", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha1],
["ECDSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha256],
["ECDSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha384],
["ECDSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha512],
["DSA-SHA1", SignatureAlgorithmEnum._200009XmldsigdsaSha1],
]);

View File

@ -6,7 +6,12 @@ import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { CertificateGenerationRequest, CertificateKeyPair, CryptoApi } from "@goauthentik/api";
import {
AlgEnum,
CertificateGenerationRequest,
CertificateKeyPair,
CryptoApi,
} from "@goauthentik/api";
@customElement("ak-crypto-certificate-generate-form")
export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
@ -40,6 +45,29 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
?required=${true}
>
<input class="pf-c-form-control" type="number" value="365" />
</ak-form-element-horizontal>`;
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Private key Algorithm")}
?required=${true}
name="alg"
>
<ak-radio
.options=${[
{
label: msg("RSA"),
value: AlgEnum.Rsa,
default: true,
},
{
label: msg("ECDSA"),
value: AlgEnum.Ecdsa,
},
]}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg("Algorithm used to generate the private key.")}
</p>
</ak-form-element-horizontal> `;
}
}

View File

@ -97,7 +97,7 @@ export class EventListPage extends TablePage<Event> {
}
renderExpanded(item: Event): TemplateResult {
return html` <td role="cell" colspan="3">
return html` <td role="cell" colspan="5">
<div class="pf-c-table__expandable-row-content">
<ak-event-info .event=${item as EventWithContext}></ak-event-info>
</div>

View File

@ -12,13 +12,7 @@ import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CoreApi,
CoreUsersListRequest,
ExpressionPolicy,
PoliciesApi,
User,
} from "@goauthentik/api";
import { ExpressionPolicy, PoliciesApi } from "@goauthentik/api";
@customElement("ak-policy-expression-form")
export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
@ -98,39 +92,6 @@ export class ExpressionPolicyForm extends BasePolicyForm<ExpressionPolicy> {
</a>
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Execution user")} name="executionUser">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<User[]> => {
const args: CoreUsersListRequest = {
ordering: "username",
};
if (query !== undefined) {
args.search = query;
}
const users = await new CoreApi(DEFAULT_CONFIG).coreUsersList(args);
return users.results;
}}
.renderElement=${(user: User): string => {
return user.username;
}}
.renderDescription=${(user: User): TemplateResult => {
return html`${user.name}`;
}}
.value=${(user: User | undefined): number | undefined => {
return user?.pk;
}}
.selected=${(user: User): boolean => {
return this.instance?.executionUser === user.pk;
}}
blankable
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"Configure which user the bundled API client authenticates as. When left empty, the API client will inherit the permissions of the user triggering the policy execution.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}

View File

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

View File

@ -44,7 +44,7 @@ export function docLink(path: string): string {
const ak = globalAK();
// Default case or beta build which should always point to latest
if (!ak || ak.build !== "") {
return `https://docs.goauthentik.io${path}`;
return `https://goauthentik.io${path}`;
}
return `https://${ak.versionSubdomain}.goauthentik.io${path}`;
}

View File

@ -18,6 +18,7 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFTable from "@patternfly/patternfly/components/Table/table.css";
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
import PFSplit from "@patternfly/patternfly/layouts/Split/split.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EventActions, FlowsApi } from "@goauthentik/api";
@ -81,6 +82,7 @@ export class EventInfo extends AKElement {
PFCard,
PFTable,
PFList,
PFSplit,
PFDescriptionList,
css`
code {
@ -246,11 +248,17 @@ export class EventInfo extends AKElement {
renderModelChanged() {
const diff = this.event.context.diff as unknown as {
[key: string]: { new_value: unknown; previous_value: unknown };
[key: string]: {
new_value: unknown;
previous_value: unknown;
add?: unknown[];
remove?: unknown[];
clear?: boolean;
};
};
let diffBody = html``;
if (diff) {
diffBody = html`<div class="pf-l-flex__item">
diffBody = html`<div class="pf-l-split__item pf-m-fill">
<div class="pf-c-card__title">${msg("Changes made:")}</div>
<table class="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
<thead>
@ -262,16 +270,36 @@ export class EventInfo extends AKElement {
</thead>
<tbody role="rowgroup">
${Object.keys(diff).map((key) => {
const value = diff[key];
const previousCol = value.previous_value
? JSON.stringify(value.previous_value, null, 4)
: msg("-");
let newCol = html``;
if (value.add || value.remove) {
newCol = html`<ul class="pf-c-list">
${(value.add || value.remove)?.map((item) => {
let itemLabel = "";
if (value.add) {
itemLabel = msg(str`Added ID ${item}`);
} else if (value.remove) {
itemLabel = msg(str`Removed ID ${item}`);
}
return html`<li>${itemLabel}</li>`;
})}
</ul>`;
} else if (value.clear) {
newCol = html`${msg("Cleared")}`;
} else {
newCol = html`<pre>
${JSON.stringify(value.new_value, null, 4)}</pre
>`;
}
return html` <tr role="row">
<td role="cell"><pre>${key}</pre></td>
<td role="cell">
<pre>
${JSON.stringify(diff[key].previous_value, null, 4)}</pre
>
</td>
<td role="cell">
<pre>${JSON.stringify(diff[key].new_value, null, 4)}</pre>
<pre>${previousCol}</pre>
</td>
<td role="cell">${newCol}</td>
</tr>`;
})}
</tbody>
@ -280,8 +308,8 @@ ${JSON.stringify(diff[key].previous_value, null, 4)}</pre
</div>`;
}
return html`
<div class="pf-l-flex">
<div class="pf-l-flex__item">
<div class="pf-l-split">
<div class="pf-l-split__item pf-m-fill">
<div class="pf-c-card__title">${msg("Affected model:")}</div>
<div class="pf-c-card__body">
${this.getModelInfo(this.event.context?.model as EventModel)}

View File

@ -1,22 +1,23 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context";
import type { ReactiveController } from "lit";
import { ReactiveController, ReactiveControllerHost } from "lit";
import type { CurrentBrand } from "@goauthentik/api";
import { CoreApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
export class BrandContextController implements ReactiveController {
host!: ReactiveElementHost<AkInterface>;
host!: ReactiveElementHost;
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
constructor(host: ReactiveElementHost<AkInterface>) {
constructor(host: ReactiveElementHost) {
this.host = host;
this.context = new ContextProvider(this.host, {
context: authentikBrandContext,

View File

@ -2,22 +2,23 @@ import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { globalAK } from "@goauthentik/common/global";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context";
import type { ReactiveController } from "lit";
import { ReactiveController, ReactiveControllerHost } from "lit";
import type { Config } from "@goauthentik/api";
import { RootApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
export class ConfigContextController implements ReactiveController {
host!: ReactiveElementHost<AkInterface>;
host!: ReactiveElementHost;
context!: ContextProvider<{ __context__: Config | undefined }>;
constructor(host: ReactiveElementHost<AkInterface>) {
constructor(host: ReactiveElementHost) {
this.host = host;
this.context = new ContextProvider(this.host, {
context: authentikConfigContext,

View File

@ -1,22 +1,23 @@
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context";
import type { ReactiveController } from "lit";
import { ReactiveController, ReactiveControllerHost } from "lit";
import type { LicenseSummary } from "@goauthentik/api";
import { EnterpriseApi } from "@goauthentik/api";
import type { AkEnterpriseInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkEnterpriseInterface;
export class EnterpriseContextController implements ReactiveController {
host!: ReactiveElementHost<AkEnterpriseInterface>;
host!: ReactiveElementHost;
context!: ContextProvider<{ __context__: LicenseSummary | undefined }>;
constructor(host: ReactiveElementHost<AkEnterpriseInterface>) {
constructor(host: ReactiveElementHost) {
this.host = host;
this.context = new ContextProvider(this.host, {
context: authentikEnterpriseContext,

View File

@ -1,11 +1,13 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { Constructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context";
import type { LitElement } from "lit";
import type { Config } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = new (...args: any[]) => T;
export function WithAuthentikConfig<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true,

View File

@ -1,12 +1,14 @@
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context";
import type { LitElement } from "lit";
import type { CurrentBrand } from "@goauthentik/api";
export function WithBrandConfig<T extends AbstractConstructor<LitElement>>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
export function WithBrandConfig<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true,
) {

View File

@ -1,5 +1,4 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context";
import type { LitElement } from "lit";
@ -7,6 +6,9 @@ import type { LitElement } from "lit";
import { CapabilitiesEnum } from "@goauthentik/api";
import { Config } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
// Using a unique, lexically scoped, and locally static symbol as the field name for the context
// means that it's inaccessible to any child class looking for it. It's one of the strongest privacy
// guarantees in JavaScript.
@ -43,7 +45,7 @@ class WCC {
*
*/
export function WithCapabilitiesConfig<T extends AbstractConstructor<LitElement>>(
export function WithCapabilitiesConfig<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true,
) {

View File

@ -1,11 +1,13 @@
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import { Constructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context";
import type { LitElement } from "lit";
import type { LicenseSummary } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
export function WithLicenseSummary<T extends Constructor<LitElement>>(
superclass: T,
subscribe = true,

View File

@ -2,8 +2,9 @@ import { AKElement } from "@goauthentik/elements/Base";
import { CustomEmitterElement } from "@goauthentik/elements/utils/eventEmitter";
import { msg } from "@lit/localize";
import { PropertyValues } from "@lit/reactive-element/reactive-element";
import { TemplateResult, css, html } from "lit";
import { customElement, property, queryAll } from "lit/decorators.js";
import { customElement, property, queryAll, state } from "lit/decorators.js";
import { map } from "lit/directives/map.js";
import PFCheck from "@patternfly/patternfly/components/Check/check.css";
@ -112,10 +113,14 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
@queryAll('input[type="checkbox"]')
checkboxes!: NodeListOf<HTMLInputElement>;
internals?: ElementInternals;
@state()
values: string[] = [];
get json() {
return this.value;
internals?: ElementInternals;
doneFirstUpdate = false;
json() {
return this.values;
}
private get formValue() {
@ -124,7 +129,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
}
const name = this.name;
const entries = new FormData();
this.value.forEach((v) => entries.append(name, v));
this.values.forEach((v) => entries.append(name, v));
return entries;
}
@ -136,14 +141,14 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
onClick(ev: Event) {
ev.stopPropagation();
this.value = Array.from(this.checkboxes)
this.values = Array.from(this.checkboxes)
.filter((checkbox) => checkbox.checked)
.map((checkbox) => checkbox.name);
this.dispatchCustomEvent("change", this.value);
this.dispatchCustomEvent("input", this.value);
this.dispatchCustomEvent("change", this.values);
this.dispatchCustomEvent("input", this.values);
if (this.internals) {
this.internals.setValidity({});
if (this.required && this.value.length === 0) {
if (this.required && this.values.length === 0) {
this.internals.setValidity(
{
valueMissing: true,
@ -154,6 +159,16 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
}
this.internals.setFormValue(this.formValue);
}
// Doing a write-back so anyone examining the checkbox.value field will get something
// meaningful. Doesn't do anything for anyone, usually, but it's nice to have.
this.value = this.values;
}
willUpdate(changed: PropertyValues<this>) {
if (changed.has("value") && !this.doneFirstUpdate) {
this.doneFirstUpdate = true;
this.values = this.value;
}
}
connectedCallback() {
@ -183,7 +198,7 @@ export class CheckboxGroup extends AkElementWithCustomEvents {
render() {
const renderOne = ([name, label]: CheckboxPr) => {
const selected = this.value.includes(name);
const selected = this.values.includes(name);
const blockFwd = (e: Event) => {
e.stopImmediatePropagation();
};

View File

@ -53,6 +53,9 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
private isLoading = false;
private doneFirstUpdate = false;
private internalSelected: DualSelectPair[] = [];
private pagination?: Pagination;
constructor() {
@ -69,6 +72,11 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
}
willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("selected") && !this.doneFirstUpdate) {
this.doneFirstUpdate = true;
this.internalSelected = this.selected;
}
if (changedProperties.has("searchDelay")) {
this.doSearch = debounce(
AkDualSelectProvider.prototype.doSearch.bind(this),
@ -105,7 +113,8 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
if (!(event instanceof CustomEvent)) {
throw new Error(`Expecting a CustomEvent for change, received ${event} instead`);
}
this.selected = event.detail.value;
this.internalSelected = event.detail.value;
this.selected = this.internalSelected;
}
onSearch(event: Event) {
@ -124,12 +133,16 @@ export class AkDualSelectProvider extends CustomListenerElement(AKElement) {
return this.dualSelector.value!.selected.map(([k, _]) => k);
}
json() {
return this.value;
}
render() {
return html`<ak-dual-select
${ref(this.dualSelector)}
.options=${this.options}
.pages=${this.pagination}
.selected=${this.selected}
.selected=${this.internalSelected}
available-label=${this.availableLabel}
selected-label=${this.selectedLabel}
></ak-dual-select>`;

View File

@ -80,7 +80,7 @@ export function serializeForm<T extends KeyUnknown>(
}
if ("akControl" in inputElement.dataset) {
assignValue(element, inputElement.value, json);
assignValue(element, (inputElement as unknown as AkControlElement).json(), json);
return;
}

View File

@ -1,11 +0,0 @@
import { AKElement } from "@goauthentik/elements/Base";
import { ReactiveControllerHost } from "lit";
export type ReactiveElementHost<T = AKElement> = Partial<ReactiveControllerHost> & T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Constructor<T = object> = new (...args: any[]) => T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AbstractConstructor<T = object> = abstract new (...args: any[]) => T;

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