Compare commits
57 Commits
main
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
6bb180f94e | |||
03dea17519 | |||
49d83f11bd | |||
5f0af81e4d | |||
63591e1710 | |||
6503a7b048 | |||
7e244e0679 | |||
c1998bf3f2 | |||
83372618a8 | |||
89a876e141 | |||
26d6e8bc5c | |||
d9dc373170 | |||
4ec37c5239 | |||
a9cfa6fe35 | |||
5ac5084149 | |||
eda38a30b1 | |||
9b84bf7174 | |||
f74549be6d | |||
76f4d7fb0a | |||
d1cf1dd083 | |||
2835fbd390 | |||
76ad2c8925 | |||
2270629fdc | |||
43a629efc1 | |||
4044e52403 | |||
aa7c846467 | |||
8ab7f4073b | |||
a05856c2ef | |||
9e9154e04a | |||
32549066c0 | |||
5ed3e879a2 | |||
4e4923ad0e | |||
0302d147e9 | |||
8256f1897d | |||
16d321835d | |||
f34612efe6 | |||
e82f147130 | |||
0ea6ad8eea | |||
f731443220 | |||
b70a66cde5 | |||
b733dbbcb0 | |||
e34d4c0669 | |||
310983a4d0 | |||
47b0fc86f7 | |||
b6e961b1f3 | |||
874d7ff320 | |||
e4a5bc9df6 | |||
318e0cf9f8 | |||
bd0815d894 | |||
af35ecfe66 | |||
0c05cd64bb | |||
cb80b76490 | |||
061d4bc758 | |||
8ff27f69e1 | |||
045cd98276 | |||
b520843984 | |||
92216e4ea8 |
@ -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
|
||||||
|
66
.github/actions/docker-push-variables/action.yml
vendored
66
.github/actions/docker-push-variables/action.yml
vendored
@ -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)
|
|
||||||
|
62
.github/actions/docker-push-variables/push_vars.py
vendored
Normal file
62
.github/actions/docker-push-variables/push_vars.py
vendored
Normal 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)
|
7
.github/actions/docker-push-variables/test.sh
vendored
Executable file
7
.github/actions/docker-push-variables/test.sh
vendored
Executable 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
|
10
.github/workflows/ci-main.yml
vendored
10
.github/workflows/ci-main.yml
vendored
@ -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
|
||||||
|
6
.github/workflows/ci-outpost.yml
vendored
6
.github/workflows/ci-outpost.yml
vendored
@ -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
|
||||||
|
10
.github/workflows/release-publish.yml
vendored
10
.github/workflows/release-publish.yml
vendored
@ -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
|
||||||
|
2
.github/workflows/release-tag.yml
vendored
2
.github/workflows/release-tag.yml
vendored
@ -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
|
||||||
|
@ -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
|
||||||
|
2
Makefile
2
Makefile
@ -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"
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
|
@ -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():
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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, **_
|
|
||||||
)
|
|
||||||
|
@ -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
|
||||||
|
@ -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]
|
||||||
|
@ -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()
|
||||||
|
@ -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):
|
||||||
|
@ -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()
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
58
authentik/rbac/tests/test_decorators.py
Normal file
58
authentik/rbac/tests/test_decorators.py
Normal 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)
|
@ -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]
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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=(
|
||||||
|
@ -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),
|
||||||
|
@ -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={},
|
||||||
)
|
)
|
||||||
|
@ -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={
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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(
|
||||||
|
@ -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>"])
|
||||||
|
@ -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:
|
||||||
|
@ -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"""
|
||||||
|
@ -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
|
||||||
|
14
authentik/tenants/signals.py
Normal file
14
authentik/tenants/signals.py
Normal 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)
|
@ -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:
|
||||||
|
@ -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 {
|
||||||
|
@ -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"
|
||||||
|
@ -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)
|
||||||
|
@ -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,))
|
||||||
|
|
||||||
|
|
||||||
|
12
lifecycle/system_migrations/template_schema.py
Normal file
12
lifecycle/system_migrations/template_schema.py
Normal 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
188
poetry.lock
generated
@ -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]
|
||||||
|
@ -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>"]
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -6,3 +6,4 @@ dist
|
|||||||
coverage
|
coverage
|
||||||
src/locale-codes.ts
|
src/locale-codes.ts
|
||||||
storybook-static/
|
storybook-static/
|
||||||
|
src/locales/**
|
||||||
|
@ -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")],
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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 ?? []}
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 = ";";
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
>
|
>
|
||||||
<ak-spinner-button
|
<ak-spinner-button
|
||||||
.callAction=${async () => {
|
.callAction=${async () => {
|
||||||
|
@ -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,
|
||||||
|
@ -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 {
|
||||||
|
@ -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>`}
|
||||||
|
@ -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>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"> </p>
|
</div>
|
||||||
<p class="pf-m-block"> </p>
|
</form>
|
||||||
<p class="pf-m-block"> </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
Reference in New Issue
Block a user