Compare commits
47 Commits
main
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
59401e07ad | |||
4008f0f28f | |||
210fbb0067 | |||
9c081d084d | |||
c7c75dc195 | |||
5448dc93db | |||
6d152bcc60 | |||
f1b7a9f934 | |||
4af75d0979 | |||
af0a314e0b | |||
6c7f901220 | |||
3570bfa39d | |||
35ab51e4c5 | |||
22eaf97d62 | |||
764b211bd4 | |||
7afc59d691 | |||
349572bfe4 | |||
bef55bc3a5 | |||
d86da24c01 | |||
25d0ee02a8 | |||
bccfb0b48c | |||
4b14eca2da | |||
7d5cfb6356 | |||
7ce46ccbe0 | |||
d0217c9135 | |||
d82ba344d9 | |||
01959132e8 | |||
9d81f0598c | |||
cbe429f3fa | |||
1cf0f57608 | |||
052da72acf | |||
9a1c76efe7 | |||
96b5bee912 | |||
09b3a1d0bd | |||
e87a17fd81 | |||
bb1bcb29cd | |||
0a5bdad972 | |||
d313225956 | |||
249dc276d4 | |||
5fb7dc4cb3 | |||
82930ee807 | |||
ac25fbab54 | |||
15cb6b18f6 | |||
fdd39b4b4c | |||
589304df4f | |||
4d920ff477 | |||
88dc616c5e |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2024.10.5
|
||||
current_version = 2024.12.5
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
|
@ -35,14 +35,6 @@ runs:
|
||||
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
|
||||
```
|
||||
|
||||
For arm64, use these values:
|
||||
|
||||
```shell
|
||||
AUTHENTIK_IMAGE=ghcr.io/goauthentik/dev-server
|
||||
AUTHENTIK_TAG=${{ inputs.tag }}-arm64
|
||||
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
|
||||
```
|
||||
|
||||
Afterwards, run the upgrade commands from the latest release notes.
|
||||
</details>
|
||||
<details>
|
||||
@ -60,18 +52,6 @@ runs:
|
||||
tag: ${{ inputs.tag }}
|
||||
```
|
||||
|
||||
For arm64, use these values:
|
||||
|
||||
```yaml
|
||||
authentik:
|
||||
outposts:
|
||||
container_image_base: ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
|
||||
global:
|
||||
image:
|
||||
repository: ghcr.io/goauthentik/dev-server
|
||||
tag: ${{ inputs.tag }}-arm64
|
||||
```
|
||||
|
||||
Afterwards, run the upgrade commands from the latest release notes.
|
||||
</details>
|
||||
edit-mode: replace
|
||||
|
14
.github/actions/docker-push-variables/action.yml
vendored
14
.github/actions/docker-push-variables/action.yml
vendored
@ -9,6 +9,9 @@ inputs:
|
||||
image-arch:
|
||||
required: false
|
||||
description: "Docker image arch"
|
||||
release:
|
||||
required: true
|
||||
description: "True if this is a release build, false if this is a dev/PR build"
|
||||
|
||||
outputs:
|
||||
shouldPush:
|
||||
@ -29,15 +32,24 @@ outputs:
|
||||
imageTags:
|
||||
description: "Docker image tags"
|
||||
value: ${{ steps.ev.outputs.imageTags }}
|
||||
imageTagsJSON:
|
||||
description: "Docker image tags, as a JSON array"
|
||||
value: ${{ steps.ev.outputs.imageTagsJSON }}
|
||||
attestImageNames:
|
||||
description: "Docker image names used for attestation"
|
||||
value: ${{ steps.ev.outputs.attestImageNames }}
|
||||
cacheTo:
|
||||
description: "cache-to value for the docker build step"
|
||||
value: ${{ steps.ev.outputs.cacheTo }}
|
||||
imageMainTag:
|
||||
description: "Docker image main tag"
|
||||
value: ${{ steps.ev.outputs.imageMainTag }}
|
||||
imageMainName:
|
||||
description: "Docker image main name"
|
||||
value: ${{ steps.ev.outputs.imageMainName }}
|
||||
imageBuildArgs:
|
||||
description: "Docker image build args"
|
||||
value: ${{ steps.ev.outputs.imageBuildArgs }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@ -48,6 +60,8 @@ runs:
|
||||
env:
|
||||
IMAGE_NAME: ${{ inputs.image-name }}
|
||||
IMAGE_ARCH: ${{ inputs.image-arch }}
|
||||
RELEASE: ${{ inputs.release }}
|
||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
REF: ${{ github.ref }}
|
||||
run: |
|
||||
python3 ${{ github.action_path }}/push_vars.py
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
import configparser
|
||||
import os
|
||||
from json import dumps
|
||||
from time import time
|
||||
|
||||
parser = configparser.ConfigParser()
|
||||
@ -48,7 +49,7 @@ if is_release:
|
||||
]
|
||||
else:
|
||||
suffix = ""
|
||||
if image_arch and image_arch != "amd64":
|
||||
if image_arch:
|
||||
suffix = f"-{image_arch}"
|
||||
for name in image_names:
|
||||
image_tags += [
|
||||
@ -70,12 +71,31 @@ def get_attest_image_names(image_with_tags: list[str]):
|
||||
return ",".join(set(image_tags))
|
||||
|
||||
|
||||
# Generate `cache-to` param
|
||||
cache_to = ""
|
||||
if should_push:
|
||||
_cache_tag = "buildcache"
|
||||
if image_arch:
|
||||
_cache_tag += f"-{image_arch}"
|
||||
cache_to = f"type=registry,ref={get_attest_image_names(image_tags)}:{_cache_tag},mode=max"
|
||||
|
||||
|
||||
image_build_args = []
|
||||
if os.getenv("RELEASE", "false").lower() == "true":
|
||||
image_build_args = [f"VERSION={os.getenv('REF')}"]
|
||||
else:
|
||||
image_build_args = [f"GIT_BUILD_HASH={sha}"]
|
||||
image_build_args = "\n".join(image_build_args)
|
||||
|
||||
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||
print(f"shouldPush={str(should_push).lower()}", file=_output)
|
||||
print(f"sha={sha}", file=_output)
|
||||
print(f"version={version}", file=_output)
|
||||
print(f"prerelease={prerelease}", file=_output)
|
||||
print(f"imageTags={','.join(image_tags)}", file=_output)
|
||||
print(f"imageTagsJSON={dumps(image_tags)}", file=_output)
|
||||
print(f"attestImageNames={get_attest_image_names(image_tags)}", file=_output)
|
||||
print(f"imageMainTag={image_main_tag}", file=_output)
|
||||
print(f"imageMainName={image_tags[0]}", file=_output)
|
||||
print(f"cacheTo={cache_to}", file=_output)
|
||||
print(f"imageBuildArgs={image_build_args}", file=_output)
|
||||
|
11
.github/actions/docker-push-variables/test.sh
vendored
11
.github/actions/docker-push-variables/test.sh
vendored
@ -1,7 +1,18 @@
|
||||
#!/bin/bash -x
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
# Non-pushing PR
|
||||
GITHUB_OUTPUT=/dev/stdout \
|
||||
GITHUB_REF=ref \
|
||||
GITHUB_SHA=sha \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||
GITHUB_REPOSITORY=goauthentik/authentik \
|
||||
python $SCRIPT_DIR/push_vars.py
|
||||
|
||||
# Pushing PR/main
|
||||
GITHUB_OUTPUT=/dev/stdout \
|
||||
GITHUB_REF=ref \
|
||||
GITHUB_SHA=sha \
|
||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
||||
GITHUB_REPOSITORY=goauthentik/authentik \
|
||||
DOCKER_USERNAME=foo \
|
||||
python $SCRIPT_DIR/push_vars.py
|
||||
|
95
.github/workflows/_reusable-docker-build-single.yaml
vendored
Normal file
95
.github/workflows/_reusable-docker-build-single.yaml
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
# Re-usable workflow for a single-architecture build
|
||||
name: Single-arch Container build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_name:
|
||||
required: true
|
||||
type: string
|
||||
image_arch:
|
||||
required: true
|
||||
type: string
|
||||
runs-on:
|
||||
required: true
|
||||
type: string
|
||||
registry_dockerhub:
|
||||
default: false
|
||||
type: boolean
|
||||
registry_ghcr:
|
||||
default: false
|
||||
type: boolean
|
||||
release:
|
||||
default: false
|
||||
type: boolean
|
||||
outputs:
|
||||
image-digest:
|
||||
value: ${{ jobs.build.outputs.image-digest }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ inputs.image_arch }}
|
||||
runs-on: ${{ inputs.runs-on }}
|
||||
outputs:
|
||||
image-digest: ${{ steps.push.outputs.digest }}
|
||||
permissions:
|
||||
# Needed to upload container images to ghcr.io
|
||||
packages: write
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-qemu-action@v3.3.0
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
image-arch: ${{ inputs.image_arch }}
|
||||
release: ${{ inputs.release }}
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: make empty clients
|
||||
if: ${{ inputs.release }}
|
||||
run: |
|
||||
mkdir -p ./gen-ts-api
|
||||
mkdir -p ./gen-go-api
|
||||
- name: generate ts client
|
||||
if: ${{ !inputs.release }}
|
||||
run: make gen-client-ts
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
id: push
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
secrets: |
|
||||
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
||||
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
|
||||
build-args: |
|
||||
${{ steps.ev.outputs.imageBuildArgs }}
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
platforms: linux/${{ inputs.image_arch }}
|
||||
cache-from: type=registry,ref=${{ steps.ev.outputs.attestImageNames }}:buildcache-${{ inputs.image_arch }}
|
||||
cache-to: ${{ steps.ev.outputs.cacheTo }}
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
102
.github/workflows/_reusable-docker-build.yaml
vendored
Normal file
102
.github/workflows/_reusable-docker-build.yaml
vendored
Normal file
@ -0,0 +1,102 @@
|
||||
# Re-usable workflow for a multi-architecture build
|
||||
name: Multi-arch container build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_name:
|
||||
required: true
|
||||
type: string
|
||||
registry_dockerhub:
|
||||
default: false
|
||||
type: boolean
|
||||
registry_ghcr:
|
||||
default: true
|
||||
type: boolean
|
||||
release:
|
||||
default: false
|
||||
type: boolean
|
||||
outputs: {}
|
||||
|
||||
jobs:
|
||||
build-server-amd64:
|
||||
uses: ./.github/workflows/_reusable-docker-build-single.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
image_name: ${{ inputs.image_name }}
|
||||
image_arch: amd64
|
||||
runs-on: ubuntu-latest
|
||||
registry_dockerhub: ${{ inputs.registry_dockerhub }}
|
||||
registry_ghcr: ${{ inputs.registry_ghcr }}
|
||||
release: ${{ inputs.release }}
|
||||
build-server-arm64:
|
||||
uses: ./.github/workflows/_reusable-docker-build-single.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
image_name: ${{ inputs.image_name }}
|
||||
image_arch: arm64
|
||||
runs-on: ubuntu-22.04-arm
|
||||
registry_dockerhub: ${{ inputs.registry_dockerhub }}
|
||||
registry_ghcr: ${{ inputs.registry_ghcr }}
|
||||
release: ${{ inputs.release }}
|
||||
get-tags:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-server-amd64
|
||||
- build-server-arm64
|
||||
outputs:
|
||||
tags: ${{ steps.ev.outputs.imageTagsJSON }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
merge-server:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- get-tags
|
||||
- build-server-amd64
|
||||
- build-server-arm64
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ${{ inputs.image_name }}
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ inputs.registry_dockerhub }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
if: ${{ inputs.registry_ghcr }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: int128/docker-manifest-create-action@v2
|
||||
id: build
|
||||
with:
|
||||
tags: ${{ matrix.tag }}
|
||||
sources: |
|
||||
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-amd64.outputs.image-digest }}
|
||||
${{ steps.ev.outputs.attestImageNames }}@${{ needs.build-server-arm64.outputs.image-digest }}
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
subject-digest: ${{ steps.build.outputs.digest }}
|
||||
push-to-registry: true
|
64
.github/workflows/ci-main.yml
vendored
64
.github/workflows/ci-main.yml
vendored
@ -223,68 +223,18 @@ jobs:
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- amd64
|
||||
- arm64
|
||||
needs: ci-core-mark
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
# Needed to upload container images to ghcr.io
|
||||
packages: write
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
attestations: write
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/dev-server
|
||||
image-arch: ${{ matrix.arch }}
|
||||
- name: Login to Container Registry
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: generate ts client
|
||||
run: make gen-client-ts
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
id: push
|
||||
with:
|
||||
context: .
|
||||
secrets: |
|
||||
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
||||
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache
|
||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max' || '' }}
|
||||
platforms: linux/${{ matrix.arch }}
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
needs: ci-core-mark
|
||||
uses: ./.github/workflows/_reusable-docker-build.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
image_name: ghcr.io/goauthentik/dev-server
|
||||
release: false
|
||||
pr-comment:
|
||||
needs:
|
||||
- build
|
||||
|
2
.github/workflows/ci-outpost.yml
vendored
2
.github/workflows/ci-outpost.yml
vendored
@ -72,7 +72,7 @@ jobs:
|
||||
- rac
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
# Needed to upload container images to ghcr.io
|
||||
packages: write
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -2,7 +2,7 @@ name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, "*", next, version*]
|
||||
branches: [main, next, version*]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
|
63
.github/workflows/release-publish.yml
vendored
63
.github/workflows/release-publish.yml
vendored
@ -7,64 +7,23 @@ on:
|
||||
|
||||
jobs:
|
||||
build-server:
|
||||
runs-on: ubuntu-latest
|
||||
uses: ./.github/workflows/_reusable-docker-build.yaml
|
||||
secrets: inherit
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
# Needed to upload container images to ghcr.io
|
||||
packages: write
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
with:
|
||||
image-name: ghcr.io/goauthentik/server,beryju/authentik
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: make empty clients
|
||||
run: |
|
||||
mkdir -p ./gen-ts-api
|
||||
mkdir -p ./gen-go-api
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v6
|
||||
id: push
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
secrets: |
|
||||
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
||||
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
|
||||
build-args: |
|
||||
VERSION=${{ github.ref }}
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- uses: actions/attest-build-provenance@v2
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
with:
|
||||
image_name: ghcr.io/goauthentik/server,beryju/authentik
|
||||
release: true
|
||||
registry_dockerhub: true
|
||||
registry_ghcr: true
|
||||
build-outpost:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload contianer images to ghcr.io
|
||||
# Needed to upload container images to ghcr.io
|
||||
packages: write
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
@ -188,8 +147,8 @@ jobs:
|
||||
aws-region: ${{ env.AWS_REGION }}
|
||||
- name: Upload template
|
||||
run: |
|
||||
aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
|
||||
aws s3 cp website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
|
||||
aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
|
||||
aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
|
||||
test-release:
|
||||
needs:
|
||||
- build-server
|
||||
|
11
.github/workflows/release-tag.yml
vendored
11
.github/workflows/release-tag.yml
vendored
@ -14,16 +14,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Pre-release test
|
||||
run: |
|
||||
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
|
||||
docker buildx install
|
||||
mkdir -p ./gen-ts-api
|
||||
docker build -t testing:latest .
|
||||
echo "AUTHENTIK_IMAGE=testing" >> .env
|
||||
echo "AUTHENTIK_TAG=latest" >> .env
|
||||
docker compose up --no-start
|
||||
docker compose start postgresql redis
|
||||
docker compose run -u root server test-all
|
||||
make test-docker
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
|
32
Dockerfile
32
Dockerfile
@ -94,7 +94,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 5: Python dependencies
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS python-deps
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-deps
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETVARIANT
|
||||
@ -116,15 +116,30 @@ RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
|
||||
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
|
||||
--mount=type=cache,target=/root/.cache/pip \
|
||||
--mount=type=cache,target=/root/.cache/pypoetry \
|
||||
pip install --no-cache cffi && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
build-essential libffi-dev \
|
||||
# Required for cryptography
|
||||
curl pkg-config \
|
||||
# Required for lxml
|
||||
libxslt-dev zlib1g-dev \
|
||||
# Required for xmlsec
|
||||
libltdl-dev \
|
||||
# Required for kadmin
|
||||
sccache clang && \
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y && \
|
||||
. "$HOME/.cargo/env" && \
|
||||
python -m venv /ak-root/venv/ && \
|
||||
bash -c "source ${VENV_PATH}/bin/activate && \
|
||||
pip3 install --upgrade pip && \
|
||||
pip3 install poetry && \
|
||||
pip3 install --upgrade pip poetry && \
|
||||
poetry config --local installer.no-binary cryptography,xmlsec,lxml,python-kadmin-rs && \
|
||||
poetry install --only=main --no-ansi --no-interaction --no-root && \
|
||||
pip install --force-reinstall /wheels/*"
|
||||
pip uninstall cryptography -y && \
|
||||
poetry install --only=main --no-ansi --no-interaction --no-root"
|
||||
|
||||
# Stage 6: Run
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS final-image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS final-image
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
@ -141,7 +156,7 @@ WORKDIR /
|
||||
# We cannot cache this layer otherwise we'll end up with a bigger image
|
||||
RUN apt-get update && \
|
||||
# Required for runtime
|
||||
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 && \
|
||||
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 libltdl7 libxslt1.1 && \
|
||||
# Required for bootstrap & healtcheck
|
||||
apt-get install -y --no-install-recommends runit && \
|
||||
apt-get clean && \
|
||||
@ -176,9 +191,8 @@ ENV TMPDIR=/dev/shm/ \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
|
||||
VENV_PATH="/ak-root/venv" \
|
||||
POETRY_VIRTUALENVS_CREATE=false
|
||||
|
||||
ENV GOFIPS=1
|
||||
POETRY_VIRTUALENVS_CREATE=false \
|
||||
GOFIPS=1
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
||||
|
||||
|
12
Makefile
12
Makefile
@ -45,15 +45,6 @@ help: ## Show this help
|
||||
go-test:
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
test-docker: ## Run all tests in a docker-compose
|
||||
echo "PG_PASS=$(shell openssl rand 32 | base64 -w 0)" >> .env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64 -w 0)" >> .env
|
||||
docker compose pull -q
|
||||
docker compose up --no-start
|
||||
docker compose start postgresql redis
|
||||
docker compose run -u root server test-all
|
||||
rm -f .env
|
||||
|
||||
test: ## Run the server tests and produce a coverage report (locally)
|
||||
coverage run manage.py test --keepdb authentik
|
||||
coverage html
|
||||
@ -263,6 +254,9 @@ docker: ## Build a docker image of the current source tree
|
||||
mkdir -p ${GEN_API_TS}
|
||||
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
|
||||
|
||||
test-docker:
|
||||
./scripts/test_docker.sh
|
||||
|
||||
#########################
|
||||
## CI
|
||||
#########################
|
||||
|
@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | --------- |
|
||||
| 2024.8.x | ✅ |
|
||||
| 2024.10.x | ✅ |
|
||||
| 2024.12.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2024.10.5"
|
||||
__version__ = "2024.12.5"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -1,15 +1,16 @@
|
||||
"""Application Roles API Viewset"""
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import ModelSerializer
|
||||
from authentik.core.models import (
|
||||
Application,
|
||||
ApplicationEntitlement,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
@ -18,7 +19,10 @@ class ApplicationEntitlementSerializer(ModelSerializer):
|
||||
|
||||
def validate_app(self, app: Application) -> Application:
|
||||
"""Ensure user has permission to view"""
|
||||
user: User = self._context["request"].user
|
||||
request: HttpRequest = self.context.get("request")
|
||||
if not request and SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||
return app
|
||||
user = request.user
|
||||
if user.has_perm("view_application", app) or user.has_perm(
|
||||
"authentik_core.view_application"
|
||||
):
|
||||
|
@ -3,6 +3,7 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
@ -16,7 +17,6 @@ from rest_framework.viewsets import ViewSet
|
||||
|
||||
from authentik.core.api.utils import MetaNameSerializer
|
||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.stages.authenticator import device_classes, devices_for_user
|
||||
from authentik.stages.authenticator.models import Device
|
||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
@ -73,7 +73,9 @@ class AdminDeviceViewSet(ViewSet):
|
||||
def get_devices(self, **kwargs):
|
||||
"""Get all devices in all child classes"""
|
||||
for model in device_classes():
|
||||
device_set = model.objects.filter(**kwargs)
|
||||
device_set = get_objects_for_user(
|
||||
self.request.user, f"{model._meta.app_label}.view_{model._meta.model_name}", model
|
||||
).filter(**kwargs)
|
||||
yield from device_set
|
||||
|
||||
@extend_schema(
|
||||
@ -86,10 +88,6 @@ class AdminDeviceViewSet(ViewSet):
|
||||
],
|
||||
responses={200: DeviceSerializer(many=True)},
|
||||
)
|
||||
@permission_required(
|
||||
None,
|
||||
[f"{model._meta.app_label}.view_{model._meta.model_name}" for model in device_classes()],
|
||||
)
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Get all devices for current user"""
|
||||
kwargs = {}
|
||||
|
@ -21,6 +21,7 @@ from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
|
||||
from authentik.core.models import GroupSourceConnection, Source, UserSourceConnection
|
||||
from authentik.core.types import UserSettingSerializer
|
||||
from authentik.lib.api import MultipleFieldLookupMixin
|
||||
from authentik.lib.utils.file import (
|
||||
FilePathSerializer,
|
||||
FileUploadSerializer,
|
||||
@ -75,6 +76,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
|
||||
|
||||
class SourceViewSet(
|
||||
MultipleFieldLookupMixin,
|
||||
TypesMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
@ -87,6 +89,7 @@ class SourceViewSet(
|
||||
queryset = Source.objects.none()
|
||||
serializer_class = SourceSerializer
|
||||
lookup_field = "slug"
|
||||
lookup_fields = ["slug", "pbm_uuid"]
|
||||
search_fields = ["slug", "name"]
|
||||
filterset_fields = ["slug", "name", "managed"]
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
"""User API Views"""
|
||||
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
from json import loads
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.db.models.functions import ExtractHour
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
@ -91,6 +92,7 @@ from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
LOGGER = get_logger()
|
||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
class UserGroupSerializer(ModelSerializer):
|
||||
@ -767,7 +769,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
if not instance.is_active:
|
||||
sessions = AuthenticatedSession.objects.filter(user=instance)
|
||||
session_ids = sessions.values_list("session_key", flat=True)
|
||||
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
|
||||
for session in session_ids:
|
||||
SessionStore(session).delete()
|
||||
sessions.delete()
|
||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||
return response
|
||||
|
@ -1,7 +1,10 @@
|
||||
"""authentik core signals"""
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.core.cache import cache
|
||||
from django.core.signals import Signal
|
||||
from django.db.models import Model
|
||||
@ -25,6 +28,7 @@ password_changed = Signal()
|
||||
login_failed = Signal()
|
||||
|
||||
LOGGER = get_logger()
|
||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@receiver(post_save, sender=Application)
|
||||
@ -60,8 +64,7 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||
"""Delete session when authenticated session is deleted"""
|
||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||
cache.delete(cache_key)
|
||||
SessionStore(instance.session_key).delete()
|
||||
|
||||
|
||||
@receiver(pre_save)
|
||||
|
@ -159,9 +159,9 @@ class ConnectionToken(ExpiringModel):
|
||||
default_settings["port"] = str(port)
|
||||
else:
|
||||
default_settings["hostname"] = self.endpoint.host
|
||||
default_settings["client-name"] = "authentik"
|
||||
# default_settings["enable-drive"] = "true"
|
||||
# default_settings["drive-name"] = "authentik"
|
||||
if self.endpoint.protocol == Protocols.RDP:
|
||||
default_settings["resize-method"] = "display-update"
|
||||
default_settings["client-name"] = f"authentik - {self.session.user}"
|
||||
settings = {}
|
||||
always_merger.merge(settings, default_settings)
|
||||
always_merger.merge(settings, self.endpoint.provider.settings)
|
||||
|
@ -50,9 +50,10 @@ class TestModels(TransactionTestCase):
|
||||
{
|
||||
"hostname": self.endpoint.host.split(":")[0],
|
||||
"port": "1324",
|
||||
"client-name": "authentik",
|
||||
"client-name": f"authentik - {self.user}",
|
||||
"drive-path": path,
|
||||
"create-drive-path": "true",
|
||||
"resize-method": "display-update",
|
||||
},
|
||||
)
|
||||
# Set settings in provider
|
||||
@ -63,10 +64,11 @@ class TestModels(TransactionTestCase):
|
||||
{
|
||||
"hostname": self.endpoint.host.split(":")[0],
|
||||
"port": "1324",
|
||||
"client-name": "authentik",
|
||||
"client-name": f"authentik - {self.user}",
|
||||
"drive-path": path,
|
||||
"create-drive-path": "true",
|
||||
"level": "provider",
|
||||
"resize-method": "display-update",
|
||||
},
|
||||
)
|
||||
# Set settings in endpoint
|
||||
@ -79,10 +81,11 @@ class TestModels(TransactionTestCase):
|
||||
{
|
||||
"hostname": self.endpoint.host.split(":")[0],
|
||||
"port": "1324",
|
||||
"client-name": "authentik",
|
||||
"client-name": f"authentik - {self.user}",
|
||||
"drive-path": path,
|
||||
"create-drive-path": "true",
|
||||
"level": "endpoint",
|
||||
"resize-method": "display-update",
|
||||
},
|
||||
)
|
||||
# Set settings in token
|
||||
@ -95,10 +98,11 @@ class TestModels(TransactionTestCase):
|
||||
{
|
||||
"hostname": self.endpoint.host.split(":")[0],
|
||||
"port": "1324",
|
||||
"client-name": "authentik",
|
||||
"client-name": f"authentik - {self.user}",
|
||||
"drive-path": path,
|
||||
"create-drive-path": "true",
|
||||
"level": "token",
|
||||
"resize-method": "display-update",
|
||||
},
|
||||
)
|
||||
# Set settings in property mapping (provider)
|
||||
@ -114,10 +118,11 @@ class TestModels(TransactionTestCase):
|
||||
{
|
||||
"hostname": self.endpoint.host.split(":")[0],
|
||||
"port": "1324",
|
||||
"client-name": "authentik",
|
||||
"client-name": f"authentik - {self.user}",
|
||||
"drive-path": path,
|
||||
"create-drive-path": "true",
|
||||
"level": "property_mapping_provider",
|
||||
"resize-method": "display-update",
|
||||
},
|
||||
)
|
||||
# Set settings in property mapping (endpoint)
|
||||
@ -135,11 +140,12 @@ class TestModels(TransactionTestCase):
|
||||
{
|
||||
"hostname": self.endpoint.host.split(":")[0],
|
||||
"port": "1324",
|
||||
"client-name": "authentik",
|
||||
"client-name": f"authentik - {self.user}",
|
||||
"drive-path": path,
|
||||
"create-drive-path": "true",
|
||||
"level": "property_mapping_endpoint",
|
||||
"foo": "true",
|
||||
"bar": "6",
|
||||
"resize-method": "display-update",
|
||||
},
|
||||
)
|
||||
|
@ -138,7 +138,6 @@ def notification_cleanup(self: SystemTask):
|
||||
"""Cleanup seen notifications and notifications whose event expired."""
|
||||
notifications = Notification.objects.filter(Q(event=None) | Q(seen=True))
|
||||
amount = notifications.count()
|
||||
for notification in notifications:
|
||||
notification.delete()
|
||||
notifications.delete()
|
||||
LOGGER.debug("Expired notifications", amount=amount)
|
||||
self.set_status(TaskStatus.SUCCESSFUL, f"Expired {amount} Notifications")
|
||||
|
@ -109,6 +109,8 @@ class FlowPlan:
|
||||
|
||||
def pop(self):
|
||||
"""Pop next pending stage from bottom of list"""
|
||||
if not self.markers and not self.bindings:
|
||||
return
|
||||
self.markers.pop(0)
|
||||
self.bindings.pop(0)
|
||||
|
||||
@ -156,8 +158,13 @@ class FlowPlan:
|
||||
final_stage: type[StageView] = self.bindings[-1].stage.view
|
||||
temp_exec = FlowExecutorView(flow=flow, request=request, plan=self)
|
||||
temp_exec.current_stage = self.bindings[-1].stage
|
||||
temp_exec.current_stage_view = final_stage
|
||||
temp_exec.setup(request, flow.slug)
|
||||
stage = final_stage(request=request, executor=temp_exec)
|
||||
return stage.dispatch(request)
|
||||
response = stage.dispatch(request)
|
||||
# Ensure we clean the flow state we have in the session before we redirect away
|
||||
temp_exec.stage_ok()
|
||||
return response
|
||||
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
|
@ -103,7 +103,7 @@ class FlowExecutorView(APIView):
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
flow: Flow
|
||||
flow: Flow = None
|
||||
|
||||
plan: FlowPlan | None = None
|
||||
current_binding: FlowStageBinding | None = None
|
||||
@ -114,7 +114,8 @@ class FlowExecutorView(APIView):
|
||||
|
||||
def setup(self, request: HttpRequest, flow_slug: str):
|
||||
super().setup(request, flow_slug=flow_slug)
|
||||
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
|
||||
if not self.flow:
|
||||
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
|
||||
self._logger = get_logger().bind(flow_slug=flow_slug)
|
||||
set_tag("authentik.flow", self.flow.slug)
|
||||
|
||||
|
@ -78,7 +78,9 @@ class FlowInspectorView(APIView):
|
||||
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
|
||||
if settings.DEBUG:
|
||||
return
|
||||
if request.user.has_perm("authentik_flow.inspect_flow", self.flow):
|
||||
if request.user.has_perm("authentik_flows.inspect_flow") or request.user.has_perm(
|
||||
"authentik_flows.inspect_flow", self.flow
|
||||
):
|
||||
return
|
||||
raise Http404
|
||||
|
||||
|
37
authentik/lib/api.py
Normal file
37
authentik/lib/api.py
Normal file
@ -0,0 +1,37 @@
|
||||
from collections.abc import Callable, Sequence
|
||||
from typing import Self
|
||||
from uuid import UUID
|
||||
|
||||
from django.db.models import Model, Q, QuerySet, UUIDField
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
|
||||
class MultipleFieldLookupMixin:
|
||||
"""Helper mixin class to add support for multiple lookup_fields.
|
||||
`lookup_fields` needs to be set which specifies the actual fields to query, `lookup_field`
|
||||
is only used to generate the URL."""
|
||||
|
||||
lookup_field: str
|
||||
lookup_fields: str | Sequence[str]
|
||||
|
||||
get_queryset: Callable[[Self], QuerySet]
|
||||
filter_queryset: Callable[[Self, QuerySet], QuerySet]
|
||||
|
||||
def get_object(self):
|
||||
queryset: QuerySet = self.get_queryset()
|
||||
queryset = self.filter_queryset(queryset)
|
||||
if isinstance(self.lookup_fields, str):
|
||||
self.lookup_fields = [self.lookup_fields]
|
||||
query = Q()
|
||||
model: Model = queryset.model
|
||||
for field in self.lookup_fields:
|
||||
field_inst = model._meta.get_field(field)
|
||||
# Sanity check, if the field we're filtering again, only apply the filter if
|
||||
# our value looks like a UUID
|
||||
if isinstance(field_inst, UUIDField):
|
||||
try:
|
||||
UUID(self.kwargs[self.lookup_field])
|
||||
except ValueError:
|
||||
continue
|
||||
query |= Q(**{field: self.kwargs[self.lookup_field]})
|
||||
return get_object_or_404(queryset, query)
|
@ -280,9 +280,25 @@ class ConfigLoader:
|
||||
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
||||
return default
|
||||
|
||||
def get_optional_int(self, path: str, default=None) -> int | None:
|
||||
"""Wrapper for get that converts value into int or None if set"""
|
||||
value = self.get(path, default)
|
||||
if value is UNSET:
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except (ValueError, TypeError) as exc:
|
||||
if value is None or (isinstance(value, str) and value.lower() == "null"):
|
||||
return None
|
||||
self.log("warning", "Failed to parse config as int", path=path, exc=str(exc))
|
||||
return default
|
||||
|
||||
def get_bool(self, path: str, default=False) -> bool:
|
||||
"""Wrapper for get that converts value into boolean"""
|
||||
return str(self.get(path, default)).lower() == "true"
|
||||
value = self.get(path, UNSET)
|
||||
if value is UNSET:
|
||||
return default
|
||||
return str(self.get(path)).lower() == "true"
|
||||
|
||||
def get_keys(self, path: str, sep=".") -> list[str]:
|
||||
"""List attribute keys by using yaml path"""
|
||||
@ -354,20 +370,33 @@ def django_db_config(config: ConfigLoader | None = None) -> dict:
|
||||
"sslcert": config.get("postgresql.sslcert"),
|
||||
"sslkey": config.get("postgresql.sslkey"),
|
||||
},
|
||||
"CONN_MAX_AGE": CONFIG.get_optional_int("postgresql.conn_max_age", 0),
|
||||
"CONN_HEALTH_CHECKS": CONFIG.get_bool("postgresql.conn_health_checks", False),
|
||||
"DISABLE_SERVER_SIDE_CURSORS": CONFIG.get_bool(
|
||||
"postgresql.disable_server_side_cursors", False
|
||||
),
|
||||
"TEST": {
|
||||
"NAME": config.get("postgresql.test.name"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
conn_max_age = CONFIG.get_optional_int("postgresql.conn_max_age", UNSET)
|
||||
disable_server_side_cursors = CONFIG.get_bool("postgresql.disable_server_side_cursors", UNSET)
|
||||
if config.get_bool("postgresql.use_pgpool", False):
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
||||
if disable_server_side_cursors is not UNSET:
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = disable_server_side_cursors
|
||||
|
||||
if config.get_bool("postgresql.use_pgbouncer", False):
|
||||
# https://docs.djangoproject.com/en/4.0/ref/databases/#transaction-pooling-server-side-cursors
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
|
||||
# https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections
|
||||
db["default"]["CONN_MAX_AGE"] = None # persistent
|
||||
if disable_server_side_cursors is not UNSET:
|
||||
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = disable_server_side_cursors
|
||||
if conn_max_age is not UNSET:
|
||||
db["default"]["CONN_MAX_AGE"] = conn_max_age
|
||||
|
||||
for replica in config.get_keys("postgresql.read_replicas"):
|
||||
_database = deepcopy(db["default"])
|
||||
|
@ -6,8 +6,6 @@ postgresql:
|
||||
user: authentik
|
||||
port: 5432
|
||||
password: "env://POSTGRES_PASSWORD"
|
||||
use_pgbouncer: false
|
||||
use_pgpool: false
|
||||
test:
|
||||
name: test_authentik
|
||||
read_replicas: {}
|
||||
|
@ -214,6 +214,9 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
}
|
||||
},
|
||||
)
|
||||
@ -251,6 +254,9 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
},
|
||||
"replica_0": {
|
||||
"ENGINE": "authentik.root.db",
|
||||
@ -266,6 +272,72 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
def test_db_read_replicas_pgbouncer(self):
|
||||
"""Test read replicas"""
|
||||
config = ConfigLoader()
|
||||
config.set("postgresql.host", "foo")
|
||||
config.set("postgresql.name", "foo")
|
||||
config.set("postgresql.user", "foo")
|
||||
config.set("postgresql.password", "foo")
|
||||
config.set("postgresql.port", "foo")
|
||||
config.set("postgresql.sslmode", "foo")
|
||||
config.set("postgresql.sslrootcert", "foo")
|
||||
config.set("postgresql.sslcert", "foo")
|
||||
config.set("postgresql.sslkey", "foo")
|
||||
config.set("postgresql.test.name", "foo")
|
||||
config.set("postgresql.use_pgbouncer", True)
|
||||
# Read replica
|
||||
config.set("postgresql.read_replicas.0.host", "bar")
|
||||
# Override conn_max_age
|
||||
config.set("postgresql.read_replicas.0.conn_max_age", 10)
|
||||
# This isn't supported
|
||||
config.set("postgresql.read_replicas.0.use_pgbouncer", False)
|
||||
conf = django_db_config(config)
|
||||
self.assertEqual(
|
||||
conf,
|
||||
{
|
||||
"default": {
|
||||
"DISABLE_SERVER_SIDE_CURSORS": True,
|
||||
"CONN_MAX_AGE": None,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "foo",
|
||||
"NAME": "foo",
|
||||
"OPTIONS": {
|
||||
"sslcert": "foo",
|
||||
"sslkey": "foo",
|
||||
"sslmode": "foo",
|
||||
"sslrootcert": "foo",
|
||||
},
|
||||
"PASSWORD": "foo",
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
},
|
||||
"replica_0": {
|
||||
"DISABLE_SERVER_SIDE_CURSORS": True,
|
||||
"CONN_MAX_AGE": 10,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "bar",
|
||||
"NAME": "foo",
|
||||
"OPTIONS": {
|
||||
"sslcert": "foo",
|
||||
"sslkey": "foo",
|
||||
"sslmode": "foo",
|
||||
"sslrootcert": "foo",
|
||||
},
|
||||
"PASSWORD": "foo",
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
},
|
||||
},
|
||||
)
|
||||
@ -294,6 +366,8 @@ class TestConfig(TestCase):
|
||||
{
|
||||
"default": {
|
||||
"DISABLE_SERVER_SIDE_CURSORS": True,
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "foo",
|
||||
"NAME": "foo",
|
||||
@ -310,6 +384,8 @@ class TestConfig(TestCase):
|
||||
},
|
||||
"replica_0": {
|
||||
"DISABLE_SERVER_SIDE_CURSORS": True,
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
"ENGINE": "authentik.root.db",
|
||||
"HOST": "bar",
|
||||
"NAME": "foo",
|
||||
@ -362,6 +438,9 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
},
|
||||
"replica_0": {
|
||||
"ENGINE": "authentik.root.db",
|
||||
@ -377,6 +456,9 @@ class TestConfig(TestCase):
|
||||
"PORT": "foo",
|
||||
"TEST": {"NAME": "foo"},
|
||||
"USER": "foo",
|
||||
"DISABLE_SERVER_SIDE_CURSORS": False,
|
||||
"CONN_MAX_AGE": 0,
|
||||
"CONN_HEALTH_CHECKS": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -499,11 +499,11 @@ class OAuthFulfillmentStage(StageView):
|
||||
)
|
||||
|
||||
challenge.is_valid()
|
||||
|
||||
self.executor.stage_ok()
|
||||
return HttpChallengeResponse(
|
||||
challenge=challenge,
|
||||
)
|
||||
|
||||
self.executor.stage_ok()
|
||||
return HttpResponseRedirectScheme(uri, allowed_schemes=[parsed.scheme])
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
|
@ -256,7 +256,7 @@ class AssertionProcessor:
|
||||
assertion.attrib["IssueInstant"] = self._issue_instant
|
||||
assertion.append(self.get_issuer())
|
||||
|
||||
if self.provider.signing_kp:
|
||||
if self.provider.signing_kp and self.provider.sign_assertion:
|
||||
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
|
||||
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
|
||||
)
|
||||
@ -295,6 +295,18 @@ class AssertionProcessor:
|
||||
|
||||
response.append(self.get_issuer())
|
||||
|
||||
if self.provider.signing_kp and self.provider.sign_response:
|
||||
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
|
||||
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
|
||||
)
|
||||
signature = xmlsec.template.create(
|
||||
response,
|
||||
xmlsec.constants.TransformExclC14N,
|
||||
sign_algorithm_transform,
|
||||
ns=xmlsec.constants.DSigNs,
|
||||
)
|
||||
response.append(signature)
|
||||
|
||||
status = SubElement(response, f"{{{NS_SAML_PROTOCOL}}}Status")
|
||||
status_code = SubElement(status, f"{{{NS_SAML_PROTOCOL}}}StatusCode")
|
||||
status_code.attrib["Value"] = "urn:oasis:names:tc:SAML:2.0:status:Success"
|
||||
|
@ -2,8 +2,10 @@
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
from defusedxml.lxml import fromstring
|
||||
from django.http.request import QueryDict
|
||||
from django.test import TestCase
|
||||
from lxml import etree # nosec
|
||||
|
||||
from authentik.blueprints.tests import apply_blueprint
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
@ -11,12 +13,14 @@ from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.lib.xml import lxml_from_string
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser
|
||||
from authentik.sources.saml.exceptions import MismatchedRequestID
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
NS_MAP,
|
||||
SAML_BINDING_REDIRECT,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||
@ -185,6 +189,19 @@ class TestAuthNRequest(TestCase):
|
||||
self.assertEqual(response.count(response_proc._assertion_id), 2)
|
||||
self.assertEqual(response.count(response_proc._response_id), 2)
|
||||
|
||||
schema = etree.XMLSchema(
|
||||
etree.parse("schemas/saml-schema-protocol-2.0.xsd", parser=etree.XMLParser()) # nosec
|
||||
)
|
||||
self.assertTrue(schema.validate(lxml_from_string(response)))
|
||||
|
||||
response_xml = fromstring(response)
|
||||
self.assertEqual(
|
||||
len(response_xml.xpath("//saml:Assertion/ds:Signature", namespaces=NS_MAP)), 1
|
||||
)
|
||||
self.assertEqual(
|
||||
len(response_xml.xpath("//samlp:Response/ds:Signature", namespaces=NS_MAP)), 1
|
||||
)
|
||||
|
||||
# Now parse the response (source)
|
||||
http_request.POST = QueryDict(mutable=True)
|
||||
http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode()
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models import Q, QuerySet
|
||||
from django_filters.filters import ModelChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import (
|
||||
CharField,
|
||||
@ -13,8 +14,11 @@ from rest_framework.fields import (
|
||||
ReadOnlyField,
|
||||
SerializerMethodField,
|
||||
)
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||
|
||||
from authentik.blueprints.v1.importer import excluded_models
|
||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.validators import RequiredTogetherValidator
|
||||
@ -92,7 +96,9 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
|
||||
queryset = Permission.objects.none()
|
||||
serializer_class = PermissionSerializer
|
||||
ordering = ["name"]
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
filterset_class = PermissionFilter
|
||||
permission_classes = [IsAuthenticated]
|
||||
search_fields = [
|
||||
"codename",
|
||||
"content_type__model",
|
||||
@ -100,13 +106,13 @@ class RBACPermissionViewSet(ReadOnlyModelViewSet):
|
||||
]
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
return (
|
||||
Permission.objects.all()
|
||||
.select_related("content_type")
|
||||
.filter(
|
||||
content_type__app_label__startswith="authentik",
|
||||
query = Q()
|
||||
for model in excluded_models():
|
||||
query |= Q(
|
||||
content_type__app_label=model._meta.app_label,
|
||||
content_type__model=model._meta.model_name,
|
||||
)
|
||||
)
|
||||
return Permission.objects.all().select_related("content_type").exclude(query)
|
||||
|
||||
|
||||
class PermissionAssignSerializer(PassiveSerializer):
|
||||
|
@ -13,6 +13,7 @@ from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.events.api.tasks import SystemTaskSerializer
|
||||
from authentik.lib.api import MultipleFieldLookupMixin
|
||||
from authentik.sources.kerberos.models import KerberosSource
|
||||
from authentik.sources.kerberos.tasks import CACHE_KEY_STATUS
|
||||
|
||||
@ -59,12 +60,13 @@ class KerberosSyncStatusSerializer(PassiveSerializer):
|
||||
tasks = SystemTaskSerializer(many=True, read_only=True)
|
||||
|
||||
|
||||
class KerberosSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
class KerberosSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
|
||||
"""Kerberos Source Viewset"""
|
||||
|
||||
queryset = KerberosSource.objects.all()
|
||||
serializer_class = KerberosSourceSerializer
|
||||
lookup_field = "slug"
|
||||
lookup_fields = ["slug", "pbm_uuid"]
|
||||
filterset_fields = [
|
||||
"name",
|
||||
"slug",
|
||||
|
@ -38,7 +38,9 @@ class KerberosBackend(InbuiltBackend):
|
||||
self, username: str, realm: str | None, password: str, **filters
|
||||
) -> tuple[User | None, KerberosSource | None]:
|
||||
sources = KerberosSource.objects.filter(enabled=True)
|
||||
user = User.objects.filter(usersourceconnection__source__in=sources, **filters).first()
|
||||
user = User.objects.filter(
|
||||
usersourceconnection__source__in=sources, username=username, **filters
|
||||
).first()
|
||||
|
||||
if user is not None:
|
||||
# User found, let's get its connections for the sources that are available
|
||||
@ -77,7 +79,7 @@ class KerberosBackend(InbuiltBackend):
|
||||
password, sender=user_source_connection.source
|
||||
)
|
||||
user_source_connection.user.save()
|
||||
return user, user_source_connection.source
|
||||
return user_source_connection.user, user_source_connection.source
|
||||
# Password doesn't match, onto next source
|
||||
LOGGER.debug(
|
||||
"failed to kinit, password invalid",
|
||||
|
@ -18,6 +18,7 @@ from authentik.core.api.property_mappings import PropertyMappingFilterSet, Prope
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.api import MultipleFieldLookupMixin
|
||||
from authentik.lib.sync.outgoing.api import SyncStatusSerializer
|
||||
from authentik.sources.ldap.models import LDAPSource, LDAPSourcePropertyMapping
|
||||
from authentik.sources.ldap.tasks import CACHE_KEY_STATUS, SYNC_CLASSES
|
||||
@ -103,12 +104,13 @@ class LDAPSourceSerializer(SourceSerializer):
|
||||
extra_kwargs = {"bind_password": {"write_only": True}}
|
||||
|
||||
|
||||
class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
class LDAPSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
|
||||
"""LDAP Source Viewset"""
|
||||
|
||||
queryset = LDAPSource.objects.all()
|
||||
serializer_class = LDAPSourceSerializer
|
||||
lookup_field = "slug"
|
||||
lookup_fields = ["slug", "pbm_uuid"]
|
||||
filterset_fields = [
|
||||
"name",
|
||||
"slug",
|
||||
|
@ -16,6 +16,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.lib.api import MultipleFieldLookupMixin
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.registry import SourceType, registry
|
||||
@ -170,12 +171,13 @@ class OAuthSourceFilter(FilterSet):
|
||||
]
|
||||
|
||||
|
||||
class OAuthSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
class OAuthSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = OAuthSource.objects.all()
|
||||
serializer_class = OAuthSourceSerializer
|
||||
lookup_field = "slug"
|
||||
lookup_fields = ["slug", "pbm_uuid"]
|
||||
filterset_class = OAuthSourceFilter
|
||||
search_fields = ["name", "slug"]
|
||||
ordering = ["name"]
|
||||
|
@ -18,6 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.flows.challenge import RedirectChallenge
|
||||
from authentik.flows.views.executor import to_stage_response
|
||||
from authentik.lib.api import MultipleFieldLookupMixin
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.sources.plex.models import PlexSource, UserPlexSourceConnection
|
||||
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager
|
||||
@ -45,12 +46,13 @@ class PlexTokenRedeemSerializer(PassiveSerializer):
|
||||
plex_token = CharField()
|
||||
|
||||
|
||||
class PlexSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
class PlexSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
|
||||
"""Plex source Viewset"""
|
||||
|
||||
queryset = PlexSource.objects.all()
|
||||
serializer_class = PlexSourceSerializer
|
||||
lookup_field = "slug"
|
||||
lookup_fields = ["slug", "pbm_uuid"]
|
||||
filterset_fields = [
|
||||
"name",
|
||||
"slug",
|
||||
|
@ -9,6 +9,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.lib.api import MultipleFieldLookupMixin
|
||||
from authentik.providers.saml.api.providers import SAMLMetadataSerializer
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
||||
@ -37,12 +38,13 @@ class SAMLSourceSerializer(SourceSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SAMLSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
class SAMLSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
|
||||
"""SAMLSource Viewset"""
|
||||
|
||||
queryset = SAMLSource.objects.all()
|
||||
serializer_class = SAMLSourceSerializer
|
||||
lookup_field = "slug"
|
||||
lookup_fields = ["slug", "pbm_uuid"]
|
||||
filterset_fields = [
|
||||
"name",
|
||||
"slug",
|
||||
|
@ -7,6 +7,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.core.api.tokens import TokenSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.lib.api import MultipleFieldLookupMixin
|
||||
from authentik.sources.scim.models import SCIMSource
|
||||
|
||||
|
||||
@ -47,12 +48,13 @@ class SCIMSourceSerializer(SourceSerializer):
|
||||
]
|
||||
|
||||
|
||||
class SCIMSourceViewSet(UsedByMixin, ModelViewSet):
|
||||
class SCIMSourceViewSet(MultipleFieldLookupMixin, UsedByMixin, ModelViewSet):
|
||||
"""SCIMSource Viewset"""
|
||||
|
||||
queryset = SCIMSource.objects.all()
|
||||
serializer_class = SCIMSourceSerializer
|
||||
lookup_field = "slug"
|
||||
lookup_fields = ["slug", "pbm_uuid"]
|
||||
filterset_fields = ["name", "slug"]
|
||||
search_fields = ["name", "slug", "token__identifier", "token__user__username"]
|
||||
ordering = ["name"]
|
||||
|
@ -20,7 +20,7 @@ from authentik.flows.planner import (
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN, InvalidStageError
|
||||
from authentik.flows.views.executor import SESSION_KEY_GET, SESSION_KEY_PLAN, InvalidStageError
|
||||
from authentik.lib.utils.urls import reverse_with_qs
|
||||
from authentik.stages.redirect.models import RedirectMode, RedirectStage
|
||||
|
||||
@ -72,7 +72,9 @@ class RedirectStageView(ChallengeStageView):
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
kwargs = self.executor.kwargs
|
||||
kwargs.update({"flow_slug": flow.slug})
|
||||
return reverse_with_qs("authentik_core:if-flow", self.request.GET, kwargs=kwargs)
|
||||
return reverse_with_qs(
|
||||
"authentik_core:if-flow", self.request.session[SESSION_KEY_GET], kwargs=kwargs
|
||||
)
|
||||
|
||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
"""Get the redirect target. Prioritize `redirect_stage_target` if present."""
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""Test Redirect stage"""
|
||||
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.urls.base import reverse
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
@ -58,6 +60,23 @@ class TestRedirectStage(FlowTestCase):
|
||||
response, reverse("authentik_core:if-flow", kwargs={"flow_slug": self.target_flow.slug})
|
||||
)
|
||||
|
||||
def test_flow_query(self):
|
||||
self.stage.mode = RedirectMode.FLOW
|
||||
self.stage.save()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
+ "?"
|
||||
+ urlencode({"query": urlencode({"test": "foo"})})
|
||||
)
|
||||
|
||||
self.assertStageRedirects(
|
||||
response,
|
||||
reverse("authentik_core:if-flow", kwargs={"flow_slug": self.target_flow.slug})
|
||||
+ "?"
|
||||
+ urlencode({"test": "foo"}),
|
||||
)
|
||||
|
||||
def test_override_static(self):
|
||||
policy = ExpressionPolicy.objects.create(
|
||||
name=generate_id(),
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2024.10.5 Blueprint schema",
|
||||
"title": "authentik 2024.12.5 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.5}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.5}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -54,7 +54,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.10.5}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.12.5}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2024.10.5"
|
||||
const VERSION = "2024.12.5"
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@ -16,11 +17,22 @@ import (
|
||||
"goauthentik.io/internal/constants"
|
||||
)
|
||||
|
||||
func (ac *APIController) getWebsocketURL(akURL url.URL, outpostUUID string, query url.Values) *url.URL {
|
||||
wsUrl := &url.URL{}
|
||||
wsUrl.Scheme = strings.ReplaceAll(akURL.Scheme, "http", "ws")
|
||||
wsUrl.Host = akURL.Host
|
||||
_p, _ := url.JoinPath(akURL.Path, "ws/outpost/", outpostUUID, "/")
|
||||
wsUrl.Path = _p
|
||||
v := url.Values{}
|
||||
maps.Insert(v, maps.All(akURL.Query()))
|
||||
maps.Insert(v, maps.All(query))
|
||||
wsUrl.RawQuery = v.Encode()
|
||||
return wsUrl
|
||||
}
|
||||
|
||||
func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
|
||||
pathTemplate := "%s://%s%sws/outpost/%s/?%s"
|
||||
query := akURL.Query()
|
||||
query.Set("instance_uuid", ac.instanceUUID.String())
|
||||
scheme := strings.ReplaceAll(akURL.Scheme, "http", "ws")
|
||||
|
||||
authHeader := fmt.Sprintf("Bearer %s", ac.token)
|
||||
|
||||
@ -37,7 +49,9 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
|
||||
},
|
||||
}
|
||||
|
||||
ws, _, err := dialer.Dial(fmt.Sprintf(pathTemplate, scheme, akURL.Host, akURL.Path, outpostUUID, akURL.Query().Encode()), header)
|
||||
wsu := ac.getWebsocketURL(akURL, outpostUUID, query).String()
|
||||
ac.logger.WithField("url", wsu).Debug("connecting to websocket")
|
||||
ws, _, err := dialer.Dial(wsu, header)
|
||||
if err != nil {
|
||||
ac.logger.WithError(err).Warning("failed to connect websocket")
|
||||
return err
|
||||
|
42
internal/outpost/ak/api_ws_test.go
Normal file
42
internal/outpost/ak/api_ws_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package ak
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func URLMustParse(u string) *url.URL {
|
||||
ur, err := url.Parse(u)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ur
|
||||
}
|
||||
|
||||
func TestWebsocketURL(t *testing.T) {
|
||||
u := URLMustParse("http://localhost:9000?foo=bar")
|
||||
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
|
||||
ac := &APIController{}
|
||||
nu := ac.getWebsocketURL(*u, uuid, url.Values{})
|
||||
assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?foo=bar", nu.String())
|
||||
}
|
||||
|
||||
func TestWebsocketURL_Query(t *testing.T) {
|
||||
u := URLMustParse("http://localhost:9000?foo=bar")
|
||||
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
|
||||
ac := &APIController{}
|
||||
v := url.Values{}
|
||||
v.Set("bar", "baz")
|
||||
nu := ac.getWebsocketURL(*u, uuid, v)
|
||||
assert.Equal(t, "ws://localhost:9000/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/?bar=baz&foo=bar", nu.String())
|
||||
}
|
||||
|
||||
func TestWebsocketURL_Subpath(t *testing.T) {
|
||||
u := URLMustParse("http://localhost:9000/foo/bar/")
|
||||
uuid := "23470845-7263-4fe3-bd79-ec1d7bf77d77"
|
||||
ac := &APIController{}
|
||||
nu := ac.getWebsocketURL(*u, uuid, url.Values{})
|
||||
assert.Equal(t, "ws://localhost:9000/foo/bar/ws/outpost/23470845-7263-4fe3-bd79-ec1d7bf77d77/", nu.String())
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env -S bash -e
|
||||
#!/usr/bin/env -S bash
|
||||
set -e -o pipefail
|
||||
MODE_FILE="${TMPDIR}/authentik-mode"
|
||||
|
||||
function log {
|
||||
@ -87,7 +88,6 @@ elif [[ "$1" == "bash" ]]; then
|
||||
elif [[ "$1" == "test-all" ]]; then
|
||||
prepare_debug
|
||||
chmod 777 /root
|
||||
pip install --force-reinstall /wheels/*
|
||||
check_if_root "python -m manage test authentik"
|
||||
elif [[ "$1" == "healthcheck" ]]; then
|
||||
run_authentik healthcheck $(cat $MODE_FILE)
|
||||
|
@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
|
||||
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
@ -1898,6 +1898,10 @@ msgstr "Kerberos 领域"
|
||||
msgid "Custom krb5.conf to use. Uses the system one by default"
|
||||
msgstr "要使用的自定义 krb5.conf。默认使用系统自带"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "KAdmin server type"
|
||||
msgstr "KAdmin 服务器类型"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Sync users from Kerberos into authentik"
|
||||
msgstr "从 Kerberos 同步用户到 authentik"
|
||||
@ -2858,7 +2862,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
" If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
@ -2882,7 +2886,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
"If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"如果您没有请求更改密码,请忽略此电子邮件。上面的链接在 %(expires)s 内有效。\n"
|
||||
@ -3151,6 +3155,22 @@ msgstr "输入阶段"
|
||||
msgid "Passwords don't match."
|
||||
msgstr "密码不匹配。"
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target URL should be present when mode is Static."
|
||||
msgstr "当模式为静态时,目标 URL 应存在。"
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target Flow should be present when mode is Flow."
|
||||
msgstr "当模式为流程时,目标流程应存在。"
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stage"
|
||||
msgstr "重定向阶段"
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stages"
|
||||
msgstr "重定向阶段"
|
||||
|
||||
#: authentik/stages/user_delete/models.py
|
||||
msgid "User Delete Stage"
|
||||
msgstr "用户删除阶段"
|
||||
|
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-11-26 00:09+0000\n"
|
||||
"POT-Creation-Date: 2024-12-18 13:31+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2024\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -1897,6 +1897,10 @@ msgstr "Kerberos 领域"
|
||||
msgid "Custom krb5.conf to use. Uses the system one by default"
|
||||
msgstr "要使用的自定义 krb5.conf。默认使用系统自带"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "KAdmin server type"
|
||||
msgstr "KAdmin 服务器类型"
|
||||
|
||||
#: authentik/sources/kerberos/models.py
|
||||
msgid "Sync users from Kerberos into authentik"
|
||||
msgstr "从 Kerberos 同步用户到 authentik"
|
||||
@ -2857,7 +2861,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
" If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
@ -2881,7 +2885,7 @@ msgstr ""
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"If you did not request a password change, please ignore this Email. The link above is valid for %(expires)s.\n"
|
||||
"If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"如果您没有请求更改密码,请忽略此电子邮件。上面的链接在 %(expires)s 内有效。\n"
|
||||
@ -3150,6 +3154,22 @@ msgstr "输入阶段"
|
||||
msgid "Passwords don't match."
|
||||
msgstr "密码不匹配。"
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target URL should be present when mode is Static."
|
||||
msgstr "当模式为静态时,目标 URL 应存在。"
|
||||
|
||||
#: authentik/stages/redirect/api.py
|
||||
msgid "Target Flow should be present when mode is Flow."
|
||||
msgstr "当模式为流程时,目标流程应存在。"
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stage"
|
||||
msgstr "重定向阶段"
|
||||
|
||||
#: authentik/stages/redirect/models.py
|
||||
msgid "Redirect Stages"
|
||||
msgstr "重定向阶段"
|
||||
|
||||
#: authentik/stages/user_delete/models.py
|
||||
msgid "User Delete Stage"
|
||||
msgstr "用户删除阶段"
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2024.10.5",
|
||||
"version": "2024.12.5",
|
||||
"private": true
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2024.10.5"
|
||||
version = "2024.12.5"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2024.10.5
|
||||
version: 2024.12.5
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
|
29
scripts/test_docker.sh
Executable file
29
scripts/test_docker.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
set -e -x -o pipefail
|
||||
hash="$(git rev-parse HEAD || openssl rand -base64 36)"
|
||||
|
||||
AUTHENTIK_IMAGE="xghcr.io/goauthentik/server"
|
||||
AUTHENTIK_TAG="$(echo "$hash" | cut -c1-15)"
|
||||
|
||||
if [ -f .env ]; then
|
||||
echo "Existing .env file, aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo PG_PASS="$(openssl rand -base64 36 | tr -d '\n')" >.env
|
||||
echo AUTHENTIK_SECRET_KEY="$(openssl rand -base64 60 | tr -d '\n')" >>.env
|
||||
echo AUTHENTIK_IMAGE="${AUTHENTIK_IMAGE}" >>.env
|
||||
echo AUTHENTIK_TAG="${AUTHENTIK_TAG}" >>.env
|
||||
export COMPOSE_PROJECT_NAME="authentik-test-${AUTHENTIK_TAG}"
|
||||
|
||||
# Ensure buildx is installed
|
||||
docker buildx install
|
||||
# For release builds we have an empty client here as we use the NPM package
|
||||
mkdir -p ./gen-ts-api
|
||||
touch .env
|
||||
|
||||
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" .
|
||||
docker compose up --no-start
|
||||
docker compose start postgresql redis
|
||||
docker compose run -u root server test-all
|
||||
docker compose down -v
|
@ -46,7 +46,7 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
|
||||
const connections = await new OutpostsApi(DEFAULT_CONFIG).outpostsServiceConnectionsAllList(
|
||||
await this.defaultEndpointConfig(),
|
||||
);
|
||||
Promise.all(
|
||||
await Promise.all(
|
||||
connections.results.map((connection) => {
|
||||
return new OutpostsApi(DEFAULT_CONFIG)
|
||||
.outpostsServiceConnectionsAllStateRetrieve({
|
||||
|
@ -291,7 +291,7 @@ export function renderForm(
|
||||
|
||||
${showHttpBasic ? renderHttpBasic(provider) : nothing}
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Trusted OIDC Sources")}
|
||||
label=${msg("Federated OIDC Sources")}
|
||||
name="jwtFederationSources"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
|
@ -131,9 +131,10 @@ export class UserListPage extends WithBrandConfig(WithCapabilitiesConfig(TablePa
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.activePath = getURLParam<string>("path", "/");
|
||||
const defaultPath = new DefaultUIConfig().defaults.userPath;
|
||||
this.activePath = getURLParam<string>("path", defaultPath);
|
||||
uiConfig().then((c) => {
|
||||
if (c.defaults.userPath !== new DefaultUIConfig().defaults.userPath) {
|
||||
if (c.defaults.userPath !== defaultPath) {
|
||||
this.activePath = c.defaults.userPath;
|
||||
}
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2024.10.5";
|
||||
export const VERSION = "2024.12.5";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -18,6 +18,12 @@ export class LoadingOverlay extends AKElement implements ILoadingOverlay {
|
||||
@property({ type: Boolean, attribute: "topmost" })
|
||||
topmost = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
loading = true;
|
||||
|
||||
@property({ type: String })
|
||||
icon = "";
|
||||
|
||||
static get styles() {
|
||||
return [
|
||||
PFBase,
|
||||
@ -40,7 +46,7 @@ export class LoadingOverlay extends AKElement implements ILoadingOverlay {
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ak-empty-state loading header="">
|
||||
return html`<ak-empty-state ?loading=${this.loading} header="" icon=${this.icon}>
|
||||
<span slot="body"><slot></slot></span>
|
||||
</ak-empty-state>`;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@ -86,7 +87,7 @@ export class SidebarBrand extends WithBrandConfig(AKElement) {
|
||||
<div class="pf-c-brand ak-brand">
|
||||
<img
|
||||
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
|
||||
alt="authentik Logo"
|
||||
alt="${msg("authentik Logo")}"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import "@goauthentik/elements/LoadingOverlay";
|
||||
import Guacamole from "guacamole-common-js";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
||||
@ -21,6 +21,23 @@ enum GuacClientState {
|
||||
DISCONNECTED = 5,
|
||||
}
|
||||
|
||||
export function GuacStateToString(state: GuacClientState): string {
|
||||
switch (state) {
|
||||
case GuacClientState.IDLE:
|
||||
return msg("Idle");
|
||||
case GuacClientState.CONNECTING:
|
||||
return msg("Connecting");
|
||||
case GuacClientState.WAITING:
|
||||
return msg("Waiting");
|
||||
case GuacClientState.CONNECTED:
|
||||
return msg("Connected");
|
||||
case GuacClientState.DISCONNECTING:
|
||||
return msg("Disconnecting");
|
||||
case GuacClientState.DISCONNECTED:
|
||||
return msg("Disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
const AUDIO_INPUT_MIMETYPE = "audio/L16;rate=44100,channels=2";
|
||||
const RECONNECT_ATTEMPTS_INITIAL = 5;
|
||||
const RECONNECT_ATTEMPTS = 5;
|
||||
@ -64,6 +81,9 @@ export class RacInterface extends Interface {
|
||||
@state()
|
||||
clientState?: GuacClientState;
|
||||
|
||||
@state()
|
||||
clientStatus?: Guacamole.Status;
|
||||
|
||||
@state()
|
||||
reconnectingMessage = "";
|
||||
|
||||
@ -138,6 +158,7 @@ export class RacInterface extends Interface {
|
||||
};
|
||||
this.client = new Guacamole.Client(this.tunnel);
|
||||
this.client.onerror = (err) => {
|
||||
this.clientStatus = err;
|
||||
console.debug("authentik/rac: error: ", err);
|
||||
this.reconnect();
|
||||
};
|
||||
@ -175,6 +196,10 @@ export class RacInterface extends Interface {
|
||||
const params = new URLSearchParams();
|
||||
params.set("screen_width", Math.floor(RacInterface.domSize().width).toString());
|
||||
params.set("screen_height", Math.floor(RacInterface.domSize().height).toString());
|
||||
// https://github.com/goauthentik/authentik/pull/11757
|
||||
// there are DPI issues when using SSH on HiDPi screens
|
||||
// but if we're not setting DPI at all the resolution is not respected at all
|
||||
params.set("screen_dpi", "96");
|
||||
this.client.connect(params.toString());
|
||||
}
|
||||
|
||||
@ -221,6 +246,7 @@ export class RacInterface extends Interface {
|
||||
return;
|
||||
}
|
||||
this.hasConnected = true;
|
||||
this.clientStatus = undefined;
|
||||
this.container = this.client.getDisplay().getElement();
|
||||
this.initMouse(this.container);
|
||||
this.client?.sendSize(
|
||||
@ -310,19 +336,35 @@ export class RacInterface extends Interface {
|
||||
console.debug("authentik/rac: Sent clipboard");
|
||||
}
|
||||
|
||||
renderOverlay() {
|
||||
if (!this.clientState || this.clientState == GuacClientState.CONNECTED) {
|
||||
return nothing;
|
||||
}
|
||||
let message = html`${GuacStateToString(this.clientState)}`;
|
||||
if (this.clientState == GuacClientState.WAITING) {
|
||||
message = html`${msg("Connecting...")}`;
|
||||
}
|
||||
if (this.hasConnected) {
|
||||
message = html`${this.reconnectingMessage}`;
|
||||
}
|
||||
if (this.clientStatus?.message) {
|
||||
message = html`${message}<br />${this.clientStatus.message}`;
|
||||
}
|
||||
const isLoading = [
|
||||
GuacClientState.CONNECTING,
|
||||
GuacClientState.DISCONNECTING,
|
||||
GuacClientState.WAITING,
|
||||
].includes(this.clientState);
|
||||
return html`
|
||||
<ak-loading-overlay ?loading=${isLoading} icon="fa fa-times">
|
||||
<span> ${message} </span>
|
||||
</ak-loading-overlay>
|
||||
`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
${this.clientState !== GuacClientState.CONNECTED
|
||||
? html`
|
||||
<ak-loading-overlay>
|
||||
<span>
|
||||
${this.hasConnected
|
||||
? html`${this.reconnectingMessage}`
|
||||
: html`${msg("Connecting...")}`}
|
||||
</span>
|
||||
</ak-loading-overlay>
|
||||
`
|
||||
: html``}
|
||||
${this.renderOverlay()}
|
||||
<div class="container">${this.container}</div>
|
||||
`;
|
||||
}
|
||||
|
@ -511,7 +511,7 @@ export class FlowExecutor extends Interface implements StageHost {
|
||||
DefaultBrand.brandingLogo,
|
||||
),
|
||||
)}"
|
||||
alt="authentik Logo"
|
||||
alt="${msg("authentik Logo")}"
|
||||
/>
|
||||
</div>
|
||||
${until(this.renderChallenge())}
|
||||
|
@ -8,6 +8,7 @@ import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||
import "rapidoc";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
@ -102,7 +103,7 @@ export class APIBrowser extends Interface {
|
||||
>
|
||||
<div slot="nav-logo">
|
||||
<img
|
||||
alt="authentik Logo"
|
||||
alt="${msg("authentik Logo")}"
|
||||
class="logo"
|
||||
src="${themeImage(
|
||||
first(this.brand?.brandingLogo, DefaultBrand.brandingLogo),
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file target-language="zh-Hans" source-language="en" original="lit-localize-inputs" datatype="plaintext">
|
||||
<body>
|
||||
<trans-unit id="s4caed5b7a7e5d89b">
|
||||
@ -596,9 +596,9 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="saa0e2675da69651b">
|
||||
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
||||
<target>未找到 URL "
|
||||
<x id="0" equiv-text="${this.url}"/>"。</target>
|
||||
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
||||
<target>未找到 URL "
|
||||
<x id="0" equiv-text="${this.url}"/>"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s58cd9c2fe836d9c6">
|
||||
@ -1737,8 +1737,8 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa90b7809586c35ce">
|
||||
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
||||
<target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
|
||||
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
||||
<target>输入完整 URL、相对路径,或者使用 'fa://fa-test' 来使用 Font Awesome 图标 "fa-test"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s0410779cb47de312">
|
||||
@ -2901,8 +2901,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s76768bebabb7d543">
|
||||
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
|
||||
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
|
||||
<source>Field which contains members of a group. Note that if using the "memberUid" field, the value is assumed to contain a relative distinguished name. e.g. 'memberUid=some-user' instead of 'memberUid=cn=some-user,ou=groups,...'</source>
|
||||
<target>包含组成员的字段。请注意,如果使用 "memberUid" 字段,则假定该值包含相对可分辨名称。例如,'memberUid=some-user' 而不是 'memberUid=cn=some-user,ou=groups,...'</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s026555347e589f0e">
|
||||
@ -3648,8 +3648,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s7b1fba26d245cb1c">
|
||||
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
|
||||
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
|
||||
<source>When using an external logging solution for archiving, this can be set to "minutes=5".</source>
|
||||
<target>使用外部日志记录解决方案进行存档时,可以将其设置为 "minutes=5"。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s44536d20bb5c8257">
|
||||
@ -3825,10 +3825,10 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa95a538bfbb86111">
|
||||
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
||||
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
||||
<target>您确定要更新
|
||||
<x id="0" equiv-text="${this.objectLabel}"/>"
|
||||
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
|
||||
<x id="0" equiv-text="${this.objectLabel}"/>"
|
||||
<x id="1" equiv-text="${this.obj?.name}"/>" 吗?</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc92d7cfb6ee1fec6">
|
||||
@ -4904,7 +4904,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sdf1d8edef27236f0">
|
||||
<source>A "roaming" authenticator, like a YubiKey</source>
|
||||
<source>A "roaming" authenticator, like a YubiKey</source>
|
||||
<target>像 YubiKey 这样的“漫游”身份验证器</target>
|
||||
|
||||
</trans-unit>
|
||||
@ -5273,7 +5273,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
<target>如果设置时长大于 0,用户可以选择“保持登录”选项,这将使用户的会话延长此处设置的时间。</target>
|
||||
|
||||
</trans-unit>
|
||||
@ -7674,7 +7674,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>成功创建用户并添加到组 <x id="0" equiv-text="${this.group.name}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s824e0943a7104668">
|
||||
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
||||
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
||||
<target>此用户将会被添加到组 &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s62e7f6ed7d9cb3ca">
|
||||
@ -9020,7 +9020,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>同步组</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2d5f69929bb7221d">
|
||||
<source><x id="0" equiv-text="${p.name}"/> ("<x id="1" equiv-text="${p.fieldKey}"/>", of type <x id="2" equiv-text="${p.type}"/>)</source>
|
||||
<source><x id="0" equiv-text="${p.name}"/> ("<x id="1" equiv-text="${p.fieldKey}"/>", of type <x id="2" equiv-text="${p.type}"/>)</source>
|
||||
<target><x id="0" equiv-text="${p.name}"/>(&quot;<x id="1" equiv-text="${p.fieldKey}"/>&quot;,类型为 <x id="2" equiv-text="${p.type}"/>)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa38c5a2731be3a46">
|
||||
@ -9272,8 +9272,8 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>授权流程成功后有效的重定向 URI。还可以在此处为隐式流程指定任何来源。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4c49d27de60a532b">
|
||||
<source>To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.</source>
|
||||
<target>要允许任何重定向 URI,请设置模式为正则表达式,并将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
|
||||
<source>To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.</source>
|
||||
<target>要允许任何重定向 URI,请设置模式为正则表达式,并将此值设置为 ".*"。请注意这可能带来的安全影响。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s43f899a86c6a3484">
|
||||
<source>Redirect URIs/Origins</source>
|
||||
@ -9301,67 +9301,88 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3cc2b33d2a8000d3">
|
||||
<source>KAdmin type</source>
|
||||
<target>KAdmin 类型</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s624e1c8739507529">
|
||||
<source>MIT krb5 kadmin</source>
|
||||
<target>MIT krb5 kadmin</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d225d9e74dfff6f">
|
||||
<source>Heimdal kadmin</source>
|
||||
<target>Heimdal kadmin</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc9e494c8346b7cb5">
|
||||
<source>Other</source>
|
||||
<target>其他</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbf6c78047e8ec8f8">
|
||||
<source>Other type of kadmin</source>
|
||||
<target>其他类型 kadmin</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb53d0b77abef2316">
|
||||
<source>To let a user directly reset their password, configure a recovery flow on the currently active brand.</source>
|
||||
<target>要让用户直接重置密码,请在当前活动的品牌上配置恢复流程。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e5226fcf269689b">
|
||||
<source>Consent given lasts indefinitely</source>
|
||||
<target>无限期同意授权</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7eff620292ed9349">
|
||||
<source>Consent expires</source>
|
||||
<target>同意授权会过期</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1cc032bcc50b2942">
|
||||
<source>Available Policies</source>
|
||||
<target>可用策略</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3ad64193ad5f4a5e">
|
||||
<source>Selected Policies</source>
|
||||
<target>已选策略</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc487e11d5987dbb4">
|
||||
<source>Redirect the user to another flow, potentially with all gathered context</source>
|
||||
<target>将用户重定向到另一个流程,可能包含所有已收集的上下文</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad9d5481474d4f5b">
|
||||
<source>Static</source>
|
||||
<target>静态</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se87a96950464bc89">
|
||||
<source>Target URL</source>
|
||||
<target>目标 URL</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7f3097955b19736a">
|
||||
<source>Redirect the user to a static URL.</source>
|
||||
<target>将用户重定向到一个静态 URL。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9bdee1c5130c8240">
|
||||
<source>Target Flow</source>
|
||||
<target>目标流程</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa5d1405b8d6529c7">
|
||||
<source>Redirect the user to a Flow.</source>
|
||||
<target>将用户重定向到一个流程。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7c9db337d14d42b3">
|
||||
<source>Keep flow context</source>
|
||||
<target>保留流程上下文</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0d7dea184036a74d">
|
||||
<source>Require no authentication</source>
|
||||
<target>需要无身份验证</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66f533986ba6182c">
|
||||
<source>Require superuser</source>
|
||||
<target>需要管理员用户</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s26c0a8789930b5fd">
|
||||
<source>Require being redirected from another flow</source>
|
||||
<target>需要重定向自另一个流程</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbfaee8cfbf4e44e8">
|
||||
<source>Require Outpost (flow can only be executed from an outpost)</source>
|
||||
<target>需要前哨(流程只能从前哨执行)</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
@ -4967,16 +4967,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Always require consent</source>
|
||||
<target>始终需要征得同意授权</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s8ce8bdc9cc9c8604">
|
||||
<source>Consent given last indefinitely</source>
|
||||
<target>无限期同意授权</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sb986f15fa9b17805">
|
||||
<source>Consent expires.</source>
|
||||
<target>同意授权会过期。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s6f328f2d8382d998">
|
||||
<source>Consent expires in</source>
|
||||
@ -5478,16 +5468,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Require authentication</source>
|
||||
<target>需要身份验证</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s239c2a351cde6d39">
|
||||
<source>Require no authentication.</source>
|
||||
<target>需要无身份验证。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s98beadfeeb3acb66">
|
||||
<source>Require superuser.</source>
|
||||
<target>需要管理员用户。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sfad9279cc42c6b61">
|
||||
<source>Required authentication level for this flow.</source>
|
||||
@ -7765,10 +7745,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Event volume</source>
|
||||
<target>事件容量</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s047a5f0211fedc72">
|
||||
<source>Require Outpost (flow can only be executed from an outpost).</source>
|
||||
<target>需要前哨(流程只能从前哨执行)。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3271da6c18c25b18">
|
||||
<source>Connection settings.</source>
|
||||
<target>连接设置。</target>
|
||||
@ -9322,6 +9298,90 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s4f8a3f7792e6b940">
|
||||
<source>JWTs signed by the selected providers can be used to authenticate to this provider.</source>
|
||||
<target>由已选提供程序签发的 JWT 可以用于此提供程序的身份验证。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3cc2b33d2a8000d3">
|
||||
<source>KAdmin type</source>
|
||||
<target>KAdmin 类型</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s624e1c8739507529">
|
||||
<source>MIT krb5 kadmin</source>
|
||||
<target>MIT krb5 kadmin</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6d225d9e74dfff6f">
|
||||
<source>Heimdal kadmin</source>
|
||||
<target>Heimdal kadmin</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc9e494c8346b7cb5">
|
||||
<source>Other</source>
|
||||
<target>其他</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbf6c78047e8ec8f8">
|
||||
<source>Other type of kadmin</source>
|
||||
<target>其他类型 kadmin</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb53d0b77abef2316">
|
||||
<source>To let a user directly reset their password, configure a recovery flow on the currently active brand.</source>
|
||||
<target>要让用户直接重置密码,请在当前活动的品牌上配置恢复流程。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e5226fcf269689b">
|
||||
<source>Consent given lasts indefinitely</source>
|
||||
<target>无限期同意授权</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7eff620292ed9349">
|
||||
<source>Consent expires</source>
|
||||
<target>同意授权会过期</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1cc032bcc50b2942">
|
||||
<source>Available Policies</source>
|
||||
<target>可用策略</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s3ad64193ad5f4a5e">
|
||||
<source>Selected Policies</source>
|
||||
<target>已选策略</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc487e11d5987dbb4">
|
||||
<source>Redirect the user to another flow, potentially with all gathered context</source>
|
||||
<target>将用户重定向到另一个流程,可能包含所有已收集的上下文</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sad9d5481474d4f5b">
|
||||
<source>Static</source>
|
||||
<target>静态</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se87a96950464bc89">
|
||||
<source>Target URL</source>
|
||||
<target>目标 URL</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7f3097955b19736a">
|
||||
<source>Redirect the user to a static URL.</source>
|
||||
<target>将用户重定向到一个静态 URL。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9bdee1c5130c8240">
|
||||
<source>Target Flow</source>
|
||||
<target>目标流程</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa5d1405b8d6529c7">
|
||||
<source>Redirect the user to a Flow.</source>
|
||||
<target>将用户重定向到一个流程。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7c9db337d14d42b3">
|
||||
<source>Keep flow context</source>
|
||||
<target>保留流程上下文</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0d7dea184036a74d">
|
||||
<source>Require no authentication</source>
|
||||
<target>需要无身份验证</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s66f533986ba6182c">
|
||||
<source>Require superuser</source>
|
||||
<target>需要管理员用户</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s26c0a8789930b5fd">
|
||||
<source>Require being redirected from another flow</source>
|
||||
<target>需要重定向自另一个流程</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbfaee8cfbf4e44e8">
|
||||
<source>Require Outpost (flow can only be executed from an outpost)</source>
|
||||
<target>需要前哨(流程只能从前哨执行)</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -20,13 +20,21 @@ To add an application to authentik and have it display on users' **My applicatio
|
||||
|
||||
2. Click **Create with Wizard**. (Alternatively, use our legacy process and click **Create**. The legacy process requires that the application and its authentication provider be configured separately.)
|
||||
|
||||
3. In the **New application** wizard, define the application details, the provider type and configuration, and then click **Submit**.
|
||||
3. In the **New application** wizard, define the application details, the provider type, bindings for the application.
|
||||
|
||||
4. To manage the display of the new application on the **My applications** page, you can optionally define the bindings for a specific policy, group, or user. Note that if you do not define bindings, then all users have access to the application, For more information, refer to [authorization](#authorization).
|
||||
- **Application**: provide a name, an optional group for the type of application, the policy engine mode, and optional UI settings.
|
||||
|
||||
## Authorization
|
||||
- **Choose a Provider**: select the provider types for this application.
|
||||
|
||||
Application access can be configured using (Policy) bindings. Click on an application in the applications list, and select the _Policy / Group / User Bindings_ tab. There you can bind users/groups/policies to grant them access. When nothing is bound, everyone has access. You can use this to grant access to one or multiple users/groups, or dynamically give access using policies.
|
||||
- **Configure a Provider**: provide a name (or accept the auto-provided name), the authorization flow to use for this provider, and any additional required configurations.
|
||||
|
||||
- **Configure Bindings**: to manage the listing and access to applications on a user's **My applications** page, you can optionally create a [binding](../flows-stages/bindings/index.md) between the application and a specific policy, group, or user. Note that if you do not define any bindings, then all users have access to the application. For more information about user access, refer to our documentation about [authorization](#policy-driven-authorization) and [hiding an application](#hide-applications).
|
||||
|
||||
4. On the **Review and Submit Application** panel, review the configuration for the new application and its provider, and then click **Submit**.
|
||||
|
||||
## Policy-driven authorization
|
||||
|
||||
To use a [policy](../../customize/policies/index.md) to control which users or groups can access an application, click on an application in the applications list and then select the **Policy/Group/User Bindings** tab. There you can bind users/groups/policies to grant them access. When nothing is bound, everyone has access. Binding a policy restricts access to specific Users or Groups, or by other custom policies such as restriction to a set time-of-day or a geographic region.
|
||||
|
||||
By default, all users can access applications when no policies are bound.
|
||||
|
||||
@ -35,6 +43,49 @@ When multiple policies/groups/users are attached, you can configure the _Policy
|
||||
- Require users to pass all bindings/be member of all groups (ALL), or
|
||||
- Require users to pass either binding/be member of either group (ANY)
|
||||
|
||||
## Application Entitlements
|
||||
|
||||
<span class="badge badge--preview">Preview</span>
|
||||
<span class="badge badge--version">authentik 2024.12+</span>
|
||||
|
||||
Application entitlements can be used through authentik to manage authorization within an application (what areas of the app users or groups can access). Entitlements are scoped to a single application and can be bound to multiple users and/or groups (binding policies is not currently supported), giving them access to the entitlement. An application can either check for the name of the entitlement (via the `entitlements` scope), or via attributes stored in entitlements.
|
||||
|
||||
An authentik admin can create an entitlement [in the Admin interface](#create-an-application-entitlement) or using the [authentik API](../../developer-docs/api/api.md).
|
||||
|
||||
Because entitlements exist within an application, names of entitlements must be unique within an application. This also means that entitlements are deleted when an application is deleted.
|
||||
|
||||
### Using entitlements
|
||||
|
||||
Entitlements to which a user has access can be retrieved using the `user.app_entitlements()` function in property mappings/policies. This function needs to be passed the specific application for which to get the entitlements. For example:
|
||||
|
||||
```python
|
||||
entitlements = [entitlement.name for entitlement in request.user.app_entitlements(provider.application)]
|
||||
return {
|
||||
"entitlements": entitlements,
|
||||
}
|
||||
```
|
||||
|
||||
### Attributes
|
||||
|
||||
Each entitlement can store attributes similar to user and group attributes. These attributes can be accessed in property mappings and passed to applications via `user.app_entitlements_attributes`. For example:
|
||||
|
||||
```python
|
||||
attrs = request.user.app_entitlements(provider.application)
|
||||
return {
|
||||
"my_attr": attrs.get("my_attr")
|
||||
}
|
||||
```
|
||||
|
||||
### Create an application entitlement
|
||||
|
||||
1. Open the Admin interface and navigate to **Applications -> Applications**.
|
||||
2. Click the name of the application for which you want to create an entitlement.
|
||||
3. Click the **Application entitlements** tab at the top of the page, and then click **Create entitlement**. Provide a name for the entitlement, enter any optional **Attributes**, and then click **Create**.
|
||||
4. In the list locate the entitlement to which you want to bind a user or group, and then **click the caret (>) to expand the entitlement details.**
|
||||
5. In the expanded area, click **Bind existing Group/User**.
|
||||
6. In the **Create Binding** modal box, select either the tab for **Group** or **User**, and then in the drop-down list, select the group or user.
|
||||
7. Optionally, configure additional settings for the binding, and then click **Create** to create the binding and close the modal box.
|
||||
|
||||
## Hide applications
|
||||
|
||||
To hide an application without modifying its policy settings or removing it, you can simply set the _Launch URL_ to `blank://blank`, which will hide the application from users.
|
||||
|
33
website/docs/add-secure-apps/flows-stages/bindings/index.md
Normal file
33
website/docs/add-secure-apps/flows-stages/bindings/index.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Bindings
|
||||
---
|
||||
|
||||
A binding is, simply put, a connection between two components (a flow, stage, policy, user, or group). The use of a binding adds additional functionality to one those existing components; for example, a policy binding can cause a new stage to be presented within a flow to a specific user or group.
|
||||
|
||||
:::info
|
||||
For information about creating and managing bindings, refer to [Working with bindings](./work_with_bindings.md).
|
||||
:::
|
||||
|
||||
Bindings are an important part of authentik; the majority of configuration options are set in bindings.
|
||||
|
||||
Bindings are analyzed by authentik's Flow Plan, which starts with the flow, then assesses all of the bound policies, and then runs them in order to build out the plan.
|
||||
|
||||
The two most common types of bindings in authentik are:
|
||||
|
||||
- stage bindings
|
||||
- policy bindings
|
||||
- user and group bindings
|
||||
|
||||
A _stage binding_ connects a stage to a flow. The "additional content" (i.e. the content in the stage) is now added to the flow.
|
||||
|
||||
A _policy binding_ connects a specific policy to a flow or to a stage. With the binding, the flow (or stage) will now have additional content (i.e. the policy rules).
|
||||
|
||||
You can also bind groups and users to another component (a policy, a stage, a flow, etc.). For example, you can create a binding for a specific group, and then [bind that to a stage binding](../stages/index.md#bind-users-and-groups-to-a-flows-stage-binding), with the result that everyone in that group now will see that stage (and any policies bound to that stage) as part of their flow. Or more specifically, and going one step deeper, you can also _bind a binding to a binding_.
|
||||
|
||||
Bindings are also used for [Application Entitlements](../../applications/manage_apps.md#application-entitlements), where you can bind specific users or groups to an application as a way to manage who has access to the application.
|
||||
|
||||
It's important to remember that bindings are instantiated objects themselves, and conceptually can be considered as a "connector" between two components. This is why you might read about "binding a binding", because technically, a binding is "spliced" into another binding, in order to intercept and enforce the criteria defined in the second binding.
|
||||
|
||||
:::info
|
||||
Be aware that some stages and flows do not allow user or group bindings, because in certain scenarios (authentication or enrollment), the flow plan doesn't yet know who the user or group is.
|
||||
:::
|
@ -0,0 +1,13 @@
|
||||
---
|
||||
title: Work with bindings
|
||||
---
|
||||
|
||||
As covered in the [overview](./index.md), bindings interact with many other components.
|
||||
|
||||
For instructions to create a binding, refer to the documentation for the specific components:
|
||||
|
||||
- [Bind a stage to a flow](../stages/index.md#bind-a-stage-to-a-flow)
|
||||
- [Bind a policy to a flow or stage](../../../customize/policies/working_with_policies#bind-a-policy-to-a-flow-or-stage)
|
||||
- [Bind users or groups to a specific application with an Application Entitlement](../../applications/manage_apps.md#application-entitlements)
|
||||
- [Bind a policy to a specific application when you create a new app using the Wizard](../../applications/manage_apps.md#instructions)
|
||||
- [Bind users and groups to a stage binding, to define whether or not that stage is shown](../stages/index.md#bind-users-and-groups-to-a-flows-stage-binding)
|
Binary file not shown.
After Width: | Height: | Size: 208 KiB |
@ -54,3 +54,24 @@ To bind a stage to a flow, follow these steps:
|
||||
3. In the list of flows, click the name of the flow to which you want to bind one or more stages.
|
||||
4. On the Flow page, click the **Stage Bindings** tab at the top.
|
||||
5. Here, you can decide if you want to create a new stage and bind it to the flow (**Create and bind Stage**), or if you want to select an existing stage and bind it to the flow (**Bind existing stage**).
|
||||
|
||||
## Bind users and groups to a flow's stage binding
|
||||
|
||||
You can use bindings to determine whehther or not a stage is presented to a single user or any users within a group. You do this by binding the user or group to a stage binding within a specific flow. For example, if you have a flow that contains a stage that prompts the user for multi-factor authentication, but you only want certain users to see this stage (and fulfill the MFA prompt), then you would bind the appropriate group (or single user) to the stage binding for that flow.
|
||||
|
||||
To bind a user or a group to a stage binding for a specific flow, follow these steps:
|
||||
|
||||
1. Log in as an admin to authentik, and go to the Admin interface.
|
||||
2. In the Admin interface, navigate to **Flows and Stages -> Flows**.
|
||||
3. In the list of flows, click the name of the flow to which you want to bind one or more stages.
|
||||
4. On the Flow page, click the **Stage Bindings** tab at the top.
|
||||
5. Locate the stage binding to which you want to bind a user or group, and then **click the caret (>) to expand the stage binding details.**
|
||||
|
||||

|
||||
|
||||
6. In the expanded area, click **Bind existing policy/group/user**.
|
||||
7. In the **Create Binding** modal box, select either the tab for **Group** or **User**.
|
||||
8. In the drop-down list, select the group or user.
|
||||
9. Optionally, configure additional settings for the binding, and then click **Create** to create the binding and close the modal box.
|
||||
|
||||
Learn more about [bindings](../bindings/index.md) and [working with them](../bindings/work_with_bindings.md).
|
||||
|
@ -9,7 +9,7 @@ The main settings that brands influence are flows and branding.
|
||||
|
||||
## Flows
|
||||
|
||||
You can explicitly select, in your instance's Brand settings, the default flow to use for the following configurations:
|
||||
You can explicitly select, in your instance's Brand settings, the _default flows_ to use for the current brand. To do so, log in as an administrator, open the Admin interface, and navigate to **System -> Brands**. There you can optionally configure these default flows:
|
||||
|
||||
- Authentication flow: the flow used to authenticate users. If left empty, the first applicable flow sorted by the slug is used.
|
||||
- Invalidation flow: for typical use cases, select the `default-invalidation-flow` (Logout) flow. This flow logs the user out of authentik when the application session ends (user logs out of the app).
|
||||
|
@ -8,6 +8,8 @@ authentik provides several [standard policy types](./index.md#standard-policies)
|
||||
|
||||
We also document how to use a policy to [whitelist email domains](./expression/whitelist_email.md) and to [ensure unique email addresses](./expression/unique_email.md).
|
||||
|
||||
To learn more see also [bindings](../../add-secure-apps/flows-stages/bindings/index.md) and how to use the [authentik Wizard to bind policy bindings to the new application](../../add-secure-apps/applications/manage_apps.md#add-new-applications) (for example, to configure application-specific access).
|
||||
|
||||
## Create a policy
|
||||
|
||||
To create a new policy, follow these steps:
|
||||
@ -22,7 +24,7 @@ To create a new policy, follow these steps:
|
||||
After creating the policy, you can bind it to either a [flow](../../add-secure-apps/flows-stages/flow/index.md) or to a [stage](../../add-secure-apps/flows-stages/stages/index.md).
|
||||
|
||||
:::info
|
||||
Bindings are instantiated objects themselves, and conceptually can be considered as the "connector" between the policy and the stage or flow. This is why you might read about "binding a binding", because technically, a binding is "spliced" into another binding, in order to intercept and enforce the criteria defined in the policy. You can edit bindings on a flow's **Stage Bindings** tab.
|
||||
Bindings are instantiated objects themselves, and conceptually can be considered as the "connector" between the policy and the stage or flow. This is why you might read about "binding a binding", because technically, a binding is "spliced" into another binding, in order to intercept and enforce the criteria defined in the policy. To learn more refer to our [Bindings documentation](../../add-secure-apps/flows-stages/bindings/index.md).
|
||||
:::
|
||||
|
||||
### Bind a policy to a flow
|
||||
|
@ -27,8 +27,6 @@ AUTHENTIK_TAG=gh-next
|
||||
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
|
||||
```
|
||||
|
||||
The Beta image is amd64 only. For arm64 platforms, append `-arm64` to the tag name (no spaces).
|
||||
|
||||
Next, run the upgrade commands below.
|
||||
|
||||
</TabItem>
|
||||
@ -47,8 +45,6 @@ image:
|
||||
pullPolicy: Always
|
||||
```
|
||||
|
||||
The Beta image is amd64 only. For arm64 platforms, append `-arm64` to the tag name (no spaces).
|
||||
|
||||
Next, run the upgrade commands below.
|
||||
|
||||
</TabItem>
|
||||
|
@ -70,14 +70,17 @@ To check if your config has been applied correctly, you can run the following co
|
||||
- `AUTHENTIK_POSTGRESQL__USER`: Database user
|
||||
- `AUTHENTIK_POSTGRESQL__PORT`: Database port, defaults to 5432
|
||||
- `AUTHENTIK_POSTGRESQL__PASSWORD`: Database password, defaults to the environment variable `POSTGRES_PASSWORD`
|
||||
- `AUTHENTIK_POSTGRESQL__USE_PGBOUNCER`: Adjust configuration to support connection to PgBouncer
|
||||
- `AUTHENTIK_POSTGRESQL__USE_PGPOOL`: Adjust configuration to support connection to Pgpool
|
||||
- `AUTHENTIK_POSTGRESQL__USE_PGBOUNCER`: Adjust configuration to support connection to PgBouncer. Deprecated, see below
|
||||
- `AUTHENTIK_POSTGRESQL__USE_PGPOOL`: Adjust configuration to support connection to Pgpool. Deprecated, see below
|
||||
- `AUTHENTIK_POSTGRESQL__SSLMODE`: Strictness of ssl verification. Defaults to `"verify-ca"`
|
||||
- `AUTHENTIK_POSTGRESQL__SSLROOTCERT`: CA root for server ssl verification
|
||||
- `AUTHENTIK_POSTGRESQL__SSLCERT`: Path to x509 client certificate to authenticate to server
|
||||
- `AUTHENTIK_POSTGRESQL__SSLKEY`: Path to private key of `SSLCERT` certificate
|
||||
- `AUTHENTIK_POSTGRESQL__CONN_MAX_AGE`: Database connection lifetime. Defaults to `0` (no persistent connections). Can be set to `null` for unlimited persistent connections. See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/settings/#conn-max-age) for more details.
|
||||
- `AUTHENTIK_POSTGRESQL__CONN_HEALTH_CHECK`: Existing persistent database connections will be health checked before they are reused if set to `true`. Defaults to `false`. See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/settings/#conn-health-checks) for more details.
|
||||
- `AUTHENTIK_POSTGRESQL__DISABLE_SERVER_SIDE_CURSORS`: Disable server side cursors when set to `true`. Defaults to `false`. See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/settings/#disable-server-side-cursors) for more details.
|
||||
|
||||
All PostgreSQL settings, apart from `USE_PGBOUNCER` and `USE_PGPOOL`, support hot-reloading. Adding and removing read replicas doesn't support hot-reloading.
|
||||
The PostgreSQL settings `HOST`, `PORT`, `USER`, and `PASSWORD` support hot-reloading. Adding and removing read replicas doesn't support hot-reloading.
|
||||
|
||||
### Read replicas
|
||||
|
||||
@ -96,8 +99,25 @@ The same PostgreSQL settings as described above are used for each read replica.
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__SSLROOTCERT`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__SSLCERT`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__SSLKEY`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__CONN_MAX_AGE`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__CONN_HEALTH_CHECK`
|
||||
- `AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__DISABLE_SERVER_SIDE_CURSORS`
|
||||
|
||||
Note that `USE_PGBOUNCER` and `USE_PGPOOL` are inherited from the main database configuration and are _not_ overridable on read replicas.
|
||||
### Using a PostgreSQL connection pooler (PgBouncer or PgPool)
|
||||
|
||||
When your PostgreSQL database(s) are running behind a connection pooler, like PgBouncer or PgPool, two settings need to be overridden:
|
||||
|
||||
- `AUTHENTIK_POSTGRESQL__CONN_MAX_AGE`
|
||||
|
||||
A connection pooler running in session pool mode (PgBouncer default) can be incompatible with unlimited persistent connections enabled by setting this to `null`: If the connection from the connection pooler to the database server is dropped, the connection pooler will wait for the client to disconnect before releasing the connection; however this will **never** happen as authentik is configured to keep the connection to the connection pooler forever.
|
||||
|
||||
To address this incompatibility, either configure the connection pooler to run in transaction pool mode, or update this setting to a value lower than any timeouts that may cause the connection to the database to be dropped (up to `0`).
|
||||
|
||||
- `AUTHENTIK_POSTGRESQL__DISABLE_SERVER_SIDE_CURSORS`
|
||||
|
||||
Using a connection pooler in transaction pool mode (e.g. PgPool, or PgBouncer in transaction or statement pool mode) requires disabling server-side cursors, so this setting must be set to `false`.
|
||||
|
||||
Additionally, you can set `AUTHENTIK_POSTGRESQL__CONN_HEALTH_CHECK` to perform health checks on persistent database connections before they are re-used.
|
||||
|
||||
## Redis Settings
|
||||
|
||||
|
@ -24,7 +24,7 @@ Parameters:
|
||||
Description: authentik server memory in MiB
|
||||
Type: Number
|
||||
AuthentikVersion:
|
||||
Default: 2024.10.5
|
||||
Default: 2024.12.5
|
||||
Description: authentik Docker image tag
|
||||
Type: String
|
||||
AuthentikWorkerCPU:
|
||||
|
@ -3,12 +3,6 @@ title: Release 2024.12
|
||||
slug: "/releases/2024.12"
|
||||
---
|
||||
|
||||
:::::note
|
||||
2024.12 has not been released yet! We're publishing these release notes as a preview of what's to come, and for our awesome beta testers trying out release candidates.
|
||||
|
||||
To try out the release candidate, replace your Docker image tag with the latest release candidate number, such as 2024.12.0-rc1. You can find the latest one in [the latest releases on GitHub](https://github.com/goauthentik/authentik/releases). If you don't find any, it means we haven't released one yet.
|
||||
:::::
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Redirect stage** Conditionally redirect users to other flows and URLs.
|
||||
@ -24,6 +18,16 @@ To try out the release candidate, replace your Docker image tag with the latest
|
||||
|
||||
You can disable this behavior in the **Admin interface** under **System** > **Settings**.
|
||||
|
||||
- **Deprecated PostgreSQL `USE_PGBOUNCER` and `USE_PGPOOL` settings**
|
||||
|
||||
With this release, the `AUTHENTIK_POSTGRESQL__USE_PGBOUNCER` and `AUTHENTIK_POSTGRESQL__USE_PGPOOL` settings have been deprecated in favor of exposing the underlying database settings: `AUTHENTIK_POSTGRESQL__CONN_MAX_AGE` and `AUTHENTIK_POSTGRESQL__DISABLE_SERVER_SIDE_CURSORS`.
|
||||
|
||||
If you are using PgBouncer or PgPool as connection poolers and wish to maintain the same behavior as previous versions, `AUTHENTIK_POSTGRESQL__DISABLE_SERVER_SIDE_CURSORS` must be set to `true`. Moreover, if you are using PgBouncer `AUTHENTIK_POSTGRESQL__CONN_MAX_AGE` must be set to `null`.
|
||||
|
||||
The newly exposed settings allow supporting a wider set of connection pooler configurations. For details on how these settings interact with different configurations of connection poolers, please refer to the [PostgreSQL documentation](../../install-config/configuration/configuration.mdx#postgresql-settings).
|
||||
|
||||
These settings will be removed in a future version.
|
||||
|
||||
## New features
|
||||
|
||||
- **Redirect stage**
|
||||
@ -92,6 +96,7 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.12
|
||||
- enterprise/rac: fix API Schema for invalidation_flow (#11907)
|
||||
- enterprise/stages/authenticator_endpoint_gdtc: don't set frame options globally (#12311)
|
||||
- enterprise: allow deletion/modification of users when in read-only mode (#12289)
|
||||
- events: notification_cleanup: avoid unnecessary loop (cherry-pick #12417) (#12418)
|
||||
- flows: better test stage's challenge responses (#12316)
|
||||
- flows: silent authz flow (#12213)
|
||||
- internal: add CSP header to files in `/media` (#12092)
|
||||
@ -112,6 +117,7 @@ helm upgrade authentik authentik/authentik -f values.yaml --version ^2024.12
|
||||
- providers/scim: accept string and int for SCIM IDs (#12093)
|
||||
- rbac: fix incorrect object_description for object-level permissions (#12029)
|
||||
- root: check remote IP for proxy protocol same as HTTP/etc (#12094)
|
||||
- root: expose CONN_MAX_AGE, CONN_HEALTH_CHECKS and DISABLE_SERVER_SIDE_CURSORS for PostgreSQL config (cherry-pick #10159) (#12419)
|
||||
- root: fix activation of locale not being scoped (#12091)
|
||||
- root: fix database ssl options not set correctly (#12180)
|
||||
- root: fix health status code (#12255)
|
||||
|
23
website/docs/security/cves/CVE-2025-29928.md
Normal file
23
website/docs/security/cves/CVE-2025-29928.md
Normal file
@ -0,0 +1,23 @@
|
||||
# CVE-2025-29928
|
||||
|
||||
## Deletion of sessions did not revoke sessions when using database session storage
|
||||
|
||||
### Summary
|
||||
|
||||
When authentik was configured to use the database for session storage (which is a non-default setting), deleting sessions via the Web Interface or the API would not revoke the session and the session holder would continue to have access to authentik.
|
||||
|
||||
This also affects automatic session deletion when a user is set to inactive or a user is deleted.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2025.2.3 and 2024.12.4 fix this issue.
|
||||
|
||||
### Workarounds
|
||||
|
||||
Switching to the cache-based session storage until the authentik instance can be upgraded is recommended.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io).
|
@ -57,6 +57,10 @@ When enabled, all the events caused by a user will be deleted upon the user's de
|
||||
|
||||
Globally enable/disable impersonation. Defaults to `true`.
|
||||
|
||||
### Require reason for impersonation
|
||||
|
||||
Require administrators to provide a reason for impersonating a user. Defaults to `true`.
|
||||
|
||||
### Default token duration
|
||||
|
||||
Default duration for generated tokens. Defaults to `minutes=30`.
|
||||
|
@ -105,7 +105,7 @@ If the user does not receive the email, check if the mail server parameters [are
|
||||
As an Admin, you can simply reset the password for the user.
|
||||
|
||||
1. In the Admin interface, navigate to **Directory > Users** to display all users.
|
||||
2. Either click the name of the user to display the full User details page, or click the chevron beside their name to expand the toptions.
|
||||
2. Either click the name of the user to display the full User details page, or click the chevron beside their name to expand the options.
|
||||
3. To reset the user's password, click **Reset password**, and then define the new value.
|
||||
|
||||
## Deactivate or Delete user
|
||||
@ -128,3 +128,18 @@ You may instead deactivate the account to preserve identity data.
|
||||
2. Review the changes and click **Delete**.
|
||||
|
||||
The user list refreshes and no longer displays the removed users.
|
||||
|
||||
## Impersonate a user
|
||||
|
||||
With authentik, an Admin can impersonate a user, meaning that the Admin temporarily assumes the identity of the user.
|
||||
|
||||
1. In the Admin interface, navigate to **Directory > Users** to display all users.
|
||||
2. Click the name of the user to display the full User details page.
|
||||
3. On the Overview tab, beneath **User Details**, in the **Actions** area, click **Impersonate**.
|
||||
4. At the prompt, provide a reason why you are impersonating this user, and then click **Impersonate**.
|
||||
|
||||
:::info
|
||||
An Admin can globally enable or disable impersonation in the [System Settings](../../sys-mgmt/settings.md#impersonation). By default, this option is set to true, meaning all users can be impersonated.
|
||||
|
||||
An Admin can also configure whether inputting a reason for impersonation is required in the [System Settings](../../sys-mgmt/settings.md#require-reason-for-impersonation).
|
||||
:::
|
||||
|
@ -306,6 +306,17 @@ export default {
|
||||
"add-secure-apps/flows-stages/stages/user_write",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Bindings",
|
||||
link: {
|
||||
type: "doc",
|
||||
id: "add-secure-apps/flows-stages/bindings/index",
|
||||
},
|
||||
items: [
|
||||
"add-secure-apps/flows-stages/bindings/work_with_bindings",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -663,6 +674,11 @@ export default {
|
||||
type: "category",
|
||||
label: "CVEs",
|
||||
items: [
|
||||
{
|
||||
type: "category",
|
||||
label: "2024",
|
||||
items: ["security/cves/CVE-2025-29928"],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "2024",
|
||||
|
Reference in New Issue
Block a user