Compare commits

...

57 Commits

Author SHA1 Message Date
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
128 changed files with 1089 additions and 880 deletions

View File

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

View File

@ -11,6 +11,10 @@ inputs:
description: "Docker image arch" description: "Docker image arch"
outputs: outputs:
shouldBuild:
description: "Whether to build image or not"
value: ${{ steps.ev.outputs.shouldBuild }}
sha: sha:
description: "sha" description: "sha"
value: ${{ steps.ev.outputs.sha }} value: ${{ steps.ev.outputs.sha }}
@ -34,60 +38,10 @@ runs:
steps: steps:
- name: Generate config - name: Generate config
id: ev 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: | run: |
"""Helper script to get the actual branch name, docker safe""" python3 ${{ github.action_path }}/push_vars.py
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}",
f"{name}:{version_family}",
]
if not prerelease:
image_tags += [f"{name}:latest"]
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}",
f"{name}:gh-{safe_branch_name}{suffix}",
]
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)

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

@ -70,7 +70,7 @@ jobs:
cp authentik/lib/default.yml local.env.yml cp authentik/lib/default.yml local.env.yml
cp -R .github .. cp -R .github ..
cp -R scripts .. cp -R scripts ..
git checkout version/$(python -c "from authentik import __version__; print(__version__)") git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
rm -rf .github/ scripts/ rm -rf .github/ scripts/
mv ../.github ../scripts . mv ../.github ../scripts .
- name: Setup authentik env (stable) - name: Setup authentik env (stable)
@ -219,7 +219,6 @@ jobs:
# Needed to upload contianer images to ghcr.io # Needed to upload contianer images to ghcr.io
packages: write packages: write
timeout-minutes: 120 timeout-minutes: 120
if: "github.repository == 'goauthentik/authentik'"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -231,10 +230,13 @@ jobs:
- name: prepare variables - name: prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with: with:
image-name: ghcr.io/goauthentik/dev-server image-name: ghcr.io/goauthentik/dev-server
image-arch: ${{ matrix.arch }} image-arch: ${{ matrix.arch }}
- name: Login to Container Registry - name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
@ -250,7 +252,7 @@ jobs:
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }} GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }} GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
tags: ${{ steps.ev.outputs.imageTags }} tags: ${{ steps.ev.outputs.imageTags }}
push: true push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
build-args: | build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }} GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
cache-from: type=gha cache-from: type=gha
@ -272,6 +274,8 @@ jobs:
- name: prepare variables - name: prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with: with:
image-name: ghcr.io/goauthentik/dev-server image-name: ghcr.io/goauthentik/dev-server
- name: Comment on PR - name: Comment on PR

View File

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

View File

@ -20,6 +20,8 @@ jobs:
- name: prepare variables - name: prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with: with:
image-name: ghcr.io/goauthentik/server,beryju/authentik image-name: ghcr.io/goauthentik/server,beryju/authentik
- name: Docker Login Registry - name: Docker Login Registry
@ -72,6 +74,8 @@ jobs:
- name: prepare variables - name: prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with: with:
image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }} image-name: ghcr.io/goauthentik/${{ matrix.type }},beryju/authentik-${{ matrix.type }}
- name: make empty clients - name: make empty clients
@ -168,12 +172,14 @@ jobs:
- name: prepare variables - name: prepare variables
uses: ./.github/actions/docker-push-variables uses: ./.github/actions/docker-push-variables
id: ev id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
with: with:
image-name: ghcr.io/goauthentik/server image-name: ghcr.io/goauthentik/server
- name: Get static files from docker image - name: Get static files from docker image
run: | run: |
docker pull ghcr.io/goauthentik/server:${{ steps.ev.outputs.imageMainTag }} docker pull ${{ steps.ev.outputs.imageMainTag }}
container=$(docker container create ghcr.io/goauthentik/server:${{ steps.ev.outputs.imageMainTag }}) container=$(docker container create ${{ steps.ev.outputs.imageMainTag }})
docker cp ${container}:web/ . docker cp ${container}:web/ .
- name: Create a Sentry.io release - name: Create a Sentry.io release
uses: getsentry/action-release@v1 uses: getsentry/action-release@v1

View File

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

View File

@ -103,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/pip \
--mount=type=cache,target=/root/.cache/pypoetry \ --mount=type=cache,target=/root/.cache/pypoetry \
python -m venv /ak-root/venv/ && \ python -m venv /ak-root/venv/ && \
pip3 install --upgrade pip && \ bash -c "source ${VENV_PATH}/bin/activate && \
pip3 install poetry && \ pip3 install --upgrade pip && \
poetry install --only=main --no-ansi --no-interaction pip3 install poetry && \
poetry install --only=main --no-ansi --no-interaction --no-root"
# Stage 6: Run # Stage 6: Run
FROM docker.io/python:3.12.2-slim-bookworm AS final-image 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) UID = $(shell id -u)
GID = $(shell id -g) GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.npm_version) NPM_VERSION = $(shell python -m scripts.npm_version)
PY_SOURCES = authentik tests scripts lifecycle PY_SOURCES = authentik tests scripts lifecycle .github
DOCKER_IMAGE ?= "authentik:test" DOCKER_IMAGE ?= "authentik:test"
GEN_API_TS = "gen-ts-api" GEN_API_TS = "gen-ts-api"

View File

@ -3,7 +3,7 @@
from os import environ from os import environ
from typing import Optional from typing import Optional
__version__ = "2023.10.7" __version__ = "2024.2.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" 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""" """Get all capabilities this server instance supports"""
caps = [] caps = []
deb_test = settings.DEBUG or settings.TEST 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) caps.append(Capabilities.CAN_SAVE_MEDIA)
for processor in get_context_processors(): for processor in get_context_processors():
if cap := processor.capability(): 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.serializers import ListSerializer, ModelSerializer
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from authentik.api.decorators import permission_required
from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.importer import Importer
from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, PassiveSerializer from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.rbac.decorators import permission_required
class ManagedSerializer: class ManagedSerializer:

View File

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

View File

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

View File

@ -23,7 +23,6 @@ from structlog.stdlib import get_logger
from structlog.testing import capture_logs from structlog.testing import capture_logs
from authentik.admin.api.metrics import CoordinateSerializer 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.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.providers import ProviderSerializer from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin 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.api.exec import PolicyTestResultSerializer
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.types import PolicyResult from authentik.policies.types import PolicyResult
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger() 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.serializers import ListSerializer, ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet 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.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, PassiveSerializer from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.rbac.api.roles import RoleSerializer from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required
class GroupMemberSerializer(ModelSerializer): 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.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.blueprints.api import ManagedSerializer from authentik.blueprints.api import ManagedSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer 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.events.utils import sanitize_item
from authentik.lib.utils.reflection import all_subclasses from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.exec import PolicyTestSerializer from authentik.policies.api.exec import PolicyTestSerializer
from authentik.rbac.decorators import permission_required
class PropertyMappingTestResultSerializer(PassiveSerializer): class PropertyMappingTestResultSerializer(PassiveSerializer):

View File

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

View File

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

View File

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

View File

@ -24,13 +24,13 @@ from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.authorization import SecretKeyFilter from authentik.api.authorization import SecretKeyFilter
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.builder import CertificateBuilder from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.models import CertificateKeyPair from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -16,12 +16,12 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet 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.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User, UserTypes from authentik.core.models import User, UserTypes
from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
from authentik.enterprise.models import License from authentik.enterprise.models import License
from authentik.rbac.decorators import permission_required
from authentik.root.install_id import get_install_id from authentik.root.install_id import get_install_id
@ -31,7 +31,7 @@ class EnterpriseRequiredMixin:
def validate(self, attrs: dict) -> dict: def validate(self, attrs: dict) -> dict:
"""Check that a valid license exists""" """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.")) raise ValidationError(_("Enterprise is required to create/update this object."))
return super().validate(attrs) 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.db.models.signals import post_init
from django.http import HttpRequest from django.http import HttpRequest
from authentik.core.models import User
from authentik.events.middleware import AuditMiddleware, should_log_model from authentik.events.middleware import AuditMiddleware, should_log_model
from authentik.events.utils import cleanse_dict, sanitize_item from authentik.events.utils import cleanse_dict, sanitize_item
@ -28,13 +27,10 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
super().connect(request) super().connect(request)
if not self.enabled: if not self.enabled:
return return
user = getattr(request, "user", self.anonymous_user)
if not user.is_authenticated:
user = self.anonymous_user
if not hasattr(request, "request_id"): if not hasattr(request, "request_id"):
return return
post_init.connect( post_init.connect(
partial(self.post_init_handler, user=user, request=request), partial(self.post_init_handler, request=request),
dispatch_uid=request.request_id, dispatch_uid=request.request_id,
weak=False, weak=False,
) )
@ -76,7 +72,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
diff[key] = {"previous_value": value, "new_value": after.get(key)} diff[key] = {"previous_value": value, "new_value": after.get(key)}
return sanitize_item(diff) 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""" """post_init django model handler"""
if not should_log_model(instance): if not should_log_model(instance):
return return
@ -91,7 +87,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def post_save_handler( def post_save_handler(
self, self,
user: User,
request: HttpRequest, request: HttpRequest,
sender, sender,
instance: Model, instance: Model,
@ -113,6 +108,4 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
for field_set in ignored_field_sets: for field_set in ignored_field_sets:
if set(diff.keys()) == set(field_set): if set(diff.keys()) == set(field_set):
return None return None
return super().post_save_handler( return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)
user, request, sender, instance, created, thread_kwargs, **_
)

View File

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

View File

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

View File

@ -2,11 +2,14 @@
from datetime import datetime 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.dispatch import receiver
from django.utils.timezone import get_current_timezone 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.models import License
from authentik.enterprise.tasks import enterprise_update_usage
@receiver(pre_save, sender=License) @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.internal_users = status.internal_users
instance.external_users = status.external_users instance.external_users = status.external_users
instance.expiry = datetime.fromtimestamp(status.exp, tz=get_current_timezone()) 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.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet 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.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import ( from authentik.events.models import (
@ -24,6 +23,7 @@ from authentik.events.models import (
TransportMode, TransportMode,
) )
from authentik.events.utils import get_user from authentik.events.utils import get_user
from authentik.rbac.decorators import permission_required
class NotificationTransportSerializer(ModelSerializer): class NotificationTransportSerializer(ModelSerializer):

View File

@ -21,8 +21,8 @@ from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.events.models import SystemTask, TaskStatus from authentik.events.models import SystemTask, TaskStatus
from authentik.rbac.decorators import permission_required
LOGGER = get_logger() LOGGER = get_logger()
@ -81,7 +81,7 @@ class SystemTaskViewSet(ReadOnlyModelViewSet):
500: OpenApiResponse(description="Failed to retry task"), 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: def run(self, request: Request, pk=None) -> Response:
"""Run task""" """Run task"""
task: SystemTask = self.get_object() task: SystemTask = self.get_object()

View File

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

View File

@ -451,6 +451,13 @@ class NotificationTransport(SerializerModel):
def send_email(self, notification: "Notification") -> list[str]: def send_email(self, notification: "Notification") -> list[str]:
"""Send notification via global email configuration""" """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: " subject_prefix = "authentik Notification: "
context = { context = {
"key_value": { "key_value": {
@ -480,7 +487,7 @@ class NotificationTransport(SerializerModel):
} }
mail = TemplateEmailMessage( mail = TemplateEmailMessage(
subject=subject_prefix + context["title"], subject=subject_prefix + context["title"],
to=[f"{notification.user.name} <{notification.user.email}>"], to=[(notification.user.name, notification.user.email)],
language=notification.user.locale(), language=notification.user.locale(),
template_name="email/event_notification.html", template_name="email/event_notification.html",
template_context=context, template_context=context,

View File

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

View File

@ -3,9 +3,10 @@
from django.urls import reverse from django.urls import reverse
from rest_framework.test import APITestCase 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.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
class TestEventsMiddleware(APITestCase): class TestEventsMiddleware(APITestCase):
@ -47,3 +48,30 @@ class TestEventsMiddleware(APITestCase):
context__model__name="test-delete", context__model__name="test-delete",
).exists() ).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 rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.blueprints.v1.exporter import FlowExporter from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
@ -33,6 +32,7 @@ from authentik.lib.utils.file import (
set_file_url, set_file_url,
) )
from authentik.lib.views import bad_request_message from authentik.lib.views import bad_request_message
from authentik.rbac.decorators import permission_required
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -1,6 +1,7 @@
"""flow views tests""" """flow views tests"""
from unittest.mock import MagicMock, PropertyMock, patch from unittest.mock import MagicMock, PropertyMock, patch
from urllib.parse import urlencode
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.test.client import RequestFactory 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.planner import FlowPlan, FlowPlanner
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
from authentik.flows.tests import FlowTestCase 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.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
@ -121,16 +127,73 @@ class TestFlowExecutor(FlowTestCase):
TO_STAGE_RESPONSE_MOCK, TO_STAGE_RESPONSE_MOCK,
) )
def test_invalid_flow_redirect(self): def test_invalid_flow_redirect(self):
"""Tests that an invalid flow still redirects""" """Test invalid flow with valid redirect destination"""
flow = create_test_flow( flow = create_test_flow(
FlowDesignation.AUTHENTICATION, FlowDesignation.AUTHENTICATION,
) )
dest = "/unique-string" dest = "/unique-string"
url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}) 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.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( @patch(
"authentik.flows.views.executor.to_stage_response", "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.template.response import TemplateResponse
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator 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.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import View from django.views.generic import View
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
@ -178,6 +179,8 @@ class FlowExecutorView(APIView):
self.cancel() self.cancel()
self._logger.debug("f(exec): Continuing existing plan") self._logger.debug("f(exec): Continuing existing plan")
# Initial flow request, check if we have an upstream query string passed in
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 # Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan: if not self.plan:
request.session[SESSION_KEY_HISTORY] = [] request.session[SESSION_KEY_HISTORY] = []
@ -192,8 +195,6 @@ class FlowExecutorView(APIView):
# To match behaviour with loading an empty flow plan from cache, # To match behaviour with loading an empty flow plan from cache,
# we don't show an error message here, but rather call _flow_done() # we don't show an error message here, but rather call _flow_done()
return self._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 # We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet # as it hasn't been successfully passed yet
try: try:
@ -392,7 +393,11 @@ class FlowExecutorView(APIView):
NEXT_ARG_NAME, "authentik_core:root-redirect" NEXT_ARG_NAME, "authentik_core:root-redirect"
) )
self.cancel() 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: def stage_ok(self) -> HttpResponse:
"""Callback called by stages upon successful completion. """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.stdlib import get_logger
from structlog.testing import capture_logs 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.applications import user_app_cache_key
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import CacheSerializer, MetaNameSerializer, TypeCreateSerializer 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.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess from authentik.policies.process import PolicyProcess
from authentik.policies.types import CACHE_PREFIX, PolicyRequest from authentik.policies.types import CACHE_PREFIX, PolicyRequest
from authentik.rbac.decorators import permission_required
LOGGER = get_logger() LOGGER = get_logger()

View File

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

View File

@ -36,8 +36,21 @@ class TestAuthorize(OAuthTestCase):
def test_invalid_grant_type(self): def test_invalid_grant_type(self):
"""Test with invalid grant type""" """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): 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) OAuthAuthorizationParams.from_request(request)
def test_invalid_client_id(self): 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() state = generate_id()
user = create_test_admin_user() user = create_test_admin_user()
self.client.force_login(user) self.client.force_login(user)
@ -365,7 +383,7 @@ class TestAuthorize(OAuthTestCase):
"response_type": "id_token", "response_type": "id_token",
"client_id": "test", "client_id": "test",
"state": state, "state": state,
"scope": "openid", "scope": "openid test",
"redirect_uri": "http://localhost", "redirect_uri": "http://localhost",
"nonce": generate_id(), "nonce": generate_id(),
}, },
@ -390,6 +408,7 @@ class TestAuthorize(OAuthTestCase):
) )
jwt = self.validate_jwt(token, provider) jwt = self.validate_jwt(token, provider)
self.assertEqual(jwt["amr"], ["pwd"]) self.assertEqual(jwt["amr"], ["pwd"])
self.assertEqual(jwt["sub"], "foo")
self.assertAlmostEqual( self.assertAlmostEqual(
jwt["exp"] - now().timestamp(), jwt["exp"] - now().timestamp(),
expires, expires,

View File

@ -121,44 +121,18 @@ class OAuthAuthorizationParams:
redirect_uri = query_dict.get("redirect_uri", "") redirect_uri = query_dict.get("redirect_uri", "")
response_type = query_dict.get("response_type", "") 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 # Validate and check the response_mode against the predefined dict
# Set to Query or Fragment if not defined in request # Set to Query or Fragment if not defined in request
response_mode = query_dict.get("response_mode", False) 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") max_age = query_dict.get("max_age")
return OAuthAuthorizationParams( return OAuthAuthorizationParams(
client_id=query_dict.get("client_id", ""), client_id=query_dict.get("client_id", ""),
redirect_uri=redirect_uri, redirect_uri=redirect_uri,
response_type=response_type, response_type=response_type,
response_mode=response_mode, response_mode=response_mode,
grant_type=grant_type, grant_type="",
scope=set(query_dict.get("scope", "").split()), scope=set(query_dict.get("scope", "").split()),
state=state, state=state,
nonce=query_dict.get("nonce"), nonce=query_dict.get("nonce"),
@ -178,6 +152,7 @@ class OAuthAuthorizationParams:
LOGGER.warning("Invalid client identifier", client_id=self.client_id) LOGGER.warning("Invalid client identifier", client_id=self.client_id)
raise ClientIdError(client_id=self.client_id) raise ClientIdError(client_id=self.client_id)
self.check_redirect_uri() self.check_redirect_uri()
self.check_grant()
self.check_scope(github_compat) self.check_scope(github_compat)
self.check_nonce() self.check_nonce()
self.check_code_challenge() self.check_code_challenge()
@ -186,6 +161,34 @@ class OAuthAuthorizationParams:
self.redirect_uri, "request_not_supported", self.grant_type, self.state 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): def check_redirect_uri(self):
"""Redirect URI validation.""" """Redirect URI validation."""
allowed_redirect_urls = self.provider.redirect_uris.split() allowed_redirect_urls = self.provider.redirect_uris.split()
@ -257,9 +260,9 @@ class OAuthAuthorizationParams:
if SCOPE_OFFLINE_ACCESS in self.scope: if SCOPE_OFFLINE_ACCESS in self.scope:
# https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess # https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
if PROMPT_CONSENT not in self.prompt: if PROMPT_CONSENT not in self.prompt:
raise AuthorizeError( # Instead of ignoring the `offline_access` scope when `prompt`
self.redirect_uri, "consent_required", self.grant_type, self.state # isn't set to `consent`, we set override it ourselves
) self.prompt.add(PROMPT_CONSENT)
if self.response_type not in [ if self.response_type not in [
ResponseTypes.CODE, ResponseTypes.CODE,
ResponseTypes.CODE_TOKEN, ResponseTypes.CODE_TOKEN,

View File

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

View File

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

View File

@ -15,10 +15,10 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.policies.event_matcher.models import model_choices from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.api.rbac import PermissionAssignSerializer from authentik.rbac.api.rbac import PermissionAssignSerializer
from authentik.rbac.decorators import permission_required
from authentik.rbac.models import Role 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.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet from rest_framework.viewsets import GenericViewSet
from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupMemberSerializer from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.models import User, UserTypes from authentik.core.models import User, UserTypes
from authentik.policies.event_matcher.models import model_choices from authentik.policies.event_matcher.models import model_choices
from authentik.rbac.api.rbac import PermissionAssignSerializer from authentik.rbac.api.rbac import PermissionAssignSerializer
from authentik.rbac.decorators import permission_required
class UserObjectPermissionSerializer(ModelSerializer): 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): def permission_required(obj_perm: Optional[str] = None, global_perms: Optional[list[str]] = None):
"""Check permissions for a single custom action""" """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""" """Check permissions for a single custom action"""
@wraps(func) @wraps(func)
def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response: def wrapper(self: ModelViewSet, request: Request, *args, **kwargs) -> Response:
if obj_perm: if obj_perm:
obj = self.get_object() _check_obj_perm(self, request)
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)
if global_perms: if global_perms:
for other_perm in global_perms: for other_perm in global_perms:
if not request.user.has_perm(other_perm): 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
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 from authentik.lib.config import CONFIG
QUERY = """SELECT id FROM public.authentik_install_id ORDER BY id LIMIT 1;"""
@lru_cache @lru_cache
def get_install_id() -> str: def get_install_id() -> str:
@ -18,7 +20,7 @@ def get_install_id() -> str:
if settings.TEST: if settings.TEST:
return str(uuid4()) return str(uuid4())
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute("SELECT id FROM public.authentik_install_id LIMIT 1;") cursor.execute(QUERY)
return cursor.fetchone()[0] return cursor.fetchone()[0]
@ -38,5 +40,5 @@ def get_install_id_raw():
sslkey=CONFIG.get("postgresql.sslkey"), sslkey=CONFIG.get("postgresql.sslkey"),
) )
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("SELECT id FROM public.authentik_install_id LIMIT 1;") cursor.execute(QUERY)
return cursor.fetchone()[0] return cursor.fetchone()[0]

View File

@ -481,13 +481,6 @@ def _update_settings(app_path: str):
pass pass
# Load subapps's settings
for _app in set(SHARED_APPS + TENANT_APPS):
if not _app.startswith("authentik"):
continue
_update_settings(f"{_app}.settings")
_update_settings("data.user_settings")
if DEBUG: if DEBUG:
CELERY["task_always_eager"] = True CELERY["task_always_eager"] = True
os.environ[ENV_GIT_HASH_KEY] = "dev" os.environ[ENV_GIT_HASH_KEY] = "dev"
@ -512,5 +505,13 @@ except ImportError:
# being imported for @prefill_task # being imported for @prefill_task
TENANT_APPS.append("authentik.events") TENANT_APPS.append("authentik.events")
# Load subapps's settings
for _app in set(SHARED_APPS + TENANT_APPS):
if not _app.startswith("authentik"):
continue
_update_settings(f"{_app}.settings")
_update_settings("data.user_settings")
SHARED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) SHARED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS))
INSTALLED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS)) INSTALLED_APPS = list(OrderedDict.fromkeys(SHARED_APPS + TENANT_APPS))

View File

@ -13,12 +13,12 @@ from rest_framework.serializers import ValidationError
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.api.decorators import permission_required
from authentik.core.api.sources import SourceSerializer from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views.executor import to_stage_response 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.models import PlexSource, PlexSourceConnection
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager 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 structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.api.decorators import permission_required
from authentik.core.api.used_by import UsedByMixin from authentik.core.api.used_by import UsedByMixin
from authentik.flows.api.stages import StageSerializer 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.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL from authentik.stages.authenticator_duo.stage import SESSION_KEY_DUO_ENROLL
from authentik.stages.authenticator_duo.tasks import duo_import_devices 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( authentication_options = generate_authentication_options(
rp_id=get_rp_id(request), rp_id=get_rp_id(request),
allow_credentials=[], allow_credentials=[],
user_verification=stage.webauthn_user_verification, user_verification=UserVerificationRequirement(stage.webauthn_user_verification),
) )
request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge request.session[SESSION_KEY_WEBAUTHN_CHALLENGE] = authentication_options.challenge

View File

@ -164,8 +164,9 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
"""Test webauthn (userless)""" """Test webauthn (userless)"""
request = get_request("/") request = get_request("/")
stage = AuthenticatorValidateStage.objects.create( stage = AuthenticatorValidateStage.objects.create(
name=generate_id(), name=generate_id(), webauthn_user_verification=UserVerification.PREFERRED
) )
stage.refresh_from_db()
WebAuthnDevice.objects.create( WebAuthnDevice.objects.create(
user=self.user, user=self.user,
public_key=( 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.bytes_to_base64url import bytes_to_base64url
from webauthn.helpers.exceptions import InvalidRegistrationResponse from webauthn.helpers.exceptions import InvalidRegistrationResponse
from webauthn.helpers.structs import ( from webauthn.helpers.structs import (
AuthenticatorAttachment,
AuthenticatorSelectionCriteria, AuthenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions, PublicKeyCredentialCreationOptions,
ResidentKeyRequirement, ResidentKeyRequirement,
@ -91,7 +92,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
# set, cast it to string to ensure it's not a django class # set, cast it to string to ensure it's not a django class
authenticator_attachment = stage.authenticator_attachment authenticator_attachment = stage.authenticator_attachment
if authenticator_attachment: if authenticator_attachment:
authenticator_attachment = str(authenticator_attachment) authenticator_attachment = AuthenticatorAttachment(str(authenticator_attachment))
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options( registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
rp_id=get_rp_id(self.request), rp_id=get_rp_id(self.request),

View File

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

View File

@ -111,7 +111,7 @@ class EmailStageView(ChallengeStageView):
try: try:
message = TemplateEmailMessage( message = TemplateEmailMessage(
subject=_(current_stage.subject), subject=_(current_stage.subject),
to=[f"{pending_user.name} <{email}>"], to=[(pending_user.name, email)],
language=pending_user.locale(self.request), language=pending_user.locale(self.request),
template_name=current_stage.template, template_name=current_stage.template,
template_context={ 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." %} {% 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. 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:" %} {% translate "The following notification was created:" %}
@ -16,3 +16,4 @@ This email was sent from the notification transport {{ name }}.
-- --
Powered by goauthentik.io. 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 %} {% blocktrans %}
You recently requested to change your password for your authentik account. Use the link below to set a new password. 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. Powered by goauthentik.io.
{% endautoescape %}

View File

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

View File

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

View File

@ -9,6 +9,7 @@ from unittest.mock import PropertyMock, patch
from django.conf import settings from django.conf import settings
from django.core.mail.backends.locmem import EmailBackend from django.core.mail.backends.locmem import EmailBackend
from django.core.mail.message import sanitize_address
from django.urls import reverse from django.urls import reverse
from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.core.tests.utils import create_test_admin_user, create_test_flow
@ -19,6 +20,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.stages.email.models import EmailStage, get_template_choices 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]: 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" event.context["message"], "Exception occurred while rendering E-mail template"
) )
self.assertEqual(event.context["template"], "invalid.html") 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): class TemplateEmailMessage(EmailMultiAlternatives):
"""Wrapper around EmailMultiAlternatives with integrated template rendering""" """Wrapper around EmailMultiAlternatives with integrated template rendering"""
def __init__(self, template_name=None, template_context=None, language="", **kwargs): def __init__(
super().__init__(**kwargs) 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): with translation.override(language):
html_content = render_to_string(template_name, template_context) html_content = render_to_string(template_name, template_context)
try: 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.middleware import SESSION_KEY_IMPERSONATE_USER
from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes from authentik.core.models import USER_ATTRIBUTE_SOURCES, User, UserSourceConnection, UserTypes
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION 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.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import StageView from authentik.flows.stage import StageView
from authentik.flows.views.executor import FlowExecutorView 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 # this is just a sanity check to ensure that is removed
if parts[0] == "attributes": if parts[0] == "attributes":
parts = parts[1:] 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]: def ensure_user(self) -> tuple[Optional[User], bool]:
"""Ensure a user exists""" """Ensure a user exists"""

View File

@ -87,11 +87,6 @@ class Tenant(TenantMixin, SerializerModel):
raise IntegrityError("Cannot create schema named template") raise IntegrityError("Cannot create schema named template")
super().save(*args, **kwargs) 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 @property
def serializer(self) -> Serializer: def serializer(self) -> Serializer:
from authentik.tenants.api.tenants import TenantSerializer 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: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.3}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -53,7 +53,7 @@ services:
- postgresql - postgresql
- redis - redis
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.10.7} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.2.3}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

View File

@ -50,12 +50,12 @@ type StorageConfig struct {
} }
type StorageMediaConfig 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"` File StorageFileConfig `yaml:"file"`
} }
type StorageFileConfig struct { 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 { type ErrorReportingConfig struct {

View File

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

View File

@ -55,6 +55,7 @@ function cleanup {
} }
function prepare_debug { function prepare_debug {
source ${VENV_PATH}/bin/activate
poetry install --no-ansi --no-interaction poetry install --no-ansi --no-interaction
touch /unittest.xml touch /unittest.xml
chown authentik:authentik /unittest.xml chown authentik:authentik /unittest.xml
@ -86,6 +87,7 @@ elif [[ "$1" == "bash" ]]; then
/bin/bash /bin/bash
elif [[ "$1" == "test-all" ]]; then elif [[ "$1" == "test-all" ]]; then
prepare_debug prepare_debug
chmod 777 /root
check_if_root "python -m manage test authentik" check_if_root "python -m manage test authentik"
elif [[ "$1" == "healthcheck" ]]; then elif [[ "$1" == "healthcheck" ]]; then
run_authentik healthcheck $(cat $MODE_FILE) run_authentik healthcheck $(cat $MODE_FILE)

View File

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

188
poetry.lock generated
View File

@ -402,33 +402,33 @@ files = [
[[package]] [[package]]
name = "black" name = "black"
version = "24.1.1" version = "24.2.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
{file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
{file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
{file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
{file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
{file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
{file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
{file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
{file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
{file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
{file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
{file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
{file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
{file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
{file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
{file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
{file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
{file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
{file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
{file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
{file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
{file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
] ]
[package.dependencies] [package.dependencies]
@ -506,48 +506,48 @@ files = [
[[package]] [[package]]
name = "cbor2" name = "cbor2"
version = "5.5.1" version = "5.6.2"
description = "CBOR (de)serializer with extensive tag support" description = "CBOR (de)serializer with extensive tag support"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "cbor2-5.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:37ba4f719384bd4ea317e92a8763ea343e205f3112c8241778fd9dbc64ae1498"}, {file = "cbor2-5.6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:516b8390936bb172ff18d7b609a452eaa51991513628949b0a9bf25cbe5a7129"},
{file = "cbor2-5.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:425ae919120b9d05b4794b3e5faf6584fc47a9d61db059d4f00ce16ae93a3f63"}, {file = "cbor2-5.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1b8b504b590367a51fe8c0d9b8cb458a614d782d37b24483097e2b1e93ed0fff"},
{file = "cbor2-5.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c511ff6356d6f4292ced856d5048a24ee61a85634816f29dadf1f089e8cb4f9"}, {file = "cbor2-5.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f687e6731b1198811223576800258a712ddbfdcfa86c0aee2cc8269193e6b96"},
{file = "cbor2-5.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6ab54a9282dd99a3a70d0f64706d3b3592e7920564a93101caa74dec322346c"}, {file = "cbor2-5.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e94043d99fe779f62a15a5e156768588a2a7047bb3a127fa312ac1135ff5ecb"},
{file = "cbor2-5.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:39d94852dd61bda5b3d2bfe74e7b194a7199937d270f90099beec3e7584f0c9b"}, {file = "cbor2-5.6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8af7162fcf7aa2649f02563bdb18b2fa6478b751eee4df0257bffe19ea8f107a"},
{file = "cbor2-5.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:65532ba929beebe1c63317ad00c79d4936b60a5c29a3c329d2aa7df4e72ad907"}, {file = "cbor2-5.6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ea7ecd81c5c6e02c2635973f52a0dd1e19c0bf5ef51f813d8cd5e3e7ed072726"},
{file = "cbor2-5.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:1206180f66a9ad23e692cf457610c877f186ad303a1264b6c5335015b7bee83e"}, {file = "cbor2-5.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3c7f223f1fedc74d33f363d184cb2bab9e4bdf24998f73b5e3bef366d6c41628"},
{file = "cbor2-5.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:42155a20be46312fad2ceb85a408e2d90da059c2d36a65e0b99abca57c5357fd"}, {file = "cbor2-5.6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ea9e150029c3976c46ee9870b6dcdb0a5baae21008fe3290564886b11aa2b64"},
{file = "cbor2-5.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f3827ae14c009df9b37790f1da5cd1f9d64f7ffec472a49ebf865c0af6b77e9"}, {file = "cbor2-5.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:922e06710e5cf6f56b82b0b90d2f356aa229b99e570994534206985f675fd307"},
{file = "cbor2-5.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bfa417dbb8b4581ad3c2312469899518596551cfb0fe5bdaf8a6921cff69d7e"}, {file = "cbor2-5.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b01a718e083e6de8b43296c3ccdb3aa8af6641f6bbb3ea1700427c6af73db28a"},
{file = "cbor2-5.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3317e7dfb4f3180be90bcd853204558d89f119b624c2168153b53dea305e79d"}, {file = "cbor2-5.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac85eb731c524d148f608b9bdb2069fa79e374a10ed5d10a2405eba9a6561e60"},
{file = "cbor2-5.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a5770bdf4340de55679efe6c38fc6d64529fda547e7a85eb0217a82717a8235"}, {file = "cbor2-5.6.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03e5b68867b9d89ff2abd14ef7c6d42fbd991adc3e734a19a294935f22a4d05a"},
{file = "cbor2-5.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b5d53826ad0c92fcb004b2a475896610b51e0ca010f6c37d762aae44ab0807b2"}, {file = "cbor2-5.6.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7221b83000ee01d674572eec1d1caa366eac109d1d32c14d7af9a4aaaf496563"},
{file = "cbor2-5.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc77cac985f7f7a20f2d8b1957d1e79393d7df823f61c7c6173d3a0011c1d770"}, {file = "cbor2-5.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:9aca73b63bdc6561e1a0d38618e78b9c204c942260d51e663c92c4ba6c961684"},
{file = "cbor2-5.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9e45d5aa8e484b4bf57240d8e7949389f1c9d4073758abb30954386321b55c9d"}, {file = "cbor2-5.6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:377cfe9d5560c682486faef6d856226abf8b2801d95fa29d4e5d75b1615eb091"},
{file = "cbor2-5.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93b949a66bec40dd0ca87a6d026136fea2cf1660120f921199a47ac8027af253"}, {file = "cbor2-5.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fdc564ef2e9228bcd96ec8c6cdaa431a48ab03b3fb8326ead4b3f986330e5b9e"},
{file = "cbor2-5.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93d601ca92d917f769370a5e6c3ead62dca6451b2b603915e4fcf300083b9fcd"}, {file = "cbor2-5.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d1c0021d9a1f673066de7c8941f71a59abb11909cc355892dda01e79a2b3045"},
{file = "cbor2-5.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11876abd50b9f70d114fcdbb0b5a3249ccd7d321465f0350028fd6d2317e114"}, {file = "cbor2-5.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fde9e704e96751e0729cc58b912d0e77c34387fb6bcceea0817069e8683df45"},
{file = "cbor2-5.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fd77c558decdba2a2a7a463e6346d53781d2163bacf205f77b999f561ba4ac73"}, {file = "cbor2-5.6.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:30e9ba8f4896726ca61869efacda50b6859aff92162ae5a0e192859664f36c81"},
{file = "cbor2-5.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efb81920d80410b8e80a4a6a8b06ec9b766be0ae7f3029af8ae4b30914edcfa3"}, {file = "cbor2-5.6.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:211a1e18e65ac71e04434ff5b58bde5c53f85b9c5bc92a3c0e2265089d3034f3"},
{file = "cbor2-5.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:4bb35f3b1ebd4b7b37628f0cd5c839f3008dec669194a2a4a33d91bab7f8663b"}, {file = "cbor2-5.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:94981277b4bf448a2754c1f34a9d0055a9d1c5a8d102c933ffe95c80f1085bae"},
{file = "cbor2-5.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f41e4a439f642954ed728dc18915098b5f2ebec7029eaebe52c06c52b6a9a63a"}, {file = "cbor2-5.6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f70db0ebcf005c25408e8d5cc4b9558c899f13a3e2f8281fa3d3be4894e0e821"},
{file = "cbor2-5.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4eae4d56314f22920a28bf7affefdfc918646877ce3b16220dc6cf38a584aa41"}, {file = "cbor2-5.6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:22c24fe9ef1696a84b8fd80ff66eb0e5234505d8b9a9711fc6db57bce10771f3"},
{file = "cbor2-5.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:559a0c1ec8dcedd6142b81727403e0f5a2e8f4c18e8bb3c548107ec39af4e9cb"}, {file = "cbor2-5.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a4a3420f80d6b942874d66eaad07658066370df994ddee4125b48b2cbc61ece"},
{file = "cbor2-5.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:537da7bfee97ee44a11b300c034c18e674af6a5dc4718a6fba141037f099c7ec"}, {file = "cbor2-5.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b28d8ff0e726224a7429281700c28afe0e665f83f9ae79648cbae3f1a391cbf"},
{file = "cbor2-5.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5c99fd8bbc6bbf3bf4d6b2996594ae633b778b27b0531559487950762c4e1e3f"}, {file = "cbor2-5.6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c10ede9462458998f1b9c488e25fe3763aa2491119b7af472b72bf538d789e24"},
{file = "cbor2-5.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ee46e6dbc8e2cf302a022fec513d57dba65e9d5ec495bcd1ad97a5dbdbab249"}, {file = "cbor2-5.6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ea686dfb5e54d690e704ce04993bc8ca0052a7cd2d4b13dd333a41cca8a05a05"},
{file = "cbor2-5.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:67e2be461320197495fff55f250b111d4125a0a2d02e6256e41f8598adc3ad3f"}, {file = "cbor2-5.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:22996159b491d545ecfd489392d3c71e5d0afb9a202dfc0edc8b2cf413a58326"},
{file = "cbor2-5.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4384a56afef0b908b61c8ea3cca3e257a316427ace3411308f51ee301b23adf9"}, {file = "cbor2-5.6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9faa0712d414a88cc1244c78cd4b28fced44f1827dbd8c1649e3c40588aa670f"},
{file = "cbor2-5.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8cc64acc606b7f2a4b673a1d6cde5a9cb1860a6ce27b353e269c9535efbd62c"}, {file = "cbor2-5.6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6031a284d93fc953fc2a2918f261c4f5100905bd064ca3b46961643e7312a828"},
{file = "cbor2-5.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50019fea3cb07fa9b2b53772a52b4243e87de232591570c4c272b3ebdb419493"}, {file = "cbor2-5.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30c8a9a9df79f26e72d8d5fa51ef08eb250d9869a711bcf9539f1865916c983"},
{file = "cbor2-5.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a18be0af9241883bc67a036c1f33e3f9956d31337ccd412194bf759bc1095e03"}, {file = "cbor2-5.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44bf7457fca23209e14dab8181dff82466a83b72e55b444dbbfe90fa67659492"},
{file = "cbor2-5.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:60e7e0073291096605de27de3ce006148cf9a095199160439555f14f93d044d5"}, {file = "cbor2-5.6.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc29c068687aa2e7778f63b653f1346065b858427a2555df4dc2191f4a0de8ce"},
{file = "cbor2-5.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:41f7501338228b27dac88c1197928cf8985f6fc775f59be89c6fdaddb4e69658"}, {file = "cbor2-5.6.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42eaf0f768bd27afcb38135d5bfc361d3a157f1f5c7dddcd8d391f7fa43d9de8"},
{file = "cbor2-5.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c85ab7697252af2240e939707c935ea18081ccb580d4b5b9a94b04148ab2c32b"}, {file = "cbor2-5.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:8839b73befa010358477736680657b9d08c1ed935fd973decb1909712a41afdc"},
{file = "cbor2-5.5.1-py3-none-any.whl", hash = "sha256:dca639c8ff81b9f0c92faf97324adfdbfb5c2a5bb97f249606c6f5b94c77cc0d"}, {file = "cbor2-5.6.2-py3-none-any.whl", hash = "sha256:c0b53a65673550fde483724ff683753f49462d392d45d7b6576364b39e76e54c"},
{file = "cbor2-5.5.1.tar.gz", hash = "sha256:f9e192f461a9f8f6082df28c035b006d153904213dc8640bed8a72d72bbc9475"}, {file = "cbor2-5.6.2.tar.gz", hash = "sha256:b7513c2dea8868991fad7ef8899890ebcf8b199b9b4461c3c11d7ad3aef4820d"},
] ]
[package.extras] [package.extras]
@ -993,43 +993,43 @@ toml = ["tomli"]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "42.0.0" version = "42.0.4"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "cryptography-42.0.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:c640b0ef54138fde761ec99a6c7dc4ce05e80420262c20fa239e694ca371d434"}, {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"},
{file = "cryptography-42.0.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:678cfa0d1e72ef41d48993a7be75a76b0725d29b820ff3cfd606a5b2b33fda01"}, {file = "cryptography-42.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18"},
{file = "cryptography-42.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:146e971e92a6dd042214b537a726c9750496128453146ab0ee8971a0299dc9bd"}, {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2"},
{file = "cryptography-42.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87086eae86a700307b544625e3ba11cc600c3c0ef8ab97b0fda0705d6db3d4e3"}, {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1"},
{file = "cryptography-42.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0a68bfcf57a6887818307600c3c0ebc3f62fbb6ccad2240aa21887cda1f8df1b"}, {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b"},
{file = "cryptography-42.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5a217bca51f3b91971400890905a9323ad805838ca3fa1e202a01844f485ee87"}, {file = "cryptography-42.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1"},
{file = "cryptography-42.0.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ca20550bb590db16223eb9ccc5852335b48b8f597e2f6f0878bbfd9e7314eb17"}, {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992"},
{file = "cryptography-42.0.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:33588310b5c886dfb87dba5f013b8d27df7ffd31dc753775342a1e5ab139e59d"}, {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885"},
{file = "cryptography-42.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9515ea7f596c8092fdc9902627e51b23a75daa2c7815ed5aa8cf4f07469212ec"}, {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824"},
{file = "cryptography-42.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:35cf6ed4c38f054478a9df14f03c1169bb14bd98f0b1705751079b25e1cb58bc"}, {file = "cryptography-42.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b"},
{file = "cryptography-42.0.0-cp37-abi3-win32.whl", hash = "sha256:8814722cffcfd1fbd91edd9f3451b88a8f26a5fd41b28c1c9193949d1c689dc4"}, {file = "cryptography-42.0.4-cp37-abi3-win32.whl", hash = "sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925"},
{file = "cryptography-42.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:a2a8d873667e4fd2f34aedab02ba500b824692c6542e017075a2efc38f60a4c0"}, {file = "cryptography-42.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923"},
{file = "cryptography-42.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8fedec73d590fd30c4e3f0d0f4bc961aeca8390c72f3eaa1a0874d180e868ddf"}, {file = "cryptography-42.0.4-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7"},
{file = "cryptography-42.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be41b0c7366e5549265adf2145135dca107718fa44b6e418dc7499cfff6b4689"}, {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52"},
{file = "cryptography-42.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ca482ea80626048975360c8e62be3ceb0f11803180b73163acd24bf014133a0"}, {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a"},
{file = "cryptography-42.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c58115384bdcfe9c7f644c72f10f6f42bed7cf59f7b52fe1bf7ae0a622b3a139"}, {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9"},
{file = "cryptography-42.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:56ce0c106d5c3fec1038c3cca3d55ac320a5be1b44bf15116732d0bc716979a2"}, {file = "cryptography-42.0.4-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764"},
{file = "cryptography-42.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:324721d93b998cb7367f1e6897370644751e5580ff9b370c0a50dc60a2003513"}, {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff"},
{file = "cryptography-42.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:d97aae66b7de41cdf5b12087b5509e4e9805ed6f562406dfcf60e8481a9a28f8"}, {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257"},
{file = "cryptography-42.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:85f759ed59ffd1d0baad296e72780aa62ff8a71f94dc1ab340386a1207d0ea81"}, {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929"},
{file = "cryptography-42.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:206aaf42e031b93f86ad60f9f5d9da1b09164f25488238ac1dc488334eb5e221"}, {file = "cryptography-42.0.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0"},
{file = "cryptography-42.0.0-cp39-abi3-win32.whl", hash = "sha256:74f18a4c8ca04134d2052a140322002fef535c99cdbc2a6afc18a8024d5c9d5b"}, {file = "cryptography-42.0.4-cp39-abi3-win32.whl", hash = "sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129"},
{file = "cryptography-42.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:14e4b909373bc5bf1095311fa0f7fcabf2d1a160ca13f1e9e467be1ac4cbdf94"}, {file = "cryptography-42.0.4-cp39-abi3-win_amd64.whl", hash = "sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854"},
{file = "cryptography-42.0.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3005166a39b70c8b94455fdbe78d87a444da31ff70de3331cdec2c568cf25b7e"}, {file = "cryptography-42.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298"},
{file = "cryptography-42.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:be14b31eb3a293fc6e6aa2807c8a3224c71426f7c4e3639ccf1a2f3ffd6df8c3"}, {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88"},
{file = "cryptography-42.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bd7cf7a8d9f34cc67220f1195884151426ce616fdc8285df9054bfa10135925f"}, {file = "cryptography-42.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20"},
{file = "cryptography-42.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c310767268d88803b653fffe6d6f2f17bb9d49ffceb8d70aed50ad45ea49ab08"}, {file = "cryptography-42.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce"},
{file = "cryptography-42.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bdce70e562c69bb089523e75ef1d9625b7417c6297a76ac27b1b8b1eb51b7d0f"}, {file = "cryptography-42.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74"},
{file = "cryptography-42.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e9326ca78111e4c645f7e49cbce4ed2f3f85e17b61a563328c85a5208cf34440"}, {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd"},
{file = "cryptography-42.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:69fd009a325cad6fbfd5b04c711a4da563c6c4854fc4c9544bff3088387c77c0"}, {file = "cryptography-42.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b"},
{file = "cryptography-42.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:988b738f56c665366b1e4bfd9045c3efae89ee366ca3839cd5af53eaa1401bce"}, {file = "cryptography-42.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660"},
{file = "cryptography-42.0.0.tar.gz", hash = "sha256:6cf9b76d6e93c62114bd19485e5cb003115c134cf9ce91f8ac924c44f8c8c3f4"}, {file = "cryptography-42.0.4.tar.gz", hash = "sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb"},
] ]
[package.dependencies] [package.dependencies]

View File

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

View File

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

View File

@ -6,3 +6,4 @@ dist
coverage coverage
src/locale-codes.ts src/locale-codes.ts
storybook-static/ storybook-static/
src/locales/**

View File

@ -122,7 +122,7 @@ export class AkAdminSidebar extends WithCapabilitiesConfig(AKElement) {
["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]], ["/events/log", msg("Logs"), [`^/events/log/(?<id>${UUID_REGEX})$`]],
["/events/rules", msg("Notification Rules")], ["/events/rules", msg("Notification Rules")],
["/events/transports", msg("Notification Transports")]]], ["/events/transports", msg("Notification Transports")]]],
[null, msg("Customisation"), null, [ [null, msg("Customization"), null, [
["/policy/policies", msg("Policies")], ["/policy/policies", msg("Policies")],
["/core/property-mappings", msg("Property Mappings")], ["/core/property-mappings", msg("Property Mappings")],
["/blueprints/instances", msg("Blueprints")], ["/blueprints/instances", msg("Blueprints")],

View File

@ -22,25 +22,36 @@ import { AdminApi, Settings, SettingsRequest } from "@goauthentik/api";
@customElement("ak-admin-settings-form") @customElement("ak-admin-settings-form")
export class AdminSettingsForm extends Form<SettingsRequest> { export class AdminSettingsForm extends Form<SettingsRequest> {
@property({ attribute: false }) //
set settings(value: Settings) { // Custom property accessors in Lit 2 require a manual call to requestUpdate(). See:
// https://lit.dev/docs/v2/components/properties/#accessors-custom
//
set settings(value: Settings | undefined) {
this._settings = value; this._settings = value;
this.requestUpdate();
}
@property({ type: Object })
get settings() {
return this._settings;
} }
private _settings?: Settings; private _settings?: Settings;
static get styles(): CSSResult[] {
return super.styles.concat(PFList);
}
getSuccessMessage(): string { getSuccessMessage(): string {
return msg("Successfully updated settings."); return msg("Successfully updated settings.");
} }
async send(data: SettingsRequest): Promise<Settings> { async send(data: SettingsRequest): Promise<Settings> {
return new AdminApi(DEFAULT_CONFIG).adminSettingsUpdate({ const result = await new AdminApi(DEFAULT_CONFIG).adminSettingsUpdate({
settingsRequest: data, settingsRequest: data,
}); });
} this.dispatchEvent(new CustomEvent("ak-admin-setting-changed"));
return result;
static get styles(): CSSResult[] {
return super.styles.concat(PFList);
} }
renderForm(): TemplateResult { renderForm(): TemplateResult {

View File

@ -14,8 +14,8 @@ import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/ModalForm"; import "@goauthentik/elements/forms/ModalForm";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, query, state } from "lit/decorators.js";
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css"; import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -32,7 +32,7 @@ import { AdminApi, Settings } from "@goauthentik/api";
@customElement("ak-admin-settings") @customElement("ak-admin-settings")
export class AdminSettingsPage extends AKElement { export class AdminSettingsPage extends AKElement {
static get styles(): CSSResult[] { static get styles() {
return [ return [
PFBase, PFBase,
PFButton, PFButton,
@ -46,41 +46,46 @@ export class AdminSettingsPage extends AKElement {
PFBanner, PFBanner,
]; ];
} }
@property({ attribute: false })
@query("ak-admin-settings-form#form")
form?: AdminSettingsForm;
@state()
settings?: Settings; settings?: Settings;
loadSettings(): void { constructor() {
new AdminApi(DEFAULT_CONFIG).adminSettingsRetrieve().then((settings) => { super();
AdminSettingsPage.fetchSettings().then((settings) => {
this.settings = settings; this.settings = settings;
}); });
this.save = this.save.bind(this);
this.reset = this.reset.bind(this);
this.addEventListener("ak-admin-setting-changed", this.handleUpdate.bind(this));
} }
firstUpdated(): void { static async fetchSettings() {
this.loadSettings(); return await new AdminApi(DEFAULT_CONFIG).adminSettingsRetrieve();
} }
async save(): Promise<void> { async handleUpdate() {
const form = this.shadowRoot?.querySelector<AdminSettingsForm>("ak-admin-settings-form"); this.settings = await AdminSettingsPage.fetchSettings();
if (!form) { }
async save() {
if (!this.form) {
return; return;
} }
await form.submit(new Event("submit")); await this.form.submit(new Event("submit"));
this.resetForm(); this.settings = await AdminSettingsPage.fetchSettings();
} }
resetForm(): void { async reset() {
const form = this.shadowRoot?.querySelector<AdminSettingsForm>("ak-admin-settings-form"); this.form?.resetForm();
if (!form) {
return;
}
this.loadSettings();
form.settings = this.settings!;
form.resetForm();
} }
render(): TemplateResult { render() {
if (!this.settings) { if (!this.settings) {
return html``; return nothing;
} }
return html` return html`
<ak-page-header icon="fa fa-cog" header="" description=""> <ak-page-header icon="fa fa-cog" header="" description="">
@ -93,18 +98,10 @@ export class AdminSettingsPage extends AKElement {
</ak-admin-settings-form> </ak-admin-settings-form>
</div> </div>
<div class="pf-c-card__footer"> <div class="pf-c-card__footer">
<ak-spinner-button <ak-spinner-button .callAction=${this.save} class="pf-m-primary"
.callAction=${async () => {
await this.save();
}}
class="pf-m-primary"
>${msg("Save")}</ak-spinner-button >${msg("Save")}</ak-spinner-button
> >
<ak-spinner-button <ak-spinner-button .callAction=${this.reset} class="pf-m-secondary"
.callAction=${() => {
this.resetForm();
}}
class="pf-m-secondary"
>${msg("Cancel")}</ak-spinner-button >${msg("Cancel")}</ak-spinner-button
> >
</div> </div>

View File

@ -183,7 +183,6 @@ export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
<ak-multi-select <ak-multi-select
label=${msg("AdditionalScopes")} label=${msg("AdditionalScopes")}
name="propertyMappings" name="propertyMappings"
required
.options=${scopePairs} .options=${scopePairs}
.values=${scopeValues} .values=${scopeValues}
.errorMessages=${errors?.propertyMappings ?? []} .errorMessages=${errors?.propertyMappings ?? []}

View File

@ -83,7 +83,6 @@ export class ApplicationWizardAuthenticationByRAC extends BaseProviderPanel {
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Property mappings")} label=${msg("Property mappings")}
?required=${true}
name="propertyMappings" name="propertyMappings"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>

View File

@ -194,7 +194,6 @@ export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPane
<ak-multi-select <ak-multi-select
label=${msg("Property Mappings")} label=${msg("Property Mappings")}
name="propertyMappings" name="propertyMappings"
required
.options=${propertyPairs} .options=${propertyPairs}
.values=${pmValues} .values=${pmValues}
.richhelp=${html` <p class="pf-c-form__helper-text"> .richhelp=${html` <p class="pf-c-form__helper-text">

View File

@ -123,7 +123,6 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-multi-select <ak-multi-select
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
required
name="propertyMappings" name="propertyMappings"
.options=${propertyPairs} .options=${propertyPairs}
.values=${pmUserValues} .values=${pmUserValues}
@ -136,7 +135,6 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
></ak-multi-select> ></ak-multi-select>
<ak-multi-select <ak-multi-select
label=${msg("Group Property Mappings")} label=${msg("Group Property Mappings")}
required
name="propertyMappingsGroup" name="propertyMappingsGroup"
.options=${propertyPairs} .options=${propertyPairs}
.values=${pmGroupValues} .values=${pmGroupValues}

View File

@ -125,6 +125,7 @@ export class RelatedGroupList extends Table<Group> {
actionSubtext=${msg( actionSubtext=${msg(
str`Are you sure you want to remove user ${this.targetUser?.username} from the following groups?`, str`Are you sure you want to remove user ${this.targetUser?.username} from the following groups?`,
)} )}
buttonLabel=${msg("Remove")}
.objects=${this.selectedElements} .objects=${this.selectedElements}
.delete=${(item: Group) => { .delete=${(item: Group) => {
if (!this.targetUser) return; if (!this.targetUser) return;

View File

@ -13,7 +13,7 @@ import { customElement, property } from "lit/decorators.js";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css"; import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import { ConnectionToken, Endpoint, RACProvider, RacApi } from "@goauthentik/api"; import { ConnectionToken, RACProvider, RacApi } from "@goauthentik/api";
@customElement("ak-rac-connection-token-list") @customElement("ak-rac-connection-token-list")
export class ConnectionTokenListPage extends Table<ConnectionToken> { export class ConnectionTokenListPage extends Table<ConnectionToken> {
@ -53,18 +53,18 @@ export class ConnectionTokenListPage extends Table<ConnectionToken> {
return html`<ak-forms-delete-bulk return html`<ak-forms-delete-bulk
objectLabel=${msg("Connection Token(s)")} objectLabel=${msg("Connection Token(s)")}
.objects=${this.selectedElements} .objects=${this.selectedElements}
.metadata=${(item: Endpoint) => { .metadata=${(item: ConnectionToken) => {
return [ return [
{ key: msg("Name"), value: item.name }, { key: msg("Endpoint"), value: item.endpointObj.name },
{ key: msg("Host"), value: item.host }, { key: msg("User"), value: item.user.username },
]; ];
}} }}
.usedBy=${(item: Endpoint) => { .usedBy=${(item: ConnectionToken) => {
return new RacApi(DEFAULT_CONFIG).racConnectionTokensUsedByList({ return new RacApi(DEFAULT_CONFIG).racConnectionTokensUsedByList({
connectionTokenUuid: item.pk, connectionTokenUuid: item.pk,
}); });
}} }}
.delete=${(item: Endpoint) => { .delete=${(item: ConnectionToken) => {
return new RacApi(DEFAULT_CONFIG).racConnectionTokensDestroy({ return new RacApi(DEFAULT_CONFIG).racConnectionTokensDestroy({
connectionTokenUuid: item.pk, connectionTokenUuid: item.pk,
}); });

View File

@ -123,11 +123,7 @@ export class EndpointForm extends ModelForm<Endpoint, string> {
)} )}
</p> </p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal label=${msg("Property mappings")} name="propertyMappings">
label=${msg("Property mappings")}
?required=${true}
name="propertyMappings"
>
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
${this.propertyMappings?.results.map((mapping) => { ${this.propertyMappings?.results.map((mapping) => {
let selected = false; let selected = false;

View File

@ -135,7 +135,6 @@ export class RACProviderFormPage extends ModelForm<RACProvider, number> {
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Property mappings")} label=${msg("Property mappings")}
?required=${true}
name="propertyMappings" name="propertyMappings"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>

View File

@ -87,7 +87,11 @@ export class RACProviderViewPage extends AKElement {
<section slot="page-overview" data-tab-title="${msg("Overview")}"> <section slot="page-overview" data-tab-title="${msg("Overview")}">
${this.renderTabOverview()} ${this.renderTabOverview()}
</section> </section>
<section slot="page-connections" data-tab-title="${msg("Connections")}"> <section
slot="page-connections"
data-tab-title="${msg("Connections")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-c-card"> <div class="pf-c-card">
<div class="pf-c-card__body"> <div class="pf-c-card__body">
<ak-rac-connection-token-list <ak-rac-connection-token-list

View File

@ -191,7 +191,6 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Property mappings")} label=${msg("Property mappings")}
?required=${true}
name="propertyMappings" name="propertyMappings"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>

View File

@ -151,7 +151,6 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
?required=${true}
name="propertyMappings" name="propertyMappings"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
@ -185,7 +184,6 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Group Property Mappings")} label=${msg("Group Property Mappings")}
?required=${true}
name="propertyMappingsGroup" name="propertyMappingsGroup"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>

View File

@ -253,7 +253,6 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
<div slot="body" class="pf-c-form"> <div slot="body" class="pf-c-form">
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("User Property Mappings")} label=${msg("User Property Mappings")}
?required=${true}
name="propertyMappings" name="propertyMappings"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>
@ -292,7 +291,6 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${msg("Group Property Mappings")} label=${msg("Group Property Mappings")}
?required=${true}
name="propertyMappingsGroup" name="propertyMappingsGroup"
> >
<select class="pf-c-form-control" multiple> <select class="pf-c-form-control" multiple>

View File

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

View File

@ -1,7 +1,7 @@
import { AKElement } from "@goauthentik/elements/Base"; import { AKElement } from "@goauthentik/elements/Base";
import { PFSize } from "@goauthentik/elements/Spinner"; import { PFSize } from "@goauthentik/elements/Spinner";
import { CSSResult, TemplateResult, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css"; import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
@ -23,7 +23,17 @@ export class EmptyState extends AKElement {
header = ""; header = "";
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [PFBase, PFEmptyState, PFTitle]; return [
PFBase,
PFEmptyState,
PFTitle,
css`
i.pf-c-empty-state__icon {
height: var(--pf-global--icon--FontSize--2xl);
line-height: var(--pf-global--icon--FontSize--2xl);
}
`,
];
} }
render(): TemplateResult { render(): TemplateResult {

View File

@ -131,6 +131,9 @@ export class DeleteBulkForm<T> extends ModalButton {
@property() @property()
actionSubtext?: string; actionSubtext?: string;
@property()
buttonLabel = msg("Delete");
@property({ attribute: false }) @property({ attribute: false })
metadata: (item: T) => BulkDeleteMetadata = (item: T) => { metadata: (item: T) => BulkDeleteMetadata = (item: T) => {
const rec = item as Record<string, unknown>; const rec = item as Record<string, unknown>;
@ -222,7 +225,7 @@ export class DeleteBulkForm<T> extends ModalButton {
}} }}
class="pf-m-danger" class="pf-m-danger"
> >
${msg("Delete")} </ak-spinner-button ${this.buttonLabel} </ak-spinner-button
>&nbsp; >&nbsp;
<ak-spinner-button <ak-spinner-button
.callAction=${async () => { .callAction=${async () => {

View File

@ -15,7 +15,7 @@ import "@goauthentik/flow/sources/apple/AppleLoginInit";
import "@goauthentik/flow/sources/plex/PlexLoginInit"; import "@goauthentik/flow/sources/plex/PlexLoginInit";
import "@goauthentik/flow/stages/FlowErrorStage"; import "@goauthentik/flow/stages/FlowErrorStage";
import "@goauthentik/flow/stages/RedirectStage"; import "@goauthentik/flow/stages/RedirectStage";
import { StageHost } from "@goauthentik/flow/stages/base"; import { StageHost, SubmitOptions } from "@goauthentik/flow/stages/base";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit"; import { CSSResult, TemplateResult, css, html, nothing } from "lit";
@ -189,12 +189,17 @@ export class FlowExecutor extends Interface implements StageHost {
return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic; return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
} }
async submit(payload?: FlowChallengeResponseRequest): Promise<boolean> { async submit(
payload?: FlowChallengeResponseRequest,
options?: SubmitOptions,
): Promise<boolean> {
if (!payload) return Promise.reject(); if (!payload) return Promise.reject();
if (!this.challenge) return Promise.reject(); if (!this.challenge) return Promise.reject();
// @ts-ignore // @ts-expect-error
payload.component = this.challenge.component; payload.component = this.challenge.component;
this.loading = true; if (!options?.invisible) {
this.loading = true;
}
try { try {
const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
flowSlug: this.flowSlug, flowSlug: this.flowSlug,

View File

@ -40,6 +40,7 @@ export class AuthenticatorStaticStage extends BaseStage<
columns: 2; columns: 2;
-webkit-columns: 2; -webkit-columns: 2;
-moz-columns: 2; -moz-columns: 2;
column-width: 1em;
margin-left: var(--pf-global--spacer--xs); margin-left: var(--pf-global--spacer--xs);
} }
ul li { ul li {

View File

@ -2,13 +2,12 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageCode"; import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageCode";
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageDuo"; import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageDuo";
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn"; import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn";
import { BaseStage, StageHost } from "@goauthentik/flow/stages/base"; import { BaseStage, StageHost, SubmitOptions } from "@goauthentik/flow/stages/base";
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage"; import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, state } from "lit/decorators.js"; import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css"; import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css"; import PFForm from "@patternfly/patternfly/components/Form/form.css";
@ -59,7 +58,7 @@ export class AuthenticatorValidateStage
// We don't use this.submit here, as we don't want to advance the flow. // We don't use this.submit here, as we don't want to advance the flow.
// We just want to notify the backend which challenge has been selected. // We just want to notify the backend which challenge has been selected.
new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({ new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
flowSlug: this.host.flowSlug || "", flowSlug: this.host?.flowSlug || "",
query: window.location.search.substring(1), query: window.location.search.substring(1),
flowChallengeResponseRequest: { flowChallengeResponseRequest: {
// @ts-ignore // @ts-ignore
@ -73,8 +72,11 @@ export class AuthenticatorValidateStage
return this._selectedDeviceChallenge; return this._selectedDeviceChallenge;
} }
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<boolean> { submit(
return this.host?.submit(payload) || Promise.resolve(); payload: AuthenticatorValidationChallengeResponseRequest,
options?: SubmitOptions,
): Promise<boolean> {
return this.host?.submit(payload, options) || Promise.resolve();
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@ -253,23 +255,7 @@ export class AuthenticatorValidateStage
? this.renderDeviceChallenge() ? this.renderDeviceChallenge()
: html`<div class="pf-c-login__main-body"> : html`<div class="pf-c-login__main-body">
<form class="pf-c-form"> <form class="pf-c-form">
<ak-form-static ${this.renderUserInfo()}
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
<input
name="username"
autocomplete="username"
type="hidden"
value="${this.challenge.pendingUser}"
/>
${this.selectedDeviceChallenge ${this.selectedDeviceChallenge
? "" ? ""
: html`<p>${msg("Select an authentication method.")}</p>`} : html`<p>${msg("Select an authentication method.")}</p>`}

View File

@ -1,59 +1,34 @@
import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement"; import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/FormStatic";
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage"; import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit"; import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { import {
AuthenticatorValidationChallenge, AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest, AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
DeviceClassesEnum, DeviceClassesEnum,
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-stage-authenticator-validate-code") @customElement("ak-stage-authenticator-validate-code")
export class AuthenticatorValidateStageWebCode extends BaseStage< export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
AuthenticatorValidationChallenge, AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest AuthenticatorValidationChallengeResponseRequest
> { > {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@property({ type: Boolean })
showBackButton = false;
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
return [ return super.styles.concat(css`
PFBase, .icon-description {
PFLogin, display: flex;
PFForm, }
PFFormControl, .icon-description i {
PFTitle, font-size: 2em;
PFButton, padding: 0.25em;
css` padding-right: 0.5em;
.icon-description { }
display: flex; `);
}
.icon-description i {
font-size: 2em;
padding: 0.25em;
padding-right: 0.5em;
}
`,
];
} }
render(): TemplateResult { render(): TemplateResult {
@ -62,92 +37,62 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<
</ak-empty-state>`; </ak-empty-state>`;
} }
return html`<div class="pf-c-login__main-body"> return html`<div class="pf-c-login__main-body">
<form <form
class="pf-c-form" class="pf-c-form"
@submit=${(e: Event) => { @submit=${(e: Event) => {
this.submitForm(e); this.submitForm(e);
}} }}
>
${this.renderUserInfo()}
<div class="icon-description">
<i
class="fa ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
? "fa-key"
: "fa-mobile-alt"}"
aria-hidden="true"
></i>
${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
? html`<p>${msg("A code has been sent to you via SMS.")}</p>`
: html`<p>
${msg(
"Open your two-factor authenticator app to view your authentication code.",
)}
</p>`}
</div>
<ak-form-element
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? msg("Static token")
: msg("Authentication code")}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
> >
<ak-form-static <!-- @ts-ignore -->
class="pf-c-form__group" <input
userAvatar="${this.challenge.pendingUserAvatar}" type="text"
user=${this.challenge.pendingUser} name="code"
> inputmode="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
<div slot="link"> ? "text"
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}" : "numeric"}"
>${msg("Not you?")}</a pattern="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
> ? "[0-9a-zA-Z]*"
</div> : "[0-9]*"}"
</ak-form-static> placeholder="${msg("Please enter your code")}"
<div class="icon-description"> autofocus=""
<i autocomplete="one-time-code"
class="fa ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms class="pf-c-form-control"
? "fa-key" value="${PasswordManagerPrefill.totp || ""}"
: "fa-mobile-alt"}" required
aria-hidden="true" />
></i> </ak-form-element>
${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
? html`<p>${msg("A code has been sent to you via SMS.")}</p>`
: html`<p>
${msg(
"Open your two-factor authenticator app to view your authentication code.",
)}
</p>`}
</div>
<ak-form-element
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
? msg("Static token")
: msg("Authentication code")}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<!-- @ts-ignore -->
<input
type="text"
name="code"
inputmode="${this.deviceChallenge?.deviceClass ===
DeviceClassesEnum.Static
? "text"
: "numeric"}"
pattern="${this.deviceChallenge?.deviceClass ===
DeviceClassesEnum.Static
? "[0-9a-zA-Z]*"
: "[0-9]*"}"
placeholder="${msg("Please enter your code")}"
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
value="${PasswordManagerPrefill.totp || ""}"
required
/>
</ak-form-element>
<div class="pf-c-form__group pf-m-action"> <div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block"> <button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${msg("Continue")} ${msg("Continue")}
</button> </button>
</div> ${this.renderReturnToDevicePicker()}
</form> </div>
</div> </form>
<footer class="pf-c-login__main-footer"> </div>`;
<ul class="pf-c-login__main-footer-links">
${this.showBackButton
? html`<li class="pf-c-login__main-footer-links-item">
<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(
this.host as AuthenticatorValidateStage
).selectedDeviceChallenge = undefined;
}}
>
${msg("Return to device picker")}
</button>
</li>`
: html``}
</ul>
</footer>`;
} }
} }

View File

@ -1,20 +1,10 @@
import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base";
import "@goauthentik/elements/EmptyState"; import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement"; import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/FormStatic";
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { import {
AuthenticatorValidationChallenge, AuthenticatorValidationChallenge,
@ -23,7 +13,7 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-stage-authenticator-validate-duo") @customElement("ak-stage-authenticator-validate-duo")
export class AuthenticatorValidateStageWebDuo extends BaseStage< export class AuthenticatorValidateStageWebDuo extends BaseDeviceStage<
AuthenticatorValidationChallenge, AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest AuthenticatorValidationChallengeResponseRequest
> { > {
@ -33,14 +23,24 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<
@property({ type: Boolean }) @property({ type: Boolean })
showBackButton = false; showBackButton = false;
static get styles(): CSSResult[] { @state()
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton]; authenticating = false;
}
firstUpdated(): void { firstUpdated(): void {
this.host?.submit({ this.authenticating = true;
duo: this.deviceChallenge?.deviceUid, this.host
}); ?.submit(
{
duo: this.deviceChallenge?.deviceUid,
},
{ invisible: true },
)
.then(() => {
this.authenticating = false;
})
.catch(() => {
this.authenticating = false;
});
} }
render(): TemplateResult { render(): TemplateResult {
@ -49,56 +49,25 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<
</ak-empty-state>`; </ak-empty-state>`;
} }
const errors = this.challenge.responseErrors?.duo || []; const errors = this.challenge.responseErrors?.duo || [];
const errorMessage = errors.map((err) => err.string);
return html`<div class="pf-c-login__main-body"> return html`<div class="pf-c-login__main-body">
<form <form
class="pf-c-form" class="pf-c-form"
@submit=${(e: Event) => { @submit=${(e: Event) => {
this.submitForm(e); this.submitForm(e);
}} }}
>
${this.renderUserInfo()}
<ak-empty-state
?loading="${this.authenticating}"
header=${this.authenticating
? msg("Sending Duo push notification...")
: errorMessage.join(", ") || msg("Failed to authenticate")}
icon="fas fa-times"
> >
<ak-form-static </ak-empty-state>
class="pf-c-form__group" <div class="pf-c-form__group pf-m-action">${this.renderReturnToDevicePicker()}</div>
userAvatar="${this.challenge.pendingUserAvatar}" </form>
user=${this.challenge.pendingUser} </div>`;
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${msg("Not you?")}</a
>
</div>
</ak-form-static>
${errors.length > 0
? errors.map((err) => {
if (err.code === "denied") {
return html` <ak-stage-access-denied-icon
errorMessage=${err.string}
>
</ak-stage-access-denied-icon>`;
}
return html`<p>${err.string}</p>`;
})
: html`${msg("Sending Duo push notification")}`}
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton
? html`<li class="pf-c-login__main-footer-links-item">
<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(
this.host as AuthenticatorValidateStage
).selectedDeviceChallenge = undefined;
}}
>
${msg("Return to device picker")}
</button>
</li>`
: html``}
</ul>
</footer>`;
} }
} }

View File

@ -1,23 +1,14 @@
import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base";
import { import {
checkWebAuthnSupport, checkWebAuthnSupport,
transformAssertionForServer, transformAssertionForServer,
transformCredentialRequestOptions, transformCredentialRequestOptions,
} from "@goauthentik/common/helpers/webauthn"; } from "@goauthentik/common/helpers/webauthn";
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage"; import "@goauthentik/elements/EmptyState";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { msg, str } from "@lit/localize"; import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit"; import { TemplateResult, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { import {
AuthenticatorValidationChallenge, AuthenticatorValidationChallenge,
@ -26,7 +17,7 @@ import {
} from "@goauthentik/api"; } from "@goauthentik/api";
@customElement("ak-stage-authenticator-validate-webauthn") @customElement("ak-stage-authenticator-validate-webauthn")
export class AuthenticatorValidateStageWebAuthn extends BaseStage< export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
AuthenticatorValidationChallenge, AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest AuthenticatorValidationChallengeResponseRequest
> { > {
@ -34,25 +25,15 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
deviceChallenge?: DeviceChallenge; deviceChallenge?: DeviceChallenge;
@property() @property()
authenticateMessage?: string; errorMessage?: string;
@property({ type: Boolean }) @property({ type: Boolean })
showBackButton = false; showBackButton = false;
transformedCredentialRequestOptions?: PublicKeyCredentialRequestOptions; @state()
authenticating = false;
static get styles(): CSSResult[] { transformedCredentialRequestOptions?: PublicKeyCredentialRequestOptions;
return [
PFBase,
PFLogin,
PFEmptyState,
PFBullseye,
PFForm,
PFFormControl,
PFTitle,
PFButton,
];
}
async authenticate(): Promise<void> { async authenticate(): Promise<void> {
// request the authenticator to create an assertion signature using the // request the authenticator to create an assertion signature using the
@ -64,10 +45,10 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
publicKey: this.transformedCredentialRequestOptions, publicKey: this.transformedCredentialRequestOptions,
}); });
if (!assertion) { if (!assertion) {
throw new Error(msg("Assertions is empty")); throw new Error("Assertions is empty");
} }
} catch (err) { } catch (err) {
throw new Error(msg(str`Error when creating credential: ${err}`)); throw new Error(`Error when creating credential: ${err}`);
} }
// we now have an authentication assertion! encode the byte arrays contained // we now have an authentication assertion! encode the byte arrays contained
@ -78,11 +59,16 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
// post the assertion to the server for verification. // post the assertion to the server for verification.
try { try {
await this.host?.submit({ await this.host?.submit(
webauthn: transformedAssertionForServer, {
}); webauthn: transformedAssertionForServer,
},
{
invisible: true,
},
);
} catch (err) { } catch (err) {
throw new Error(msg(str`Error when validating assertion on server: ${err}`)); throw new Error(`Error when validating assertion on server: ${err}`);
} }
} }
@ -97,58 +83,47 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
} }
async authenticateWrapper(): Promise<void> { async authenticateWrapper(): Promise<void> {
if (this.host.loading) { if (this.authenticating) {
return; return;
} }
this.host.loading = true; this.authenticating = true;
this.authenticate() this.authenticate()
.catch((e) => { .catch((e: Error) => {
console.error(e); console.warn("authentik/flows/authenticator_validate/webauthn: failed to auth", e);
this.authenticateMessage = e.toString(); this.errorMessage = msg("Authentication failed. Please try again.");
}) })
.finally(() => { .finally(() => {
this.host.loading = false; this.authenticating = false;
}); });
} }
render(): TemplateResult { render(): TemplateResult {
return html`<div class="pf-c-login__main-body"> return html`<div class="pf-c-login__main-body">
${this.authenticateMessage <form class="pf-c-form">
? html`<div class="pf-c-form__group pf-m-action"> ${this.renderUserInfo()}
<p class="pf-m-block">${this.authenticateMessage}</p> <ak-empty-state
<button ?loading="${this.authenticating}"
header=${this.authenticating
? msg("Authenticating...")
: this.errorMessage || msg("Failed to authenticate")}
icon="fa-times"
>
</ak-empty-state>
<div class="pf-c-form__group pf-m-action">
${!this.authenticating
? html` <button
class="pf-c-button pf-m-primary pf-m-block" class="pf-c-button pf-m-primary pf-m-block"
@click=${() => { @click=${() => {
this.authenticateWrapper(); this.authenticateWrapper();
}} }}
type="button"
> >
${msg("Retry authentication")} ${msg("Retry authentication")}
</button> </button>`
</div>` : nothing}
: html`<div class="pf-c-form__group pf-m-action"> ${this.renderReturnToDevicePicker()}
<p class="pf-m-block">&nbsp;</p> </div>
<p class="pf-m-block">&nbsp;</p> </form>
<p class="pf-m-block">&nbsp;</p> </div>`;
</div> `}
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
${this.showBackButton
? html`<li class="pf-c-login__main-footer-links-item">
<button
class="pf-c-button pf-m-secondary pf-m-block"
@click=${() => {
if (!this.host) return;
(
this.host as AuthenticatorValidateStage
).selectedDeviceChallenge = undefined;
}}
>
${msg("Return to device picker")}
</button>
</li>`
: html``}
</ul>
</footer>`;
} }
} }

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