Compare commits

..

60 Commits

Author SHA1 Message Date
1c03cfa906 release: 2024.2.4 2024-06-26 19:36:18 +09:00
e2dbab5bca security: fix CVE-2024-37905 (cherry-pick #10230) (#10238)
security: fix CVE-2024-37905 (#10230)

Co-authored-by: Jens L <jens@goauthentik.io>
2024-06-26 19:28:55 +09:00
3a6c42fefb security: fix CVE-2024-38371 (cherry-pick #10229) (#10235)
* security: fix CVE-2024-38371 (#10229)

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-06-26 19:02:30 +09:00
6bb180f94e release: 2024.2.3 2024-04-17 13:19:15 +02:00
03dea17519 events: fix incorrect user logged when using API token authentication (#9302)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
# Conflicts:
#	authentik/events/middleware.py
#	authentik/events/tests/test_middleware.py
2024-04-17 00:21:26 +02:00
49d83f11bd lifecycle: migrate: ensure template schema exists before migrating (cherry-pick #8952) (#9022)
lifecycle: migrate: ensure template schema exists before migrating (#8952)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-03-25 13:41:50 +01:00
5f0af81e4d website/docs: config: remove options moved to tenants (cherry-pick #8976) (#8977)
website/docs: config: remove options moved to tenants (#8976)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-03-20 14:28:43 +00:00
63591e1710 events: discard notification if user has empty email (cherry-pick #8938) (#8951)
events: discard notification if user has empty email (#8938)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-03-18 12:01:46 +01:00
6503a7b048 stages/user_write: ensure user data is json-serializable (cherry-pick #8926) (#8928)
stages/user_write: ensure user data is json-serializable (#8926)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-03-15 18:06:31 +01:00
7e244e0679 enterprise/rac: fix connection token management (cherry-pick #8909) (#8912)
enterprise/rac: fix connection token management (#8909)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-03-14 20:10:42 +01:00
c1998bf3f2 api: capabilities: properly set can_save_media when s3 is enabled (cherry-pick #8896) (#8897)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-03-13 17:13:52 +00:00
83372618a8 tenants: really ensure default tenant cannot be deleted (cherry-pick #8875) (#8876)
tenants: really ensure default tenant cannot be deleted (#8875)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-03-11 18:00:30 +01:00
89a876e141 stages/email: fix issue when sending emails to users with same display as email (cherry-pick #8850) (#8852)
stages/email: fix issue when sending emails to users with same display as email (#8850)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-03-08 16:07:30 +01:00
26d6e8bc5c stages/email: Disable autoescape for text templates (cherry-pick #8812) (#8824)
stages/email: Disable autoescape for text templates (#8812)

* Disable autoescape for text templates

* Re-add trailing whitespace after seperator

Co-authored-by: Chasethechicken <neuringe1234@gmail.com>
2024-03-06 17:53:53 +01:00
d9dc373170 enterprise: only check for valid license existing for creating Enterprise objects (cherry-pick #8813) (#8822)
enterprise: only check for valid license existing for creating Enterprise objects (#8813)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-03-06 12:08:54 +01:00
4ec37c5239 release: 2024.2.2 2024-03-04 20:20:25 +01:00
a9cfa6fe35 root: enable virtualenv for test-all command
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-03-04 20:20:21 +01:00
5ac5084149 flows: fix mismatched redirect behaviour for invalid and valid flows (cherry-pick #8794) (#8796)
flows: fix mismatched redirect behaviour for invalid and valid flows (#8794)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-03-04 18:56:50 +01:00
eda38a30b1 providers/oauth2: fix validation ordering (cherry-pick #8793) (#8795)
providers/oauth2: fix validation ordering (#8793)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-03-04 18:56:43 +01:00
9b84bf7174 website/docs: installation: kubernetes: fix values (cherry-pick #8783) (#8792)
website/docs: installation: kubernetes: fix values (#8783)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-03-04 13:56:24 +01:00
f74549be6d root: ensure consistent install_id (cherry-pick #8775) (#8776)
Co-authored-by: Jens L <jens@goauthentik.io>
2024-03-01 18:39:44 +01:00
76f4d7fb0a web/admin: don't mark remaining property mappings as required (cherry-pick #8772) (#8773)
web/admin: don't mark LDAP group property mappings as required (#8772)

Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-03-01 13:57:47 +01:00
d1cf1dd083 web/admin: don't mark property mappings as required anywhere (cherry-pick #8752) (#8755)
web/admin: don't mark property mappings as required anywhere (#8752)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-29 18:35:40 +01:00
2835fbd390 ci: fix missing output on composite action (cherry-pick #8741) (#8742)
ci: fix missing output on composite action (#8741)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-28 23:14:08 +01:00
76ad2c8925 stages/authenticator_webauthn: fix error when enrolling new device (cherry-pick #8738) (#8740)
stages/authenticator_webauthn: fix error when enrolling new device (#8738)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-28 22:48:24 +01:00
2270629fdc website/docs: s3: fix migration docs (cherry-pick #8735) (#8737)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix migration docs (#8735)
2024-02-28 17:03:38 +00:00
43a629efc1 providers/oauth2: fix offline_access requests when prompt doesn't include consent (cherry-pick #8731) (#8732)
Co-authored-by: Jens L <jens@goauthentik.io>
fix offline_access requests when prompt doesn't include consent (#8731)
2024-02-28 17:09:18 +01:00
4044e52403 ci: fix missing DOCKER_USERNAME secret (cherry-pick #8730) (#8733)
Co-authored-by: Jens L <jens@goauthentik.io>
fix missing DOCKER_USERNAME secret (#8730)
2024-02-28 14:46:50 +00:00
aa7c846467 ci: fix missing DOCKER_USERNAME secret (#8730)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-02-28 15:26:58 +01:00
8ab7f4073b ci: do not push docker image if fork (#8724) 2024-02-28 15:26:53 +01:00
a05856c2ef root: fix container build (cherry-pick #8727) (#8728)
Co-authored-by: Jens L <jens@goauthentik.io>
fix container build (#8727)
2024-02-28 13:30:12 +00:00
9e9154e04a enterprise: force license usage update after change to license (cherry-pick #8723) (#8725)
enterprise: force license usage update after change to license (#8723)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-28 13:06:40 +01:00
32549066c0 website/docs: s3: fix environment variables (cherry-pick #8722) (#8726)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix environment variables (#8722)
2024-02-28 11:42:26 +00:00
5ed3e879a2 enterprise: fix read_only activating when no license is installed (cherry-pick #8697) (#8698)
enterprise: fix read_only activating when no license is installed (#8697)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-26 18:59:18 +01:00
4e4923ad0e core: fix blueprint export (cherry-pick #8695) (#8696)
core: fix blueprint export (#8695)

* core: fix error when exporting blueprint



* also slightly reword source selection



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-26 13:04:54 +01:00
0302d147e9 providers/oauth2: fix inconsistent sub value when setting via mapping (cherry-pick #8677) (#8682) 2024-02-25 18:32:16 +01:00
8256f1897d release: 2024.2.1 2024-02-22 15:26:14 +01:00
16d321835d brands: fix context processor when request doesn't have a tenant (cherry-pick #8643) (#8646)
brands: fix context processor when request doesn't have a tenant (#8643)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-22 12:47:39 +01:00
f34612efe6 events: sanitize args and kwargs saved in system tasks (cherry-pick #8644) (#8648)
events: sanitize args and kwargs saved in system tasks (#8644)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-22 12:47:30 +01:00
e82f147130 ci: fix missing tags from release (cherry-pick #8645) (#8647)
Co-authored-by: Jens L <jens@goauthentik.io>
fix missing tags from release (#8645)
2024-02-22 11:10:37 +00:00
0ea6ad8eea core: bump cryptography from 42.0.2 to 42.0.4 (#8629)
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.2 to 42.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.2...42.0.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 11:50:30 +01:00
f731443220 core: bump cryptography from 42.0.0 to 42.0.2 (#8553)
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.0 to 42.0.2.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.0...42.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 11:50:25 +01:00
b70a66cde5 core: bump black from 24.1.1 to 24.2.0 (#8524)
Bumps [black](https://github.com/psf/black) from 24.1.1 to 24.2.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.1.1...24.2.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 11:49:54 +01:00
b733dbbcb0 core: bump cbor2 from 5.5.1 to 5.6.2 (#8607)
Bumps [cbor2](https://github.com/agronholm/cbor2) from 5.5.1 to 5.6.2.
- [Release notes](https://github.com/agronholm/cbor2/releases)
- [Changelog](https://github.com/agronholm/cbor2/blob/master/docs/versionhistory.rst)
- [Commits](https://github.com/agronholm/cbor2/compare/5.5.1...5.6.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 11:48:34 +01:00
e34d4c0669 stages/authenticator_validate: fix error with get_webauthn_challenge_without_user (cherry-pick #8625) (#8626)
stages/authenticator_validate: fix error with get_webauthn_challenge_without_user (#8625)

* stages/authenticator_validate: fix error with get_webauthn_challenge_without_user



* fix tests



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-21 19:19:53 +01:00
310983a4d0 release: 2024.2.0 2024-02-21 15:34:56 +01:00
47b0fc86f7 web/flows: fix webauthn retry (cherry-pick #8599) (#8603)
web/flows: fix webauthn retry (#8599)

* web/flows: fix retry button on webauthn device stage



* web/flows: rework webauth register design to match



---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-21 15:01:05 +01:00
b6e961b1f3 web: spell customization with a Z (cherry-pick #8596) (#8602)
web: spell customization with a Z (#8596)

Co-authored-by: Fletcher Heisler <fheisler@users.noreply.github.com>
Co-authored-by: Fletcher Heisler <fletcher@goauthentik.io>
2024-02-21 15:00:54 +01:00
874d7ff320 rbac: fix permission decorator for global permissions (cherry-pick #8591) (#8597)
rbac: fix permission decorator for global permissions (#8591)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-20 18:31:29 +01:00
e4a5bc9df6 website/docs: kubernetes installation: update values (cherry-pick #8575) (#8576)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-02-19 15:27:13 +01:00
318e0cf9f8 release: 2024.2.0-rc2 2024-02-19 14:10:53 +01:00
bd0815d894 root: fix app settings load order (cherry-pick #8569) (#8571)
Co-authored-by: Jens L <jens@goauthentik.io>
fix app settings load order (#8569)
2024-02-19 11:18:39 +00:00
af35ecfe66 ci: main: use correct previous version (cherry-pick #8539) (#8572)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-02-19 11:03:01 +00:00
0c05cd64bb web/flows: improve authenticator styling (cherry-pick #8560) (#8570)
Co-authored-by: Jens L <jens@goauthentik.io>
2024-02-19 10:37:19 +00:00
cb80b76490 web: change "delete" verb to "remove" for one-to-many relationships (cherry-pick #8535) (#8537)
web: change "delete" verb to "remove" for one-to-many relationships (#8535)

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
2024-02-16 14:32:41 +01:00
061d4bc758 ci: fix release sentry step (cherry-pick #8540) (#8541)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
fix release sentry step (#8540)
2024-02-15 21:11:15 +00:00
8ff27f69e1 release: 2024.2.0-rc1 2024-02-15 19:19:40 +01:00
045cd98276 web: fix save & reset behavior on System ➲ Settings page. (cherry-pick #8528) (#8534)
web: fix save & reset behavior on System ➲ Settings page. (#8528)

Co-authored-by: Ken Sternberg <133134217+kensternberg-authentik@users.noreply.github.com>
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-02-15 19:01:47 +01:00
b520843984 ci: fix release pipeline (cherry-pick #8530) (#8533)
Co-authored-by: Jens L <jens@goauthentik.io>
fix release pipeline (#8530)
2024-02-15 17:33:55 +00:00
92216e4ea8 ci: docker push: re-add timestamp image tag (cherry-pick #8529) (#8532)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-02-15 17:12:30 +00:00
625 changed files with 2202 additions and 22696 deletions

View File

@ -1,16 +1,16 @@
[bumpversion]
current_version = 2023.10.7
current_version = 2024.2.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*))?
serialize =
serialize =
{major}.{minor}.{patch}-{rc_t}{rc_n}
{major}.{minor}.{patch}
message = release: {new_version}
tag_name = version/{new_version}
[bumpversion:part:rc_t]
values =
values =
rc
final
optional_value = final

View File

@ -6,6 +6,7 @@ build/**
build_docs/**
*Dockerfile
blueprints/local
.git
!gen-ts-api/node_modules
!gen-ts-api/dist/**
!gen-go-api/

View File

@ -11,6 +11,10 @@ inputs:
description: "Docker image arch"
outputs:
shouldBuild:
description: "Whether to build image or not"
value: ${{ steps.ev.outputs.shouldBuild }}
sha:
description: "sha"
value: ${{ steps.ev.outputs.sha }}
@ -34,63 +38,10 @@ runs:
steps:
- name: Generate config
id: ev
shell: python
shell: bash
env:
IMAGE_NAME: ${{ inputs.image-name }}
IMAGE_ARCH: ${{ inputs.image-arch }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
"""Helper script to get the actual branch name, docker safe"""
import configparser
import os
from time import time
parser = configparser.ConfigParser()
parser.read(".bumpversion.cfg")
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("/", "-")
image_names = "${{ inputs.image-name }}".split(",")
image_arch = "${{ inputs.image-arch }}" or None
is_pull_request = bool("${{ github.event.pull_request.head.sha }}")
is_release = "dev" not in image_names[0]
sha = os.environ["GITHUB_SHA"] if not is_pull_request else "${{ github.event.pull_request.head.sha }}"
# 2042.1.0 or 2042.1.0-rc1
version = parser.get("bumpversion", "current_version")
# 2042.1
version_family = ".".join(version.split("-", 1)[0].split(".")[:-1])
prerelease = "-" in version
image_tags = []
if is_release:
for name in image_names:
image_tags += [
f"{name}:{version}",
]
if not prerelease:
image_tags += [
f"{name}:latest",
f"{name}:{version_family}",
]
else:
suffix = ""
if image_arch and image_arch != "amd64":
suffix = f"-{image_arch}"
for name in image_names:
image_tags += [
f"{name}:gh-{sha}{suffix}", # Used for ArgoCD and PR comments
f"{name}:gh-{safe_branch_name}{suffix}", # For convenience
f"{name}:gh-{safe_branch_name}-{int(time())}-{sha[:7]}{suffix}", # Use by FluxCD
]
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("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)
python3 ${{ github.action_path }}/push_vars.py

View File

@ -0,0 +1,62 @@
"""Helper script to get the actual branch name, docker safe"""
import configparser
import os
from time import time
parser = configparser.ConfigParser()
parser.read(".bumpversion.cfg")
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("/", "-")
image_names = os.getenv("IMAGE_NAME").split(",")
image_arch = os.getenv("IMAGE_ARCH") or None
is_pull_request = bool(os.getenv("PR_HEAD_SHA"))
is_release = "dev" not in image_names[0]
sha = os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA")
# 2042.1.0 or 2042.1.0-rc1
version = parser.get("bumpversion", "current_version")
# 2042.1
version_family = ".".join(version.split("-", 1)[0].split(".")[:-1])
prerelease = "-" in version
image_tags = []
if is_release:
for name in image_names:
image_tags += [
f"{name}:{version}",
]
if not prerelease:
image_tags += [
f"{name}:latest",
f"{name}:{version_family}",
]
else:
suffix = ""
if image_arch and image_arch != "amd64":
suffix = f"-{image_arch}"
for name in image_names:
image_tags += [
f"{name}:gh-{sha}{suffix}", # Used for ArgoCD and PR comments
f"{name}:gh-{safe_branch_name}{suffix}", # For convenience
f"{name}:gh-{safe_branch_name}-{int(time())}-{sha[:7]}{suffix}", # Use by FluxCD
]
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("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

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

View File

@ -219,7 +219,6 @@ jobs:
# Needed to upload contianer images to ghcr.io
packages: write
timeout-minutes: 120
if: "github.repository == 'goauthentik/authentik'"
steps:
- uses: actions/checkout@v4
with:
@ -231,10 +230,13 @@ jobs:
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/dev-server
image-arch: ${{ matrix.arch }}
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
@ -250,7 +252,7 @@ jobs:
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
tags: ${{ steps.ev.outputs.imageTags }}
push: true
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
cache-from: type=gha
@ -272,6 +274,8 @@ jobs:
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/dev-server
- name: Comment on PR

View File

@ -71,7 +71,6 @@ jobs:
permissions:
# Needed to upload contianer images to ghcr.io
packages: write
if: "github.repository == 'goauthentik/authentik'"
steps:
- uses: actions/checkout@v4
with:
@ -83,9 +82,12 @@ jobs:
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
@ -98,7 +100,7 @@ jobs:
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: ${{ matrix.type }}.Dockerfile
push: true
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
platforms: linux/amd64,linux/arm64

View File

@ -20,6 +20,8 @@ jobs:
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/server,beryju/authentik
- name: Docker Login Registry
@ -72,6 +74,8 @@ jobs:
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }}
- name: make empty clients
@ -168,6 +172,8 @@ jobs:
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/server
- name: Get static files from docker image

View File

@ -32,6 +32,8 @@ jobs:
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with:
image-name: ghcr.io/goauthentik/server
- name: Create Release

View File

@ -27,7 +27,6 @@ WORKDIR /work/web
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
--mount=type=bind,target=/work/.git,src=./.git,readonly \
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
npm ci --include=dev
@ -104,9 +103,10 @@ RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
--mount=type=cache,target=/root/.cache/pip \
--mount=type=cache,target=/root/.cache/pypoetry \
python -m venv /ak-root/venv/ && \
pip3 install --upgrade pip && \
pip3 install poetry && \
poetry install --only=main --no-ansi --no-interaction
bash -c "source ${VENV_PATH}/bin/activate && \
pip3 install --upgrade pip && \
pip3 install poetry && \
poetry install --only=main --no-ansi --no-interaction --no-root"
# Stage 6: Run
FROM docker.io/python:3.12.2-slim-bookworm AS final-image

View File

@ -5,7 +5,7 @@ PWD = $(shell pwd)
UID = $(shell id -u)
GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.npm_version)
PY_SOURCES = authentik tests scripts lifecycle
PY_SOURCES = authentik tests scripts lifecycle .github
DOCKER_IMAGE ?= "authentik:test"
GEN_API_TS = "gen-ts-api"

View File

@ -3,7 +3,7 @@
from os import environ
from typing import Optional
__version__ = "2023.10.7"
__version__ = "2024.2.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -1,35 +0,0 @@
"""test decorators api"""
from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase
from authentik.core.models import Application, User
from authentik.lib.generators import generate_id
class TestAPIDecorators(APITestCase):
"""test decorators api"""
def setUp(self) -> None:
super().setUp()
self.user = User.objects.create(username="test-user")
def test_obj_perm_denied(self):
"""Test object perm denied"""
self.client.force_login(self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 403)
def test_other_perm_denied(self):
"""Test other perm denied"""
self.client.force_login(self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
assign_perm("authentik_core.view_application", self.user, app)
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 403)

View File

@ -68,7 +68,11 @@ class ConfigView(APIView):
"""Get all capabilities this server instance supports"""
caps = []
deb_test = settings.DEBUG or settings.TEST
if Path(settings.MEDIA_ROOT).is_mount() or deb_test:
if (
CONFIG.get("storage.media.backend", "file") == "s3"
or Path(settings.STORAGES["default"]["OPTIONS"]["location"]).is_mount()
or deb_test
):
caps.append(Capabilities.CAN_SAVE_MEDIA)
for processor in get_context_processors():
if cap := processor.capability():

View File

@ -10,13 +10,13 @@ from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.rbac.decorators import permission_required
class ManagedSerializer:

View File

@ -74,7 +74,7 @@ class Exporter:
class FlowExporter(Exporter):
"""Exporter customised to only return objects related to `flow`"""
"""Exporter customized to only return objects related to `flow`"""
flow: Flow
with_policies: bool

View File

@ -9,6 +9,7 @@ from sentry_sdk.hub import Hub
from authentik import get_full_version
from authentik.brands.models import Brand
from authentik.tenants.models import Tenant
_q_default = Q(default=True)
DEFAULT_BRAND = Brand(domain="fallback")
@ -30,13 +31,14 @@ def get_brand_for_request(request: HttpRequest) -> Brand:
def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects brand object into every template"""
brand = getattr(request, "brand", DEFAULT_BRAND)
tenant = getattr(request, "tenant", Tenant())
trace = ""
span = Hub.current.scope.span
if span:
trace = span.to_traceparent()
return {
"brand": brand,
"footer_links": request.tenant.footer_links,
"footer_links": tenant.footer_links,
"sentry_trace": trace,
"version": get_full_version(),
}

View File

@ -23,7 +23,6 @@ from structlog.stdlib import get_logger
from structlog.testing import capture_logs
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
@ -39,6 +38,7 @@ from authentik.lib.utils.file import (
from authentik.policies.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()

View File

@ -15,11 +15,11 @@ from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.models import Group, User
from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required
class GroupMemberSerializer(ModelSerializer):

View File

@ -14,7 +14,6 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.blueprints.api import ManagedSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
@ -23,6 +22,7 @@ from authentik.core.models import PropertyMapping
from authentik.events.utils import sanitize_item
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.exec import PolicyTestSerializer
from authentik.rbac.decorators import permission_required
class PropertyMappingTestResultSerializer(PassiveSerializer):

View File

@ -16,7 +16,6 @@ from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
@ -30,6 +29,7 @@ from authentik.lib.utils.file import (
)
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.engine import PolicyEngine
from authentik.rbac.decorators import permission_required
LOGGER = get_logger()

View File

@ -15,15 +15,15 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.api.decorators import permission_required
from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict
from authentik.rbac.decorators import permission_required
class TokenSerializer(ManagedSerializer, ModelSerializer):
@ -36,6 +36,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

@ -49,7 +49,6 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin
@ -74,6 +73,7 @@ from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.lib.avatars import get_avatar
from authentik.rbac.decorators import permission_required
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -154,7 +154,7 @@ class UserSerializer(ModelSerializer):
def get_avatar(self, user: User) -> str:
"""User's avatar, either a http/https URL or a data URI"""
return get_avatar(user, self.context["request"])
return get_avatar(user, self.context.get("request"))
def validate_path(self, path: str) -> str:
"""Validate path"""
@ -218,7 +218,7 @@ class UserSelfSerializer(ModelSerializer):
def get_avatar(self, user: User) -> str:
"""User's avatar, either a http/https URL or a data URI"""
return get_avatar(user, self.context["request"])
return get_avatar(user, self.context.get("request"))
@extend_schema_field(
ListSerializer(
@ -533,7 +533,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
400: OpenApiResponse(description="Bad request"),
},
)
@action(detail=True, methods=["POST"])
@action(detail=True, methods=["POST"], permission_classes=[])
def set_password(self, request: Request, pk: int) -> Response:
"""Set password for user"""
user: User = self.get_object()
@ -611,7 +611,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
email_stage: EmailStage = stages.first()
message = TemplateEmailMessage(
subject=_(email_stage.subject),
to=[for_user.email],
to=[(for_user.name, for_user.email)],
template_name=email_stage.template,
language=for_user.locale(request),
template_context={
@ -631,7 +631,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"401": OpenApiResponse(description="Access denied"),
},
)
@action(detail=True, methods=["POST"])
@action(detail=True, methods=["POST"], permission_classes=[])
def impersonate(self, request: Request, pk: int) -> Response:
"""Impersonate a user"""
if not request.tenant.impersonation:

View File

@ -7,8 +7,8 @@ from guardian.shortcuts import get_anonymous_user
from rest_framework.test import APITestCase
from authentik.core.api.tokens import TokenSerializer
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id
@ -17,7 +17,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)
@ -76,6 +76,24 @@ class TestTokenAPI(APITestCase):
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, False)
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

@ -24,13 +24,13 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import SecretKeyFilter
from authentik.api.decorators import permission_required
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.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required
LOGGER = get_logger()

View File

@ -44,7 +44,7 @@ class CertificateBuilder:
def generate_private_key(self) -> PrivateKeyTypes:
"""Generate private key"""
if self._use_ec_private_key:
return ec.generate_private_key(curve=ec.SECP256R1())
return ec.generate_private_key(curve=ec.SECP256R1)
return rsa.generate_private_key(
public_exponent=65537, key_size=4096, backend=default_backend()
)

View File

@ -16,12 +16,12 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User, UserTypes
from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
from authentik.enterprise.models import License
from authentik.rbac.decorators import permission_required
from authentik.root.install_id import get_install_id
@ -31,7 +31,7 @@ class EnterpriseRequiredMixin:
def validate(self, attrs: dict) -> dict:
"""Check that a valid license exists"""
if not LicenseKey.cached_summary().valid:
if not LicenseKey.cached_summary().has_license:
raise ValidationError(_("Enterprise is required to create/update this object."))
return super().validate(attrs)

View File

@ -11,7 +11,6 @@ from django.db.models.expressions import BaseExpression, Combinable
from django.db.models.signals import post_init
from django.http import HttpRequest
from authentik.core.models import User
from authentik.events.middleware import AuditMiddleware, should_log_model
from authentik.events.utils import cleanse_dict, sanitize_item
@ -28,13 +27,10 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
super().connect(request)
if not self.enabled:
return
user = getattr(request, "user", self.anonymous_user)
if not user.is_authenticated:
user = self.anonymous_user
if not hasattr(request, "request_id"):
return
post_init.connect(
partial(self.post_init_handler, user=user, request=request),
partial(self.post_init_handler, request=request),
dispatch_uid=request.request_id,
weak=False,
)
@ -76,7 +72,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
diff[key] = {"previous_value": value, "new_value": after.get(key)}
return sanitize_item(diff)
def post_init_handler(self, user: User, request: HttpRequest, sender, instance: Model, **_):
def post_init_handler(self, request: HttpRequest, sender, instance: Model, **_):
"""post_init django model handler"""
if not should_log_model(instance):
return
@ -91,7 +87,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
# pylint: disable=too-many-arguments
def post_save_handler(
self,
user: User,
request: HttpRequest,
sender,
instance: Model,
@ -113,6 +108,4 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
for field_set in ignored_field_sets:
if set(diff.keys()) == set(field_set):
return None
return super().post_save_handler(
user, request, sender, instance, created, thread_kwargs, **_
)
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)

View File

@ -188,20 +188,21 @@ class LicenseKey:
def summary(self) -> LicenseSummary:
"""Summary of license status"""
has_license = License.objects.all().count() > 0
last_valid = LicenseKey.last_valid_date()
show_admin_warning = last_valid < now() - timedelta(weeks=2)
show_user_warning = last_valid < now() - timedelta(weeks=4)
read_only = last_valid < now() - timedelta(weeks=6)
latest_valid = datetime.fromtimestamp(self.exp)
return LicenseSummary(
show_admin_warning=show_admin_warning,
show_user_warning=show_user_warning,
read_only=read_only,
show_admin_warning=show_admin_warning and has_license,
show_user_warning=show_user_warning and has_license,
read_only=read_only and has_license,
latest_valid=latest_valid,
internal_users=self.internal_users,
external_users=self.external_users,
valid=self.is_valid(),
has_license=License.objects.all().count() > 0,
has_license=has_license,
)
@staticmethod

View File

@ -6,13 +6,13 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint
from authentik.enterprise.providers.rac.models import ConnectionToken
class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
@ -23,7 +23,7 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
user = GroupMemberSerializer(source="session.user", read_only=True)
class Meta:
model = Endpoint
model = ConnectionToken
fields = [
"pk",
"provider",
@ -49,5 +49,5 @@ class ConnectionTokenViewSet(
filterset_fields = ["endpoint", "session__user", "provider"]
search_fields = ["endpoint__name", "provider__name"]
ordering = ["endpoint__name", "provider__name"]
permission_classes = [OwnerPermissions]
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]

View File

@ -2,11 +2,14 @@
from datetime import datetime
from django.db.models.signals import pre_save
from django.core.cache import cache
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
from django.utils.timezone import get_current_timezone
from authentik.enterprise.license import CACHE_KEY_ENTERPRISE_LICENSE
from authentik.enterprise.models import License
from authentik.enterprise.tasks import enterprise_update_usage
@receiver(pre_save, sender=License)
@ -17,3 +20,10 @@ def pre_save_license(sender: type[License], instance: License, **_):
instance.internal_users = status.internal_users
instance.external_users = status.external_users
instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone())
@receiver(post_save, sender=License)
def post_save_license(sender: type[License], instance: License, **_):
"""Trigger license usage calculation when license is saved"""
cache.delete(CACHE_KEY_ENTERPRISE_LICENSE)
enterprise_update_usage.delay()

View File

@ -12,7 +12,6 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import (
@ -24,6 +23,7 @@ from authentik.events.models import (
TransportMode,
)
from authentik.events.utils import get_user
from authentik.rbac.decorators import permission_required
class NotificationTransportSerializer(ModelSerializer):

View File

@ -21,8 +21,8 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ReadOnlyModelViewSet
from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.events.models import SystemTask, TaskStatus
from authentik.rbac.decorators import permission_required
LOGGER = get_logger()
@ -81,7 +81,7 @@ class SystemTaskViewSet(ReadOnlyModelViewSet):
500: OpenApiResponse(description="Failed to retry task"),
},
)
@action(detail=True, methods=["post"])
@action(detail=True, methods=["POST"], permission_classes=[])
def run(self, request: Request, pk=None) -> Response:
"""Run task"""
task: SystemTask = self.get_object()

View File

@ -82,26 +82,29 @@ class AuditMiddleware:
self.anonymous_user = get_anonymous_user()
def get_user(self, request: HttpRequest) -> User:
user = getattr(request, "user", self.anonymous_user)
if not user.is_authenticated:
return self.anonymous_user
return user
def connect(self, request: HttpRequest):
"""Connect signal for automatic logging"""
self._ensure_fallback_user()
user = getattr(request, "user", self.anonymous_user)
if not user.is_authenticated:
user = self.anonymous_user
if not hasattr(request, "request_id"):
return
post_save.connect(
partial(self.post_save_handler, user=user, request=request),
partial(self.post_save_handler, request=request),
dispatch_uid=request.request_id,
weak=False,
)
pre_delete.connect(
partial(self.pre_delete_handler, user=user, request=request),
partial(self.pre_delete_handler, request=request),
dispatch_uid=request.request_id,
weak=False,
)
m2m_changed.connect(
partial(self.m2m_changed_handler, user=user, request=request),
partial(self.m2m_changed_handler, request=request),
dispatch_uid=request.request_id,
weak=False,
)
@ -147,7 +150,6 @@ class AuditMiddleware:
# pylint: disable=too-many-arguments
def post_save_handler(
self,
user: User,
request: HttpRequest,
sender,
instance: Model,
@ -158,16 +160,18 @@ class AuditMiddleware:
"""Signal handler for all object's post_save"""
if not should_log_model(instance):
return
user = self.get_user(request)
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
thread = EventNewThread(action, request, user=user, model=model_to_dict(instance))
thread.kwargs.update(thread_kwargs or {})
thread.run()
def pre_delete_handler(self, user: User, request: HttpRequest, sender, instance: Model, **_):
def pre_delete_handler(self, request: HttpRequest, sender, instance: Model, **_):
"""Signal handler for all object's pre_delete"""
if not should_log_model(instance): # pragma: no cover
return
user = self.get_user(request)
EventNewThread(
EventAction.MODEL_DELETED,
@ -176,14 +180,13 @@ class AuditMiddleware:
model=model_to_dict(instance),
).run()
def m2m_changed_handler(
self, user: User, request: HttpRequest, sender, instance: Model, action: str, **_
):
def m2m_changed_handler(self, request: HttpRequest, sender, instance: Model, action: str, **_):
"""Signal handler for all object's m2m_changed"""
if action not in ["pre_add", "pre_remove", "post_clear"]:
return
if not should_log_m2m(instance):
return
user = self.get_user(request)
EventNewThread(
EventAction.MODEL_UPDATED,

View File

@ -451,6 +451,13 @@ class NotificationTransport(SerializerModel):
def send_email(self, notification: "Notification") -> list[str]:
"""Send notification via global email configuration"""
if notification.user.email.strip() == "":
LOGGER.info(
"Discarding notification as user has no email address",
user=notification.user,
notification=notification,
)
return None
subject_prefix = "authentik Notification: "
context = {
"key_value": {
@ -480,7 +487,7 @@ class NotificationTransport(SerializerModel):
}
mail = TemplateEmailMessage(
subject=subject_prefix + context["title"],
to=[f"{notification.user.name} <{notification.user.email}>"],
to=[(notification.user.name, notification.user.email)],
language=notification.user.locale(),
template_name="email/event_notification.html",
template_context=context,

View File

@ -88,8 +88,8 @@ class SystemTask(TenantTask):
"duration": max(perf_counter() - self._start_precise, 0),
"task_call_module": self.__module__,
"task_call_func": self.__name__,
"task_call_args": args,
"task_call_kwargs": kwargs,
"task_call_args": sanitize_item(args),
"task_call_kwargs": sanitize_item(kwargs),
"status": self._status,
"messages": sanitize_item(self._messages),
"expires": now() + timedelta(hours=self.result_timeout_hours),
@ -113,8 +113,8 @@ class SystemTask(TenantTask):
"duration": max(perf_counter() - self._start_precise, 0),
"task_call_module": self.__module__,
"task_call_func": self.__name__,
"task_call_args": args,
"task_call_kwargs": kwargs,
"task_call_args": sanitize_item(args),
"task_call_kwargs": sanitize_item(kwargs),
"status": self._status,
"messages": sanitize_item(self._messages),
"expires": now() + timedelta(hours=self.result_timeout_hours),

View File

@ -3,9 +3,10 @@
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.models import Application, Token, TokenIntents
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
class TestEventsMiddleware(APITestCase):
@ -47,3 +48,30 @@ class TestEventsMiddleware(APITestCase):
context__model__name="test-delete",
).exists()
)
def test_create_with_api(self):
"""Test model creation event (with API token auth)"""
self.client.logout()
token = Token.objects.create(user=self.user, intent=TokenIntents.INTENT_API, expiring=False)
uid = generate_id()
self.client.post(
reverse("authentik_api:application-list"),
data={"name": uid, "slug": uid},
HTTP_AUTHORIZATION=f"Bearer {token.key}",
)
self.assertTrue(Application.objects.filter(name=uid).exists())
event = Event.objects.filter(
action=EventAction.MODEL_CREATED,
context__model__model_name="application",
context__model__app="authentik_core",
context__model__name=uid,
).first()
self.assertIsNotNone(event)
self.assertEqual(
event.user,
{
"pk": self.user.pk,
"email": self.user.email,
"username": self.user.username,
},
)

View File

@ -15,7 +15,6 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer
from authentik.core.api.used_by import UsedByMixin
@ -33,6 +32,7 @@ from authentik.lib.utils.file import (
set_file_url,
)
from authentik.lib.views import bad_request_message
from authentik.rbac.decorators import permission_required
LOGGER = get_logger()

View File

@ -1,6 +1,7 @@
"""flow views tests"""
from unittest.mock import MagicMock, PropertyMock, patch
from urllib.parse import urlencode
from django.http import HttpRequest, HttpResponse
from django.test.client import RequestFactory
@ -18,7 +19,12 @@ from authentik.flows.models import (
from authentik.flows.planner import FlowPlan, FlowPlanner
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
from authentik.flows.views.executor import (
NEXT_ARG_NAME,
QS_QUERY,
SESSION_KEY_PLAN,
FlowExecutorView,
)
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
@ -121,16 +127,73 @@ class TestFlowExecutor(FlowTestCase):
TO_STAGE_RESPONSE_MOCK,
)
def test_invalid_flow_redirect(self):
"""Tests that an invalid flow still redirects"""
"""Test invalid flow with valid redirect destination"""
flow = create_test_flow(
FlowDesignation.AUTHENTICATION,
)
dest = "/unique-string"
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(url + f"?{NEXT_ARG_NAME}={dest}")
response = self.client.get(url + f"?{QS_QUERY}={urlencode({NEXT_ARG_NAME: dest})}")
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("authentik_core:root-redirect"))
self.assertEqual(response.url, "/unique-string")
@patch(
"authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_invalid_flow_invalid_redirect(self):
"""Test invalid flow redirect with an invalid URL"""
flow = create_test_flow(
FlowDesignation.AUTHENTICATION,
)
dest = "http://something.example.com/unique-string"
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(url + f"?{QS_QUERY}={urlencode({NEXT_ARG_NAME: dest})}")
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
flow,
component="ak-stage-access-denied",
error_message="Invalid next URL",
)
@patch(
"authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_valid_flow_redirect(self):
"""Test valid flow with valid redirect destination"""
flow = create_test_flow()
dest = "/unique-string"
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(url + f"?{QS_QUERY}={urlencode({NEXT_ARG_NAME: dest})}")
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/unique-string")
@patch(
"authentik.flows.views.executor.to_stage_response",
TO_STAGE_RESPONSE_MOCK,
)
def test_valid_flow_invalid_redirect(self):
"""Test valid flow redirect with an invalid URL"""
flow = create_test_flow()
dest = "http://something.example.com/unique-string"
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug})
response = self.client.get(url + f"?{QS_QUERY}={urlencode({NEXT_ARG_NAME: dest})}")
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
response,
flow,
component="ak-stage-access-denied",
error_message="Invalid next URL",
)
@patch(
"authentik.flows.views.executor.to_stage_response",

View File

@ -12,6 +12,7 @@ from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import View
from drf_spectacular.types import OpenApiTypes
@ -178,6 +179,8 @@ class FlowExecutorView(APIView):
self.cancel()
self._logger.debug("f(exec): Continuing existing plan")
# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params
# Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan:
request.session[SESSION_KEY_HISTORY] = []
@ -192,8 +195,6 @@ class FlowExecutorView(APIView):
# To match behaviour with loading an empty flow plan from cache,
# we don't show an error message here, but rather call _flow_done()
return self._flow_done()
# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params
# We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
try:
@ -392,7 +393,11 @@ class FlowExecutorView(APIView):
NEXT_ARG_NAME, "authentik_core:root-redirect"
)
self.cancel()
return to_stage_response(self.request, redirect_with_qs(next_param))
if next_param and not is_url_absolute(next_param):
return to_stage_response(self.request, redirect_with_qs(next_param))
return to_stage_response(
self.request, self.stage_invalid(error_message=_("Invalid next URL"))
)
def stage_ok(self) -> HttpResponse:
"""Callback called by stages upon successful completion.

View File

@ -13,7 +13,6 @@ from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from structlog.testing import capture_logs
from authentik.api.decorators import permission_required
from authentik.core.api.applications import user_app_cache_key
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, MetaNameSerializer, TypeCreateSerializer
@ -23,6 +22,7 @@ from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSe
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
from authentik.policies.types import CACHE_PREFIX, PolicyRequest
from authentik.rbac.decorators import permission_required
LOGGER = get_logger()

View File

@ -15,13 +15,13 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
from authentik.core.models import Provider
from authentik.providers.oauth2.id_token import IDToken
from authentik.providers.oauth2.models import AccessToken, OAuth2Provider, ScopeMapping
from authentik.rbac.decorators import permission_required
class OAuth2ProviderSerializer(ProviderSerializer):

View File

@ -36,8 +36,21 @@ class TestAuthorize(OAuthTestCase):
def test_invalid_grant_type(self):
"""Test with invalid grant type"""
OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid/Foo",
)
with self.assertRaises(AuthorizeError):
request = self.factory.get("/", data={"response_type": "invalid"})
request = self.factory.get(
"/",
data={
"response_type": "invalid",
"client_id": "test",
"redirect_uri": "http://local.invalid/Foo",
},
)
OAuthAuthorizationParams.from_request(request)
def test_invalid_client_id(self):
@ -344,7 +357,12 @@ class TestAuthorize(OAuthTestCase):
]
)
)
Application.objects.create(name="app", slug="app", provider=provider)
provider.property_mappings.add(
ScopeMapping.objects.create(
name=generate_id(), scope_name="test", expression="""return {"sub": "foo"}"""
)
)
Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider)
state = generate_id()
user = create_test_admin_user()
self.client.force_login(user)
@ -365,7 +383,7 @@ class TestAuthorize(OAuthTestCase):
"response_type": "id_token",
"client_id": "test",
"state": state,
"scope": "openid",
"scope": "openid test",
"redirect_uri": "http://localhost",
"nonce": generate_id(),
},
@ -390,6 +408,7 @@ class TestAuthorize(OAuthTestCase):
)
jwt = self.validate_jwt(token, provider)
self.assertEqual(jwt["amr"], ["pwd"])
self.assertEqual(jwt["sub"], "foo")
self.assertAlmostEqual(
jwt["exp"] - now().timestamp(),
expires,

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

@ -23,7 +23,7 @@ from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
from authentik.providers.oauth2.tests.utils import OAuthTestCase
class TestTokenClientCredentialsUserNamePassword(OAuthTestCase):
class TestTokenClientCredentials(OAuthTestCase):
"""Test token (client_credentials) view"""
@apply_blueprint("system/providers-oauth2.yaml")

View File

@ -1,170 +0,0 @@
"""Test token view"""
from json import loads
from django.test import RequestFactory
from django.urls import reverse
from jwt import decode
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, Token, TokenIntents, UserTypes
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.constants import (
GRANT_TYPE_CLIENT_CREDENTIALS,
GRANT_TYPE_PASSWORD,
SCOPE_OPENID,
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
TOKEN_TYPE,
)
from authentik.providers.oauth2.errors import TokenError
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
from authentik.providers.oauth2.tests.utils import OAuthTestCase
class TestTokenClientCredentialsStandard(OAuthTestCase):
"""Test token (client_credentials) view"""
@apply_blueprint("system/providers-oauth2.yaml")
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
self.provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
redirect_uris="http://testserver",
signing_key=create_test_cert(),
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
self.user = create_test_admin_user("sa")
self.user.type = UserTypes.SERVICE_ACCOUNT
self.user.save()
self.token = Token.objects.create(
identifier="sa-token",
user=self.user,
intent=TokenIntents.INTENT_APP_PASSWORD,
expiring=False,
)
def test_wrong_user(self):
"""test invalid username"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"client_secret": self.provider.client_secret + "foo",
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)
def test_no_provider(self):
"""test no provider"""
self.app.provider = None
self.app.save()
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"client_secret": self.provider.client_secret,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)
def test_permission_denied(self):
"""test permission denied"""
group = Group.objects.create(name="foo")
PolicyBinding.objects.create(
group=group,
target=self.app,
order=0,
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"client_secret": self.provider.client_secret,
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)
def test_successful(self):
"""test successful"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
"client_id": self.provider.client_id,
"client_secret": self.provider.client_secret,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["token_type"], TOKEN_TYPE)
_, alg = self.provider.jwt_key
jwt = decode(
body["access_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(
jwt["given_name"], "Autogenerated user from application test (client credentials)"
)
self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")
jwt = decode(
body["id_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(
jwt["given_name"], "Autogenerated user from application test (client credentials)"
)
self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")
def test_successful_password(self):
"""test successful (password grant)"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_PASSWORD,
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
"client_id": self.provider.client_id,
"client_secret": self.provider.client_secret,
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["token_type"], TOKEN_TYPE)
_, alg = self.provider.jwt_key
jwt = decode(
body["access_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(
jwt["given_name"], "Autogenerated user from application test (client credentials)"
)
self.assertEqual(jwt["preferred_username"], "ak-test-client_credentials")

View File

@ -1,182 +0,0 @@
"""Test token view"""
from base64 import b64encode
from json import loads
from django.test import RequestFactory
from django.urls import reverse
from jwt import decode
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, Token, TokenIntents, UserTypes
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.constants import (
GRANT_TYPE_CLIENT_CREDENTIALS,
GRANT_TYPE_PASSWORD,
SCOPE_OPENID,
SCOPE_OPENID_EMAIL,
SCOPE_OPENID_PROFILE,
TOKEN_TYPE,
)
from authentik.providers.oauth2.errors import TokenError
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
from authentik.providers.oauth2.tests.utils import OAuthTestCase
class TestTokenClientCredentialsStandardCompat(OAuthTestCase):
"""Test token (client_credentials) view"""
@apply_blueprint("system/providers-oauth2.yaml")
def setUp(self) -> None:
super().setUp()
self.factory = RequestFactory()
self.provider = OAuth2Provider.objects.create(
name="test",
authorization_flow=create_test_flow(),
redirect_uris="http://testserver",
signing_key=create_test_cert(),
)
self.provider.property_mappings.set(ScopeMapping.objects.all())
self.app = Application.objects.create(name="test", slug="test", provider=self.provider)
self.user = create_test_admin_user("sa")
self.user.type = UserTypes.SERVICE_ACCOUNT
self.user.save()
self.token = Token.objects.create(
identifier="sa-token",
user=self.user,
intent=TokenIntents.INTENT_APP_PASSWORD,
expiring=False,
)
def test_wrong_user(self):
"""test invalid username"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"client_secret": b64encode(f"saa:{self.token.key}".encode()).decode(),
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)
def test_wrong_token(self):
"""test invalid token"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"client_secret": b64encode(f"sa:{self.token.key}foo".encode()).decode(),
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)
def test_no_provider(self):
"""test no provider"""
self.app.provider = None
self.app.save()
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(),
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)
def test_permission_denied(self):
"""test permission denied"""
group = Group.objects.create(name="foo")
PolicyBinding.objects.create(
group=group,
target=self.app,
order=0,
)
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": SCOPE_OPENID,
"client_id": self.provider.client_id,
"client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(),
},
)
self.assertEqual(response.status_code, 400)
self.assertJSONEqual(
response.content.decode(),
{"error": "invalid_grant", "error_description": TokenError.errors["invalid_grant"]},
)
def test_successful(self):
"""test successful"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_CLIENT_CREDENTIALS,
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
"client_id": self.provider.client_id,
"client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(),
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["token_type"], TOKEN_TYPE)
_, alg = self.provider.jwt_key
jwt = decode(
body["access_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
jwt = decode(
body["id_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)
def test_successful_password(self):
"""test successful (password grant)"""
response = self.client.post(
reverse("authentik_providers_oauth2:token"),
{
"grant_type": GRANT_TYPE_PASSWORD,
"scope": f"{SCOPE_OPENID} {SCOPE_OPENID_EMAIL} {SCOPE_OPENID_PROFILE}",
"client_id": self.provider.client_id,
"client_secret": b64encode(f"sa:{self.token.key}".encode()).decode(),
},
)
self.assertEqual(response.status_code, 200)
body = loads(response.content.decode())
self.assertEqual(body["token_type"], TOKEN_TYPE)
_, alg = self.provider.jwt_key
jwt = decode(
body["access_token"],
key=self.provider.signing_key.public_key,
algorithms=[alg],
audience=self.provider.client_id,
)
self.assertEqual(jwt["given_name"], self.user.name)
self.assertEqual(jwt["preferred_username"], self.user.username)

View File

@ -121,44 +121,18 @@ class OAuthAuthorizationParams:
redirect_uri = query_dict.get("redirect_uri", "")
response_type = query_dict.get("response_type", "")
grant_type = None
# Determine which flow to use.
if response_type in [ResponseTypes.CODE]:
grant_type = GrantTypes.AUTHORIZATION_CODE
elif response_type in [
ResponseTypes.ID_TOKEN,
ResponseTypes.ID_TOKEN_TOKEN,
]:
grant_type = GrantTypes.IMPLICIT
elif response_type in [
ResponseTypes.CODE_TOKEN,
ResponseTypes.CODE_ID_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
]:
grant_type = GrantTypes.HYBRID
# Grant type validation.
if not grant_type:
LOGGER.warning("Invalid response type", type=response_type)
raise AuthorizeError(redirect_uri, "unsupported_response_type", "", state)
# Validate and check the response_mode against the predefined dict
# Set to Query or Fragment if not defined in request
response_mode = query_dict.get("response_mode", False)
if response_mode not in ResponseMode.values:
response_mode = ResponseMode.QUERY
if grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]:
response_mode = ResponseMode.FRAGMENT
max_age = query_dict.get("max_age")
return OAuthAuthorizationParams(
client_id=query_dict.get("client_id", ""),
redirect_uri=redirect_uri,
response_type=response_type,
response_mode=response_mode,
grant_type=grant_type,
grant_type="",
scope=set(query_dict.get("scope", "").split()),
state=state,
nonce=query_dict.get("nonce"),
@ -178,6 +152,7 @@ class OAuthAuthorizationParams:
LOGGER.warning("Invalid client identifier", client_id=self.client_id)
raise ClientIdError(client_id=self.client_id)
self.check_redirect_uri()
self.check_grant()
self.check_scope(github_compat)
self.check_nonce()
self.check_code_challenge()
@ -186,6 +161,34 @@ class OAuthAuthorizationParams:
self.redirect_uri, "request_not_supported", self.grant_type, self.state
)
def check_grant(self):
"""Check grant"""
# Determine which flow to use.
if self.response_type in [ResponseTypes.CODE]:
self.grant_type = GrantTypes.AUTHORIZATION_CODE
elif self.response_type in [
ResponseTypes.ID_TOKEN,
ResponseTypes.ID_TOKEN_TOKEN,
]:
self.grant_type = GrantTypes.IMPLICIT
elif self.response_type in [
ResponseTypes.CODE_TOKEN,
ResponseTypes.CODE_ID_TOKEN,
ResponseTypes.CODE_ID_TOKEN_TOKEN,
]:
self.grant_type = GrantTypes.HYBRID
# Grant type validation.
if not self.grant_type:
LOGGER.warning("Invalid response type", type=self.response_type)
raise AuthorizeError(self.redirect_uri, "unsupported_response_type", "", self.state)
if self.response_mode not in ResponseMode.values:
self.response_mode = ResponseMode.QUERY
if self.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]:
self.response_mode = ResponseMode.FRAGMENT
def check_redirect_uri(self):
"""Redirect URI validation."""
allowed_redirect_urls = self.provider.redirect_uris.split()
@ -257,9 +260,9 @@ class OAuthAuthorizationParams:
if SCOPE_OFFLINE_ACCESS in self.scope:
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if PROMPT_CONSENT not in self.prompt:
raise AuthorizeError(
self.redirect_uri, "consent_required", self.grant_type, self.state
)
# Instead of ignoring the `offline_access` scope when `prompt`
# isn't set to `consent`, we set override it ourselves
self.prompt.add(PROMPT_CONSENT)
if self.response_type not in [
ResponseTypes.CODE,
ResponseTypes.CODE_TOKEN,

View File

@ -12,10 +12,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()
@ -38,7 +39,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,11 +1,10 @@
"""Device flow views"""
from typing import Optional
from typing import Any, Optional
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from django.views import View
from rest_framework.exceptions import ErrorDetail
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, IntegerField
from structlog.stdlib import get_logger
@ -18,6 +17,7 @@ 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.policies.views import PolicyAccessView
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.views.device_finish import (
PLAN_CONTEXT_DEVICE,
@ -44,48 +44,52 @@ def get_application(provider: OAuth2Provider) -> Optional[Application]:
return None
def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
"""Validate user token"""
token = DeviceToken.objects.filter(
user_code=code,
).first()
if not token:
return None
class CodeValidatorView(PolicyAccessView):
"""Helper to validate frontside token"""
app = get_application(token.provider)
if not app:
return None
def __init__(self, code: str, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.code = code
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider)
planner = FlowPlanner(token.provider.authorization_flow)
planner.allow_empty_flows = True
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,
},
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
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:
@ -95,7 +99,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")
@ -130,6 +136,13 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
code = IntegerField()
component = CharField(default="ak-provider-oauth2-device-code")
def validate_code(self, code: int) -> HttpResponse | None:
"""Validate code and save the returned http response"""
response = CodeValidatorView(code, request=self.stage.request).dispatch(self.stage.request)
if not response:
raise ValidationError(_("Invalid code"), "invalid")
return response
class OAuthDeviceCodeStage(ChallengeStageView):
"""Flow challenge for users to enter device codes"""
@ -145,12 +158,4 @@ class OAuthDeviceCodeStage(ChallengeStageView):
)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
code = response.validated_data["code"]
validation = validate_code(code, self.request)
if not validation:
response._errors.setdefault("code", [])
response._errors["code"].append(ErrorDetail(_("Invalid code"), "invalid"))
return self.challenge_invalid(response)
# Run cancel to cleanup the current flow
self.executor.cancel()
return validation
return response.validated_data["code"]

View File

@ -1,7 +1,6 @@
"""authentik OAuth2 Token views"""
from base64 import b64decode, urlsafe_b64encode
from binascii import Error
from base64 import urlsafe_b64encode
from dataclasses import InitVar, dataclass
from datetime import datetime
from hashlib import sha256
@ -24,12 +23,10 @@ from authentik.core.middleware import CTX_AUTH_VIA
from authentik.core.models import (
USER_ATTRIBUTE_EXPIRES,
USER_ATTRIBUTE_GENERATED,
USER_PATH_SYSTEM_PREFIX,
Application,
Token,
TokenIntents,
User,
UserTypes,
)
from authentik.events.models import Event, EventAction
from authentik.events.signals import get_login_event
@ -289,29 +286,11 @@ class TokenParams:
raise TokenError("invalid_grant")
def __post_init_client_credentials(self, request: HttpRequest):
# client_credentials flow with client assertion
if request.POST.get(CLIENT_ASSERTION_TYPE, "") != "":
return self.__post_init_client_credentials_jwt(request)
# authentik-custom-ish client credentials flow
if request.POST.get("username", "") != "":
return self.__post_init_client_credentials_creds(
request, request.POST.get("username"), request.POST.get("password")
)
# Standard method which creates an automatic user
if self.client_secret == self.provider.client_secret:
return self.__post_init_client_credentials_generated(request)
# Standard workaround method which stores username:password
# as client_secret
try:
user, _, password = b64decode(self.client_secret).decode("utf-8").partition(":")
return self.__post_init_client_credentials_creds(request, user, password)
except (ValueError, Error):
raise TokenError("invalid_grant")
def __post_init_client_credentials_creds(
self, request: HttpRequest, username: str, password: str
):
# Authenticate user based on credentials
username = request.POST.get("username")
password = request.POST.get("password")
user = User.objects.filter(username=username).first()
if not user:
raise TokenError("invalid_grant")
@ -337,6 +316,7 @@ class TokenParams:
PLAN_CONTEXT_APPLICATION: app,
},
).from_http(request, user=user)
return None
# pylint: disable=too-many-locals
def __post_init_client_credentials_jwt(self, request: HttpRequest):
@ -429,35 +409,6 @@ class TokenParams:
},
).from_http(request, user=self.user)
def __post_init_client_credentials_generated(self, request: HttpRequest):
# Authorize user access
app = Application.objects.filter(provider=self.provider).first()
if not app or not app.provider:
raise TokenError("invalid_grant")
self.user, _ = User.objects.update_or_create(
# trim username to ensure the entire username is max 150 chars
# (22 chars being the length of the "template")
username=f"ak-{self.provider.name[:150-22]}-client_credentials",
defaults={
"attributes": {
USER_ATTRIBUTE_GENERATED: True,
},
"last_login": timezone.now(),
"name": f"Autogenerated user from application {app.name} (client credentials)",
"path": f"{USER_PATH_SYSTEM_PREFIX}/apps/{app.slug}",
"type": UserTypes.SERVICE_ACCOUNT,
},
)
self.__check_policy_access(app, request)
Event.new(
action=EventAction.LOGIN,
**{
PLAN_CONTEXT_METHOD: "oauth_client_secret",
PLAN_CONTEXT_APPLICATION: app,
},
).from_http(request, user=self.user)
def __post_init_device_code(self, request: HttpRequest):
device_code = request.POST.get("device_code", "")
code = DeviceToken.objects.filter(device_code=device_code, provider=self.provider).first()
@ -467,6 +418,7 @@ class TokenParams:
def __create_user_from_jwt(self, token: dict[str, Any], app: Application, source: OAuthSource):
"""Create user from JWT"""
exp = token.get("exp")
self.user, created = User.objects.update_or_create(
username=f"{self.provider.name}-{token.get('sub')}",
defaults={
@ -476,10 +428,8 @@ class TokenParams:
"last_login": timezone.now(),
"name": f"Autogenerated user from application {app.name} (client credentials JWT)",
"path": source.get_user_path(),
"type": UserTypes.SERVICE_ACCOUNT,
},
)
exp = token.get("exp")
if created and exp:
self.user.attributes[USER_ATTRIBUTE_EXPIRES] = exp
self.user.save()

View File

@ -101,8 +101,8 @@ class UserInfoView(View):
value=value,
)
continue
LOGGER.debug("updated scope", scope=scope)
always_merger.merge(final_claims, value)
LOGGER.debug("updated scope", scope=scope)
return final_claims
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
@ -121,8 +121,9 @@ class UserInfoView(View):
"""Handle GET Requests for UserInfo"""
if not self.token:
return HttpResponseBadRequest()
claims = self.get_claims(self.token.provider, self.token)
claims["sub"] = self.token.id_token.sub
claims = {}
claims.setdefault("sub", self.token.id_token.sub)
claims.update(self.get_claims(self.token.provider, self.token))
if self.token.id_token.nonce:
claims["nonce"] = self.token.id_token.nonce
response = TokenResponse(claims)

View File

@ -22,7 +22,6 @@ from rest_framework.serializers import PrimaryKeyRelatedField, ValidationError
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer, PropertyMappingPreviewSerializer
@ -33,6 +32,7 @@ from authentik.providers.saml.processors.assertion import AssertionProcessor
from authentik.providers.saml.processors.authn_request_parser import AuthNRequest
from authentik.providers.saml.processors.metadata import MetadataProcessor
from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
from authentik.rbac.decorators import permission_required
from authentik.sources.saml.processors.constants import SAML_BINDING_POST, SAML_BINDING_REDIRECT
LOGGER = get_logger()

View File

@ -15,10 +15,10 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.utils import PassiveSerializer
from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.api.rbac import PermissionAssignSerializer
from authentik.rbac.decorators import permission_required
from authentik.rbac.models import Role

View File

@ -16,11 +16,11 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.models import User, UserTypes
from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.api.rbac import PermissionAssignSerializer
from authentik.rbac.decorators import permission_required
class UserObjectPermissionSerializer(ModelSerializer):

View File

@ -14,18 +14,23 @@ LOGGER = get_logger()
def permission_required(obj_perm: Optional[str] = None, global_perms: Optional[list[str]] = None):
"""Check permissions for a single custom action"""
def wrapper_outter(func: Callable):
def _check_obj_perm(self: ModelViewSet, request: Request):
# Check obj_perm both globally and on the specific object
# Having the global permission has higher priority
if request.user.has_perm(obj_perm):
return
obj = self.get_object()
if not request.user.has_perm(obj_perm, obj):
LOGGER.debug("denying access for object", user=request.user, perm=obj_perm, obj=obj)
self.permission_denied(request)
def wrapper_outer(func: Callable):
"""Check permissions for a single custom action"""
@wraps(func)
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
if obj_perm:
obj = self.get_object()
if not request.user.has_perm(obj_perm, obj):
LOGGER.debug(
"denying access for object", user=request.user, perm=obj_perm, obj=obj
)
return self.permission_denied(request)
_check_obj_perm(self, request)
if global_perms:
for other_perm in global_perms:
if not request.user.has_perm(other_perm):
@ -35,4 +40,4 @@ def permission_required(obj_perm: Optional[str] = None, global_perms: Optional[l
return wrapper
return wrapper_outter
return wrapper_outer

View File

@ -0,0 +1,58 @@
"""test decorators api"""
from django.urls import reverse
from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_user
from authentik.lib.generators import generate_id
class TestAPIDecorators(APITestCase):
"""test decorators api"""
def setUp(self) -> None:
super().setUp()
self.user = create_test_user()
def test_obj_perm_denied(self):
"""Test object perm denied"""
self.client.force_login(self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 403)
def test_obj_perm_global(self):
"""Test object perm successful (global)"""
assign_perm("authentik_core.view_application", self.user)
assign_perm("authentik_events.view_event", self.user)
self.client.force_login(self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 200)
def test_obj_perm_scoped(self):
"""Test object perm successful (scoped)"""
assign_perm("authentik_events.view_event", self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
assign_perm("authentik_core.view_application", self.user, app)
self.client.force_login(self.user)
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 200)
def test_other_perm_denied(self):
"""Test other perm denied"""
self.client.force_login(self.user)
app = Application.objects.create(name=generate_id(), slug=generate_id())
assign_perm("authentik_core.view_application", self.user, app)
response = self.client.get(
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
)
self.assertEqual(response.status_code, 403)

View File

@ -7,6 +7,8 @@ from psycopg import connect
from authentik.lib.config import CONFIG
QUERY = """SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;"""
@lru_cache
def get_install_id() -> str:
@ -18,7 +20,7 @@ def get_install_id() -> str:
if settings.TEST:
return str(uuid4())
with connection.cursor() as cursor:
cursor.execute("SELECT id FROM public.authentik_install_id LIMIT 1;")
cursor.execute(QUERY)
return cursor.fetchone()[0]
@ -38,5 +40,5 @@ def get_install_id_raw():
sslkey=CONFIG.get("postgresql.sslkey"),
)
cursor = conn.cursor()
cursor.execute("SELECT id FROM public.authentik_install_id LIMIT 1;")
cursor.execute(QUERY)
return cursor.fetchone()[0]

View File

@ -13,12 +13,12 @@ from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views.executor import to_stage_response
from authentik.rbac.decorators import permission_required
from authentik.sources.plex.models import PlexSource, PlexSourceConnection
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager

View File

@ -17,9 +17,9 @@ from rest_framework.viewsets import GenericViewSet, ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer
from authentik.rbac.decorators import permission_required
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL
from authentik.stages.authenticator_duo.tasks import duo_import_devices

View File

@ -65,7 +65,7 @@ def get_webauthn_challenge_without_user(
authentication_options = generate_authentication_options(
rp_id=get_rp_id(request),
allow_credentials=[],
user_verification=stage.webauthn_user_verification,
user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
)
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge

View File

@ -164,8 +164,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"""Test webauthn (userless)"""
request = get_request("/")
stage = AuthenticatorValidateStage.objects.create(
name=generate_id(),
name=generate_id(), webauthn_user_verification=UserVerification.PREFERRED
)
stage.refresh_from_db()
WebAuthnDevice.objects.create(
user=self.user,
public_key=(

View File

@ -10,6 +10,7 @@ from webauthn import options_to_json
from webauthn.helpers.bytes_to_base64url import bytes_to_base64url
from webauthn.helpers.exceptions import InvalidRegistrationResponse
from webauthn.helpers.structs import (
AuthenticatorAttachment,
AuthenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions,
ResidentKeyRequirement,
@ -91,7 +92,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
# set, cast it to string to ensure it's not a django class
authenticator_attachment = stage.authenticator_attachment
if authenticator_attachment:
authenticator_attachment = str(authenticator_attachment)
authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment))
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
rp_id=get_rp_id(self.request),

View File

@ -30,7 +30,7 @@ class Command(TenantCommand):
delete_stage = True
message = TemplateEmailMessage(
subject="authentik Test-Email",
to=[options["to"]],
to=[("", options["to"])],
template_name="email/setup.html",
template_context={},
)

View File

@ -111,7 +111,7 @@ class EmailStageView(ChallengeStageView):
try:
message = TemplateEmailMessage(
subject=_(current_stage.subject),
to=[f"{pending_user.name} <{email}>"],
to=[(pending_user.name, email)],
language=pending_user.locale(self.request),
template_name=current_stage.template,
template_context={

View File

@ -1,4 +1,4 @@
{% load i18n %}{% translate "Welcome!" %}
{% load i18n %}{% autoescape off %}{% translate "Welcome!" %}
{% translate "We're excited to have you get started. First, you need to confirm your account. Just open the link below." %}
@ -6,3 +6,4 @@
--
Powered by goauthentik.io.
{% endautoescape %}

View File

@ -1,4 +1,4 @@
{% load authentik_stages_email %}{% load i18n %}{% translate "Dear authentik user," %}
{% load authentik_stages_email %}{% load i18n %}{% autoescape off %}{% translate "Dear authentik user," %}
{% translate "The following notification was created:" %}
@ -16,3 +16,4 @@ This email was sent from the notification transport {{ name }}.
--
Powered by goauthentik.io.
{% endautoescape %}

View File

@ -1,4 +1,4 @@
{% load i18n %}{% load humanize %}{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %}
{% load i18n %}{% load humanize %}{% autoescape off %}{% blocktrans with username=user.username %}Hi {{ username }},{% endblocktrans %}
{% blocktrans %}
You recently requested to change your password for your authentik account. Use the link below to set a new password.
@ -10,3 +10,4 @@ If you did not request a password change, please ignore this Email. The link abo
--
Powered by goauthentik.io.
{% endautoescape %}

View File

@ -1,7 +1,8 @@
{% load i18n %}authentik Test-Email
{% load i18n %}{% autoescape off %}authentik Test-Email
{% blocktrans %}
This is a test email to inform you, that you've successfully configured authentik emails.
{% endblocktrans %}
--
Powered by goauthentik.io.
{% endautoescape %}

View File

@ -39,6 +39,7 @@ class TestEmailStageSending(FlowTestCase):
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
Event.objects.filter(action=EventAction.EMAIL_SENT).delete()
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
with patch(

View File

@ -9,6 +9,7 @@ from unittest.mock import PropertyMock, patch
from django.conf import settings
from django.core.mail.backends.locmem import EmailBackend
from django.core.mail.message import sanitize_address
from django.urls import reverse
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
@ -19,6 +20,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.email.models import EmailStage, get_template_choices
from authentik.stages.email.utils import TemplateEmailMessage
def get_templates_setting(temp_dir: str) -> dict[str, Any]:
@ -89,3 +91,12 @@ class TestEmailStageTemplates(FlowTestCase):
event.context["message"], "Exception occurred while rendering E-mail template"
)
self.assertEqual(event.context["template"], "invalid.html")
def test_template_address(self):
"""Test addresses are correctly parsed"""
message = TemplateEmailMessage(to=[("foo@bar.baz", "foo@bar.baz")])
[sanitize_address(addr, "utf-8") for addr in message.recipients()]
self.assertEqual(message.recipients(), ["foo@bar.baz"])
message = TemplateEmailMessage(to=[("some-name", "foo@bar.baz")])
[sanitize_address(addr, "utf-8") for addr in message.recipients()]
self.assertEqual(message.recipients(), ["some-name <foo@bar.baz>"])

View File

@ -25,8 +25,19 @@ def logo_data() -> MIMEImage:
class TemplateEmailMessage(EmailMultiAlternatives):
"""Wrapper around EmailMultiAlternatives with integrated template rendering"""
def __init__(self, template_name=None, template_context=None, language="", **kwargs):
super().__init__(**kwargs)
def __init__(
self, to: list[tuple[str]], template_name=None, template_context=None, language="", **kwargs
):
sanitized_to = []
# Ensure that all recipients are valid
for recipient_name, recipient_email in to:
if recipient_name == recipient_email:
sanitized_to.append(recipient_email)
else:
sanitized_to.append(f"{recipient_name} <{recipient_email}>")
super().__init__(to=sanitized_to, **kwargs)
if not template_name:
return
with translation.override(language):
html_content = render_to_string(template_name, template_context)
try:

View File

@ -12,6 +12,7 @@ from rest_framework.exceptions import ValidationError
from authentik.core.middleware import SESSION_KEY_IMPERSONATE_USER
from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION
from authentik.events.utils import sanitize_item
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView
from authentik.flows.views.executor import FlowExecutorView
@ -47,7 +48,7 @@ class UserWriteStageView(StageView):
# this is just a sanity check to ensure that is removed
if parts[0] == "attributes":
parts = parts[1:]
set_path_in_dict(user.attributes, ".".join(parts), value)
set_path_in_dict(user.attributes, ".".join(parts), sanitize_item(value))
def ensure_user(self) -> tuple[Optional[User], bool]:
"""Ensure a user exists"""

View File

@ -87,11 +87,6 @@ class Tenant(TenantMixin, SerializerModel):
raise IntegrityError("Cannot create schema named template")
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
if self.schema_name in ("public", "template"):
raise IntegrityError("Cannot delete schema public or template")
super().delete(*args, **kwargs)
@property
def serializer(self) -> Serializer:
from authentik.tenants.api.tenants import TenantSerializer

View File

@ -0,0 +1,14 @@
"""authentik tenants signals"""
from django.db import models
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django_tenants.utils import get_public_schema_name
from authentik.tenants.models import Tenant
@receiver(pre_delete, sender=Tenant)
def tenants_ensure_no_default_delete(sender, instance: Tenant, **kwargs):
if instance.schema_name == get_public_schema_name():
raise models.ProtectedError("Cannot delete schema public", instance)

View File

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

View File

@ -50,12 +50,12 @@ type StorageConfig struct {
}
type StorageMediaConfig struct {
Backend string `yaml:"backend" env:"AUTHENTIK_STORAGE_MEDIA_BACKEND"`
Backend string `yaml:"backend" env:"AUTHENTIK_STORAGE__MEDIA__BACKEND"`
File StorageFileConfig `yaml:"file"`
}
type StorageFileConfig struct {
Path string `yaml:"path" env:"AUTHENTIK_STORAGE_MEDIA_FILE_PATH"`
Path string `yaml:"path" env:"AUTHENTIK_STORAGE__MEDIA__FILE__PATH"`
}
type ErrorReportingConfig struct {

View File

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

View File

@ -55,6 +55,7 @@ function cleanup {
}
function prepare_debug {
source ${VENV_PATH}/bin/activate
poetry install --no-ansi --no-interaction
touch /unittest.xml
chown authentik:authentik /unittest.xml

View File

@ -64,6 +64,7 @@ def release_lock(cursor: Cursor):
"""Release database lock"""
if not LOCKED:
return
LOGGER.info("releasing database lock")
cursor.execute("SELECT pg_advisory_unlock(%s)", (ADV_LOCK_UID,))

View File

@ -0,0 +1,12 @@
from lifecycle.migrate import BaseMigration
class Migration(BaseMigration):
def needs_migration(self) -> bool:
self.cur.execute(
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'template';"
)
return not bool(self.cur.rowcount)
def run(self):
self.cur.execute("CREATE SCHEMA IF NOT EXISTS template; COMMIT;")

148
poetry.lock generated
View File

@ -506,48 +506,48 @@ files = [
[[package]]
name = "cbor2"
version = "5.5.1"
version = "5.6.2"
description = "CBOR (de)serializer with extensive tag support"
optional = false
python-versions = ">=3.8"
files = [
{file = "cbor2-5.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:37ba4f719384bd4ea317e92a8763ea343e205f3112c8241778fd9dbc64ae1498"},
{file = "cbor2-5.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:425ae919120b9d05b4794b3e5faf6584fc47a9d61db059d4f00ce16ae93a3f63"},
{file = "cbor2-5.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c511ff6356d6f4292ced856d5048a24ee61a85634816f29dadf1f089e8cb4f9"},
{file = "cbor2-5.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6ab54a9282dd99a3a70d0f64706d3b3592e7920564a93101caa74dec322346c"},
{file = "cbor2-5.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:39d94852dd61bda5b3d2bfe74e7b194a7199937d270f90099beec3e7584f0c9b"},
{file = "cbor2-5.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:65532ba929beebe1c63317ad00c79d4936b60a5c29a3c329d2aa7df4e72ad907"},
{file = "cbor2-5.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:1206180f66a9ad23e692cf457610c877f186ad303a1264b6c5335015b7bee83e"},
{file = "cbor2-5.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:42155a20be46312fad2ceb85a408e2d90da059c2d36a65e0b99abca57c5357fd"},
{file = "cbor2-5.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f3827ae14c009df9b37790f1da5cd1f9d64f7ffec472a49ebf865c0af6b77e9"},
{file = "cbor2-5.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bfa417dbb8b4581ad3c2312469899518596551cfb0fe5bdaf8a6921cff69d7e"},
{file = "cbor2-5.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3317e7dfb4f3180be90bcd853204558d89f119b624c2168153b53dea305e79d"},
{file = "cbor2-5.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a5770bdf4340de55679efe6c38fc6d64529fda547e7a85eb0217a82717a8235"},
{file = "cbor2-5.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b5d53826ad0c92fcb004b2a475896610b51e0ca010f6c37d762aae44ab0807b2"},
{file = "cbor2-5.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc77cac985f7f7a20f2d8b1957d1e79393d7df823f61c7c6173d3a0011c1d770"},
{file = "cbor2-5.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9e45d5aa8e484b4bf57240d8e7949389f1c9d4073758abb30954386321b55c9d"},
{file = "cbor2-5.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93b949a66bec40dd0ca87a6d026136fea2cf1660120f921199a47ac8027af253"},
{file = "cbor2-5.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93d601ca92d917f769370a5e6c3ead62dca6451b2b603915e4fcf300083b9fcd"},
{file = "cbor2-5.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11876abd50b9f70d114fcdbb0b5a3249ccd7d321465f0350028fd6d2317e114"},
{file = "cbor2-5.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fd77c558decdba2a2a7a463e6346d53781d2163bacf205f77b999f561ba4ac73"},
{file = "cbor2-5.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efb81920d80410b8e80a4a6a8b06ec9b766be0ae7f3029af8ae4b30914edcfa3"},
{file = "cbor2-5.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:4bb35f3b1ebd4b7b37628f0cd5c839f3008dec669194a2a4a33d91bab7f8663b"},
{file = "cbor2-5.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f41e4a439f642954ed728dc18915098b5f2ebec7029eaebe52c06c52b6a9a63a"},
{file = "cbor2-5.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4eae4d56314f22920a28bf7affefdfc918646877ce3b16220dc6cf38a584aa41"},
{file = "cbor2-5.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559a0c1ec8dcedd6142b81727403e0f5a2e8f4c18e8bb3c548107ec39af4e9cb"},
{file = "cbor2-5.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537da7bfee97ee44a11b300c034c18e674af6a5dc4718a6fba141037f099c7ec"},
{file = "cbor2-5.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c99fd8bbc6bbf3bf4d6b2996594ae633b778b27b0531559487950762c4e1e3f"},
{file = "cbor2-5.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ee46e6dbc8e2cf302a022fec513d57dba65e9d5ec495bcd1ad97a5dbdbab249"},
{file = "cbor2-5.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:67e2be461320197495fff55f250b111d4125a0a2d02e6256e41f8598adc3ad3f"},
{file = "cbor2-5.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4384a56afef0b908b61c8ea3cca3e257a316427ace3411308f51ee301b23adf9"},
{file = "cbor2-5.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8cc64acc606b7f2a4b673a1d6cde5a9cb1860a6ce27b353e269c9535efbd62c"},
{file = "cbor2-5.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50019fea3cb07fa9b2b53772a52b4243e87de232591570c4c272b3ebdb419493"},
{file = "cbor2-5.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a18be0af9241883bc67a036c1f33e3f9956d31337ccd412194bf759bc1095e03"},
{file = "cbor2-5.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:60e7e0073291096605de27de3ce006148cf9a095199160439555f14f93d044d5"},
{file = "cbor2-5.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41f7501338228b27dac88c1197928cf8985f6fc775f59be89c6fdaddb4e69658"},
{file = "cbor2-5.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c85ab7697252af2240e939707c935ea18081ccb580d4b5b9a94b04148ab2c32b"},
{file = "cbor2-5.5.1-py3-none-any.whl", hash = "sha256:dca639c8ff81b9f0c92faf97324adfdbfb5c2a5bb97f249606c6f5b94c77cc0d"},
{file = "cbor2-5.5.1.tar.gz", hash = "sha256:f9e192f461a9f8f6082df28c035b006d153904213dc8640bed8a72d72bbc9475"},
{file = "cbor2-5.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:516b8390936bb172ff18d7b609a452eaa51991513628949b0a9bf25cbe5a7129"},
{file = "cbor2-5.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1b8b504b590367a51fe8c0d9b8cb458a614d782d37b24483097e2b1e93ed0fff"},
{file = "cbor2-5.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f687e6731b1198811223576800258a712ddbfdcfa86c0aee2cc8269193e6b96"},
{file = "cbor2-5.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e94043d99fe779f62a15a5e156768588a2a7047bb3a127fa312ac1135ff5ecb"},
{file = "cbor2-5.6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8af7162fcf7aa2649f02563bdb18b2fa6478b751eee4df0257bffe19ea8f107a"},
{file = "cbor2-5.6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ea7ecd81c5c6e02c2635973f52a0dd1e19c0bf5ef51f813d8cd5e3e7ed072726"},
{file = "cbor2-5.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3c7f223f1fedc74d33f363d184cb2bab9e4bdf24998f73b5e3bef366d6c41628"},
{file = "cbor2-5.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ea9e150029c3976c46ee9870b6dcdb0a5baae21008fe3290564886b11aa2b64"},
{file = "cbor2-5.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:922e06710e5cf6f56b82b0b90d2f356aa229b99e570994534206985f675fd307"},
{file = "cbor2-5.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b01a718e083e6de8b43296c3ccdb3aa8af6641f6bbb3ea1700427c6af73db28a"},
{file = "cbor2-5.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac85eb731c524d148f608b9bdb2069fa79e374a10ed5d10a2405eba9a6561e60"},
{file = "cbor2-5.6.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03e5b68867b9d89ff2abd14ef7c6d42fbd991adc3e734a19a294935f22a4d05a"},
{file = "cbor2-5.6.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7221b83000ee01d674572eec1d1caa366eac109d1d32c14d7af9a4aaaf496563"},
{file = "cbor2-5.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9aca73b63bdc6561e1a0d38618e78b9c204c942260d51e663c92c4ba6c961684"},
{file = "cbor2-5.6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:377cfe9d5560c682486faef6d856226abf8b2801d95fa29d4e5d75b1615eb091"},
{file = "cbor2-5.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fdc564ef2e9228bcd96ec8c6cdaa431a48ab03b3fb8326ead4b3f986330e5b9e"},
{file = "cbor2-5.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d1c0021d9a1f673066de7c8941f71a59abb11909cc355892dda01e79a2b3045"},
{file = "cbor2-5.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fde9e704e96751e0729cc58b912d0e77c34387fb6bcceea0817069e8683df45"},
{file = "cbor2-5.6.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:30e9ba8f4896726ca61869efacda50b6859aff92162ae5a0e192859664f36c81"},
{file = "cbor2-5.6.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a1e18e65ac71e04434ff5b58bde5c53f85b9c5bc92a3c0e2265089d3034f3"},
{file = "cbor2-5.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:94981277b4bf448a2754c1f34a9d0055a9d1c5a8d102c933ffe95c80f1085bae"},
{file = "cbor2-5.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f70db0ebcf005c25408e8d5cc4b9558c899f13a3e2f8281fa3d3be4894e0e821"},
{file = "cbor2-5.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:22c24fe9ef1696a84b8fd80ff66eb0e5234505d8b9a9711fc6db57bce10771f3"},
{file = "cbor2-5.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4a3420f80d6b942874d66eaad07658066370df994ddee4125b48b2cbc61ece"},
{file = "cbor2-5.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b28d8ff0e726224a7429281700c28afe0e665f83f9ae79648cbae3f1a391cbf"},
{file = "cbor2-5.6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c10ede9462458998f1b9c488e25fe3763aa2491119b7af472b72bf538d789e24"},
{file = "cbor2-5.6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ea686dfb5e54d690e704ce04993bc8ca0052a7cd2d4b13dd333a41cca8a05a05"},
{file = "cbor2-5.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:22996159b491d545ecfd489392d3c71e5d0afb9a202dfc0edc8b2cf413a58326"},
{file = "cbor2-5.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9faa0712d414a88cc1244c78cd4b28fced44f1827dbd8c1649e3c40588aa670f"},
{file = "cbor2-5.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6031a284d93fc953fc2a2918f261c4f5100905bd064ca3b46961643e7312a828"},
{file = "cbor2-5.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30c8a9a9df79f26e72d8d5fa51ef08eb250d9869a711bcf9539f1865916c983"},
{file = "cbor2-5.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44bf7457fca23209e14dab8181dff82466a83b72e55b444dbbfe90fa67659492"},
{file = "cbor2-5.6.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc29c068687aa2e7778f63b653f1346065b858427a2555df4dc2191f4a0de8ce"},
{file = "cbor2-5.6.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42eaf0f768bd27afcb38135d5bfc361d3a157f1f5c7dddcd8d391f7fa43d9de8"},
{file = "cbor2-5.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:8839b73befa010358477736680657b9d08c1ed935fd973decb1909712a41afdc"},
{file = "cbor2-5.6.2-py3-none-any.whl", hash = "sha256:c0b53a65673550fde483724ff683753f49462d392d45d7b6576364b39e76e54c"},
{file = "cbor2-5.6.2.tar.gz", hash = "sha256:b7513c2dea8868991fad7ef8899890ebcf8b199b9b4461c3c11d7ad3aef4820d"},
]
[package.extras]
@ -993,43 +993,43 @@ toml = ["tomli"]
[[package]]
name = "cryptography"
version = "42.0.2"
version = "42.0.4"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
files = [
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"},
{file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"},
{file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"},
{file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"},
{file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"},
{file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"},
{file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"},
{file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"},
{file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"},
{file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"},
{file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"},
{file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"},
{file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"},
{file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"},
{file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"},
{file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18"},
{file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2"},
{file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1"},
{file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b"},
{file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1"},
{file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992"},
{file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885"},
{file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824"},
{file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b"},
{file = "cryptography-42.0.4-cp37-abi3-win32.whl", hash = "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925"},
{file = "cryptography-42.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923"},
{file = "cryptography-42.0.4-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7"},
{file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52"},
{file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a"},
{file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9"},
{file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764"},
{file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff"},
{file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257"},
{file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929"},
{file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0"},
{file = "cryptography-42.0.4-cp39-abi3-win32.whl", hash = "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129"},
{file = "cryptography-42.0.4-cp39-abi3-win_amd64.whl", hash = "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854"},
{file = "cryptography-42.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298"},
{file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88"},
{file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20"},
{file = "cryptography-42.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce"},
{file = "cryptography-42.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74"},
{file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd"},
{file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b"},
{file = "cryptography-42.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660"},
{file = "cryptography-42.0.4.tar.gz", hash = "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb"},
]
[package.dependencies]
@ -3934,13 +3934,13 @@ wsproto = ">=0.14"
[[package]]
name = "twilio"
version = "8.13.0"
version = "8.12.0"
description = "Twilio API client and TwiML generator"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "twilio-8.13.0-py2.py3-none-any.whl", hash = "sha256:f5396e355de11b80c6729bd286fdc0e12c9c0b025c465f16f090034a7ef88d3d"},
{file = "twilio-8.13.0.tar.gz", hash = "sha256:89f629fa280b51bc21cd58b35cf640f9bbf88efd3977c0c5ec6ea6821b9880cd"},
{file = "twilio-8.12.0-py2.py3-none-any.whl", hash = "sha256:ccdf78b634dff13fd50b33bafd254a9dc2fb492c6b06839683e73f09ec110ec6"},
{file = "twilio-8.12.0.tar.gz", hash = "sha256:28e3a743da18d5b298c9b9fb9e922404a60f8091441c5e0aa35bfefc46411370"},
]
[package.dependencies]

View File

@ -113,7 +113,7 @@ filterwarnings = [
[tool.poetry]
name = "authentik"
version = "2023.10.7"
version = "2024.2.4"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]

View File

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

View File

@ -6,16 +6,16 @@
"": {
"name": "@goauthentik/web-tests",
"dependencies": {
"chromedriver": "^121.0.2"
"chromedriver": "^121.0.0"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"@wdio/cli": "^8.32.2",
"@wdio/local-runner": "^8.32.2",
"@wdio/mocha-framework": "^8.32.2",
"@wdio/spec-reporter": "^8.32.2",
"@wdio/cli": "^8.31.1",
"@wdio/local-runner": "^8.31.1",
"@wdio/mocha-framework": "^8.31.1",
"@wdio/spec-reporter": "^8.31.1",
"eslint": "^8.56.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.24.0",
@ -896,9 +896,9 @@
"devOptional": true
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.2.tgz",
"integrity": "sha512-lqa4UEhhv/2sjjIQgjX8B+RBjj47eo0mzGasklVJ78UKGQY1r0VpB9XHDaZZO9qzEFDdy4MrXLuEaSmPrPSe/A==",
"dev": true
},
"node_modules/@types/semver": {
@ -1187,19 +1187,19 @@
}
},
"node_modules/@wdio/cli": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.32.2.tgz",
"integrity": "sha512-CbTALXXnDzvthu+EK0dK5QDTXToU4wNrldtonQcsD8tT726O56BLFGm9tzcGP6PZWh9NxAuvsFlWSwUxcqXq0Q==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.31.1.tgz",
"integrity": "sha512-UnAoXjUrgRTfFq7TSnnMSuA80V8G7yW/d5zo59RtzrHdrGr6QVWJfnt6aLueXJJ6SnouVA6yU2rcsXPOvGpUIA==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.1",
"@vitest/snapshot": "^1.2.1",
"@wdio/config": "8.32.2",
"@wdio/globals": "8.32.2",
"@wdio/config": "8.31.1",
"@wdio/globals": "8.31.1",
"@wdio/logger": "8.28.0",
"@wdio/protocols": "8.32.0",
"@wdio/types": "8.32.2",
"@wdio/utils": "8.32.2",
"@wdio/protocols": "8.29.7",
"@wdio/types": "8.31.1",
"@wdio/utils": "8.31.1",
"async-exit-hook": "^2.0.1",
"chalk": "^5.2.0",
"chokidar": "^3.5.3",
@ -1208,13 +1208,13 @@
"ejs": "^3.1.9",
"execa": "^8.0.1",
"import-meta-resolve": "^4.0.0",
"inquirer": "9.2.12",
"inquirer": "9.2.14",
"lodash.flattendeep": "^4.4.0",
"lodash.pickby": "^4.6.0",
"lodash.union": "^4.6.0",
"read-pkg-up": "10.0.0",
"read-pkg-up": "^10.0.0",
"recursive-readdir": "^2.2.3",
"webdriverio": "8.32.2",
"webdriverio": "8.31.1",
"yargs": "^17.7.2"
},
"bin": {
@ -1237,14 +1237,14 @@
}
},
"node_modules/@wdio/config": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.32.2.tgz",
"integrity": "sha512-ubqe4X+TgcERzXKIpMfisquNxPZNtRU5uPeV7hvas++mD75QyNpmWHCtea2+TjoXKxlZd1MVrtZAwtmqMmyhPw==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.31.1.tgz",
"integrity": "sha512-Iz4DTXQdy53VT8LRZ6ayaDKE+zEDk4QY/ILz+D0IQh0OaMWruFesfoxqFP0hnU6rbJT1YE4ehTGf7JTZLWIPcw==",
"dev": true,
"dependencies": {
"@wdio/logger": "8.28.0",
"@wdio/types": "8.32.2",
"@wdio/utils": "8.32.2",
"@wdio/types": "8.31.1",
"@wdio/utils": "8.31.1",
"decamelize": "^6.0.0",
"deepmerge-ts": "^5.0.0",
"glob": "^10.2.2",
@ -1255,29 +1255,29 @@
}
},
"node_modules/@wdio/globals": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.32.2.tgz",
"integrity": "sha512-WEN7Tq0+Ny8OHS+7e1RCu4ss3lYG2Ln8/TpicacTsYWM3jtrf1SkUT+4H8JtG1qywj4WXTH8CxD4H9zFoofiJw==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.31.1.tgz",
"integrity": "sha512-2r80BX8aS5YiQ6cFuxtt44g2Y5P01MoHJTR3w21suSyiVoH70mxvJf6vJsLB+jtGfaXLJzOjlZkgXrd+Kn+keA==",
"dev": true,
"engines": {
"node": "^16.13 || >=18"
},
"optionalDependencies": {
"expect-webdriverio": "^4.11.2",
"webdriverio": "8.32.2"
"webdriverio": "8.31.1"
}
},
"node_modules/@wdio/local-runner": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.32.2.tgz",
"integrity": "sha512-iCVnwBIIwgRqiF2Fz/XVlGf3ejHOzR2+kc3dsdMgKljLkVq4ZDFwPMtw8Hk06sq+BaiNLamdxj+8ElD6OGJ88A==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.31.1.tgz",
"integrity": "sha512-KaMok/LaVvWcXTTi61Al5+XgocBDJ2+gpG1mjxytILrwaMFohYL0YuaGDw4yb3lx3Lsej6K+/FbaWMMnGq3JIw==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/logger": "8.28.0",
"@wdio/repl": "8.24.12",
"@wdio/runner": "8.32.2",
"@wdio/types": "8.32.2",
"@wdio/runner": "8.31.1",
"@wdio/types": "8.31.1",
"async-exit-hook": "^2.0.1",
"split2": "^4.1.0",
"stream-buffers": "^3.0.2"
@ -1314,16 +1314,16 @@
}
},
"node_modules/@wdio/mocha-framework": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.32.2.tgz",
"integrity": "sha512-76DA95u0Z2AfyZSQglJn9BFJ+XveRR6+oH1K/J8rDOWoIHgMcASGEj+fsXAPSDNcSVDBe0QN5HLnVi3tciuyQw==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.31.1.tgz",
"integrity": "sha512-5297tKj9zNvzZD+X4tMSuTcJrSaQ6mmDGLEdapa/+CMA549N+0vE38tNh+5Br7dmFXXVanw40T752OQcSeFf1A==",
"dev": true,
"dependencies": {
"@types/mocha": "^10.0.0",
"@types/node": "^20.1.0",
"@wdio/logger": "8.28.0",
"@wdio/types": "8.32.2",
"@wdio/utils": "8.32.2",
"@wdio/types": "8.31.1",
"@wdio/utils": "8.31.1",
"mocha": "^10.0.0"
},
"engines": {
@ -1331,9 +1331,9 @@
}
},
"node_modules/@wdio/protocols": {
"version": "8.32.0",
"resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.32.0.tgz",
"integrity": "sha512-inLJRrtIGdTz/YPbcsvpSvPlYQFTVtF3OYBwAXhG2FiP1ZwE1CQNLP/xgRGye1ymdGCypGkexRqIx3KBGm801Q==",
"version": "8.29.7",
"resolved": "https://registry.npmjs.org/@wdio/protocols/-/protocols-8.29.7.tgz",
"integrity": "sha512-9hhEePMLmI8fm9F2v4jlg9x4w4jEoZmY3vT6fXy90ne1DFaGWfy/a853nKEagQe/ZzxkN3/cpMBh8mryv9BVjw==",
"dev": true
},
"node_modules/@wdio/repl": {
@ -1349,14 +1349,14 @@
}
},
"node_modules/@wdio/reporter": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.32.2.tgz",
"integrity": "sha512-BZdReAFfRCtgtYbyhkKgSGqqoIn/yG5/Z4COjvRvon9NJkz+eA4PiHCKdEP7+ekBIjud7H8Gy+6mPBDEu+wllw==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.31.1.tgz",
"integrity": "sha512-ayZipzyr9dSwpbKYbV4PoXuw91A1H7fjadJ5R5oMYUETx+pfBJqjR2UIHZhhPAa0lJ7bWHEn7eCsGB1PXp47Og==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/logger": "8.28.0",
"@wdio/types": "8.32.2",
"@wdio/types": "8.31.1",
"diff": "^5.0.0",
"object-inspect": "^1.12.0"
},
@ -1365,35 +1365,35 @@
}
},
"node_modules/@wdio/runner": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.32.2.tgz",
"integrity": "sha512-/24wPBg2okkfpA1bl7mCWIvWXO3pa9OhsKNav+gGm7BP+hQ1lxULyYg/o5fCEwEjFPWDLy0jjknJLqXcTWvmiQ==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.31.1.tgz",
"integrity": "sha512-9KUDaAHNUeBp5h1YoEmKVk0hZyzBDl6x0ge1dnaORACKKi6TXa76T0kLaBruuBTBn4zZd4+Xrg9/bLGAnTFvLA==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/config": "8.32.2",
"@wdio/globals": "8.32.2",
"@wdio/config": "8.31.1",
"@wdio/globals": "8.31.1",
"@wdio/logger": "8.28.0",
"@wdio/types": "8.32.2",
"@wdio/utils": "8.32.2",
"@wdio/types": "8.31.1",
"@wdio/utils": "8.31.1",
"deepmerge-ts": "^5.0.0",
"expect-webdriverio": "^4.11.2",
"gaze": "^1.1.2",
"webdriver": "8.32.2",
"webdriverio": "8.32.2"
"webdriver": "8.31.1",
"webdriverio": "8.31.1"
},
"engines": {
"node": "^16.13 || >=18"
}
},
"node_modules/@wdio/spec-reporter": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.32.2.tgz",
"integrity": "sha512-3hUXpE+idC4KOXofJnpubdDDCE8X0OTd6ykypiaXMI2hJTA2nIZcHtpRQnAE0E4JT9OzLnPWODcMq54GGBDRkg==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.31.1.tgz",
"integrity": "sha512-t2isqf/yDvc3xfNnkuR8XdRaKf34I6/40f1DHfojHZUpTF94jrt7CLACkFiDZhu5sz0KCuDWzy56aycd6IUz3w==",
"dev": true,
"dependencies": {
"@wdio/reporter": "8.32.2",
"@wdio/types": "8.32.2",
"@wdio/reporter": "8.31.1",
"@wdio/types": "8.31.1",
"chalk": "^5.1.2",
"easy-table": "^1.2.0",
"pretty-ms": "^7.0.0"
@ -1415,9 +1415,9 @@
}
},
"node_modules/@wdio/types": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.32.2.tgz",
"integrity": "sha512-jq8LcBBQpBP9ZF5kECKEpXv8hN7irCGCjLFAN0Bd5ScRR6qu6MGWvwkDkau2sFPr0b++sKDCEaMzQlwrGFjZXg==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.31.1.tgz",
"integrity": "sha512-KQ0EmjeVdshufhsxygaPzkJ8WD7hm8WlflZcLwKMZ0OM6f8pV9NMGGOvfBQXgTs447ScK6/6rX+lbJk3yvg65g==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0"
@ -1427,14 +1427,14 @@
}
},
"node_modules/@wdio/utils": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.32.2.tgz",
"integrity": "sha512-PJcP4d1Fr8Zp+YIfGN93G0fjDj/6J0I6Gf6p0IpJk8qKQpdFDm4gB+lc202iv2YkyC+oT6b4Ik2W9LzvpSKNoQ==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.31.1.tgz",
"integrity": "sha512-fGUtNeJYSqPLMqIRrooEg1ViM2+z1Izd/7bzWzhg8EQHKFXqD/G68rEwBWpoLF/ziiHZFe4fJk7SZdXUK/gFgQ==",
"dev": true,
"dependencies": {
"@puppeteer/browsers": "^1.6.0",
"@wdio/logger": "8.28.0",
"@wdio/types": "8.32.2",
"@wdio/types": "8.31.1",
"decamelize": "^6.0.0",
"deepmerge-ts": "^5.1.0",
"edgedriver": "^5.3.5",
@ -2064,9 +2064,9 @@
}
},
"node_modules/chromedriver": {
"version": "121.0.2",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.2.tgz",
"integrity": "sha512-58MUSCEE3oB3G3Y/Jo3URJ2Oa1VLHcVBufyYt7vNfGrABSJm7ienQLF9IQ8LPDlPVgLUXt2OBfggK3p2/SlEBg==",
"version": "121.0.0",
"resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-121.0.0.tgz",
"integrity": "sha512-ZIKEdZrQAfuzT/RRofjl8/EZR99ghbdBXNTOcgJMKGP6N/UL6lHUX4n6ONWBV18pDvDFfQJ0x58h5AdOaXIOMw==",
"hasInstallScript": true,
"dependencies": {
"@testim/chrome-version": "^1.1.4",
@ -2589,15 +2589,15 @@
}
},
"node_modules/devtools-protocol": {
"version": "0.0.1261483",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1261483.tgz",
"integrity": "sha512-7vJvejpzA5DTfZVkr7a8sGpEAzEiAqcgmRTB0LSUrWeOicwL09lMQTzxHtFNVhJ1OOJkgYdH6Txvy9E5j3VOUQ==",
"version": "0.0.1255431",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1255431.tgz",
"integrity": "sha512-VuKgO1U4Ew4meKKoXCEBMUNkzyQqci5F8HIuoELPJkr5yvk9kR9p07gaZfzG9QIIrcIfpJVgf6Ms8OqEMxEYgA==",
"dev": true
},
"node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
"integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
"dev": true,
"engines": {
"node": ">=0.3.1"
@ -3445,40 +3445,15 @@
}
},
"node_modules/figures": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz",
"integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
"integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
"dev": true,
"dependencies": {
"escape-string-regexp": "^5.0.0",
"is-unicode-supported": "^1.2.0"
"escape-string-regexp": "^1.0.5"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/figures/node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/figures/node_modules/is-unicode-supported": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
"dev": true,
"engines": {
"node": ">=12"
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
@ -4387,18 +4362,18 @@
"dev": true
},
"node_modules/inquirer": {
"version": "9.2.12",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz",
"integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==",
"version": "9.2.14",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.14.tgz",
"integrity": "sha512-4ByIMt677Iz5AvjyKrDpzaepIyMewNvDcvwpVVRZNmy9dLakVoVgdCHZXbK1SlVJra1db0JZ6XkJyHsanpdrdQ==",
"dev": true,
"dependencies": {
"@ljharb/through": "^2.3.11",
"@ljharb/through": "^2.3.12",
"ansi-escapes": "^4.3.2",
"chalk": "^5.3.0",
"cli-cursor": "^3.1.0",
"cli-width": "^4.1.0",
"external-editor": "^3.1.0",
"figures": "^5.0.0",
"figures": "^3.2.0",
"lodash": "^4.17.21",
"mute-stream": "1.0.0",
"ora": "^5.4.1",
@ -4409,7 +4384,7 @@
"wrap-ansi": "^6.2.0"
},
"engines": {
"node": ">=14.18.0"
"node": ">=18"
}
},
"node_modules/inquirer/node_modules/chalk": {
@ -5313,9 +5288,9 @@
"dev": true
},
"node_modules/json-parse-even-better-errors": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz",
"integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz",
"integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==",
"dev": true,
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
@ -5425,9 +5400,9 @@
}
},
"node_modules/lines-and-columns": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz",
"integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz",
"integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==",
"dev": true,
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
@ -7052,14 +7027,14 @@
}
},
"node_modules/read-pkg-up": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.0.0.tgz",
"integrity": "sha512-jgmKiS//w2Zs+YbX039CorlkOp8FIVbSAN8r8GJHDsGlmNPXo+VeHkqAwCiQVTTx5/LwLZTcEw59z3DvcLbr0g==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz",
"integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==",
"dev": true,
"dependencies": {
"find-up": "^6.3.0",
"read-pkg": "^8.0.0",
"type-fest": "^3.12.0"
"read-pkg": "^8.1.0",
"type-fest": "^4.2.0"
},
"engines": {
"node": ">=16"
@ -7157,9 +7132,9 @@
}
},
"node_modules/read-pkg-up/node_modules/parse-json": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz",
"integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.0.tgz",
"integrity": "sha512-ihtdrgbqdONYD156Ap6qTcaGcGdkdAxodO1wLqQ/j7HP1u2sFYppINiq4jyC8F+Nm+4fVufylCV00QmkTHkSUg==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.21.4",
@ -7175,6 +7150,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/read-pkg-up/node_modules/parse-json/node_modules/type-fest": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
"integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
"dev": true,
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/read-pkg-up/node_modules/path-exists": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz",
@ -7202,10 +7189,10 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/read-pkg-up/node_modules/read-pkg/node_modules/type-fest": {
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.2.tgz",
"integrity": "sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==",
"node_modules/read-pkg-up/node_modules/type-fest": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.3.1.tgz",
"integrity": "sha512-pphNW/msgOUSkJbH58x8sqpq8uQj6b0ZKGxEsLKMUnGorRcDjrUaLS+39+/ub41JNTwrrMyJcUB8+YZs3mbwqw==",
"dev": true,
"engines": {
"node": ">=16"
@ -7214,18 +7201,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/read-pkg-up/node_modules/type-fest": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
"integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
"dev": true,
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/read-pkg-up/node_modules/yocto-queue": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz",
@ -8773,18 +8748,18 @@
}
},
"node_modules/webdriver": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.32.2.tgz",
"integrity": "sha512-uyCT2QzCqoz+EsMLTApG5/+RvHJR9MVbdEnjMoxpJDt+IeZCG2Vy/Gq9oNgNQfpxrvZme/EY+PtBsltZi7BAyg==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.31.1.tgz",
"integrity": "sha512-J1Ata+ZiBVhCFKL7hnD6qCfr7ZRsBN2c/YlCgosq0lG/iYMKXWi5rlWDfpuyISprM/G/V3GjfEGxTUC6jJBSBA==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@types/ws": "^8.5.3",
"@wdio/config": "8.32.2",
"@wdio/config": "8.31.1",
"@wdio/logger": "8.28.0",
"@wdio/protocols": "8.32.0",
"@wdio/types": "8.32.2",
"@wdio/utils": "8.32.2",
"@wdio/protocols": "8.29.7",
"@wdio/types": "8.31.1",
"@wdio/utils": "8.31.1",
"deepmerge-ts": "^5.1.0",
"got": "^12.6.1",
"ky": "^0.33.0",
@ -8795,23 +8770,23 @@
}
},
"node_modules/webdriverio": {
"version": "8.32.2",
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.32.2.tgz",
"integrity": "sha512-Z0Wc/dHFfWGWJZpaQ8u910/LG0E9EIVTO7J5yjqWx2XtXz2LzQMxYwNRnvNLhY/1tI4y/cZxI6kFMWr8wD2TtA==",
"version": "8.31.1",
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.31.1.tgz",
"integrity": "sha512-b3bLBkkSGESGcRw3s3Sty84luZe2+qwPudXosSXbzcRu2Z1sccjdA6BHJA36IcLgKndNCOhf9wx3yQ3umoS7Jw==",
"dev": true,
"dependencies": {
"@types/node": "^20.1.0",
"@wdio/config": "8.32.2",
"@wdio/config": "8.31.1",
"@wdio/logger": "8.28.0",
"@wdio/protocols": "8.32.0",
"@wdio/protocols": "8.29.7",
"@wdio/repl": "8.24.12",
"@wdio/types": "8.32.2",
"@wdio/utils": "8.32.2",
"@wdio/types": "8.31.1",
"@wdio/utils": "8.31.1",
"archiver": "^6.0.0",
"aria-query": "^5.0.0",
"css-shorthand-properties": "^1.1.1",
"css-value": "^0.0.1",
"devtools-protocol": "^0.0.1261483",
"devtools-protocol": "^0.0.1255431",
"grapheme-splitter": "^1.0.2",
"import-meta-resolve": "^4.0.0",
"is-plain-obj": "^4.1.0",
@ -8823,7 +8798,7 @@
"resq": "^1.9.1",
"rgb2hex": "0.2.5",
"serialize-error": "^11.0.1",
"webdriver": "8.32.2"
"webdriver": "8.31.1"
},
"engines": {
"node": "^16.13 || >=18"

View File

@ -6,10 +6,10 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"@wdio/cli": "^8.32.2",
"@wdio/local-runner": "^8.32.2",
"@wdio/mocha-framework": "^8.32.2",
"@wdio/spec-reporter": "^8.32.2",
"@wdio/cli": "^8.31.1",
"@wdio/local-runner": "^8.31.1",
"@wdio/mocha-framework": "^8.31.1",
"@wdio/spec-reporter": "^8.31.1",
"eslint": "^8.56.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.24.0",
@ -32,6 +32,6 @@
"node": ">=20"
},
"dependencies": {
"chromedriver": "^121.0.2"
"chromedriver": "^121.0.0"
}
}

View File

@ -1,33 +0,0 @@
module.exports = {
// Basic syntax: just run these tasks all the packages. There is only one package at the moment.
pipeline: {
"extract-locales": ["^extract-locales"],
"build-locales": ["^build-locales"],
"build-locales:build": ["^build-locales:build"],
"build-locales:repair": ["^build-locales:repair"],
"rollup:build": ["^rollup:build"],
"rollup:build-proxy": ["^rollup:build-proxy"],
"rollup:watch": ["^rollup:watch"],
"build": ["^build"],
"build-proxy": ["^build-proxy"],
"watch": ["^watch"],
"lint": ["^lint"],
"lint:precommit": ["^lint:precommit"],
"lint:spelling": ["^lint:spelling"],
"lit-analyse": ["^lit-analyse"],
"precommit": ["^precommit"],
"prequick": ["^prequick"],
"prettier-check": ["^prettier-check"],
"prettier": ["^prettier"],
"pseudolocalize:build-extract-script": ["^pseudolocalize:build-extract-script"],
"pseudolocalize:extract": ["^pseudolocalize:extract"],
"pseudolocalize": ["^pseudolocalize"],
"tsc:execute": ["^tsc:execute"],
"tsc": ["^tsc"],
"storybook": ["^storybook"],
"storybook:build": ["^storybook:build"],
"storybook:build-import-map": ["^storybook:build-import-map"],
"storybook:build-import-map-script": ["^storybook:build-import-map-script"],
"storybook:run-import-map-script": ["^storybook:run-import-map-script"],
},
};

1887
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,38 +4,132 @@
"private": true,
"license": "MIT",
"scripts": {
"extract-locales": "lage extract-locales",
"build-locales": "lage build-locales",
"build-locales:build": "lage build-locales:build",
"build-locales:repair": "lage build-locales:repair",
"rollup:build": "lage rollup:build",
"rollup:build-proxy": "lage rollup:build-proxy",
"rollup:watch": "lage rollup:watch",
"build": "lage build",
"build-proxy": "lage build-proxy",
"watch": "lage watch",
"lint": "lage lint",
"lint:precommit": "lage lint:precommit",
"lint:spelling": "lage lint:spelling",
"lit-analyse": "lage lit-analyse",
"precommit": "lage precommit",
"prequick": "lage prequick",
"prettier-check": "lage prettier-check",
"prettier": "lage prettier",
"pseudolocalize:build-extract-script": "lage pseudolocalize:build-extract-script",
"pseudolocalize:extract": "lage pseudolocalize:extract",
"pseudolocalize": "lage pseudolocalize",
"tsc:execute": "lage tsc:execute",
"tsc": "lage tsc",
"storybook": "lage storybook",
"storybook:build": "lage storybook:build",
"storybook:build-import-map": "lage storybook:build-import-map",
"storybook:build-import-map-script": "lage storybook:build-import-map-script",
"storybook:run-import-map-script": "lage storybook:run-import-map-script"
"extract-locales": "lit-localize extract",
"build-locales": "run-s build-locales:build",
"build-locales:build": "lit-localize build",
"build-locales:repair": "prettier --write ./src/locale-codes.ts",
"rollup:build": "cross-env NODE_OPTIONS='--max_old_space_size=8192' rollup -c ./rollup.config.mjs",
"rollup:build-proxy": "cross-env NODE_OPTIONS='--max_old_space_size=8192' rollup -c ./rollup.proxy.mjs",
"rollup:watch": "cross-env NODE_OPTIONS='--max_old_space_size=8192' rollup -c -w",
"build": "run-s build-locales rollup:build",
"build-proxy": "run-s build-locales rollup:build-proxy",
"watch": "run-s build-locales rollup:watch",
"lint": "eslint . --max-warnings 0 --fix",
"lint:precommit": "eslint --max-warnings 0 --config ./.eslintrc.precommit.json $(git status --porcelain . | grep '^[M?][M?]' | cut -c8- | grep -E '\\.(ts|js|tsx|jsx)$') ",
"lint:spelling": "codespell -D - -D ../.github/codespell-dictionary.txt -I ../.github/codespell-words.txt -S './src/locales/**' ./src -s",
"lit-analyse": "lit-analyzer src",
"precommit": "run-s tsc lit-analyse lint:precommit lint:spelling prettier",
"prequick": "run-s tsc:execute lit-analyse lint:precommit lint:spelling",
"prettier-check": "prettier --check .",
"prettier": "prettier --write .",
"pseudolocalize:build-extract-script": "cd scripts && tsc --esModuleInterop --module es2020 --moduleResolution 'node' pseudolocalize.ts && mv pseudolocalize.js pseudolocalize.mjs",
"pseudolocalize:extract": "node scripts/pseudolocalize.mjs",
"pseudolocalize": "run-s pseudolocalize:build-extract-script pseudolocalize:extract",
"tsc:execute": "tsc --noEmit -p .",
"tsc": "run-s build-locales tsc:execute",
"storybook": "storybook dev -p 6006",
"storybook:build": "cross-env NODE_OPTIONS='--max_old_space_size=8192' storybook build",
"storybook:build-import-map": "run-s storybook:build-import-map-script storybook:run-import-map-script",
"storybook:build-import-map-script": "cd scripts && tsc --esModuleInterop --module es2020 --target es2020 --moduleResolution 'node' build-storybook-import-maps.ts && mv build-storybook-import-maps.js build-storybook-import-maps.mjs",
"storybook:run-import-map-script": "node scripts/build-storybook-import-maps.mjs"
},
"dependencies": {
"@manypkg/cli": "^0.21.1",
"lage": "^2.7.9"
"@codemirror/lang-html": "^6.4.8",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-python": "^6.1.4",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.5",
"@fortawesome/fontawesome-free": "^6.5.1",
"@goauthentik/api": "^2023.10.7-1707933453",
"@lit-labs/context": "^0.4.0",
"@lit-labs/task": "^3.1.0",
"@lit/localize": "^0.11.4",
"@open-wc/lit-helpers": "^0.6.0",
"@patternfly/elements": "^2.4.0",
"@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.101.0",
"@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1",
"chart.js": "^4.4.1",
"chartjs-adapter-moment": "^1.0.1",
"codemirror": "^6.0.1",
"construct-style-sheets-polyfill": "^3.1.0",
"core-js": "^3.35.1",
"country-flag-icons": "^1.5.9",
"fuse.js": "^7.0.0",
"guacamole-common-js": "^1.5.0",
"lit": "^2.8.0",
"mermaid": "^10.8.0",
"rapidoc": "^9.3.4",
"style-mod": "^4.1.0",
"webcomponent-qr-code": "^1.2.0",
"yaml": "^2.3.4"
},
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-proposal-decorators": "^7.23.9",
"@babel/plugin-transform-private-methods": "^7.23.3",
"@babel/plugin-transform-private-property-in-object": "^7.23.4",
"@babel/plugin-transform-runtime": "^7.23.9",
"@babel/preset-env": "^7.23.9",
"@babel/preset-typescript": "^7.23.3",
"@hcaptcha/types": "^1.0.3",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@jeysal/storybook-addon-css-user-preferences": "^0.2.0",
"@lit/localize-tools": "^0.7.2",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.5",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@spotlightjs/spotlight": "^1.2.12",
"@storybook/addon-essentials": "^7.6.15",
"@storybook/addon-links": "^7.6.15",
"@storybook/api": "^7.6.15",
"@storybook/blocks": "^7.6.4",
"@storybook/manager-api": "^7.6.15",
"@storybook/web-components": "^7.6.15",
"@storybook/web-components-vite": "^7.6.15",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "5.60.15",
"@types/grecaptcha": "^3.0.7",
"@types/guacamole-common-js": "1.5.2",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-tsconfig-paths": "^1.0.3",
"cross-env": "^7.0.3",
"eslint": "^8.56.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.8",
"eslint-plugin-lit": "^1.11.0",
"eslint-plugin-sonarjs": "^0.24.0",
"eslint-plugin-storybook": "^0.6.15",
"github-slugger": "^2.0.0",
"lit-analyzer": "^2.0.3",
"npm-run-all": "^4.1.5",
"prettier": "^3.2.5",
"pseudolocale": "^2.0.0",
"pyright": "=1.1.338",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rollup": "^4.10.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-cssimport": "^1.0.3",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^7.6.15",
"storybook-addon-mock": "^4.3.0",
"ts-lit-plugin": "^2.0.2",
"tslib": "^2.6.2",
"turnstile-types": "^1.2.0",
"typescript": "^5.3.3",
"vite-tsconfig-paths": "^4.3.1"
},
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.20.0",
@ -44,8 +138,5 @@
},
"engines": {
"node": ">=20"
},
"workspaces": [
"packages/authentik"
]
}
}

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