Compare commits
32 Commits
permission
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
66a4970014 | |||
7ab9300761 | |||
a2eccd5022 | |||
31aeaa247f | |||
f49008bbb6 | |||
feb13c8ee5 | |||
d5ef831718 | |||
64676819ec | |||
7ed268fef4 | |||
f6526d1be9 | |||
12f8b4566b | |||
665de8ef22 | |||
9eaa723bf8 | |||
b2ca9c8cbc | |||
7927392100 | |||
d8d07e32cb | |||
f7c5d329eb | |||
92dec32547 | |||
510feccd31 | |||
364a9a1f02 | |||
40cbb7567b | |||
8ad0f63994 | |||
6ce33ab912 | |||
d96b577abd | |||
8c547589f6 | |||
3775e5b84f | |||
fa30339f65 | |||
e825eda106 | |||
246cae3dfa | |||
6cfd2bd1af | |||
f0e4f93fe6 | |||
434aa57ba7 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2025.2.1
|
current_version = 2024.10.2
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||||
@ -30,5 +30,3 @@ optional_value = final
|
|||||||
[bumpversion:file:internal/constants/constants.go]
|
[bumpversion:file:internal/constants/constants.go]
|
||||||
|
|
||||||
[bumpversion:file:web/src/common/constants.ts]
|
[bumpversion:file:web/src/common/constants.ts]
|
||||||
|
|
||||||
[bumpversion:file:lifecycle/aws/template.yaml]
|
|
||||||
|
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -28,11 +28,7 @@ Output of docker-compose logs or kubectl logs respectively
|
|||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
|
|
||||||
<!--
|
- authentik version: [e.g. 2021.8.5]
|
||||||
Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/.
|
|
||||||
-->
|
|
||||||
|
|
||||||
- authentik version: [e.g. 2025.2.0]
|
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
7
.github/ISSUE_TEMPLATE/question.md
vendored
7
.github/ISSUE_TEMPLATE/question.md
vendored
@ -20,12 +20,7 @@ Output of docker-compose logs or kubectl logs respectively
|
|||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
|
|
||||||
<!--
|
- authentik version: [e.g. 2021.8.5]
|
||||||
Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
- authentik version: [e.g. 2025.2.0]
|
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
@ -35,6 +35,14 @@ runs:
|
|||||||
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=ghcr.io/goauthentik/dev-%(type)s:gh-%(build_hash)s
|
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.
|
Afterwards, run the upgrade commands from the latest release notes.
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
@ -52,6 +60,18 @@ runs:
|
|||||||
tag: ${{ inputs.tag }}
|
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.
|
Afterwards, run the upgrade commands from the latest release notes.
|
||||||
</details>
|
</details>
|
||||||
edit-mode: replace
|
edit-mode: replace
|
||||||
|
20
.github/actions/docker-push-variables/action.yml
vendored
20
.github/actions/docker-push-variables/action.yml
vendored
@ -9,14 +9,11 @@ inputs:
|
|||||||
image-arch:
|
image-arch:
|
||||||
required: false
|
required: false
|
||||||
description: "Docker image arch"
|
description: "Docker image arch"
|
||||||
release:
|
|
||||||
required: true
|
|
||||||
description: "True if this is a release build, false if this is a dev/PR build"
|
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
shouldPush:
|
shouldBuild:
|
||||||
description: "Whether to push the image or not"
|
description: "Whether to build image or not"
|
||||||
value: ${{ steps.ev.outputs.shouldPush }}
|
value: ${{ steps.ev.outputs.shouldBuild }}
|
||||||
|
|
||||||
sha:
|
sha:
|
||||||
description: "sha"
|
description: "sha"
|
||||||
@ -32,24 +29,15 @@ outputs:
|
|||||||
imageTags:
|
imageTags:
|
||||||
description: "Docker image tags"
|
description: "Docker image tags"
|
||||||
value: ${{ steps.ev.outputs.imageTags }}
|
value: ${{ steps.ev.outputs.imageTags }}
|
||||||
imageTagsJSON:
|
|
||||||
description: "Docker image tags, as a JSON array"
|
|
||||||
value: ${{ steps.ev.outputs.imageTagsJSON }}
|
|
||||||
attestImageNames:
|
attestImageNames:
|
||||||
description: "Docker image names used for attestation"
|
description: "Docker image names used for attestation"
|
||||||
value: ${{ steps.ev.outputs.attestImageNames }}
|
value: ${{ steps.ev.outputs.attestImageNames }}
|
||||||
cacheTo:
|
|
||||||
description: "cache-to value for the docker build step"
|
|
||||||
value: ${{ steps.ev.outputs.cacheTo }}
|
|
||||||
imageMainTag:
|
imageMainTag:
|
||||||
description: "Docker image main tag"
|
description: "Docker image main tag"
|
||||||
value: ${{ steps.ev.outputs.imageMainTag }}
|
value: ${{ steps.ev.outputs.imageMainTag }}
|
||||||
imageMainName:
|
imageMainName:
|
||||||
description: "Docker image main name"
|
description: "Docker image main name"
|
||||||
value: ${{ steps.ev.outputs.imageMainName }}
|
value: ${{ steps.ev.outputs.imageMainName }}
|
||||||
imageBuildArgs:
|
|
||||||
description: "Docker image build args"
|
|
||||||
value: ${{ steps.ev.outputs.imageBuildArgs }}
|
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
@ -60,8 +48,6 @@ runs:
|
|||||||
env:
|
env:
|
||||||
IMAGE_NAME: ${{ inputs.image-name }}
|
IMAGE_NAME: ${{ inputs.image-name }}
|
||||||
IMAGE_ARCH: ${{ inputs.image-arch }}
|
IMAGE_ARCH: ${{ inputs.image-arch }}
|
||||||
RELEASE: ${{ inputs.release }}
|
|
||||||
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
REF: ${{ github.ref }}
|
|
||||||
run: |
|
run: |
|
||||||
python3 ${{ github.action_path }}/push_vars.py
|
python3 ${{ github.action_path }}/push_vars.py
|
||||||
|
@ -2,20 +2,12 @@
|
|||||||
|
|
||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
from json import dumps
|
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
parser = configparser.ConfigParser()
|
parser = configparser.ConfigParser()
|
||||||
parser.read(".bumpversion.cfg")
|
parser.read(".bumpversion.cfg")
|
||||||
|
|
||||||
# Decide if we should push the image or not
|
should_build = str(len(os.environ.get("DOCKER_USERNAME", "")) > 0).lower()
|
||||||
should_push = True
|
|
||||||
if len(os.environ.get("DOCKER_USERNAME", "")) < 1:
|
|
||||||
# Don't push if we don't have DOCKER_USERNAME, i.e. no secrets are available
|
|
||||||
should_push = False
|
|
||||||
if os.environ.get("GITHUB_REPOSITORY").lower() == "goauthentik/authentik-internal":
|
|
||||||
# Don't push on the internal repo
|
|
||||||
should_push = False
|
|
||||||
|
|
||||||
branch_name = os.environ["GITHUB_REF"]
|
branch_name = os.environ["GITHUB_REF"]
|
||||||
if os.environ.get("GITHUB_HEAD_REF", "") != "":
|
if os.environ.get("GITHUB_HEAD_REF", "") != "":
|
||||||
@ -49,7 +41,7 @@ if is_release:
|
|||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
suffix = ""
|
suffix = ""
|
||||||
if image_arch:
|
if image_arch and image_arch != "amd64":
|
||||||
suffix = f"-{image_arch}"
|
suffix = f"-{image_arch}"
|
||||||
for name in image_names:
|
for name in image_names:
|
||||||
image_tags += [
|
image_tags += [
|
||||||
@ -71,31 +63,12 @@ def get_attest_image_names(image_with_tags: list[str]):
|
|||||||
return ",".join(set(image_tags))
|
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:
|
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
|
||||||
print(f"shouldPush={str(should_push).lower()}", file=_output)
|
print(f"shouldBuild={should_build}", file=_output)
|
||||||
print(f"sha={sha}", file=_output)
|
print(f"sha={sha}", file=_output)
|
||||||
print(f"version={version}", file=_output)
|
print(f"version={version}", file=_output)
|
||||||
print(f"prerelease={prerelease}", file=_output)
|
print(f"prerelease={prerelease}", file=_output)
|
||||||
print(f"imageTags={','.join(image_tags)}", 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"attestImageNames={get_attest_image_names(image_tags)}", file=_output)
|
||||||
print(f"imageMainTag={image_main_tag}", file=_output)
|
print(f"imageMainTag={image_main_tag}", file=_output)
|
||||||
print(f"imageMainName={image_tags[0]}", 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,18 +1,7 @@
|
|||||||
#!/bin/bash -x
|
#!/bin/bash -x
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
# Non-pushing PR
|
|
||||||
GITHUB_OUTPUT=/dev/stdout \
|
GITHUB_OUTPUT=/dev/stdout \
|
||||||
GITHUB_REF=ref \
|
GITHUB_REF=ref \
|
||||||
GITHUB_SHA=sha \
|
GITHUB_SHA=sha \
|
||||||
IMAGE_NAME=ghcr.io/goauthentik/server,beryju/authentik \
|
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
|
python $SCRIPT_DIR/push_vars.py
|
||||||
|
6
.github/actions/setup/action.yml
vendored
6
.github/actions/setup/action.yml
vendored
@ -30,16 +30,12 @@ runs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Setup docker cache
|
|
||||||
uses: ScribeMD/docker-cache@0.5.0
|
|
||||||
with:
|
|
||||||
key: docker-images-${{ runner.os }}-${{ hashFiles('.github/actions/setup/docker-compose.yml', 'Makefile') }}-${{ inputs.postgresql_version }}
|
|
||||||
- name: Setup dependencies
|
- name: Setup dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
export PSQL_TAG=${{ inputs.postgresql_version }}
|
export PSQL_TAG=${{ inputs.postgresql_version }}
|
||||||
docker compose -f .github/actions/setup/docker-compose.yml up -d
|
docker compose -f .github/actions/setup/docker-compose.yml up -d
|
||||||
poetry sync
|
poetry install
|
||||||
cd web && npm ci
|
cd web && npm ci
|
||||||
- name: Generate config
|
- name: Generate config
|
||||||
shell: poetry run python {0}
|
shell: poetry run python {0}
|
||||||
|
2
.github/actions/setup/docker-compose.yml
vendored
2
.github/actions/setup/docker-compose.yml
vendored
@ -11,7 +11,7 @@ services:
|
|||||||
- 5432:5432
|
- 5432:5432
|
||||||
restart: always
|
restart: always
|
||||||
redis:
|
redis:
|
||||||
image: docker.io/library/redis:7
|
image: docker.io/library/redis
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
restart: always
|
restart: always
|
||||||
|
33
.github/codespell-words.txt
vendored
33
.github/codespell-words.txt
vendored
@ -1,32 +1,7 @@
|
|||||||
akadmin
|
|
||||||
asgi
|
|
||||||
assertIn
|
|
||||||
authentik
|
|
||||||
authn
|
|
||||||
crate
|
|
||||||
docstrings
|
|
||||||
entra
|
|
||||||
goauthentik
|
|
||||||
gunicorn
|
|
||||||
hass
|
|
||||||
jwe
|
|
||||||
jwks
|
|
||||||
keypair
|
keypair
|
||||||
keypairs
|
keypairs
|
||||||
kubernetes
|
hass
|
||||||
oidc
|
|
||||||
ontext
|
|
||||||
openid
|
|
||||||
passwordless
|
|
||||||
plex
|
|
||||||
saml
|
|
||||||
scim
|
|
||||||
singed
|
|
||||||
slo
|
|
||||||
sso
|
|
||||||
totp
|
|
||||||
traefik
|
|
||||||
# https://github.com/codespell-project/codespell/issues/1224
|
|
||||||
upToDate
|
|
||||||
warmup
|
warmup
|
||||||
webauthn
|
ontext
|
||||||
|
singed
|
||||||
|
assertIn
|
||||||
|
16
.github/dependabot.yml
vendored
16
.github/dependabot.yml
vendored
@ -82,22 +82,6 @@ updates:
|
|||||||
docusaurus:
|
docusaurus:
|
||||||
patterns:
|
patterns:
|
||||||
- "@docusaurus/*"
|
- "@docusaurus/*"
|
||||||
build:
|
|
||||||
patterns:
|
|
||||||
- "@swc/*"
|
|
||||||
- "swc-*"
|
|
||||||
- "lightningcss*"
|
|
||||||
- "@rspack/binding*"
|
|
||||||
- package-ecosystem: npm
|
|
||||||
directory: "/lifecycle/aws"
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
time: "04:00"
|
|
||||||
open-pull-requests-limit: 10
|
|
||||||
commit-message:
|
|
||||||
prefix: "lifecycle/aws:"
|
|
||||||
labels:
|
|
||||||
- dependencies
|
|
||||||
- package-ecosystem: pip
|
- package-ecosystem: pip
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
|
@ -1,96 +0,0 @@
|
|||||||
# 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.6.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: ${{ steps.ev.outputs.shouldPush == '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
|
|
||||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
|
||||||
with:
|
|
||||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
push-to-registry: true
|
|
104
.github/workflows/_reusable-docker-build.yaml
vendored
104
.github/workflows/_reusable-docker-build.yaml
vendored
@ -1,104 +0,0 @@
|
|||||||
# 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 }}
|
|
||||||
shouldPush: ${{ steps.ev.outputs.shouldPush }}
|
|
||||||
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
|
|
||||||
if: ${{ needs.get-tags.outputs.shouldPush == 'true' }}
|
|
||||||
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
|
|
1
.github/workflows/api-py-publish.yml
vendored
1
.github/workflows/api-py-publish.yml
vendored
@ -7,7 +7,6 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
id-token: write
|
id-token: write
|
||||||
|
1
.github/workflows/api-ts-publish.yml
vendored
1
.github/workflows/api-ts-publish.yml
vendored
@ -7,7 +7,6 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: generate_token
|
- id: generate_token
|
||||||
|
46
.github/workflows/ci-aws-cfn.yml
vendored
46
.github/workflows/ci-aws-cfn.yml
vendored
@ -1,46 +0,0 @@
|
|||||||
name: authentik-ci-aws-cfn
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- next
|
|
||||||
- version-*
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- version-*
|
|
||||||
|
|
||||||
env:
|
|
||||||
POSTGRES_DB: authentik
|
|
||||||
POSTGRES_USER: authentik
|
|
||||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-changes-applied:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup authentik env
|
|
||||||
uses: ./.github/actions/setup
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version-file: lifecycle/aws/package.json
|
|
||||||
cache: "npm"
|
|
||||||
cache-dependency-path: lifecycle/aws/package-lock.json
|
|
||||||
- working-directory: lifecycle/aws/
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
- name: Check changes have been applied
|
|
||||||
run: |
|
|
||||||
poetry run make aws-cfn
|
|
||||||
git diff --exit-code
|
|
||||||
ci-aws-cfn-mark:
|
|
||||||
if: always()
|
|
||||||
needs:
|
|
||||||
- check-changes-applied
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: re-actors/alls-green@release/v1
|
|
||||||
with:
|
|
||||||
jobs: ${{ toJSON(needs) }}
|
|
28
.github/workflows/ci-main-daily.yml
vendored
28
.github/workflows/ci-main-daily.yml
vendored
@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
name: authentik-ci-main-daily
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
# Every night at 3am
|
|
||||||
- cron: "0 3 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-container:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
version:
|
|
||||||
- docs
|
|
||||||
- version-2025-2
|
|
||||||
- version-2024-12
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- run: |
|
|
||||||
current="$(pwd)"
|
|
||||||
dir="/tmp/authentik/${{ matrix.version }}"
|
|
||||||
mkdir -p $dir
|
|
||||||
cd $dir
|
|
||||||
wget https://${{ matrix.version }}.goauthentik.io/docker-compose.yml
|
|
||||||
${current}/scripts/test_docker.sh
|
|
110
.github/workflows/ci-main.yml
vendored
110
.github/workflows/ci-main.yml
vendored
@ -43,26 +43,15 @@ jobs:
|
|||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: run migrations
|
- name: run migrations
|
||||||
run: poetry run python -m lifecycle.migrate
|
run: poetry run python -m lifecycle.migrate
|
||||||
test-make-seed:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- id: seed
|
|
||||||
run: |
|
|
||||||
echo "seed=$(printf "%d\n" "0x$(openssl rand -hex 4)")" >> "$GITHUB_OUTPUT"
|
|
||||||
outputs:
|
|
||||||
seed: ${{ steps.seed.outputs.seed }}
|
|
||||||
test-migrations-from-stable:
|
test-migrations-from-stable:
|
||||||
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
|
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
|
||||||
needs: test-make-seed
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
psql:
|
psql:
|
||||||
- 15-alpine
|
- 15-alpine
|
||||||
- 16-alpine
|
- 16-alpine
|
||||||
run_id: [1, 2, 3, 4, 5]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@ -104,23 +93,18 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
# Test in the main database that we just migrated from the previous stable version
|
# Test in the main database that we just migrated from the previous stable version
|
||||||
AUTHENTIK_POSTGRESQL__TEST__NAME: authentik
|
AUTHENTIK_POSTGRESQL__TEST__NAME: authentik
|
||||||
CI_TEST_SEED: ${{ needs.test-make-seed.outputs.seed }}
|
|
||||||
CI_RUN_ID: ${{ matrix.run_id }}
|
|
||||||
CI_TOTAL_RUNS: "5"
|
|
||||||
run: |
|
run: |
|
||||||
poetry run make ci-test
|
poetry run make test
|
||||||
test-unittest:
|
test-unittest:
|
||||||
name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
|
name: test-unittest - PostgreSQL ${{ matrix.psql }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 20
|
timeout-minutes: 30
|
||||||
needs: test-make-seed
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
psql:
|
psql:
|
||||||
- 15-alpine
|
- 15-alpine
|
||||||
- 16-alpine
|
- 16-alpine
|
||||||
run_id: [1, 2, 3, 4, 5]
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
@ -128,14 +112,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
postgresql_version: ${{ matrix.psql }}
|
postgresql_version: ${{ matrix.psql }}
|
||||||
- name: run unittest
|
- name: run unittest
|
||||||
env:
|
|
||||||
CI_TEST_SEED: ${{ needs.test-make-seed.outputs.seed }}
|
|
||||||
CI_RUN_ID: ${{ matrix.run_id }}
|
|
||||||
CI_TOTAL_RUNS: "5"
|
|
||||||
run: |
|
run: |
|
||||||
poetry run make ci-test
|
poetry run make test
|
||||||
|
poetry run coverage xml
|
||||||
- if: ${{ always() }}
|
- if: ${{ always() }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
flags: unit
|
flags: unit
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@ -153,13 +134,13 @@ jobs:
|
|||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Create k8s Kind Cluster
|
- name: Create k8s Kind Cluster
|
||||||
uses: helm/kind-action@v1.12.0
|
uses: helm/kind-action@v1.10.0
|
||||||
- name: run integration
|
- name: run integration
|
||||||
run: |
|
run: |
|
||||||
poetry run coverage run manage.py test tests/integration
|
poetry run coverage run manage.py test tests/integration
|
||||||
poetry run coverage xml
|
poetry run coverage xml
|
||||||
- if: ${{ always() }}
|
- if: ${{ always() }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
flags: integration
|
flags: integration
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@ -217,7 +198,7 @@ jobs:
|
|||||||
poetry run coverage run manage.py test ${{ matrix.job.glob }}
|
poetry run coverage run manage.py test ${{ matrix.job.glob }}
|
||||||
poetry run coverage xml
|
poetry run coverage xml
|
||||||
- if: ${{ always() }}
|
- if: ${{ always() }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
flags: e2e
|
flags: e2e
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@ -228,7 +209,6 @@ jobs:
|
|||||||
file: unittest.xml
|
file: unittest.xml
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
ci-core-mark:
|
ci-core-mark:
|
||||||
if: always()
|
|
||||||
needs:
|
needs:
|
||||||
- lint
|
- lint
|
||||||
- test-migrations
|
- test-migrations
|
||||||
@ -238,22 +218,70 @@ jobs:
|
|||||||
- test-e2e
|
- test-e2e
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: re-actors/alls-green@release/v1
|
- run: echo mark
|
||||||
with:
|
|
||||||
jobs: ${{ toJSON(needs) }}
|
|
||||||
build:
|
build:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
needs: ci-core-mark
|
||||||
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
# Needed to upload container images to ghcr.io
|
# Needed to upload contianer images to ghcr.io
|
||||||
packages: write
|
packages: write
|
||||||
# Needed for attestation
|
# Needed for attestation
|
||||||
id-token: write
|
id-token: write
|
||||||
attestations: write
|
attestations: write
|
||||||
needs: ci-core-mark
|
timeout-minutes: 120
|
||||||
uses: ./.github/workflows/_reusable-docker-build.yaml
|
steps:
|
||||||
secrets: inherit
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
image_name: ghcr.io/goauthentik/dev-server
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
release: false
|
- 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.shouldBuild == '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.shouldBuild == '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.shouldBuild == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max' || '' }}
|
||||||
|
platforms: linux/${{ matrix.arch }}
|
||||||
|
- uses: actions/attest-build-provenance@v1
|
||||||
|
id: attest
|
||||||
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
|
with:
|
||||||
|
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||||
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
pr-comment:
|
pr-comment:
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
@ -275,7 +303,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
image-name: ghcr.io/goauthentik/dev-server
|
image-name: ghcr.io/goauthentik/dev-server
|
||||||
- name: Comment on PR
|
- name: Comment on PR
|
||||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
uses: ./.github/actions/comment-pr-instructions
|
uses: ./.github/actions/comment-pr-instructions
|
||||||
with:
|
with:
|
||||||
tag: ${{ steps.ev.outputs.imageMainTag }}
|
tag: ${{ steps.ev.outputs.imageMainTag }}
|
||||||
|
19
.github/workflows/ci-outpost.yml
vendored
19
.github/workflows/ci-outpost.yml
vendored
@ -49,15 +49,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./...
|
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./...
|
||||||
ci-outpost-mark:
|
ci-outpost-mark:
|
||||||
if: always()
|
|
||||||
needs:
|
needs:
|
||||||
- lint-golint
|
- lint-golint
|
||||||
- test-unittest
|
- test-unittest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: re-actors/alls-green@release/v1
|
- run: echo mark
|
||||||
with:
|
|
||||||
jobs: ${{ toJSON(needs) }}
|
|
||||||
build-container:
|
build-container:
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
needs:
|
needs:
|
||||||
@ -72,7 +69,7 @@ jobs:
|
|||||||
- rac
|
- rac
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
# Needed to upload container images to ghcr.io
|
# Needed to upload contianer images to ghcr.io
|
||||||
packages: write
|
packages: write
|
||||||
# Needed for attestation
|
# Needed for attestation
|
||||||
id-token: write
|
id-token: write
|
||||||
@ -82,7 +79,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.6.0
|
uses: docker/setup-qemu-action@v3.2.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: prepare variables
|
- name: prepare variables
|
||||||
@ -93,7 +90,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
|
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
|
||||||
- name: Login to Container Registry
|
- name: Login to Container Registry
|
||||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@ -107,16 +104,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tags: ${{ steps.ev.outputs.imageTags }}
|
tags: ${{ steps.ev.outputs.imageTags }}
|
||||||
file: ${{ matrix.type }}.Dockerfile
|
file: ${{ matrix.type }}.Dockerfile
|
||||||
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
build-args: |
|
build-args: |
|
||||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
context: .
|
context: .
|
||||||
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
|
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
|
||||||
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
|
cache-to: ${{ steps.ev.outputs.shouldBuild == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
|
||||||
- uses: actions/attest-build-provenance@v2
|
- uses: actions/attest-build-provenance@v1
|
||||||
id: attest
|
id: attest
|
||||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
5
.github/workflows/ci-web.yml
vendored
5
.github/workflows/ci-web.yml
vendored
@ -61,15 +61,12 @@ jobs:
|
|||||||
working-directory: web/
|
working-directory: web/
|
||||||
run: npm run build
|
run: npm run build
|
||||||
ci-web-mark:
|
ci-web-mark:
|
||||||
if: always()
|
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
- lint
|
- lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: re-actors/alls-green@release/v1
|
- run: echo mark
|
||||||
with:
|
|
||||||
jobs: ${{ toJSON(needs) }}
|
|
||||||
test:
|
test:
|
||||||
needs:
|
needs:
|
||||||
- ci-web-mark
|
- ci-web-mark
|
||||||
|
5
.github/workflows/ci-website.yml
vendored
5
.github/workflows/ci-website.yml
vendored
@ -62,13 +62,10 @@ jobs:
|
|||||||
working-directory: website/
|
working-directory: website/
|
||||||
run: npm run ${{ matrix.job }}
|
run: npm run ${{ matrix.job }}
|
||||||
ci-website-mark:
|
ci-website-mark:
|
||||||
if: always()
|
|
||||||
needs:
|
needs:
|
||||||
- lint
|
- lint
|
||||||
- test
|
- test
|
||||||
- build
|
- build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: re-actors/alls-green@release/v1
|
- run: echo mark
|
||||||
with:
|
|
||||||
jobs: ${{ toJSON(needs) }}
|
|
||||||
|
@ -11,7 +11,6 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: generate_token
|
- id: generate_token
|
||||||
|
1
.github/workflows/ghcr-retention.yml
vendored
1
.github/workflows/ghcr-retention.yml
vendored
@ -7,7 +7,6 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
clean-ghcr:
|
clean-ghcr:
|
||||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
|
||||||
name: Delete old unused container images
|
name: Delete old unused container images
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
1
.github/workflows/publish-source-docs.yml
vendored
1
.github/workflows/publish-source-docs.yml
vendored
@ -12,7 +12,6 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-source-docs:
|
publish-source-docs:
|
||||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
steps:
|
steps:
|
||||||
|
1
.github/workflows/release-next-branch.yml
vendored
1
.github/workflows/release-next-branch.yml
vendored
@ -11,7 +11,6 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-next:
|
update-next:
|
||||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: internal-production
|
environment: internal-production
|
||||||
steps:
|
steps:
|
||||||
|
86
.github/workflows/release-publish.yml
vendored
86
.github/workflows/release-publish.yml
vendored
@ -7,23 +7,64 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-server:
|
build-server:
|
||||||
uses: ./.github/workflows/_reusable-docker-build.yaml
|
runs-on: ubuntu-latest
|
||||||
secrets: inherit
|
|
||||||
permissions:
|
permissions:
|
||||||
# Needed to upload container images to ghcr.io
|
# Needed to upload contianer images to ghcr.io
|
||||||
packages: write
|
packages: write
|
||||||
# Needed for attestation
|
# Needed for attestation
|
||||||
id-token: write
|
id-token: write
|
||||||
attestations: write
|
attestations: write
|
||||||
with:
|
steps:
|
||||||
image_name: ghcr.io/goauthentik/server,beryju/authentik
|
- uses: actions/checkout@v4
|
||||||
release: true
|
- name: Set up QEMU
|
||||||
registry_dockerhub: true
|
uses: docker/setup-qemu-action@v3.2.0
|
||||||
registry_ghcr: true
|
- 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@v1
|
||||||
|
id: attest
|
||||||
|
with:
|
||||||
|
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||||
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
build-outpost:
|
build-outpost:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
# Needed to upload container images to ghcr.io
|
# Needed to upload contianer images to ghcr.io
|
||||||
packages: write
|
packages: write
|
||||||
# Needed for attestation
|
# Needed for attestation
|
||||||
id-token: write
|
id-token: write
|
||||||
@ -42,7 +83,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version-file: "go.mod"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.6.0
|
uses: docker/setup-qemu-action@v3.2.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: prepare variables
|
- name: prepare variables
|
||||||
@ -78,7 +119,7 @@ jobs:
|
|||||||
file: ${{ matrix.type }}.Dockerfile
|
file: ${{ matrix.type }}.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
context: .
|
context: .
|
||||||
- uses: actions/attest-build-provenance@v2
|
- uses: actions/attest-build-provenance@v1
|
||||||
id: attest
|
id: attest
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||||
@ -128,27 +169,6 @@ jobs:
|
|||||||
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||||
asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
asset_name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref }}
|
||||||
upload-aws-cfn-template:
|
|
||||||
permissions:
|
|
||||||
# Needed for AWS login
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
needs:
|
|
||||||
- build-server
|
|
||||||
- build-outpost
|
|
||||||
env:
|
|
||||||
AWS_REGION: eu-central-1
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: aws-actions/configure-aws-credentials@v4
|
|
||||||
with:
|
|
||||||
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
|
|
||||||
aws-region: ${{ env.AWS_REGION }}
|
|
||||||
- name: Upload template
|
|
||||||
run: |
|
|
||||||
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.${{ github.ref }}.yaml
|
|
||||||
aws s3 cp --acl=public-read lifecycle/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
|
|
||||||
test-release:
|
test-release:
|
||||||
needs:
|
needs:
|
||||||
- build-server
|
- build-server
|
||||||
@ -186,7 +206,7 @@ jobs:
|
|||||||
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
|
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
|
||||||
docker cp ${container}:web/ .
|
docker cp ${container}:web/ .
|
||||||
- name: Create a Sentry.io release
|
- name: Create a Sentry.io release
|
||||||
uses: getsentry/action-release@v3
|
uses: getsentry/action-release@v1
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||||
|
11
.github/workflows/release-tag.yml
vendored
11
.github/workflows/release-tag.yml
vendored
@ -14,7 +14,16 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Pre-release test
|
- name: Pre-release test
|
||||||
run: |
|
run: |
|
||||||
make test-docker
|
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
|
||||||
- id: generate_token
|
- id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
|
21
.github/workflows/repo-mirror.yml
vendored
21
.github/workflows/repo-mirror.yml
vendored
@ -1,21 +0,0 @@
|
|||||||
name: "authentik-repo-mirror"
|
|
||||||
|
|
||||||
on: [push, delete]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
to_internal:
|
|
||||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- if: ${{ env.MIRROR_KEY != '' }}
|
|
||||||
uses: pixta-dev/repository-mirroring-action@v1
|
|
||||||
with:
|
|
||||||
target_repo_url:
|
|
||||||
git@github.com:goauthentik/authentik-internal.git
|
|
||||||
ssh_private_key:
|
|
||||||
${{ secrets.GH_MIRROR_KEY }}
|
|
||||||
env:
|
|
||||||
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}
|
|
7
.github/workflows/repo-stale.yml
vendored
7
.github/workflows/repo-stale.yml
vendored
@ -1,8 +1,8 @@
|
|||||||
name: "authentik-repo-stale"
|
name: 'authentik-repo-stale'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "30 1 * * *"
|
- cron: '30 1 * * *'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@ -11,7 +11,6 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: generate_token
|
- id: generate_token
|
||||||
@ -25,7 +24,7 @@ jobs:
|
|||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
days-before-close: 7
|
days-before-close: 7
|
||||||
exempt-issue-labels: pinned,security,pr_wanted,enhancement,bug/confirmed,enhancement/confirmed,question,status/reviewing
|
exempt-issue-labels: pinned,security,pr_wanted,enhancement,bug/confirmed,enhancement/confirmed,question,status/reviewing
|
||||||
stale-issue-label: status/stale
|
stale-issue-label: wontfix
|
||||||
stale-issue-message: >
|
stale-issue-message: >
|
||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
---
|
---
|
||||||
name: authentik-translate-extract-compile
|
name: authentik-backend-translate-extract-compile
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *" # every day at midnight
|
- cron: "0 0 * * *" # every day at midnight
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- version-*
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: authentik
|
POSTGRES_DB: authentik
|
||||||
@ -19,21 +15,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: generate_token
|
- id: generate_token
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v2
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
- uses: actions/checkout@v4
|
|
||||||
if: ${{ github.event_name == 'pull_request' }}
|
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: Generate API
|
|
||||||
run: make gen-client-ts
|
|
||||||
- name: run extract
|
- name: run extract
|
||||||
run: |
|
run: |
|
||||||
poetry run make i18n-extract
|
poetry run make i18n-extract
|
||||||
@ -42,7 +32,6 @@ jobs:
|
|||||||
poetry run ak compilemessages
|
poetry run ak compilemessages
|
||||||
make web-check-compile
|
make web-check-compile
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
if: ${{ github.event_name != 'pull_request' }}
|
|
||||||
uses: peter-evans/create-pull-request@v7
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -209,6 +209,3 @@ source_docs/
|
|||||||
|
|
||||||
### Golang ###
|
### Golang ###
|
||||||
/vendor/
|
/vendor/
|
||||||
|
|
||||||
### Docker ###
|
|
||||||
docker-compose.override.yml
|
|
||||||
|
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@ -2,7 +2,6 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
"bashmish.es6-string-css",
|
"bashmish.es6-string-css",
|
||||||
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
|
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
|
||||||
"charliermarsh.ruff",
|
|
||||||
"dbaeumer.vscode-eslint",
|
"dbaeumer.vscode-eslint",
|
||||||
"EditorConfig.EditorConfig",
|
"EditorConfig.EditorConfig",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
@ -11,12 +10,12 @@
|
|||||||
"Gruntfuggly.todo-tree",
|
"Gruntfuggly.todo-tree",
|
||||||
"mechatroner.rainbow-csv",
|
"mechatroner.rainbow-csv",
|
||||||
"ms-python.black-formatter",
|
"ms-python.black-formatter",
|
||||||
"ms-python.black-formatter",
|
"charliermarsh.ruff",
|
||||||
"ms-python.debugpy",
|
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
|
"ms-python.black-formatter",
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"Tobermory.es6-string-html",
|
"Tobermory.es6-string-html",
|
||||||
"unifiedjs.vscode-mdx",
|
"unifiedjs.vscode-mdx"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
66
.vscode/launch.json
vendored
66
.vscode/launch.json
vendored
@ -2,76 +2,26 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Debug: Attach Server Core",
|
"name": "Python: PDB attach Server",
|
||||||
"type": "debugpy",
|
"type": "python",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"connect": {
|
"connect": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 9901
|
"port": 6800
|
||||||
},
|
},
|
||||||
"pathMappings": [
|
"justMyCode": true,
|
||||||
{
|
|
||||||
"localRoot": "${workspaceFolder}",
|
|
||||||
"remoteRoot": "."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"django": true
|
"django": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug: Attach Worker",
|
"name": "Python: PDB attach Worker",
|
||||||
"type": "debugpy",
|
"type": "python",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"connect": {
|
"connect": {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 9901
|
"port": 6900
|
||||||
},
|
},
|
||||||
"pathMappings": [
|
"justMyCode": true,
|
||||||
{
|
|
||||||
"localRoot": "${workspaceFolder}",
|
|
||||||
"remoteRoot": "."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"django": true
|
"django": true
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug: Start Server Router",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "${workspaceFolder}/cmd/server",
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug: Start LDAP Outpost",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "${workspaceFolder}/cmd/ldap",
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug: Start Proxy Outpost",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "${workspaceFolder}/cmd/proxy",
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug: Start RAC Outpost",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "${workspaceFolder}/cmd/rac",
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Debug: Start Radius Outpost",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "${workspaceFolder}/cmd/radius",
|
|
||||||
"cwd": "${workspaceFolder}"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
25
.vscode/settings.json
vendored
25
.vscode/settings.json
vendored
@ -1,4 +1,26 @@
|
|||||||
{
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"akadmin",
|
||||||
|
"asgi",
|
||||||
|
"authentik",
|
||||||
|
"authn",
|
||||||
|
"entra",
|
||||||
|
"goauthentik",
|
||||||
|
"jwe",
|
||||||
|
"jwks",
|
||||||
|
"kubernetes",
|
||||||
|
"oidc",
|
||||||
|
"openid",
|
||||||
|
"passwordless",
|
||||||
|
"plex",
|
||||||
|
"saml",
|
||||||
|
"scim",
|
||||||
|
"slo",
|
||||||
|
"sso",
|
||||||
|
"totp",
|
||||||
|
"traefik",
|
||||||
|
"webauthn"
|
||||||
|
],
|
||||||
"todo-tree.tree.showCountsInTree": true,
|
"todo-tree.tree.showCountsInTree": true,
|
||||||
"todo-tree.tree.showBadges": true,
|
"todo-tree.tree.showBadges": true,
|
||||||
"yaml.customTags": [
|
"yaml.customTags": [
|
||||||
@ -11,8 +33,7 @@
|
|||||||
"!If sequence",
|
"!If sequence",
|
||||||
"!Index scalar",
|
"!Index scalar",
|
||||||
"!KeyOf scalar",
|
"!KeyOf scalar",
|
||||||
"!Value scalar",
|
"!Value scalar"
|
||||||
"!AtIndex scalar"
|
|
||||||
],
|
],
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"typescript.preferences.importModuleSpecifierEnding": "index",
|
"typescript.preferences.importModuleSpecifierEnding": "index",
|
||||||
|
11
CODEOWNERS
11
CODEOWNERS
@ -15,23 +15,14 @@ go.mod @goauthentik/backend
|
|||||||
go.sum @goauthentik/backend
|
go.sum @goauthentik/backend
|
||||||
# Infrastructure
|
# Infrastructure
|
||||||
.github/ @goauthentik/infrastructure
|
.github/ @goauthentik/infrastructure
|
||||||
lifecycle/aws/ @goauthentik/infrastructure
|
|
||||||
Dockerfile @goauthentik/infrastructure
|
Dockerfile @goauthentik/infrastructure
|
||||||
*Dockerfile @goauthentik/infrastructure
|
*Dockerfile @goauthentik/infrastructure
|
||||||
.dockerignore @goauthentik/infrastructure
|
.dockerignore @goauthentik/infrastructure
|
||||||
docker-compose.yml @goauthentik/infrastructure
|
docker-compose.yml @goauthentik/infrastructure
|
||||||
Makefile @goauthentik/infrastructure
|
|
||||||
.editorconfig @goauthentik/infrastructure
|
|
||||||
CODEOWNERS @goauthentik/infrastructure
|
|
||||||
# Web
|
# Web
|
||||||
web/ @goauthentik/frontend
|
web/ @goauthentik/frontend
|
||||||
tests/wdio/ @goauthentik/frontend
|
tests/wdio/ @goauthentik/frontend
|
||||||
# Locale
|
|
||||||
locale/ @goauthentik/backend @goauthentik/frontend
|
|
||||||
web/xliff/ @goauthentik/backend @goauthentik/frontend
|
|
||||||
# Docs & Website
|
# Docs & Website
|
||||||
website/ @goauthentik/docs
|
website/ @goauthentik/docs
|
||||||
CODE_OF_CONDUCT.md @goauthentik/docs
|
|
||||||
# Security
|
# Security
|
||||||
SECURITY.md @goauthentik/security @goauthentik/docs
|
website/docs/security/ @goauthentik/security
|
||||||
website/docs/security/ @goauthentik/security @goauthentik/docs
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
We as members, contributors, and leaders pledge to make participation in our
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socioeconomic status,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
and orientation.
|
and orientation.
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
website/docs/developer-docs/index.md
|
website/developer-docs/index.md
|
39
Dockerfile
39
Dockerfile
@ -3,8 +3,7 @@
|
|||||||
# Stage 1: Build website
|
# Stage 1: Build website
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
|
FROM --platform=${BUILDPLATFORM} docker.io/library/node:22 AS website-builder
|
||||||
|
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production
|
||||||
GIT_UNAVAILABLE=true
|
|
||||||
|
|
||||||
WORKDIR /work/website
|
WORKDIR /work/website
|
||||||
|
|
||||||
@ -81,7 +80,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
|||||||
go build -o /go/authentik ./cmd/server
|
go build -o /go/authentik ./cmd/server
|
||||||
|
|
||||||
# Stage 4: MaxMind GeoIP
|
# Stage 4: MaxMind GeoIP
|
||||||
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip
|
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.0.1 AS geoip
|
||||||
|
|
||||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
|
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
|
||||||
ENV GEOIPUPDATE_VERBOSE="1"
|
ENV GEOIPUPDATE_VERBOSE="1"
|
||||||
@ -95,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"
|
/bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||||
|
|
||||||
# Stage 5: Python dependencies
|
# Stage 5: Python dependencies
|
||||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-deps
|
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS python-deps
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
@ -117,30 +116,15 @@ RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
|
|||||||
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
|
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
|
||||||
--mount=type=cache,target=/root/.cache/pip \
|
--mount=type=cache,target=/root/.cache/pip \
|
||||||
--mount=type=cache,target=/root/.cache/pypoetry \
|
--mount=type=cache,target=/root/.cache/pypoetry \
|
||||||
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/ && \
|
python -m venv /ak-root/venv/ && \
|
||||||
bash -c "source ${VENV_PATH}/bin/activate && \
|
bash -c "source ${VENV_PATH}/bin/activate && \
|
||||||
pip3 install --upgrade pip poetry && \
|
pip3 install --upgrade pip && \
|
||||||
poetry config --local installer.no-binary cryptography,xmlsec,lxml,python-kadmin-rs && \
|
pip3 install poetry && \
|
||||||
poetry install --only=main --no-ansi --no-interaction --no-root && \
|
poetry install --only=main --no-ansi --no-interaction --no-root && \
|
||||||
pip uninstall cryptography -y && \
|
pip install --force-reinstall /wheels/*"
|
||||||
poetry install --only=main --no-ansi --no-interaction --no-root"
|
|
||||||
|
|
||||||
# Stage 6: Run
|
# Stage 6: Run
|
||||||
FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS final-image
|
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS final-image
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ARG GIT_BUILD_HASH
|
ARG GIT_BUILD_HASH
|
||||||
@ -156,12 +140,10 @@ WORKDIR /
|
|||||||
|
|
||||||
# We cannot cache this layer otherwise we'll end up with a bigger image
|
# We cannot cache this layer otherwise we'll end up with a bigger image
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get upgrade -y && \
|
|
||||||
# Required for runtime
|
# Required for runtime
|
||||||
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 libltdl7 libxslt1.1 && \
|
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 && \
|
||||||
# Required for bootstrap & healtcheck
|
# Required for bootstrap & healtcheck
|
||||||
apt-get install -y --no-install-recommends runit && \
|
apt-get install -y --no-install-recommends runit && \
|
||||||
pip3 install --no-cache-dir --upgrade pip && \
|
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
|
||||||
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
|
||||||
@ -194,8 +176,9 @@ ENV TMPDIR=/dev/shm/ \
|
|||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
|
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
|
||||||
VENV_PATH="/ak-root/venv" \
|
VENV_PATH="/ak-root/venv" \
|
||||||
POETRY_VIRTUALENVS_CREATE=false \
|
POETRY_VIRTUALENVS_CREATE=false
|
||||||
GOFIPS=1
|
|
||||||
|
ENV GOFIPS=1
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "ak", "healthcheck" ]
|
||||||
|
|
||||||
|
80
Makefile
80
Makefile
@ -4,7 +4,7 @@
|
|||||||
PWD = $(shell pwd)
|
PWD = $(shell pwd)
|
||||||
UID = $(shell id -u)
|
UID = $(shell id -u)
|
||||||
GID = $(shell id -g)
|
GID = $(shell id -g)
|
||||||
NPM_VERSION = $(shell python -m scripts.generate_semver)
|
NPM_VERSION = $(shell python -m scripts.npm_version)
|
||||||
PY_SOURCES = authentik tests scripts lifecycle .github
|
PY_SOURCES = authentik tests scripts lifecycle .github
|
||||||
DOCKER_IMAGE ?= "authentik:test"
|
DOCKER_IMAGE ?= "authentik:test"
|
||||||
|
|
||||||
@ -12,9 +12,23 @@ GEN_API_TS = "gen-ts-api"
|
|||||||
GEN_API_PY = "gen-py-api"
|
GEN_API_PY = "gen-py-api"
|
||||||
GEN_API_GO = "gen-go-api"
|
GEN_API_GO = "gen-go-api"
|
||||||
|
|
||||||
pg_user := $(shell poetry run python -m authentik.lib.config postgresql.user 2>/dev/null)
|
pg_user := $(shell python -m authentik.lib.config postgresql.user 2>/dev/null)
|
||||||
pg_host := $(shell poetry run python -m authentik.lib.config postgresql.host 2>/dev/null)
|
pg_host := $(shell python -m authentik.lib.config postgresql.host 2>/dev/null)
|
||||||
pg_name := $(shell poetry run python -m authentik.lib.config postgresql.name 2>/dev/null)
|
pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null)
|
||||||
|
|
||||||
|
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
|
||||||
|
-I .github/codespell-words.txt \
|
||||||
|
-S 'web/src/locales/**' \
|
||||||
|
-S 'website/docs/developer-docs/api/reference/**' \
|
||||||
|
authentik \
|
||||||
|
internal \
|
||||||
|
cmd \
|
||||||
|
web/src \
|
||||||
|
website/src \
|
||||||
|
website/blog \
|
||||||
|
website/docs \
|
||||||
|
website/integrations \
|
||||||
|
website/src
|
||||||
|
|
||||||
all: lint-fix lint test gen web ## Lint, build, and test everything
|
all: lint-fix lint test gen web ## Lint, build, and test everything
|
||||||
|
|
||||||
@ -31,35 +45,41 @@ help: ## Show this help
|
|||||||
go-test:
|
go-test:
|
||||||
go test -timeout 0 -v -race -cover ./...
|
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)
|
test: ## Run the server tests and produce a coverage report (locally)
|
||||||
poetry run coverage run manage.py test --keepdb authentik
|
coverage run manage.py test --keepdb authentik
|
||||||
poetry run coverage html
|
coverage html
|
||||||
poetry run coverage report
|
coverage report
|
||||||
|
|
||||||
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
|
||||||
poetry run black $(PY_SOURCES)
|
black $(PY_SOURCES)
|
||||||
poetry run ruff check --fix $(PY_SOURCES)
|
ruff check --fix $(PY_SOURCES)
|
||||||
|
|
||||||
lint-codespell: ## Reports spelling errors.
|
lint-codespell: ## Reports spelling errors.
|
||||||
poetry run codespell -w
|
codespell -w $(CODESPELL_ARGS)
|
||||||
|
|
||||||
lint: ## Lint the python and golang sources
|
lint: ## Lint the python and golang sources
|
||||||
poetry run bandit -c pyproject.toml -r $(PY_SOURCES)
|
bandit -r $(PY_SOURCES) -x web/node_modules -x tests/wdio/node_modules -x website/node_modules
|
||||||
golangci-lint run -v
|
golangci-lint run -v
|
||||||
|
|
||||||
core-install:
|
core-install:
|
||||||
poetry install
|
poetry install
|
||||||
|
|
||||||
migrate: ## Run the Authentik Django server's migrations
|
migrate: ## Run the Authentik Django server's migrations
|
||||||
poetry run python -m lifecycle.migrate
|
python -m lifecycle.migrate
|
||||||
|
|
||||||
i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service
|
i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that require translation into files to send to a translation service
|
||||||
|
|
||||||
aws-cfn:
|
|
||||||
cd lifecycle/aws && npm run aws-cfn
|
|
||||||
|
|
||||||
core-i18n-extract:
|
core-i18n-extract:
|
||||||
poetry run ak makemessages \
|
ak makemessages \
|
||||||
--add-location file \
|
--add-location file \
|
||||||
--no-obsolete \
|
--no-obsolete \
|
||||||
--ignore web \
|
--ignore web \
|
||||||
@ -90,11 +110,11 @@ gen-build: ## Extract the schema from the database
|
|||||||
AUTHENTIK_DEBUG=true \
|
AUTHENTIK_DEBUG=true \
|
||||||
AUTHENTIK_TENANTS__ENABLED=true \
|
AUTHENTIK_TENANTS__ENABLED=true \
|
||||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||||
poetry run ak make_blueprint_schema > blueprints/schema.json
|
ak make_blueprint_schema > blueprints/schema.json
|
||||||
AUTHENTIK_DEBUG=true \
|
AUTHENTIK_DEBUG=true \
|
||||||
AUTHENTIK_TENANTS__ENABLED=true \
|
AUTHENTIK_TENANTS__ENABLED=true \
|
||||||
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
|
||||||
poetry run ak spectacular --file schema.yml
|
ak spectacular --file schema.yml
|
||||||
|
|
||||||
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
|
gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
|
||||||
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
|
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
|
||||||
@ -129,7 +149,7 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
|
|||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
|
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g typescript-fetch \
|
-g typescript-fetch \
|
||||||
-o /local/${GEN_API_TS} \
|
-o /local/${GEN_API_TS} \
|
||||||
@ -145,7 +165,7 @@ gen-client-py: gen-clean-py ## Build and install the authentik API for Python
|
|||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
|
docker.io/openapitools/openapi-generator-cli:v7.4.0 generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g python \
|
-g python \
|
||||||
-o /local/${GEN_API_PY} \
|
-o /local/${GEN_API_PY} \
|
||||||
@ -173,7 +193,7 @@ gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
|
|||||||
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
|
rm -rf ./${GEN_API_GO}/config.yaml ./${GEN_API_GO}/templates/
|
||||||
|
|
||||||
gen-dev-config: ## Generate a local development config file
|
gen-dev-config: ## Generate a local development config file
|
||||||
poetry run scripts/generate_config.py
|
python -m scripts.generate_config
|
||||||
|
|
||||||
gen: gen-build gen-client-ts
|
gen: gen-build gen-client-ts
|
||||||
|
|
||||||
@ -240,9 +260,6 @@ docker: ## Build a docker image of the current source tree
|
|||||||
mkdir -p ${GEN_API_TS}
|
mkdir -p ${GEN_API_TS}
|
||||||
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
|
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
|
||||||
|
|
||||||
test-docker:
|
|
||||||
BUILD=true ./scripts/test_docker.sh
|
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
## CI
|
## CI
|
||||||
#########################
|
#########################
|
||||||
@ -254,21 +271,16 @@ ci--meta-debug:
|
|||||||
node --version
|
node --version
|
||||||
|
|
||||||
ci-black: ci--meta-debug
|
ci-black: ci--meta-debug
|
||||||
poetry run black --check $(PY_SOURCES)
|
black --check $(PY_SOURCES)
|
||||||
|
|
||||||
ci-ruff: ci--meta-debug
|
ci-ruff: ci--meta-debug
|
||||||
poetry run ruff check $(PY_SOURCES)
|
ruff check $(PY_SOURCES)
|
||||||
|
|
||||||
ci-codespell: ci--meta-debug
|
ci-codespell: ci--meta-debug
|
||||||
poetry run codespell -s
|
codespell $(CODESPELL_ARGS) -s
|
||||||
|
|
||||||
ci-bandit: ci--meta-debug
|
ci-bandit: ci--meta-debug
|
||||||
poetry run bandit -r $(PY_SOURCES)
|
bandit -r $(PY_SOURCES)
|
||||||
|
|
||||||
ci-pending-migrations: ci--meta-debug
|
ci-pending-migrations: ci--meta-debug
|
||||||
poetry run ak makemigrations --check
|
ak makemigrations --check
|
||||||
|
|
||||||
ci-test: ci--meta-debug
|
|
||||||
poetry run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
|
||||||
poetry run coverage report
|
|
||||||
poetry run coverage xml
|
|
||||||
|
@ -2,7 +2,7 @@ authentik takes security very seriously. We follow the rules of [responsible di
|
|||||||
|
|
||||||
## Independent audits and pentests
|
## Independent audits and pentests
|
||||||
|
|
||||||
We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specific audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).
|
In May/June of 2023 [Cure53](https://cure53.de) conducted an audit and pentest. The [results](https://cure53.de/pentest-report_authentik.pdf) are published on the [Cure53 website](https://cure53.de/#publications-2023). For more details about authentik's response to the findings of the audit refer to [2023-06 Cure53 Code audit](https://goauthentik.io/docs/security/2023-06-cure53).
|
||||||
|
|
||||||
## What authentik classifies as a CVE
|
## What authentik classifies as a CVE
|
||||||
|
|
||||||
@ -20,8 +20,8 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| --------- | --------- |
|
| --------- | --------- |
|
||||||
| 2024.12.x | ✅ |
|
| 2024.8.x | ✅ |
|
||||||
| 2025.2.x | ✅ |
|
| 2024.10.x | ✅ |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2025.2.1"
|
__version__ = "2024.10.2"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
@ -16,5 +16,5 @@ def get_full_version() -> str:
|
|||||||
"""Get full version, with build hash appended"""
|
"""Get full version, with build hash appended"""
|
||||||
version = __version__
|
version = __version__
|
||||||
if (build_hash := get_build_hash()) != "":
|
if (build_hash := get_build_hash()) != "":
|
||||||
return f"{version}+{build_hash}"
|
version += "." + build_hash
|
||||||
return version
|
return version
|
||||||
|
@ -7,9 +7,7 @@ from sys import version as python_version
|
|||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
from cryptography.hazmat.backends.openssl.backend import backend
|
from cryptography.hazmat.backends.openssl.backend import backend
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.views.debug import SafeExceptionReporterFilter
|
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
@ -54,16 +52,10 @@ class SystemInfoSerializer(PassiveSerializer):
|
|||||||
def get_http_headers(self, request: Request) -> dict[str, str]:
|
def get_http_headers(self, request: Request) -> dict[str, str]:
|
||||||
"""Get HTTP Request headers"""
|
"""Get HTTP Request headers"""
|
||||||
headers = {}
|
headers = {}
|
||||||
raw_session = request._request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
|
||||||
for key, value in request.META.items():
|
for key, value in request.META.items():
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
continue
|
continue
|
||||||
actual_value = value
|
headers[key] = value
|
||||||
if raw_session in actual_value:
|
|
||||||
actual_value = actual_value.replace(
|
|
||||||
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
|
||||||
)
|
|
||||||
headers[key] = actual_value
|
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
def get_http_host(self, request: Request) -> str:
|
def get_http_host(self, request: Request) -> str:
|
||||||
|
@ -1,16 +1,12 @@
|
|||||||
"""authentik administration overview"""
|
"""authentik administration overview"""
|
||||||
|
|
||||||
from socket import gethostname
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from drf_spectacular.utils import extend_schema, inline_serializer
|
from drf_spectacular.utils import extend_schema, inline_serializer
|
||||||
from packaging.version import parse
|
from rest_framework.fields import IntegerField
|
||||||
from rest_framework.fields import BooleanField, CharField
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from authentik import get_full_version
|
|
||||||
from authentik.rbac.permissions import HasPermission
|
from authentik.rbac.permissions import HasPermission
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
@ -20,38 +16,11 @@ class WorkerView(APIView):
|
|||||||
|
|
||||||
permission_classes = [HasPermission("authentik_rbac.view_system_info")]
|
permission_classes = [HasPermission("authentik_rbac.view_system_info")]
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(responses=inline_serializer("Workers", fields={"count": IntegerField()}))
|
||||||
responses=inline_serializer(
|
|
||||||
"Worker",
|
|
||||||
fields={
|
|
||||||
"worker_id": CharField(),
|
|
||||||
"version": CharField(),
|
|
||||||
"version_matching": BooleanField(),
|
|
||||||
},
|
|
||||||
many=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request) -> Response:
|
||||||
"""Get currently connected worker count."""
|
"""Get currently connected worker count."""
|
||||||
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5)
|
count = len(CELERY_APP.control.ping(timeout=0.5))
|
||||||
our_version = parse(get_full_version())
|
|
||||||
response = []
|
|
||||||
for worker in raw:
|
|
||||||
key = list(worker.keys())[0]
|
|
||||||
version = worker[key].get("version")
|
|
||||||
version_matching = False
|
|
||||||
if version:
|
|
||||||
version_matching = parse(version) == our_version
|
|
||||||
response.append(
|
|
||||||
{"worker_id": key, "version": version, "version_matching": version_matching}
|
|
||||||
)
|
|
||||||
# In debug we run with `task_always_eager`, so tasks are ran on the main process
|
# In debug we run with `task_always_eager`, so tasks are ran on the main process
|
||||||
if settings.DEBUG: # pragma: no cover
|
if settings.DEBUG: # pragma: no cover
|
||||||
response.append(
|
count += 1
|
||||||
{
|
return Response({"count": count})
|
||||||
"worker_id": f"authentik-debug@{gethostname()}",
|
|
||||||
"version": get_full_version(),
|
|
||||||
"version_matching": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return Response(response)
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
"""authentik admin app config"""
|
"""authentik admin app config"""
|
||||||
|
|
||||||
from prometheus_client import Info
|
from prometheus_client import Gauge, Info
|
||||||
|
|
||||||
from authentik.blueprints.apps import ManagedAppConfig
|
from authentik.blueprints.apps import ManagedAppConfig
|
||||||
|
|
||||||
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
||||||
|
GAUGE_WORKERS = Gauge("authentik_admin_workers", "Currently connected workers")
|
||||||
|
|
||||||
|
|
||||||
class AuthentikAdminConfig(ManagedAppConfig):
|
class AuthentikAdminConfig(ManagedAppConfig):
|
||||||
|
@ -1,35 +1,14 @@
|
|||||||
"""admin signals"""
|
"""admin signals"""
|
||||||
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from packaging.version import parse
|
|
||||||
from prometheus_client import Gauge
|
|
||||||
|
|
||||||
from authentik import get_full_version
|
from authentik.admin.apps import GAUGE_WORKERS
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
from authentik.root.monitoring import monitoring_set
|
from authentik.root.monitoring import monitoring_set
|
||||||
|
|
||||||
GAUGE_WORKERS = Gauge(
|
|
||||||
"authentik_admin_workers",
|
|
||||||
"Currently connected workers, their versions and if they are the same version as authentik",
|
|
||||||
["version", "version_matched"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_version = parse(get_full_version())
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(monitoring_set)
|
@receiver(monitoring_set)
|
||||||
def monitoring_set_workers(sender, **kwargs):
|
def monitoring_set_workers(sender, **kwargs):
|
||||||
"""Set worker gauge"""
|
"""Set worker gauge"""
|
||||||
raw: list[dict[str, dict]] = CELERY_APP.control.ping(timeout=0.5)
|
count = len(CELERY_APP.control.ping(timeout=0.5))
|
||||||
worker_version_count = {}
|
GAUGE_WORKERS.set(count)
|
||||||
for worker in raw:
|
|
||||||
key = list(worker.keys())[0]
|
|
||||||
version = worker[key].get("version")
|
|
||||||
version_matching = False
|
|
||||||
if version:
|
|
||||||
version_matching = parse(version) == _version
|
|
||||||
worker_version_count.setdefault(version, {"count": 0, "matching": version_matching})
|
|
||||||
worker_version_count[version]["count"] += 1
|
|
||||||
for version, stats in worker_version_count.items():
|
|
||||||
GAUGE_WORKERS.labels(version, stats["matching"]).set(stats["count"])
|
|
||||||
|
@ -34,7 +34,7 @@ class TestAdminAPI(TestCase):
|
|||||||
response = self.client.get(reverse("authentik_api:admin_workers"))
|
response = self.client.get(reverse("authentik_api:admin_workers"))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
body = loads(response.content)
|
body = loads(response.content)
|
||||||
self.assertEqual(len(body), 0)
|
self.assertEqual(body["count"], 0)
|
||||||
|
|
||||||
def test_metrics(self):
|
def test_metrics(self):
|
||||||
"""Test metrics API"""
|
"""Test metrics API"""
|
||||||
|
67
authentik/api/authorization.py
Normal file
67
authentik/api/authorization.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"""API Authorization"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models import Model
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from rest_framework.authentication import get_authorization_header
|
||||||
|
from rest_framework.filters import BaseFilterBackend
|
||||||
|
from rest_framework.permissions import BasePermission
|
||||||
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
from authentik.api.authentication import validate_auth
|
||||||
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
|
|
||||||
|
class OwnerFilter(BaseFilterBackend):
|
||||||
|
"""Filter objects by their owner"""
|
||||||
|
|
||||||
|
owner_key = "user"
|
||||||
|
|
||||||
|
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
|
||||||
|
if request.user.is_superuser:
|
||||||
|
return queryset
|
||||||
|
return queryset.filter(**{self.owner_key: request.user})
|
||||||
|
|
||||||
|
|
||||||
|
class SecretKeyFilter(DjangoFilterBackend):
|
||||||
|
"""Allow access to all objects when authenticated with secret key as token.
|
||||||
|
|
||||||
|
Replaces both DjangoFilterBackend and ObjectFilter"""
|
||||||
|
|
||||||
|
def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet:
|
||||||
|
auth_header = get_authorization_header(request)
|
||||||
|
token = validate_auth(auth_header)
|
||||||
|
if token and token == settings.SECRET_KEY:
|
||||||
|
return queryset
|
||||||
|
queryset = ObjectFilter().filter_queryset(request, queryset, view)
|
||||||
|
return super().filter_queryset(request, queryset, view)
|
||||||
|
|
||||||
|
|
||||||
|
class OwnerPermissions(BasePermission):
|
||||||
|
"""Authorize requests by an object's owner matching the requesting user"""
|
||||||
|
|
||||||
|
owner_key = "user"
|
||||||
|
|
||||||
|
def has_permission(self, request: Request, view) -> bool:
|
||||||
|
"""If the user is authenticated, we allow all requests here. For listing, the
|
||||||
|
object-level permissions are done by the filter backend"""
|
||||||
|
return request.user.is_authenticated
|
||||||
|
|
||||||
|
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
|
||||||
|
"""Check if the object's owner matches the currently logged in user"""
|
||||||
|
if not hasattr(obj, self.owner_key):
|
||||||
|
return False
|
||||||
|
owner = getattr(obj, self.owner_key)
|
||||||
|
if owner != request.user:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class OwnerSuperuserPermissions(OwnerPermissions):
|
||||||
|
"""Similar to OwnerPermissions, except always allow access for superusers"""
|
||||||
|
|
||||||
|
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
|
||||||
|
if request.user.is_superuser:
|
||||||
|
return True
|
||||||
|
return super().has_object_permission(request, view, obj)
|
@ -1,68 +0,0 @@
|
|||||||
"""Test and debug Blueprints"""
|
|
||||||
|
|
||||||
import atexit
|
|
||||||
import readline
|
|
||||||
from pathlib import Path
|
|
||||||
from pprint import pformat
|
|
||||||
from sys import exit as sysexit
|
|
||||||
from textwrap import indent
|
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand, no_translations
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
from yaml import load
|
|
||||||
|
|
||||||
from authentik.blueprints.v1.common import BlueprintLoader, EntryInvalidError
|
|
||||||
from authentik.core.management.commands.shell import get_banner_text
|
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
"""Test and debug Blueprints"""
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
histfolder = Path("~").expanduser() / Path(".local/share/authentik")
|
|
||||||
histfolder.mkdir(parents=True, exist_ok=True)
|
|
||||||
histfile = histfolder / Path("blueprint_shell_history")
|
|
||||||
readline.parse_and_bind("tab: complete")
|
|
||||||
readline.parse_and_bind("set editing-mode vi")
|
|
||||||
|
|
||||||
try:
|
|
||||||
readline.read_history_file(str(histfile))
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
atexit.register(readline.write_history_file, str(histfile))
|
|
||||||
|
|
||||||
@no_translations
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
"""Interactively debug blueprint files"""
|
|
||||||
self.stdout.write(get_banner_text("Blueprint shell"))
|
|
||||||
self.stdout.write("Type '.eval' to evaluate previously entered statement(s).")
|
|
||||||
|
|
||||||
def do_eval():
|
|
||||||
yaml_input = "\n".join([line for line in self.lines if line])
|
|
||||||
data = load(yaml_input, BlueprintLoader)
|
|
||||||
self.stdout.write(pformat(data))
|
|
||||||
self.lines = []
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
line = input("> ")
|
|
||||||
if line == ".eval":
|
|
||||||
do_eval()
|
|
||||||
else:
|
|
||||||
self.lines.append(line)
|
|
||||||
except EntryInvalidError as exc:
|
|
||||||
self.stdout.write("Failed to evaluate expression:")
|
|
||||||
self.stdout.write(indent(exception_to_string(exc), prefix=" "))
|
|
||||||
except EOFError:
|
|
||||||
break
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
self.stdout.write()
|
|
||||||
sysexit(0)
|
|
||||||
self.stdout.write()
|
|
@ -126,7 +126,7 @@ class Command(BaseCommand):
|
|||||||
def_name_perm = f"model_{model_path}_permissions"
|
def_name_perm = f"model_{model_path}_permissions"
|
||||||
def_path_perm = f"#/$defs/{def_name_perm}"
|
def_path_perm = f"#/$defs/{def_name_perm}"
|
||||||
self.schema["$defs"][def_name_perm] = self.model_permissions(model)
|
self.schema["$defs"][def_name_perm] = self.model_permissions(model)
|
||||||
template = {
|
return {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["model", "identifiers"],
|
"required": ["model", "identifiers"],
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -143,11 +143,6 @@ class Command(BaseCommand):
|
|||||||
"identifiers": {"$ref": def_path},
|
"identifiers": {"$ref": def_path},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
# Meta models don't require identifiers, as there's no matching database model to find
|
|
||||||
if issubclass(model, BaseMetaModel):
|
|
||||||
del template["properties"]["identifiers"]
|
|
||||||
template["required"].remove("identifiers")
|
|
||||||
return template
|
|
||||||
|
|
||||||
def field_to_jsonschema(self, field: Field) -> dict:
|
def field_to_jsonschema(self, field: Field) -> dict:
|
||||||
"""Convert a single field to json schema"""
|
"""Convert a single field to json schema"""
|
||||||
|
@ -146,10 +146,6 @@ entries:
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
nested_context: !Context context2
|
nested_context: !Context context2
|
||||||
at_index_sequence: !AtIndex [!Context sequence, 0]
|
|
||||||
at_index_sequence_default: !AtIndex [!Context sequence, 100, "non existent"]
|
|
||||||
at_index_mapping: !AtIndex [!Context mapping, "key2"]
|
|
||||||
at_index_mapping_default: !AtIndex [!Context mapping, "invalid", "non existent"]
|
|
||||||
identifiers:
|
identifiers:
|
||||||
name: test
|
name: test
|
||||||
conditions:
|
conditions:
|
||||||
|
@ -215,10 +215,6 @@ class TestBlueprintsV1(TransactionTestCase):
|
|||||||
},
|
},
|
||||||
"nested_context": "context-nested-value",
|
"nested_context": "context-nested-value",
|
||||||
"env_null": None,
|
"env_null": None,
|
||||||
"at_index_sequence": "foo",
|
|
||||||
"at_index_sequence_default": "non existent",
|
|
||||||
"at_index_mapping": 2,
|
|
||||||
"at_index_mapping_default": "non existent",
|
|
||||||
}
|
}
|
||||||
).exists()
|
).exists()
|
||||||
)
|
)
|
||||||
|
@ -24,10 +24,6 @@ from authentik.lib.sentry import SentryIgnoredException
|
|||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
|
|
||||||
|
|
||||||
class UNSET:
|
|
||||||
"""Used to test whether a key has not been set."""
|
|
||||||
|
|
||||||
|
|
||||||
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
||||||
"""Get object's attributes via their serializer, and convert it to a normal dict"""
|
"""Get object's attributes via their serializer, and convert it to a normal dict"""
|
||||||
serializer: Serializer = obj.serializer(obj)
|
serializer: Serializer = obj.serializer(obj)
|
||||||
@ -202,9 +198,6 @@ class Blueprint:
|
|||||||
class YAMLTag:
|
class YAMLTag:
|
||||||
"""Base class for all YAML Tags"""
|
"""Base class for all YAML Tags"""
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return str(self.resolve(BlueprintEntry(""), Blueprint()))
|
|
||||||
|
|
||||||
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
||||||
"""Implement yaml tag logic"""
|
"""Implement yaml tag logic"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -563,53 +556,6 @@ class Value(EnumeratedItem):
|
|||||||
raise EntryInvalidError.from_entry(f"Empty/invalid context: {context}", entry) from exc
|
raise EntryInvalidError.from_entry(f"Empty/invalid context: {context}", entry) from exc
|
||||||
|
|
||||||
|
|
||||||
class AtIndex(YAMLTag):
|
|
||||||
"""Get value at index of a sequence or mapping"""
|
|
||||||
|
|
||||||
obj: YAMLTag | dict | list | tuple
|
|
||||||
attribute: int | str | YAMLTag
|
|
||||||
default: Any | UNSET
|
|
||||||
|
|
||||||
def __init__(self, loader: "BlueprintLoader", node: SequenceNode) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.obj = loader.construct_object(node.value[0])
|
|
||||||
self.attribute = loader.construct_object(node.value[1])
|
|
||||||
if len(node.value) == 2: # noqa: PLR2004
|
|
||||||
self.default = UNSET
|
|
||||||
else:
|
|
||||||
self.default = loader.construct_object(node.value[2])
|
|
||||||
|
|
||||||
def resolve(self, entry: BlueprintEntry, blueprint: Blueprint) -> Any:
|
|
||||||
if isinstance(self.obj, YAMLTag):
|
|
||||||
obj = self.obj.resolve(entry, blueprint)
|
|
||||||
else:
|
|
||||||
obj = self.obj
|
|
||||||
if isinstance(self.attribute, YAMLTag):
|
|
||||||
attribute = self.attribute.resolve(entry, blueprint)
|
|
||||||
else:
|
|
||||||
attribute = self.attribute
|
|
||||||
|
|
||||||
if isinstance(obj, list | tuple):
|
|
||||||
try:
|
|
||||||
return obj[attribute]
|
|
||||||
except TypeError as exc:
|
|
||||||
raise EntryInvalidError.from_entry(
|
|
||||||
f"Invalid index for list: {attribute}", entry
|
|
||||||
) from exc
|
|
||||||
except IndexError as exc:
|
|
||||||
if self.default is UNSET:
|
|
||||||
raise EntryInvalidError.from_entry(
|
|
||||||
f"Index out of range: {attribute}", entry
|
|
||||||
) from exc
|
|
||||||
return self.default
|
|
||||||
if attribute in obj:
|
|
||||||
return obj[attribute]
|
|
||||||
else:
|
|
||||||
if self.default is UNSET:
|
|
||||||
raise EntryInvalidError.from_entry(f"Key does not exist: {attribute}", entry)
|
|
||||||
return self.default
|
|
||||||
|
|
||||||
|
|
||||||
class BlueprintDumper(SafeDumper):
|
class BlueprintDumper(SafeDumper):
|
||||||
"""Dump dataclasses to yaml"""
|
"""Dump dataclasses to yaml"""
|
||||||
|
|
||||||
@ -660,7 +606,6 @@ class BlueprintLoader(SafeLoader):
|
|||||||
self.add_constructor("!Enumerate", Enumerate)
|
self.add_constructor("!Enumerate", Enumerate)
|
||||||
self.add_constructor("!Value", Value)
|
self.add_constructor("!Value", Value)
|
||||||
self.add_constructor("!Index", Index)
|
self.add_constructor("!Index", Index)
|
||||||
self.add_constructor("!AtIndex", AtIndex)
|
|
||||||
|
|
||||||
|
|
||||||
class EntryInvalidError(SentryIgnoredException):
|
class EntryInvalidError(SentryIgnoredException):
|
||||||
|
@ -50,7 +50,7 @@ from authentik.enterprise.providers.microsoft_entra.models import (
|
|||||||
MicrosoftEntraProviderGroup,
|
MicrosoftEntraProviderGroup,
|
||||||
MicrosoftEntraProviderUser,
|
MicrosoftEntraProviderUser,
|
||||||
)
|
)
|
||||||
from authentik.enterprise.providers.ssf.models import StreamEvent
|
from authentik.enterprise.providers.rac.models import ConnectionToken
|
||||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import (
|
||||||
EndpointDevice,
|
EndpointDevice,
|
||||||
EndpointDeviceConnection,
|
EndpointDeviceConnection,
|
||||||
@ -65,13 +65,7 @@ from authentik.lib.utils.reflection import get_apps
|
|||||||
from authentik.outposts.models import OutpostServiceConnection
|
from authentik.outposts.models import OutpostServiceConnection
|
||||||
from authentik.policies.models import Policy, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBindingModel
|
||||||
from authentik.policies.reputation.models import Reputation
|
from authentik.policies.reputation.models import Reputation
|
||||||
from authentik.providers.oauth2.models import (
|
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
|
||||||
AccessToken,
|
|
||||||
AuthorizationCode,
|
|
||||||
DeviceToken,
|
|
||||||
RefreshToken,
|
|
||||||
)
|
|
||||||
from authentik.providers.rac.models import ConnectionToken
|
|
||||||
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
|
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
|
||||||
from authentik.rbac.models import Role
|
from authentik.rbac.models import Role
|
||||||
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
|
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
|
||||||
@ -131,8 +125,6 @@ def excluded_models() -> list[type[Model]]:
|
|||||||
MicrosoftEntraProviderGroup,
|
MicrosoftEntraProviderGroup,
|
||||||
EndpointDevice,
|
EndpointDevice,
|
||||||
EndpointDeviceConnection,
|
EndpointDeviceConnection,
|
||||||
DeviceToken,
|
|
||||||
StreamEvent,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -301,11 +293,7 @@ class Importer:
|
|||||||
|
|
||||||
serializer_kwargs = {}
|
serializer_kwargs = {}
|
||||||
model_instance = existing_models.first()
|
model_instance = existing_models.first()
|
||||||
if (
|
if not isinstance(model(), BaseMetaModel) and model_instance:
|
||||||
not isinstance(model(), BaseMetaModel)
|
|
||||||
and model_instance
|
|
||||||
and entry.state != BlueprintEntryDesiredState.MUST_CREATED
|
|
||||||
):
|
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"Initialise serializer with instance",
|
"Initialise serializer with instance",
|
||||||
model=model,
|
model=model,
|
||||||
@ -315,12 +303,11 @@ class Importer:
|
|||||||
serializer_kwargs["instance"] = model_instance
|
serializer_kwargs["instance"] = model_instance
|
||||||
serializer_kwargs["partial"] = True
|
serializer_kwargs["partial"] = True
|
||||||
elif model_instance and entry.state == BlueprintEntryDesiredState.MUST_CREATED:
|
elif model_instance and entry.state == BlueprintEntryDesiredState.MUST_CREATED:
|
||||||
msg = (
|
|
||||||
f"State is set to {BlueprintEntryDesiredState.MUST_CREATED.value} "
|
|
||||||
"and object exists already",
|
|
||||||
)
|
|
||||||
raise EntryInvalidError.from_entry(
|
raise EntryInvalidError.from_entry(
|
||||||
ValidationError({k: msg for k in entry.identifiers.keys()}, "unique"),
|
(
|
||||||
|
f"State is set to {BlueprintEntryDesiredState.MUST_CREATED} "
|
||||||
|
"and object exists already",
|
||||||
|
),
|
||||||
entry,
|
entry,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -159,7 +159,7 @@ def blueprints_discovery(self: SystemTask, path: str | None = None):
|
|||||||
check_blueprint_v1_file(blueprint)
|
check_blueprint_v1_file(blueprint)
|
||||||
count += 1
|
count += 1
|
||||||
self.set_status(
|
self.set_status(
|
||||||
TaskStatus.SUCCESSFUL, _("Successfully imported {count} files.".format(count=count))
|
TaskStatus.SUCCESSFUL, _("Successfully imported %(count)d files." % {"count": count})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,10 +14,10 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.validators import UniqueValidator
|
from rest_framework.validators import UniqueValidator
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.api.authorization import SecretKeyFilter
|
||||||
from authentik.brands.models import Brand
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||||
from authentik.rbac.filters import SecretKeyFilter
|
|
||||||
from authentik.tenants.utils import get_current_tenant
|
from authentik.tenants.utils import get_current_tenant
|
||||||
|
|
||||||
|
|
||||||
@ -84,8 +84,8 @@ class CurrentBrandSerializer(PassiveSerializer):
|
|||||||
|
|
||||||
matched_domain = CharField(source="domain")
|
matched_domain = CharField(source="domain")
|
||||||
branding_title = CharField()
|
branding_title = CharField()
|
||||||
branding_logo = CharField(source="branding_logo_url")
|
branding_logo = CharField()
|
||||||
branding_favicon = CharField(source="branding_favicon_url")
|
branding_favicon = CharField()
|
||||||
ui_footer_links = ListField(
|
ui_footer_links = ListField(
|
||||||
child=FooterLinkSerializer(),
|
child=FooterLinkSerializer(),
|
||||||
read_only=True,
|
read_only=True,
|
||||||
|
@ -4,7 +4,7 @@ from collections.abc import Callable
|
|||||||
|
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.utils.translation import override
|
from django.utils.translation import activate
|
||||||
|
|
||||||
from authentik.brands.utils import get_brand_for_request
|
from authentik.brands.utils import get_brand_for_request
|
||||||
|
|
||||||
@ -18,14 +18,10 @@ class BrandMiddleware:
|
|||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
|
||||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
locale_to_set = None
|
|
||||||
if not hasattr(request, "brand"):
|
if not hasattr(request, "brand"):
|
||||||
brand = get_brand_for_request(request)
|
brand = get_brand_for_request(request)
|
||||||
request.brand = brand
|
request.brand = brand
|
||||||
locale = brand.default_locale
|
locale = brand.default_locale
|
||||||
if locale != "":
|
if locale != "":
|
||||||
locale_to_set = locale
|
activate(locale)
|
||||||
if locale_to_set:
|
|
||||||
with override(locale_to_set):
|
|
||||||
return self.get_response(request)
|
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
@ -10,7 +10,6 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.models import SerializerModel
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -72,18 +71,6 @@ class Brand(SerializerModel):
|
|||||||
)
|
)
|
||||||
attributes = models.JSONField(default=dict, blank=True)
|
attributes = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
def branding_logo_url(self) -> str:
|
|
||||||
"""Get branding_logo with the correct prefix"""
|
|
||||||
if self.branding_logo.startswith("/static"):
|
|
||||||
return CONFIG.get("web.path", "/")[:-1] + self.branding_logo
|
|
||||||
return self.branding_logo
|
|
||||||
|
|
||||||
def branding_favicon_url(self) -> str:
|
|
||||||
"""Get branding_favicon with the correct prefix"""
|
|
||||||
if self.branding_favicon.startswith("/static"):
|
|
||||||
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
|
|
||||||
return self.branding_favicon
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Serializer:
|
def serializer(self) -> Serializer:
|
||||||
from authentik.brands.api import BrandSerializer
|
from authentik.brands.api import BrandSerializer
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
"""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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationEntitlementSerializer(ModelSerializer):
|
|
||||||
"""ApplicationEntitlement Serializer"""
|
|
||||||
|
|
||||||
def validate_app(self, app: Application) -> Application:
|
|
||||||
"""Ensure user has permission to view"""
|
|
||||||
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"
|
|
||||||
):
|
|
||||||
return app
|
|
||||||
raise ValidationError(_("User does not have access to application."), code="invalid")
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ApplicationEntitlement
|
|
||||||
fields = [
|
|
||||||
"pbm_uuid",
|
|
||||||
"name",
|
|
||||||
"app",
|
|
||||||
"attributes",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationEntitlementViewSet(UsedByMixin, ModelViewSet):
|
|
||||||
"""ApplicationEntitlement Viewset"""
|
|
||||||
|
|
||||||
queryset = ApplicationEntitlement.objects.all()
|
|
||||||
serializer_class = ApplicationEntitlementSerializer
|
|
||||||
search_fields = [
|
|
||||||
"pbm_uuid",
|
|
||||||
"name",
|
|
||||||
"app",
|
|
||||||
"attributes",
|
|
||||||
]
|
|
||||||
filterset_fields = [
|
|
||||||
"pbm_uuid",
|
|
||||||
"name",
|
|
||||||
"app",
|
|
||||||
]
|
|
||||||
ordering = ["name"]
|
|
@ -2,12 +2,16 @@
|
|||||||
|
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from guardian.utils import get_anonymous_user
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.fields import SerializerMethodField
|
from rest_framework.fields import SerializerMethodField
|
||||||
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from ua_parser import user_agent_parser
|
from ua_parser import user_agent_parser
|
||||||
|
|
||||||
|
from authentik.api.authorization import OwnerSuperuserPermissions
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import ModelSerializer
|
from authentik.core.api.utils import ModelSerializer
|
||||||
from authentik.core.models import AuthenticatedSession
|
from authentik.core.models import AuthenticatedSession
|
||||||
@ -106,4 +110,11 @@ class AuthenticatedSessionViewSet(
|
|||||||
search_fields = ["user__username", "last_ip", "last_user_agent"]
|
search_fields = ["user__username", "last_ip", "last_user_agent"]
|
||||||
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
|
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
|
||||||
ordering = ["user__username"]
|
ordering = ["user__username"]
|
||||||
owner_field = "user"
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
|
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user if self.request else get_anonymous_user()
|
||||||
|
if user.is_superuser:
|
||||||
|
return super().get_queryset()
|
||||||
|
return super().get_queryset().filter(user=user.pk)
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
from drf_spectacular.utils import OpenApiParameter, extend_schema
|
||||||
from guardian.shortcuts import get_objects_for_user
|
|
||||||
from rest_framework.fields import (
|
from rest_framework.fields import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
CharField,
|
CharField,
|
||||||
@ -17,6 +16,7 @@ from rest_framework.viewsets import ViewSet
|
|||||||
|
|
||||||
from authentik.core.api.utils import MetaNameSerializer
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
|
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 import device_classes, devices_for_user
|
||||||
from authentik.stages.authenticator.models import Device
|
from authentik.stages.authenticator.models import Device
|
||||||
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||||
@ -73,9 +73,7 @@ class AdminDeviceViewSet(ViewSet):
|
|||||||
def get_devices(self, **kwargs):
|
def get_devices(self, **kwargs):
|
||||||
"""Get all devices in all child classes"""
|
"""Get all devices in all child classes"""
|
||||||
for model in device_classes():
|
for model in device_classes():
|
||||||
device_set = get_objects_for_user(
|
device_set = model.objects.filter(**kwargs)
|
||||||
self.request.user, f"{model._meta.app_label}.view_{model._meta.model_name}", model
|
|
||||||
).filter(**kwargs)
|
|
||||||
yield from device_set
|
yield from device_set
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
@ -88,6 +86,10 @@ class AdminDeviceViewSet(ViewSet):
|
|||||||
],
|
],
|
||||||
responses={200: DeviceSerializer(many=True)},
|
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:
|
def list(self, request: Request) -> Response:
|
||||||
"""Get all devices for current user"""
|
"""Get all devices for current user"""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
@ -4,7 +4,6 @@ from json import loads
|
|||||||
|
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from drf_spectacular.utils import (
|
from drf_spectacular.utils import (
|
||||||
@ -82,37 +81,9 @@ class GroupSerializer(ModelSerializer):
|
|||||||
if not self.instance or not parent:
|
if not self.instance or not parent:
|
||||||
return parent
|
return parent
|
||||||
if str(parent.group_uuid) == str(self.instance.group_uuid):
|
if str(parent.group_uuid) == str(self.instance.group_uuid):
|
||||||
raise ValidationError(_("Cannot set group as parent of itself."))
|
raise ValidationError("Cannot set group as parent of itself.")
|
||||||
return parent
|
return parent
|
||||||
|
|
||||||
def validate_is_superuser(self, superuser: bool):
|
|
||||||
"""Ensure that the user creating this group has permissions to set the superuser flag"""
|
|
||||||
request: Request = self.context.get("request", None)
|
|
||||||
if not request:
|
|
||||||
return superuser
|
|
||||||
# If we're updating an instance, and the state hasn't changed, we don't need to check perms
|
|
||||||
if self.instance and superuser == self.instance.is_superuser:
|
|
||||||
return superuser
|
|
||||||
user: User = request.user
|
|
||||||
perm = (
|
|
||||||
"authentik_core.enable_group_superuser"
|
|
||||||
if superuser
|
|
||||||
else "authentik_core.disable_group_superuser"
|
|
||||||
)
|
|
||||||
has_perm = user.has_perm(perm)
|
|
||||||
if self.instance and not has_perm:
|
|
||||||
has_perm = user.has_perm(perm, self.instance)
|
|
||||||
if not has_perm:
|
|
||||||
raise ValidationError(
|
|
||||||
_(
|
|
||||||
(
|
|
||||||
"User does not have permission to set "
|
|
||||||
"superuser status to {superuser_status}."
|
|
||||||
).format_map({"superuser_status": superuser})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return superuser
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Group
|
model = Group
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -2,17 +2,19 @@
|
|||||||
|
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
|
from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField
|
||||||
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
|
||||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||||
from authentik.core.api.object_types import TypesMixin
|
from authentik.core.api.object_types import TypesMixin
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
@ -86,7 +88,7 @@ class SourceViewSet(
|
|||||||
serializer_class = SourceSerializer
|
serializer_class = SourceSerializer
|
||||||
lookup_field = "slug"
|
lookup_field = "slug"
|
||||||
search_fields = ["slug", "name"]
|
search_fields = ["slug", "name"]
|
||||||
filterset_fields = ["slug", "name", "managed", "pbm_uuid"]
|
filterset_fields = ["slug", "name", "managed"]
|
||||||
|
|
||||||
def get_queryset(self): # pragma: no cover
|
def get_queryset(self): # pragma: no cover
|
||||||
return Source.objects.select_subclasses()
|
return Source.objects.select_subclasses()
|
||||||
@ -155,22 +157,11 @@ class SourceViewSet(
|
|||||||
matching_sources.append(source_settings.validated_data)
|
matching_sources.append(source_settings.validated_data)
|
||||||
return Response(matching_sources)
|
return Response(matching_sources)
|
||||||
|
|
||||||
def destroy(self, request: Request, *args, **kwargs):
|
|
||||||
"""Prevent deletion of built-in sources"""
|
|
||||||
instance: Source = self.get_object()
|
|
||||||
|
|
||||||
if instance.managed == Source.MANAGED_INBUILT:
|
|
||||||
raise ValidationError(
|
|
||||||
{"detail": "Built-in sources cannot be deleted"}, code="protected"
|
|
||||||
)
|
|
||||||
|
|
||||||
return super().destroy(request, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class UserSourceConnectionSerializer(SourceSerializer):
|
class UserSourceConnectionSerializer(SourceSerializer):
|
||||||
"""User source connection"""
|
"""OAuth Source Serializer"""
|
||||||
|
|
||||||
source_obj = SourceSerializer(read_only=True, source="source")
|
source = SourceSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UserSourceConnection
|
model = UserSourceConnection
|
||||||
@ -178,10 +169,10 @@ class UserSourceConnectionSerializer(SourceSerializer):
|
|||||||
"pk",
|
"pk",
|
||||||
"user",
|
"user",
|
||||||
"source",
|
"source",
|
||||||
"source_obj",
|
|
||||||
"created",
|
"created",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
"user": {"read_only": True},
|
||||||
"created": {"read_only": True},
|
"created": {"read_only": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,16 +189,17 @@ class UserSourceConnectionViewSet(
|
|||||||
|
|
||||||
queryset = UserSourceConnection.objects.all()
|
queryset = UserSourceConnection.objects.all()
|
||||||
serializer_class = UserSourceConnectionSerializer
|
serializer_class = UserSourceConnectionSerializer
|
||||||
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
filterset_fields = ["user", "source__slug"]
|
filterset_fields = ["user", "source__slug"]
|
||||||
search_fields = ["source__slug"]
|
search_fields = ["source__slug"]
|
||||||
|
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
ordering = ["source__slug", "pk"]
|
ordering = ["source__slug", "pk"]
|
||||||
owner_field = "user"
|
|
||||||
|
|
||||||
|
|
||||||
class GroupSourceConnectionSerializer(SourceSerializer):
|
class GroupSourceConnectionSerializer(SourceSerializer):
|
||||||
"""Group Source Connection"""
|
"""Group Source Connection Serializer"""
|
||||||
|
|
||||||
source_obj = SourceSerializer(read_only=True)
|
source = SourceSerializer(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GroupSourceConnection
|
model = GroupSourceConnection
|
||||||
@ -215,11 +207,12 @@ class GroupSourceConnectionSerializer(SourceSerializer):
|
|||||||
"pk",
|
"pk",
|
||||||
"group",
|
"group",
|
||||||
"source",
|
"source",
|
||||||
"source_obj",
|
|
||||||
"identifier",
|
"identifier",
|
||||||
"created",
|
"created",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
"group": {"read_only": True},
|
||||||
|
"identifier": {"read_only": True},
|
||||||
"created": {"read_only": True},
|
"created": {"read_only": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +229,8 @@ class GroupSourceConnectionViewSet(
|
|||||||
|
|
||||||
queryset = GroupSourceConnection.objects.all()
|
queryset = GroupSourceConnection.objects.all()
|
||||||
serializer_class = GroupSourceConnectionSerializer
|
serializer_class = GroupSourceConnectionSerializer
|
||||||
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
filterset_fields = ["group", "source__slug"]
|
filterset_fields = ["group", "source__slug"]
|
||||||
search_fields = ["source__slug"]
|
search_fields = ["source__slug"]
|
||||||
|
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
ordering = ["source__slug", "pk"]
|
ordering = ["source__slug", "pk"]
|
||||||
owner_field = "user"
|
|
||||||
|
@ -3,15 +3,18 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
||||||
from guardian.shortcuts import assign_perm, get_anonymous_user
|
from guardian.shortcuts import assign_perm, get_anonymous_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.api.authorization import OwnerSuperuserPermissions
|
||||||
from authentik.blueprints.api import ManagedSerializer
|
from authentik.blueprints.api import ManagedSerializer
|
||||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
@ -135,8 +138,8 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"managed",
|
"managed",
|
||||||
]
|
]
|
||||||
ordering = ["identifier", "expires"]
|
ordering = ["identifier", "expires"]
|
||||||
owner_field = "user"
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
rbac_allow_create_without_perm = True
|
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
user = self.request.user if self.request else get_anonymous_user()
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
"""transactional application and provider creation"""
|
"""transactional application and provider creation"""
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.db.models import Model
|
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_field
|
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_field
|
||||||
from rest_framework.exceptions import PermissionDenied, ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField, ListField
|
from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField, ListField
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAdminUser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
@ -22,9 +20,8 @@ from authentik.blueprints.v1.common import (
|
|||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
from authentik.core.api.applications import ApplicationSerializer
|
from authentik.core.api.applications import ApplicationSerializer
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import Application, Provider
|
from authentik.core.models import Provider
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
from authentik.policies.api.bindings import PolicyBindingSerializer
|
|
||||||
|
|
||||||
|
|
||||||
def get_provider_serializer_mapping():
|
def get_provider_serializer_mapping():
|
||||||
@ -48,20 +45,6 @@ class TransactionProviderField(DictField):
|
|||||||
"""Dictionary field which can hold provider creation data"""
|
"""Dictionary field which can hold provider creation data"""
|
||||||
|
|
||||||
|
|
||||||
class TransactionPolicyBindingSerializer(PolicyBindingSerializer):
|
|
||||||
"""PolicyBindingSerializer which does not require target as target is set implicitly"""
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
# As the PolicyBindingSerializer checks that the correct things can be bound to a target
|
|
||||||
# but we don't have a target here as that's set by the blueprint, pass in an empty app
|
|
||||||
# which will have the correct allowed combination of group/user/policy.
|
|
||||||
attrs["target"] = Application()
|
|
||||||
return super().validate(attrs)
|
|
||||||
|
|
||||||
class Meta(PolicyBindingSerializer.Meta):
|
|
||||||
fields = [x for x in PolicyBindingSerializer.Meta.fields if x != "target"]
|
|
||||||
|
|
||||||
|
|
||||||
class TransactionApplicationSerializer(PassiveSerializer):
|
class TransactionApplicationSerializer(PassiveSerializer):
|
||||||
"""Serializer for creating a provider and an application in one transaction"""
|
"""Serializer for creating a provider and an application in one transaction"""
|
||||||
|
|
||||||
@ -69,8 +52,6 @@ class TransactionApplicationSerializer(PassiveSerializer):
|
|||||||
provider_model = ChoiceField(choices=list(get_provider_serializer_mapping().keys()))
|
provider_model = ChoiceField(choices=list(get_provider_serializer_mapping().keys()))
|
||||||
provider = TransactionProviderField()
|
provider = TransactionProviderField()
|
||||||
|
|
||||||
policy_bindings = TransactionPolicyBindingSerializer(many=True, required=False)
|
|
||||||
|
|
||||||
_provider_model: type[Provider] = None
|
_provider_model: type[Provider] = None
|
||||||
|
|
||||||
def validate_provider_model(self, fq_model_name: str) -> str:
|
def validate_provider_model(self, fq_model_name: str) -> str:
|
||||||
@ -115,19 +96,6 @@ class TransactionApplicationSerializer(PassiveSerializer):
|
|||||||
id="app",
|
id="app",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for binding in attrs.get("policy_bindings", []):
|
|
||||||
binding["target"] = KeyOf(None, ScalarNode(tag="", value="app"))
|
|
||||||
for key, value in binding.items():
|
|
||||||
if not isinstance(value, Model):
|
|
||||||
continue
|
|
||||||
binding[key] = value.pk
|
|
||||||
blueprint.entries.append(
|
|
||||||
BlueprintEntry(
|
|
||||||
model="authentik_policies.policybinding",
|
|
||||||
state=BlueprintEntryDesiredState.MUST_CREATED,
|
|
||||||
identifiers=binding,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
importer = Importer(blueprint, {})
|
importer = Importer(blueprint, {})
|
||||||
try:
|
try:
|
||||||
valid, _ = importer.validate(raise_validation_errors=True)
|
valid, _ = importer.validate(raise_validation_errors=True)
|
||||||
@ -152,7 +120,8 @@ class TransactionApplicationResponseSerializer(PassiveSerializer):
|
|||||||
class TransactionalApplicationView(APIView):
|
class TransactionalApplicationView(APIView):
|
||||||
"""Create provider and application and attach them in a single transaction"""
|
"""Create provider and application and attach them in a single transaction"""
|
||||||
|
|
||||||
permission_classes = [IsAuthenticated]
|
# TODO: Migrate to a more specific permission
|
||||||
|
permission_classes = [IsAdminUser]
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=TransactionApplicationSerializer(),
|
request=TransactionApplicationSerializer(),
|
||||||
@ -164,23 +133,8 @@ class TransactionalApplicationView(APIView):
|
|||||||
"""Convert data into a blueprint, validate it and apply it"""
|
"""Convert data into a blueprint, validate it and apply it"""
|
||||||
data = TransactionApplicationSerializer(data=request.data)
|
data = TransactionApplicationSerializer(data=request.data)
|
||||||
data.is_valid(raise_exception=True)
|
data.is_valid(raise_exception=True)
|
||||||
blueprint: Blueprint = data.validated_data
|
|
||||||
for entry in blueprint.entries:
|
importer = Importer(data.validated_data, {})
|
||||||
full_model = entry.get_model(blueprint)
|
|
||||||
app, __, model = full_model.partition(".")
|
|
||||||
if not request.user.has_perm(f"{app}.add_{model}"):
|
|
||||||
raise PermissionDenied(
|
|
||||||
{
|
|
||||||
entry.id: _(
|
|
||||||
"User lacks permission to create {model}".format_map(
|
|
||||||
{
|
|
||||||
"model": full_model,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
importer = Importer(blueprint, {})
|
|
||||||
applied = importer.apply()
|
applied = importer.apply()
|
||||||
response = {"applied": False, "logs": []}
|
response = {"applied": False, "logs": []}
|
||||||
response["applied"] = applied
|
response["applied"] = applied
|
||||||
|
@ -236,11 +236,9 @@ class UserSerializer(ModelSerializer):
|
|||||||
"path",
|
"path",
|
||||||
"type",
|
"type",
|
||||||
"uuid",
|
"uuid",
|
||||||
"password_change_date",
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"name": {"allow_blank": True},
|
"name": {"allow_blank": True},
|
||||||
"password_change_date": {"read_only": True},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -429,7 +427,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
queryset = User.objects.none()
|
queryset = User.objects.none()
|
||||||
ordering = ["username"]
|
ordering = ["username"]
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
search_fields = ["username", "name", "is_active", "email", "uuid", "attributes"]
|
search_fields = ["username", "name", "is_active", "email", "uuid"]
|
||||||
filterset_class = UsersFilter
|
filterset_class = UsersFilter
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -587,7 +585,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""Set password for user"""
|
"""Set password for user"""
|
||||||
user: User = self.get_object()
|
user: User = self.get_object()
|
||||||
try:
|
try:
|
||||||
user.set_password(request.data.get("password"), request=request)
|
user.set_password(request.data.get("password"))
|
||||||
user.save()
|
user.save()
|
||||||
except (ValidationError, IntegrityError) as exc:
|
except (ValidationError, IntegrityError) as exc:
|
||||||
LOGGER.debug("Failed to set password", exc=exc)
|
LOGGER.debug("Failed to set password", exc=exc)
|
||||||
@ -668,12 +666,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
|
|
||||||
@permission_required("authentik_core.impersonate")
|
@permission_required("authentik_core.impersonate")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=inline_serializer(
|
request=OpenApiTypes.NONE,
|
||||||
"ImpersonationSerializer",
|
|
||||||
{
|
|
||||||
"reason": CharField(required=True),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
responses={
|
responses={
|
||||||
"204": OpenApiResponse(description="Successfully started impersonation"),
|
"204": OpenApiResponse(description="Successfully started impersonation"),
|
||||||
"401": OpenApiResponse(description="Access denied"),
|
"401": OpenApiResponse(description="Access denied"),
|
||||||
@ -686,7 +679,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
LOGGER.debug("User attempted to impersonate", user=request.user)
|
||||||
return Response(status=401)
|
return Response(status=401)
|
||||||
user_to_be = self.get_object()
|
user_to_be = self.get_object()
|
||||||
reason = request.data.get("reason", "")
|
|
||||||
# Check both object-level perms and global perms
|
# Check both object-level perms and global perms
|
||||||
if not request.user.has_perm(
|
if not request.user.has_perm(
|
||||||
"authentik_core.impersonate", user_to_be
|
"authentik_core.impersonate", user_to_be
|
||||||
@ -696,16 +688,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
if user_to_be.pk == self.request.user.pk:
|
if user_to_be.pk == self.request.user.pk:
|
||||||
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
|
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
|
||||||
return Response(status=401)
|
return Response(status=401)
|
||||||
if not reason and request.tenant.impersonation_require_reason:
|
|
||||||
LOGGER.debug(
|
|
||||||
"User attempted to impersonate without providing a reason", user=request.user
|
|
||||||
)
|
|
||||||
return Response(status=401)
|
|
||||||
|
|
||||||
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||||
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_STARTED, reason=reason).from_http(request, user_to_be)
|
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||||
|
|
||||||
return Response(status=201)
|
return Response(status=201)
|
||||||
|
|
||||||
|
@ -32,5 +32,5 @@ class AuthentikCoreConfig(ManagedAppConfig):
|
|||||||
"name": "authentik Built-in",
|
"name": "authentik Built-in",
|
||||||
"slug": "authentik-built-in",
|
"slug": "authentik-built-in",
|
||||||
},
|
},
|
||||||
managed=Source.MANAGED_INBUILT,
|
managed="goauthentik.io/sources/inbuilt",
|
||||||
)
|
)
|
||||||
|
@ -44,12 +44,13 @@ class TokenBackend(InbuiltBackend):
|
|||||||
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
|
self, request: HttpRequest, username: str | None, password: str | None, **kwargs: Any
|
||||||
) -> User | None:
|
) -> User | None:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
user = User._default_manager.get_by_natural_key(username)
|
user = User._default_manager.get_by_natural_key(username)
|
||||||
|
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
# Run the default password hasher once to reduce the timing
|
# Run the default password hasher once to reduce the timing
|
||||||
# difference between an existing and a nonexistent user (#20760).
|
# difference between an existing and a nonexistent user (#20760).
|
||||||
User().set_password(password, request=request)
|
User().set_password(password)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
tokens = Token.filter_not_expired(
|
tokens = Token.filter_not_expired(
|
||||||
|
@ -58,7 +58,6 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||||||
self._context["user"] = user
|
self._context["user"] = user
|
||||||
if request:
|
if request:
|
||||||
req.http_request = request
|
req.http_request = request
|
||||||
self._context["http_request"] = request
|
|
||||||
req.context.update(**kwargs)
|
req.context.update(**kwargs)
|
||||||
self._context["request"] = req
|
self._context["request"] = req
|
||||||
self._context.update(**kwargs)
|
self._context.update(**kwargs)
|
||||||
|
@ -5,7 +5,6 @@ from typing import TextIO
|
|||||||
from daphne.management.commands.runserver import Command as RunServer
|
from daphne.management.commands.runserver import Command as RunServer
|
||||||
from daphne.server import Server
|
from daphne.server import Server
|
||||||
|
|
||||||
from authentik.lib.debug import start_debug_server
|
|
||||||
from authentik.root.signals import post_startup, pre_startup, startup
|
from authentik.root.signals import post_startup, pre_startup, startup
|
||||||
|
|
||||||
|
|
||||||
@ -14,7 +13,6 @@ class SignalServer(Server):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
start_debug_server()
|
|
||||||
|
|
||||||
def ready_callable():
|
def ready_callable():
|
||||||
pre_startup.send(sender=self)
|
pre_startup.send(sender=self)
|
||||||
|
@ -17,9 +17,7 @@ from authentik.events.middleware import should_log_model
|
|||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.utils import model_to_dict
|
from authentik.events.utils import model_to_dict
|
||||||
|
|
||||||
|
BANNER_TEXT = f"""### authentik shell ({get_full_version()})
|
||||||
def get_banner_text(shell_type="shell") -> str:
|
|
||||||
return f"""### authentik {shell_type} ({get_full_version()})
|
|
||||||
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
|
### Node {platform.node()} | Arch {platform.machine()} | Python {platform.python_version()} """
|
||||||
|
|
||||||
|
|
||||||
@ -116,4 +114,4 @@ class Command(BaseCommand):
|
|||||||
readline.parse_and_bind("tab: complete")
|
readline.parse_and_bind("tab: complete")
|
||||||
|
|
||||||
# Run interactive shell
|
# Run interactive shell
|
||||||
code.interact(banner=get_banner_text(), local=namespace)
|
code.interact(banner=BANNER_TEXT, local=namespace)
|
||||||
|
@ -9,7 +9,6 @@ from django.db import close_old_connections
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.debug import start_debug_server
|
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -29,7 +28,10 @@ class Command(BaseCommand):
|
|||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
LOGGER.debug("Celery options", **options)
|
LOGGER.debug("Celery options", **options)
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
start_debug_server()
|
if CONFIG.get_bool("remote_debug"):
|
||||||
|
import debugpy
|
||||||
|
|
||||||
|
debugpy.listen(("0.0.0.0", 6900)) # nosec
|
||||||
worker: Worker = CELERY_APP.Worker(
|
worker: Worker = CELERY_APP.Worker(
|
||||||
no_color=False,
|
no_color=False,
|
||||||
quiet=True,
|
quiet=True,
|
||||||
|
@ -5,7 +5,7 @@ from contextvars import ContextVar
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.translation import override
|
from django.utils.translation import activate
|
||||||
from sentry_sdk.api import set_tag
|
from sentry_sdk.api import set_tag
|
||||||
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
||||||
|
|
||||||
@ -31,20 +31,16 @@ class ImpersonateMiddleware:
|
|||||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
# No permission checks are done here, they need to be checked before
|
# No permission checks are done here, they need to be checked before
|
||||||
# SESSION_KEY_IMPERSONATE_USER is set.
|
# SESSION_KEY_IMPERSONATE_USER is set.
|
||||||
locale_to_set = None
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
locale = request.user.locale(request)
|
locale = request.user.locale(request)
|
||||||
if locale != "":
|
if locale != "":
|
||||||
locale_to_set = locale
|
activate(locale)
|
||||||
|
|
||||||
if SESSION_KEY_IMPERSONATE_USER in request.session:
|
if SESSION_KEY_IMPERSONATE_USER in request.session:
|
||||||
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
|
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||||
# Ensure that the user is active, otherwise nothing will work
|
# Ensure that the user is active, otherwise nothing will work
|
||||||
request.user.is_active = True
|
request.user.is_active = True
|
||||||
|
|
||||||
if locale_to_set:
|
|
||||||
with override(locale_to_set):
|
|
||||||
return self.get_response(request)
|
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
# Generated by Django 5.0.9 on 2024-11-20 15:16
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0040_provider_invalidation_flow"),
|
|
||||||
("authentik_policies", "0011_policybinding_failure_result_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="ApplicationEntitlement",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"policybindingmodel_ptr",
|
|
||||||
models.OneToOneField(
|
|
||||||
auto_created=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
to="authentik_policies.policybindingmodel",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("attributes", models.JSONField(blank=True, default=dict)),
|
|
||||||
("name", models.TextField()),
|
|
||||||
(
|
|
||||||
"app",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.application"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Application Entitlement",
|
|
||||||
"verbose_name_plural": "Application Entitlements",
|
|
||||||
"unique_together": {("app", "name")},
|
|
||||||
},
|
|
||||||
bases=("authentik_policies.policybindingmodel", models.Model),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,45 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-01-13 18:05
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0041_applicationentitlement"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="authenticatedsession",
|
|
||||||
index=models.Index(fields=["expires"], name="authentik_c_expires_08251d_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="authenticatedsession",
|
|
||||||
index=models.Index(fields=["expiring"], name="authentik_c_expirin_9cd839_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="authenticatedsession",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["expiring", "expires"], name="authentik_c_expirin_195a84_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="authenticatedsession",
|
|
||||||
index=models.Index(fields=["session_key"], name="authentik_c_session_d0f005_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="token",
|
|
||||||
index=models.Index(fields=["expires"], name="authentik_c_expires_a62b4b_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="token",
|
|
||||||
index=models.Index(fields=["expiring"], name="authentik_c_expirin_a1b838_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="token",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["expiring", "expires"], name="authentik_c_expirin_ba04d9_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,26 +0,0 @@
|
|||||||
# Generated by Django 5.0.11 on 2025-01-30 23:55
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name="group",
|
|
||||||
options={
|
|
||||||
"permissions": [
|
|
||||||
("add_user_to_group", "Add user to group"),
|
|
||||||
("remove_user_from_group", "Remove user from group"),
|
|
||||||
("enable_group_superuser", "Enable superuser status"),
|
|
||||||
("disable_group_superuser", "Disable superuser status"),
|
|
||||||
],
|
|
||||||
"verbose_name": "Group",
|
|
||||||
"verbose_name_plural": "Groups",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -204,8 +204,6 @@ class Group(SerializerModel, AttributesMixin):
|
|||||||
permissions = [
|
permissions = [
|
||||||
("add_user_to_group", _("Add user to group")),
|
("add_user_to_group", _("Add user to group")),
|
||||||
("remove_user_from_group", _("Remove user from group")),
|
("remove_user_from_group", _("Remove user from group")),
|
||||||
("enable_group_superuser", _("Enable superuser status")),
|
|
||||||
("disable_group_superuser", _("Disable superuser status")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -316,32 +314,6 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
|||||||
always_merger.merge(final_attributes, self.attributes)
|
always_merger.merge(final_attributes, self.attributes)
|
||||||
return final_attributes
|
return final_attributes
|
||||||
|
|
||||||
def app_entitlements(self, app: "Application | None") -> QuerySet["ApplicationEntitlement"]:
|
|
||||||
"""Get all entitlements this user has for `app`."""
|
|
||||||
if not app:
|
|
||||||
return []
|
|
||||||
all_groups = self.all_groups()
|
|
||||||
qs = app.applicationentitlement_set.filter(
|
|
||||||
Q(
|
|
||||||
Q(bindings__user=self) | Q(bindings__group__in=all_groups),
|
|
||||||
bindings__negate=False,
|
|
||||||
)
|
|
||||||
| Q(
|
|
||||||
Q(~Q(bindings__user=self), bindings__user__isnull=False)
|
|
||||||
| Q(~Q(bindings__group__in=all_groups), bindings__group__isnull=False),
|
|
||||||
bindings__negate=True,
|
|
||||||
),
|
|
||||||
bindings__enabled=True,
|
|
||||||
).order_by("name")
|
|
||||||
return qs
|
|
||||||
|
|
||||||
def app_entitlements_attributes(self, app: "Application | None") -> dict:
|
|
||||||
"""Get a dictionary containing all merged attributes from app entitlements for `app`."""
|
|
||||||
final_attributes = {}
|
|
||||||
for attrs in self.app_entitlements(app).values_list("attributes", flat=True):
|
|
||||||
always_merger.merge(final_attributes, attrs)
|
|
||||||
return final_attributes
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Serializer:
|
def serializer(self) -> Serializer:
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
@ -358,13 +330,13 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
|||||||
"""superuser == staff user"""
|
"""superuser == staff user"""
|
||||||
return self.is_superuser # type: ignore
|
return self.is_superuser # type: ignore
|
||||||
|
|
||||||
def set_password(self, raw_password, signal=True, sender=None, request=None):
|
def set_password(self, raw_password, signal=True, sender=None):
|
||||||
if self.pk and signal:
|
if self.pk and signal:
|
||||||
from authentik.core.signals import password_changed
|
from authentik.core.signals import password_changed
|
||||||
|
|
||||||
if not sender:
|
if not sender:
|
||||||
sender = self
|
sender = self
|
||||||
password_changed.send(sender=sender, user=self, password=raw_password, request=request)
|
password_changed.send(sender=sender, user=self, password=raw_password)
|
||||||
self.password_change_date = now()
|
self.password_change_date = now()
|
||||||
return super().set_password(raw_password)
|
return super().set_password(raw_password)
|
||||||
|
|
||||||
@ -601,14 +573,6 @@ class Application(SerializerModel, PolicyBindingModel):
|
|||||||
return None
|
return None
|
||||||
return candidates[-1]
|
return candidates[-1]
|
||||||
|
|
||||||
def backchannel_provider_for[T: Provider](self, provider_type: type[T], **kwargs) -> T | None:
|
|
||||||
"""Get Backchannel provider for a specific type"""
|
|
||||||
providers = self.backchannel_providers.filter(
|
|
||||||
**{f"{provider_type._meta.model_name}__isnull": False},
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
return getattr(providers.first(), provider_type._meta.model_name)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return str(self.name)
|
return str(self.name)
|
||||||
|
|
||||||
@ -617,31 +581,6 @@ class Application(SerializerModel, PolicyBindingModel):
|
|||||||
verbose_name_plural = _("Applications")
|
verbose_name_plural = _("Applications")
|
||||||
|
|
||||||
|
|
||||||
class ApplicationEntitlement(AttributesMixin, SerializerModel, PolicyBindingModel):
|
|
||||||
"""Application-scoped entitlement to control authorization in an application"""
|
|
||||||
|
|
||||||
name = models.TextField()
|
|
||||||
|
|
||||||
app = models.ForeignKey(Application, on_delete=models.CASCADE)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Application Entitlement")
|
|
||||||
verbose_name_plural = _("Application Entitlements")
|
|
||||||
unique_together = (("app", "name"),)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Application Entitlement {self.name} for app {self.app_id}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def serializer(self) -> type[Serializer]:
|
|
||||||
from authentik.core.api.application_entitlements import ApplicationEntitlementSerializer
|
|
||||||
|
|
||||||
return ApplicationEntitlementSerializer
|
|
||||||
|
|
||||||
def supported_policy_binding_targets(self):
|
|
||||||
return ["group", "user"]
|
|
||||||
|
|
||||||
|
|
||||||
class SourceUserMatchingModes(models.TextChoices):
|
class SourceUserMatchingModes(models.TextChoices):
|
||||||
"""Different modes a source can handle new/returning users"""
|
"""Different modes a source can handle new/returning users"""
|
||||||
|
|
||||||
@ -678,8 +617,6 @@ class SourceGroupMatchingModes(models.TextChoices):
|
|||||||
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
||||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||||
|
|
||||||
MANAGED_INBUILT = "goauthentik.io/sources/inbuilt"
|
|
||||||
|
|
||||||
name = models.TextField(help_text=_("Source's display Name."))
|
name = models.TextField(help_text=_("Source's display Name."))
|
||||||
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
|
||||||
|
|
||||||
@ -858,11 +795,6 @@ class ExpiringModel(models.Model):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
indexes = [
|
|
||||||
models.Index(fields=["expires"]),
|
|
||||||
models.Index(fields=["expiring"]),
|
|
||||||
models.Index(fields=["expiring", "expires"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
def expire_action(self, *args, **kwargs):
|
def expire_action(self, *args, **kwargs):
|
||||||
"""Handler which is called when this object is expired. By
|
"""Handler which is called when this object is expired. By
|
||||||
@ -918,7 +850,7 @@ class Token(SerializerModel, ManagedModel, ExpiringModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Token")
|
verbose_name = _("Token")
|
||||||
verbose_name_plural = _("Tokens")
|
verbose_name_plural = _("Tokens")
|
||||||
indexes = ExpiringModel.Meta.indexes + [
|
indexes = [
|
||||||
models.Index(fields=["identifier"]),
|
models.Index(fields=["identifier"]),
|
||||||
models.Index(fields=["key"]),
|
models.Index(fields=["key"]),
|
||||||
]
|
]
|
||||||
@ -1018,9 +950,6 @@ class AuthenticatedSession(ExpiringModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Authenticated Session")
|
verbose_name = _("Authenticated Session")
|
||||||
verbose_name_plural = _("Authenticated Sessions")
|
verbose_name_plural = _("Authenticated Sessions")
|
||||||
indexes = ExpiringModel.Meta.indexes + [
|
|
||||||
models.Index(fields=["session_key"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Authenticated Session {self.session_key[:10]}"
|
return f"Authenticated Session {self.session_key[:10]}"
|
||||||
|
@ -35,7 +35,8 @@ from authentik.flows.planner import (
|
|||||||
FlowPlanner,
|
FlowPlanner,
|
||||||
)
|
)
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||||
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
from authentik.policies.utils import delete_none_values
|
from authentik.policies.utils import delete_none_values
|
||||||
@ -46,9 +47,8 @@ from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH
|
|||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
|
||||||
SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages"
|
|
||||||
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec
|
||||||
|
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"
|
||||||
|
|
||||||
|
|
||||||
class MessageStage(StageView):
|
class MessageStage(StageView):
|
||||||
@ -129,11 +129,6 @@ class SourceFlowManager:
|
|||||||
)
|
)
|
||||||
new_connection.user = self.request.user
|
new_connection.user = self.request.user
|
||||||
new_connection = self.update_user_connection(new_connection, **kwargs)
|
new_connection = self.update_user_connection(new_connection, **kwargs)
|
||||||
if existing := self.user_connection_type.objects.filter(
|
|
||||||
source=self.source, identifier=self.identifier
|
|
||||||
).first():
|
|
||||||
existing = self.update_user_connection(existing)
|
|
||||||
return Action.AUTH, existing
|
|
||||||
return Action.LINK, new_connection
|
return Action.LINK, new_connection
|
||||||
|
|
||||||
action, connection = self.matcher.get_user_action(self.identifier, self.user_properties)
|
action, connection = self.matcher.get_user_action(self.identifier, self.user_properties)
|
||||||
@ -219,28 +214,34 @@ class SourceFlowManager:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
flow_context.update(self.policy_context)
|
flow_context.update(self.policy_context)
|
||||||
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
|
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||||
|
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
||||||
|
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
|
||||||
|
plan = token.plan
|
||||||
|
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
||||||
|
plan.context.update(flow_context)
|
||||||
|
for stage in self.get_stages_to_append(flow):
|
||||||
|
plan.append_stage(stage)
|
||||||
|
if stages:
|
||||||
|
for stage in stages:
|
||||||
|
plan.append_stage(stage)
|
||||||
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
flow_slug = token.flow.slug
|
||||||
|
token.delete()
|
||||||
|
return redirect_with_qs(
|
||||||
|
"authentik_core:if-flow",
|
||||||
|
self.request.GET,
|
||||||
|
flow_slug=flow_slug,
|
||||||
|
)
|
||||||
|
# Ensure redirect is carried through when user was trying to
|
||||||
|
# authorize application
|
||||||
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
|
)
|
||||||
|
if PLAN_CONTEXT_REDIRECT not in flow_context:
|
||||||
|
flow_context[PLAN_CONTEXT_REDIRECT] = final_redirect
|
||||||
|
|
||||||
if not flow:
|
if not flow:
|
||||||
# We only check for the flow token here if we don't have a flow, otherwise we rely on
|
|
||||||
# SESSION_KEY_SOURCE_FLOW_STAGES to delegate the usage of this token and dynamically add
|
|
||||||
# stages that deal with this token to return to another flow
|
|
||||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
|
||||||
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
|
|
||||||
self._logger.info(
|
|
||||||
"Replacing source flow with overridden flow", flow=token.flow.slug
|
|
||||||
)
|
|
||||||
plan = token.plan
|
|
||||||
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
|
|
||||||
plan.context.update(flow_context)
|
|
||||||
for stage in self.get_stages_to_append(flow):
|
|
||||||
plan.append_stage(stage)
|
|
||||||
if stages:
|
|
||||||
for stage in stages:
|
|
||||||
plan.append_stage(stage)
|
|
||||||
redirect = plan.to_redirect(self.request, token.flow)
|
|
||||||
token.delete()
|
|
||||||
return redirect
|
|
||||||
return bad_request_message(
|
return bad_request_message(
|
||||||
self.request,
|
self.request,
|
||||||
_("Configured flow does not exist."),
|
_("Configured flow does not exist."),
|
||||||
@ -259,9 +260,12 @@ class SourceFlowManager:
|
|||||||
if stages:
|
if stages:
|
||||||
for stage in stages:
|
for stage in stages:
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []):
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
plan.append_stage(stage)
|
return redirect_with_qs(
|
||||||
return plan.to_redirect(self.request, flow)
|
"authentik_core:if-flow",
|
||||||
|
self.request.GET,
|
||||||
|
flow_slug=flow.slug,
|
||||||
|
)
|
||||||
|
|
||||||
def handle_auth(
|
def handle_auth(
|
||||||
self,
|
self,
|
||||||
@ -297,8 +301,6 @@ class SourceFlowManager:
|
|||||||
# When request isn't authenticated we jump straight to auth
|
# When request isn't authenticated we jump straight to auth
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return self.handle_auth(connection)
|
return self.handle_auth(connection)
|
||||||
# When an override flow token exists we actually still use a flow for link
|
|
||||||
# to continue the existing flow we came from
|
|
||||||
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
|
||||||
return self._prepare_flow(None, connection)
|
return self._prepare_flow(None, connection)
|
||||||
connection.save()
|
connection.save()
|
||||||
|
@ -67,8 +67,6 @@ def clean_expired_models(self: SystemTask):
|
|||||||
raise ImproperlyConfigured(
|
raise ImproperlyConfigured(
|
||||||
"Invalid session_storage setting, allowed values are db and cache"
|
"Invalid session_storage setting, allowed values are db and cache"
|
||||||
)
|
)
|
||||||
if CONFIG.get("session_storage", "cache") == "db":
|
|
||||||
DBSessionStore.clear_expired()
|
|
||||||
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
|
||||||
|
|
||||||
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")
|
||||||
|
@ -9,10 +9,6 @@
|
|||||||
versionFamily: "{{ version_family }}",
|
versionFamily: "{{ version_family }}",
|
||||||
versionSubdomain: "{{ version_subdomain }}",
|
versionSubdomain: "{{ version_subdomain }}",
|
||||||
build: "{{ build }}",
|
build: "{{ build }}",
|
||||||
api: {
|
|
||||||
base: "{{ base_url }}",
|
|
||||||
relBase: "{{ base_url_rel }}",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
window.addEventListener("DOMContentLoaded", function () {
|
window.addEventListener("DOMContentLoaded", function () {
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
|
@ -8,11 +8,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
{# Darkreader breaks the site regardless of theme as its not compatible with webcomponents, and we default to a dark theme based on preferred colour-scheme #}
|
|
||||||
<meta name="darkreader-lock">
|
|
||||||
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
|
||||||
<link rel="icon" href="{{ brand.branding_favicon_url }}">
|
<link rel="icon" href="{{ brand.branding_favicon }}">
|
||||||
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
<link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" />
|
<link rel="prefetch" href="/static/dist/assets/images/flow_background.jpg" />
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||||
{% include "base/header_js.html" %}
|
{% include "base/header_js.html" %}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}");
|
--ak-flow-background: url("/static/dist/assets/images/flow_background.jpg");
|
||||||
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
||||||
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
|
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
|
||||||
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
|
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
|
||||||
@ -50,7 +50,7 @@
|
|||||||
<div class="ak-login-container">
|
<div class="ak-login-container">
|
||||||
<main class="pf-c-login__main">
|
<main class="pf-c-login__main">
|
||||||
<div class="pf-c-login__main-header pf-c-brand ak-brand">
|
<div class="pf-c-login__main-header pf-c-brand ak-brand">
|
||||||
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
|
<img src="{{ brand.branding_logo }}" alt="authentik Logo" />
|
||||||
</div>
|
</div>
|
||||||
<header class="pf-c-login__main-header">
|
<header class="pf-c-login__main-header">
|
||||||
<h1 class="pf-c-title pf-m-3xl">
|
<h1 class="pf-c-title pf-m-3xl">
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
"""Test Application Entitlements API"""
|
|
||||||
|
|
||||||
from django.urls import reverse
|
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from authentik.core.models import Application, ApplicationEntitlement, Group
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user
|
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
|
||||||
from authentik.policies.models import PolicyBinding
|
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
|
||||||
|
|
||||||
|
|
||||||
class TestApplicationEntitlements(APITestCase):
|
|
||||||
"""Test application entitlements"""
|
|
||||||
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.user = create_test_user()
|
|
||||||
self.other_user = create_test_user()
|
|
||||||
self.provider = OAuth2Provider.objects.create(
|
|
||||||
name="test",
|
|
||||||
authorization_flow=create_test_flow(),
|
|
||||||
)
|
|
||||||
self.app: Application = Application.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
slug=generate_id(),
|
|
||||||
provider=self.provider,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_user(self):
|
|
||||||
"""Test user-direct assignment"""
|
|
||||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
|
||||||
PolicyBinding.objects.create(target=ent, user=self.user, order=0)
|
|
||||||
ents = self.user.app_entitlements(self.app)
|
|
||||||
self.assertEqual(len(ents), 1)
|
|
||||||
self.assertEqual(ents[0].name, ent.name)
|
|
||||||
|
|
||||||
def test_group(self):
|
|
||||||
"""Test direct group"""
|
|
||||||
group = Group.objects.create(name=generate_id())
|
|
||||||
self.user.ak_groups.add(group)
|
|
||||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
|
||||||
PolicyBinding.objects.create(target=ent, group=group, order=0)
|
|
||||||
ents = self.user.app_entitlements(self.app)
|
|
||||||
self.assertEqual(len(ents), 1)
|
|
||||||
self.assertEqual(ents[0].name, ent.name)
|
|
||||||
|
|
||||||
def test_group_indirect(self):
|
|
||||||
"""Test indirect group"""
|
|
||||||
parent = Group.objects.create(name=generate_id())
|
|
||||||
group = Group.objects.create(name=generate_id(), parent=parent)
|
|
||||||
self.user.ak_groups.add(group)
|
|
||||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
|
||||||
PolicyBinding.objects.create(target=ent, group=parent, order=0)
|
|
||||||
ents = self.user.app_entitlements(self.app)
|
|
||||||
self.assertEqual(len(ents), 1)
|
|
||||||
self.assertEqual(ents[0].name, ent.name)
|
|
||||||
|
|
||||||
def test_negate_user(self):
|
|
||||||
"""Test with negate flag"""
|
|
||||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
|
||||||
PolicyBinding.objects.create(target=ent, user=self.other_user, order=0, negate=True)
|
|
||||||
ents = self.user.app_entitlements(self.app)
|
|
||||||
self.assertEqual(len(ents), 1)
|
|
||||||
self.assertEqual(ents[0].name, ent.name)
|
|
||||||
|
|
||||||
def test_negate_group(self):
|
|
||||||
"""Test with negate flag"""
|
|
||||||
other_group = Group.objects.create(name=generate_id())
|
|
||||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
|
||||||
PolicyBinding.objects.create(target=ent, group=other_group, order=0, negate=True)
|
|
||||||
ents = self.user.app_entitlements(self.app)
|
|
||||||
self.assertEqual(len(ents), 1)
|
|
||||||
self.assertEqual(ents[0].name, ent.name)
|
|
||||||
|
|
||||||
def test_api_perms_global(self):
|
|
||||||
"""Test API creation with global permissions"""
|
|
||||||
assign_perm("authentik_core.add_applicationentitlement", self.user)
|
|
||||||
assign_perm("authentik_core.view_application", self.user)
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
res = self.client.post(
|
|
||||||
reverse("authentik_api:applicationentitlement-list"),
|
|
||||||
data={
|
|
||||||
"name": generate_id(),
|
|
||||||
"app": self.app.pk,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_code, 201)
|
|
||||||
|
|
||||||
def test_api_perms_scoped(self):
|
|
||||||
"""Test API creation with scoped permissions"""
|
|
||||||
assign_perm("authentik_core.add_applicationentitlement", self.user)
|
|
||||||
assign_perm("authentik_core.view_application", self.user, self.app)
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
res = self.client.post(
|
|
||||||
reverse("authentik_api:applicationentitlement-list"),
|
|
||||||
data={
|
|
||||||
"name": generate_id(),
|
|
||||||
"app": self.app.pk,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_code, 201)
|
|
||||||
|
|
||||||
def test_api_perms_missing(self):
|
|
||||||
"""Test API creation with no permissions"""
|
|
||||||
assign_perm("authentik_core.add_applicationentitlement", self.user)
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
res = self.client.post(
|
|
||||||
reverse("authentik_api:applicationentitlement-list"),
|
|
||||||
data={
|
|
||||||
"name": generate_id(),
|
|
||||||
"app": self.app.pk,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_code, 400)
|
|
||||||
self.assertJSONEqual(res.content, {"app": ["User does not have access to application."]})
|
|
||||||
|
|
||||||
def test_api_bindings_policy(self):
|
|
||||||
"""Test that API doesn't allow policies to be bound to this"""
|
|
||||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
|
||||||
policy = DummyPolicy.objects.create(name=generate_id())
|
|
||||||
admin = create_test_admin_user()
|
|
||||||
self.client.force_login(admin)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("authentik_api:policybinding-list"),
|
|
||||||
data={
|
|
||||||
"target": ent.pbm_uuid,
|
|
||||||
"policy": policy.pk,
|
|
||||||
"order": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertJSONEqual(
|
|
||||||
response.content.decode(),
|
|
||||||
{"non_field_errors": ["One of 'group', 'user' must be set."]},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_api_bindings_group(self):
|
|
||||||
"""Test that API doesn't allow policies to be bound to this"""
|
|
||||||
ent = ApplicationEntitlement.objects.create(app=self.app, name=generate_id())
|
|
||||||
group = Group.objects.create(name=generate_id())
|
|
||||||
admin = create_test_admin_user()
|
|
||||||
self.client.force_login(admin)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("authentik_api:policybinding-list"),
|
|
||||||
data={
|
|
||||||
"target": ent.pbm_uuid,
|
|
||||||
"group": group.pk,
|
|
||||||
"order": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 201)
|
|
||||||
self.assertTrue(PolicyBinding.objects.filter(target=ent.pbm_uuid).exists())
|
|
@ -12,7 +12,7 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
|||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.dummy.models import DummyPolicy
|
from authentik.policies.dummy.models import DummyPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyProvider
|
||||||
from authentik.providers.saml.models import SAMLProvider
|
from authentik.providers.saml.models import SAMLProvider
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
self.user = create_test_admin_user()
|
self.user = create_test_admin_user()
|
||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://some-other-domain")],
|
redirect_uris="http://some-other-domain",
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
)
|
)
|
||||||
self.allowed: Application = Application.objects.create(
|
self.allowed: Application = Application.objects.create(
|
||||||
|
@ -4,7 +4,7 @@ from django.urls.base import reverse
|
|||||||
from guardian.shortcuts import assign_perm
|
from guardian.shortcuts import assign_perm
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group, User
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_user
|
from authentik.core.tests.utils import create_test_admin_user, create_test_user
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ class TestGroupsAPI(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.login_user = create_test_user()
|
self.login_user = create_test_user()
|
||||||
self.user = create_test_user()
|
self.user = User.objects.create(username="test-user")
|
||||||
|
|
||||||
def test_list_with_users(self):
|
def test_list_with_users(self):
|
||||||
"""Test listing with users"""
|
"""Test listing with users"""
|
||||||
@ -109,57 +109,3 @@ class TestGroupsAPI(APITestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(res.status_code, 400)
|
self.assertEqual(res.status_code, 400)
|
||||||
|
|
||||||
def test_superuser_no_perm(self):
|
|
||||||
"""Test creating a superuser group without permission"""
|
|
||||||
assign_perm("authentik_core.add_group", self.login_user)
|
|
||||||
self.client.force_login(self.login_user)
|
|
||||||
res = self.client.post(
|
|
||||||
reverse("authentik_api:group-list"),
|
|
||||||
data={"name": generate_id(), "is_superuser": True},
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_code, 400)
|
|
||||||
self.assertJSONEqual(
|
|
||||||
res.content,
|
|
||||||
{"is_superuser": ["User does not have permission to set superuser status to True."]},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_superuser_update_no_perm(self):
|
|
||||||
"""Test updating a superuser group without permission"""
|
|
||||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
|
||||||
assign_perm("view_group", self.login_user, group)
|
|
||||||
assign_perm("change_group", self.login_user, group)
|
|
||||||
self.client.force_login(self.login_user)
|
|
||||||
res = self.client.patch(
|
|
||||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
|
||||||
data={"is_superuser": False},
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_code, 400)
|
|
||||||
self.assertJSONEqual(
|
|
||||||
res.content,
|
|
||||||
{"is_superuser": ["User does not have permission to set superuser status to False."]},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_superuser_update_no_change(self):
|
|
||||||
"""Test updating a superuser group without permission
|
|
||||||
and without changing the superuser status"""
|
|
||||||
group = Group.objects.create(name=generate_id(), is_superuser=True)
|
|
||||||
assign_perm("view_group", self.login_user, group)
|
|
||||||
assign_perm("change_group", self.login_user, group)
|
|
||||||
self.client.force_login(self.login_user)
|
|
||||||
res = self.client.patch(
|
|
||||||
reverse("authentik_api:group-detail", kwargs={"pk": group.pk}),
|
|
||||||
data={"name": generate_id(), "is_superuser": True},
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_code, 200)
|
|
||||||
|
|
||||||
def test_superuser_create(self):
|
|
||||||
"""Test creating a superuser group with permission"""
|
|
||||||
assign_perm("authentik_core.add_group", self.login_user)
|
|
||||||
assign_perm("authentik_core.enable_group_superuser", self.login_user)
|
|
||||||
self.client.force_login(self.login_user)
|
|
||||||
res = self.client.post(
|
|
||||||
reverse("authentik_api:group-list"),
|
|
||||||
data={"name": generate_id(), "is_superuser": True},
|
|
||||||
)
|
|
||||||
self.assertEqual(res.status_code, 201)
|
|
||||||
|
@ -29,8 +29,7 @@ class TestImpersonation(APITestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"authentik_api:user-impersonate",
|
"authentik_api:user-impersonate",
|
||||||
kwargs={"pk": self.other_user.pk},
|
kwargs={"pk": self.other_user.pk},
|
||||||
),
|
)
|
||||||
data={"reason": "some reason"},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
@ -56,8 +55,7 @@ class TestImpersonation(APITestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"authentik_api:user-impersonate",
|
"authentik_api:user-impersonate",
|
||||||
kwargs={"pk": self.other_user.pk},
|
kwargs={"pk": self.other_user.pk},
|
||||||
),
|
)
|
||||||
data={"reason": "some reason"},
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
@ -77,8 +75,7 @@ class TestImpersonation(APITestCase):
|
|||||||
reverse(
|
reverse(
|
||||||
"authentik_api:user-impersonate",
|
"authentik_api:user-impersonate",
|
||||||
kwargs={"pk": self.other_user.pk},
|
kwargs={"pk": self.other_user.pk},
|
||||||
),
|
)
|
||||||
data={"reason": "some reason"},
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 201)
|
self.assertEqual(response.status_code, 201)
|
||||||
|
|
||||||
@ -92,8 +89,7 @@ class TestImpersonation(APITestCase):
|
|||||||
self.client.force_login(self.other_user)
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}),
|
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})
|
||||||
data={"reason": "some reason"},
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@ -109,8 +105,7 @@ class TestImpersonation(APITestCase):
|
|||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-impersonate", kwargs={"pk": self.other_user.pk}),
|
reverse("authentik_api:user-impersonate", kwargs={"pk": self.other_user.pk})
|
||||||
data={"reason": "some reason"},
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
@ -123,22 +118,7 @@ class TestImpersonation(APITestCase):
|
|||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}),
|
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk})
|
||||||
data={"reason": "some reason"},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 401)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
|
||||||
response_body = loads(response.content.decode())
|
|
||||||
self.assertEqual(response_body["user"]["username"], self.user.username)
|
|
||||||
|
|
||||||
def test_impersonate_reason_required(self):
|
|
||||||
"""test impersonation that user must provide reason"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}),
|
|
||||||
data={"reason": ""},
|
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 401)
|
self.assertEqual(response.status_code, 401)
|
||||||
|
|
||||||
|
@ -81,22 +81,6 @@ class TestSourceFlowManager(TestCase):
|
|||||||
reverse("authentik_core:if-user") + "#/settings;page-sources",
|
reverse("authentik_core:if-user") + "#/settings;page-sources",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_authenticated_auth(self):
|
|
||||||
"""Test authenticated user linking"""
|
|
||||||
user = User.objects.create(username="foo", email="foo@bar.baz")
|
|
||||||
UserOAuthSourceConnection.objects.create(
|
|
||||||
user=user, source=self.source, identifier=self.identifier
|
|
||||||
)
|
|
||||||
request = get_request("/", user=user)
|
|
||||||
flow_manager = OAuthSourceFlowManager(
|
|
||||||
self.source, request, self.identifier, {"info": {}}, {}
|
|
||||||
)
|
|
||||||
action, connection = flow_manager.get_action()
|
|
||||||
self.assertEqual(action, Action.AUTH)
|
|
||||||
self.assertIsNotNone(connection.pk)
|
|
||||||
response = flow_manager.get_flow()
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
|
|
||||||
def test_unauthenticated_link(self):
|
def test_unauthenticated_link(self):
|
||||||
"""Test un-authenticated user linking"""
|
"""Test un-authenticated user linking"""
|
||||||
flow_manager = OAuthSourceFlowManager(
|
flow_manager = OAuthSourceFlowManager(
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
"""Test Transactional API"""
|
"""Test Transactional API"""
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from guardian.shortcuts import assign_perm
|
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import Application, Group
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_flow, create_test_user
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.models import PolicyBinding
|
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
|
|
||||||
|
|
||||||
@ -15,9 +13,7 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
"""Test Transactional API"""
|
"""Test Transactional API"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.user = create_test_user()
|
self.user = create_test_admin_user()
|
||||||
assign_perm("authentik_core.add_application", self.user)
|
|
||||||
assign_perm("authentik_providers_oauth2.add_oauth2provider", self.user)
|
|
||||||
|
|
||||||
def test_create_transactional(self):
|
def test_create_transactional(self):
|
||||||
"""Test transactional Application + provider creation"""
|
"""Test transactional Application + provider creation"""
|
||||||
@ -35,7 +31,6 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
"name": uid,
|
"name": uid,
|
||||||
"authorization_flow": str(create_test_flow().pk),
|
"authorization_flow": str(create_test_flow().pk),
|
||||||
"invalidation_flow": str(create_test_flow().pk),
|
"invalidation_flow": str(create_test_flow().pk),
|
||||||
"redirect_uris": [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -46,66 +41,6 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
self.assertIsNotNone(app)
|
self.assertIsNotNone(app)
|
||||||
self.assertEqual(app.provider.pk, provider.pk)
|
self.assertEqual(app.provider.pk, provider.pk)
|
||||||
|
|
||||||
def test_create_transactional_permission_denied(self):
|
|
||||||
"""Test transactional Application + provider creation (missing permissions)"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
uid = generate_id()
|
|
||||||
response = self.client.put(
|
|
||||||
reverse("authentik_api:core-transactional-application"),
|
|
||||||
data={
|
|
||||||
"app": {
|
|
||||||
"name": uid,
|
|
||||||
"slug": uid,
|
|
||||||
},
|
|
||||||
"provider_model": "authentik_providers_saml.samlprovider",
|
|
||||||
"provider": {
|
|
||||||
"name": uid,
|
|
||||||
"authorization_flow": str(create_test_flow().pk),
|
|
||||||
"invalidation_flow": str(create_test_flow().pk),
|
|
||||||
"acs_url": "https://goauthentik.io",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertJSONEqual(
|
|
||||||
response.content.decode(),
|
|
||||||
{"provider": "User lacks permission to create authentik_providers_saml.samlprovider"},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_create_transactional_bindings(self):
|
|
||||||
"""Test transactional Application + provider creation"""
|
|
||||||
assign_perm("authentik_policies.add_policybinding", self.user)
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
uid = generate_id()
|
|
||||||
group = Group.objects.create(name=generate_id())
|
|
||||||
authorization_flow = create_test_flow()
|
|
||||||
response = self.client.put(
|
|
||||||
reverse("authentik_api:core-transactional-application"),
|
|
||||||
data={
|
|
||||||
"app": {
|
|
||||||
"name": uid,
|
|
||||||
"slug": uid,
|
|
||||||
},
|
|
||||||
"provider_model": "authentik_providers_oauth2.oauth2provider",
|
|
||||||
"provider": {
|
|
||||||
"name": uid,
|
|
||||||
"authorization_flow": str(authorization_flow.pk),
|
|
||||||
"invalidation_flow": str(authorization_flow.pk),
|
|
||||||
"redirect_uris": [],
|
|
||||||
},
|
|
||||||
"policy_bindings": [{"group": group.pk, "order": 0}],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertJSONEqual(response.content.decode(), {"applied": True, "logs": []})
|
|
||||||
provider = OAuth2Provider.objects.filter(name=uid).first()
|
|
||||||
self.assertIsNotNone(provider)
|
|
||||||
app = Application.objects.filter(slug=uid).first()
|
|
||||||
self.assertIsNotNone(app)
|
|
||||||
self.assertEqual(app.provider.pk, provider.pk)
|
|
||||||
binding = PolicyBinding.objects.filter(target=app).first()
|
|
||||||
self.assertIsNotNone(binding)
|
|
||||||
self.assertEqual(binding.target, app)
|
|
||||||
self.assertEqual(binding.group, group)
|
|
||||||
|
|
||||||
def test_create_transactional_invalid(self):
|
def test_create_transactional_invalid(self):
|
||||||
"""Test transactional Application + provider creation"""
|
"""Test transactional Application + provider creation"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
@ -122,7 +57,6 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
"name": uid,
|
"name": uid,
|
||||||
"authorization_flow": "",
|
"authorization_flow": "",
|
||||||
"invalidation_flow": "",
|
"invalidation_flow": "",
|
||||||
"redirect_uris": [],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -135,32 +69,3 @@ class TestTransactionalApplicationsAPI(APITestCase):
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_create_transactional_duplicate_name_provider(self):
|
|
||||||
"""Test transactional Application + provider creation"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
uid = generate_id()
|
|
||||||
OAuth2Provider.objects.create(
|
|
||||||
name=uid,
|
|
||||||
authorization_flow=create_test_flow(),
|
|
||||||
invalidation_flow=create_test_flow(),
|
|
||||||
)
|
|
||||||
response = self.client.put(
|
|
||||||
reverse("authentik_api:core-transactional-application"),
|
|
||||||
data={
|
|
||||||
"app": {
|
|
||||||
"name": uid,
|
|
||||||
"slug": uid,
|
|
||||||
},
|
|
||||||
"provider_model": "authentik_providers_oauth2.oauth2provider",
|
|
||||||
"provider": {
|
|
||||||
"name": uid,
|
|
||||||
"authorization_flow": str(create_test_flow().pk),
|
|
||||||
"invalidation_flow": str(create_test_flow().pk),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertJSONEqual(
|
|
||||||
response.content.decode(),
|
|
||||||
{"provider": {"name": ["State is set to must_created and object exists already"]}},
|
|
||||||
)
|
|
||||||
|
@ -6,7 +6,6 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from authentik.core.api.application_entitlements import ApplicationEntitlementViewSet
|
|
||||||
from authentik.core.api.applications import ApplicationViewSet
|
from authentik.core.api.applications import ApplicationViewSet
|
||||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||||
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
||||||
@ -70,7 +69,6 @@ urlpatterns = [
|
|||||||
api_urlpatterns = [
|
api_urlpatterns = [
|
||||||
("core/authenticated_sessions", AuthenticatedSessionViewSet),
|
("core/authenticated_sessions", AuthenticatedSessionViewSet),
|
||||||
("core/applications", ApplicationViewSet),
|
("core/applications", ApplicationViewSet),
|
||||||
("core/application_entitlements", ApplicationEntitlementViewSet),
|
|
||||||
path(
|
path(
|
||||||
"core/transactional/applications/",
|
"core/transactional/applications/",
|
||||||
TransactionalApplicationView.as_view(),
|
TransactionalApplicationView.as_view(),
|
||||||
|
@ -17,8 +17,10 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
|||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import (
|
from authentik.flows.views.executor import (
|
||||||
SESSION_KEY_APPLICATION_PRE,
|
SESSION_KEY_APPLICATION_PRE,
|
||||||
|
SESSION_KEY_PLAN,
|
||||||
ToDefaultFlow,
|
ToDefaultFlow,
|
||||||
)
|
)
|
||||||
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
from authentik.stages.consent.stage import (
|
from authentik.stages.consent.stage import (
|
||||||
PLAN_CONTEXT_CONSENT_HEADER,
|
PLAN_CONTEXT_CONSENT_HEADER,
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||||
@ -55,8 +57,9 @@ class RedirectToAppLaunch(View):
|
|||||||
)
|
)
|
||||||
except FlowNonApplicableException:
|
except FlowNonApplicableException:
|
||||||
raise Http404 from None
|
raise Http404 from None
|
||||||
plan.append_stage(in_memory_stage(RedirectToAppStage))
|
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
||||||
return plan.to_redirect(request, flow)
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
||||||
|
|
||||||
|
|
||||||
class RedirectToAppStage(ChallengeStageView):
|
class RedirectToAppStage(ChallengeStageView):
|
||||||
|
@ -16,7 +16,6 @@ from authentik.api.v3.config import ConfigView
|
|||||||
from authentik.brands.api import CurrentBrandSerializer
|
from authentik.brands.api import CurrentBrandSerializer
|
||||||
from authentik.brands.models import Brand
|
from authentik.brands.models import Brand
|
||||||
from authentik.core.models import UserTypes
|
from authentik.core.models import UserTypes
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
|
|
||||||
|
|
||||||
@ -52,8 +51,6 @@ class InterfaceView(TemplateView):
|
|||||||
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
|
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
|
||||||
kwargs["build"] = get_build_hash()
|
kwargs["build"] = get_build_hash()
|
||||||
kwargs["url_kwargs"] = self.kwargs
|
kwargs["url_kwargs"] = self.kwargs
|
||||||
kwargs["base_url"] = self.request.build_absolute_uri(CONFIG.get("web.path", "/"))
|
|
||||||
kwargs["base_url_rel"] = CONFIG.get("web.path", "/")
|
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ from rest_framework.validators import UniqueValidator
|
|||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.api.authorization import SecretKeyFilter
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||||
from authentik.crypto.apps import MANAGED_KEY
|
from authentik.crypto.apps import MANAGED_KEY
|
||||||
@ -35,7 +36,7 @@ from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
|
|||||||
from authentik.crypto.models import CertificateKeyPair
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.rbac.decorators import permission_required
|
from authentik.rbac.decorators import permission_required
|
||||||
from authentik.rbac.filters import ObjectFilter, SecretKeyFilter
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -85,5 +85,5 @@ def certificate_discovery(self: SystemTask):
|
|||||||
if dirty:
|
if dirty:
|
||||||
cert.save()
|
cert.save()
|
||||||
self.set_status(
|
self.set_status(
|
||||||
TaskStatus.SUCCESSFUL, _("Successfully imported {count} files.".format(count=discovered))
|
TaskStatus.SUCCESSFUL, _("Successfully imported %(count)d files." % {"count": discovered})
|
||||||
)
|
)
|
||||||
|
@ -18,7 +18,7 @@ from authentik.crypto.models import CertificateKeyPair
|
|||||||
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
|
from authentik.crypto.tasks import MANAGED_DISCOVERED, certificate_discovery
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
|
|
||||||
|
|
||||||
class TestCrypto(APITestCase):
|
class TestCrypto(APITestCase):
|
||||||
@ -274,7 +274,7 @@ class TestCrypto(APITestCase):
|
|||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_key(),
|
client_secret=generate_key(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
redirect_uris="http://localhost",
|
||||||
signing_key=keypair,
|
signing_key=keypair,
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
@ -306,7 +306,7 @@ class TestCrypto(APITestCase):
|
|||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_key(),
|
client_secret=generate_key(),
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://localhost")],
|
redirect_uris="http://localhost",
|
||||||
signing_key=keypair,
|
signing_key=keypair,
|
||||||
)
|
)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
|
@ -97,8 +97,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
|||||||
thread_kwargs: dict | None = None,
|
thread_kwargs: dict | None = None,
|
||||||
**_,
|
**_,
|
||||||
):
|
):
|
||||||
if not self.enabled:
|
|
||||||
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)
|
|
||||||
if not should_log_model(instance):
|
if not should_log_model(instance):
|
||||||
return None
|
return None
|
||||||
thread_kwargs = {}
|
thread_kwargs = {}
|
||||||
@ -124,8 +122,6 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
|
|||||||
):
|
):
|
||||||
thread_kwargs = {}
|
thread_kwargs = {}
|
||||||
m2m_field = None
|
m2m_field = None
|
||||||
if not self.enabled:
|
|
||||||
return super().m2m_changed_handler(request, sender, instance, action, thread_kwargs)
|
|
||||||
# For the audit log we don't care about `pre_` or `post_` so we trim that part off
|
# For the audit log we don't care about `pre_` or `post_` so we trim that part off
|
||||||
_, _, action_direction = action.partition("_")
|
_, _, action_direction = action.partition("_")
|
||||||
# resolve the "through" model to an actual field
|
# resolve the "through" model to an actual field
|
||||||
|
@ -6,7 +6,6 @@ from django.http import HttpRequest, HttpResponse, JsonResponse
|
|||||||
from django.urls import resolve
|
from django.urls import resolve
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
|
|
||||||
from authentik.core.api.users import UserViewSet
|
|
||||||
from authentik.enterprise.api import LicenseViewSet
|
from authentik.enterprise.api import LicenseViewSet
|
||||||
from authentik.enterprise.license import LicenseKey
|
from authentik.enterprise.license import LicenseKey
|
||||||
from authentik.enterprise.models import LicenseUsageStatus
|
from authentik.enterprise.models import LicenseUsageStatus
|
||||||
@ -60,9 +59,6 @@ class EnterpriseMiddleware:
|
|||||||
# Flow executor is mounted as an API path but explicitly allowed
|
# Flow executor is mounted as an API path but explicitly allowed
|
||||||
if request.resolver_match._func_path == class_to_path(FlowExecutorView):
|
if request.resolver_match._func_path == class_to_path(FlowExecutorView):
|
||||||
return True
|
return True
|
||||||
# Always allow making changes to users, even in case the license has ben exceeded
|
|
||||||
if request.resolver_match._func_path == class_to_path(UserViewSet):
|
|
||||||
return True
|
|
||||||
# Only apply these restrictions to the API
|
# Only apply these restrictions to the API
|
||||||
if "authentik_api" not in request.resolver_match.app_names:
|
if "authentik_api" not in request.resolver_match.app_names:
|
||||||
return True
|
return True
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-01-13 18:05
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_enterprise", "0003_remove_licenseusage_within_limits_and_more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="licenseusage",
|
|
||||||
index=models.Index(fields=["expires"], name="authentik_e_expires_3f2956_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="licenseusage",
|
|
||||||
index=models.Index(fields=["expiring"], name="authentik_e_expirin_11d3d7_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="licenseusage",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["expiring", "expires"], name="authentik_e_expirin_4d558f_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -93,4 +93,3 @@ class LicenseUsage(ExpiringModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("License Usage")
|
verbose_name = _("License Usage")
|
||||||
verbose_name_plural = _("License Usage Records")
|
verbose_name_plural = _("License Usage Records")
|
||||||
indexes = ExpiringModel.Meta.indexes
|
|
||||||
|
@ -37,7 +37,6 @@ class GoogleWorkspaceProviderSerializer(EnterpriseRequiredMixin, ProviderSeriali
|
|||||||
"user_delete_action",
|
"user_delete_action",
|
||||||
"group_delete_action",
|
"group_delete_action",
|
||||||
"default_group_email_domain",
|
"default_group_email_domain",
|
||||||
"dry_run",
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
|
|
||||||
|
@ -8,10 +8,9 @@ from httplib2 import HttpLib2Error, HttpLib2ErrorWithResponse
|
|||||||
|
|
||||||
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
|
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
|
||||||
from authentik.lib.sync.outgoing import HTTP_CONFLICT
|
from authentik.lib.sync.outgoing import HTTP_CONFLICT
|
||||||
from authentik.lib.sync.outgoing.base import SAFE_METHODS, BaseOutgoingSyncClient
|
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
|
||||||
from authentik.lib.sync.outgoing.exceptions import (
|
from authentik.lib.sync.outgoing.exceptions import (
|
||||||
BadRequestSyncException,
|
BadRequestSyncException,
|
||||||
DryRunRejected,
|
|
||||||
NotFoundSyncException,
|
NotFoundSyncException,
|
||||||
ObjectExistsSyncException,
|
ObjectExistsSyncException,
|
||||||
StopSync,
|
StopSync,
|
||||||
@ -44,8 +43,6 @@ class GoogleWorkspaceSyncClient[TModel: Model, TConnection: Model, TSchema: dict
|
|||||||
self.domains.append(domain_name)
|
self.domains.append(domain_name)
|
||||||
|
|
||||||
def _request(self, request: HttpRequest):
|
def _request(self, request: HttpRequest):
|
||||||
if self.provider.dry_run and request.method.upper() not in SAFE_METHODS:
|
|
||||||
raise DryRunRejected(request.uri, request.method, request.body)
|
|
||||||
try:
|
try:
|
||||||
response = request.execute()
|
response = request.execute()
|
||||||
except GoogleAuthError as exc:
|
except GoogleAuthError as exc:
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
# Generated by Django 5.0.12 on 2025-02-24 19:43
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
(
|
|
||||||
"authentik_providers_google_workspace",
|
|
||||||
"0003_googleworkspaceprovidergroup_attributes_and_more",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="googleworkspaceprovider",
|
|
||||||
name="dry_run",
|
|
||||||
field=models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="When enabled, provider will not modify or create objects in the remote system.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user