Compare commits
4 Commits
sfe-packag
...
root/more-
Author | SHA1 | Date | |
---|---|---|---|
06337283e8 | |||
5d28114a4b | |||
b7c154ccd2 | |||
d1fbf2ed65 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2025.2.3
|
current_version = 2024.12.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*))?
|
||||||
@ -17,8 +17,6 @@ optional_value = final
|
|||||||
|
|
||||||
[bumpversion:file:pyproject.toml]
|
[bumpversion:file:pyproject.toml]
|
||||||
|
|
||||||
[bumpversion:file:uv.lock]
|
|
||||||
|
|
||||||
[bumpversion:file:package.json]
|
[bumpversion:file:package.json]
|
||||||
|
|
||||||
[bumpversion:file:docker-compose.yml]
|
[bumpversion:file:docker-compose.yml]
|
||||||
@ -33,4 +31,4 @@ optional_value = final
|
|||||||
|
|
||||||
[bumpversion:file:web/src/common/constants.ts]
|
[bumpversion:file:web/src/common/constants.ts]
|
||||||
|
|
||||||
[bumpversion:file:lifecycle/aws/template.yaml]
|
[bumpversion:file:website/docs/install-config/install/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**
|
||||||
|
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: Documentation issue
|
|
||||||
about: Suggest an improvement or report a problem
|
|
||||||
title: ""
|
|
||||||
labels: documentation
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
|
|
||||||
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
|
|
||||||
|
|
||||||
**Provide the URL or link to the exact page in the documentation to which you are referring.**
|
|
||||||
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the documentation issue here.
|
|
||||||
|
|
||||||
**Consider opening a PR!**
|
|
||||||
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).
|
|
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
|
||||||
|
14
.github/actions/docker-push-variables/action.yml
vendored
14
.github/actions/docker-push-variables/action.yml
vendored
@ -9,9 +9,6 @@ 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:
|
shouldPush:
|
||||||
@ -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,7 +2,6 @@
|
|||||||
|
|
||||||
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()
|
||||||
@ -44,11 +43,12 @@ if is_release:
|
|||||||
]
|
]
|
||||||
if not prerelease:
|
if not prerelease:
|
||||||
image_tags += [
|
image_tags += [
|
||||||
|
f"{name}:latest",
|
||||||
f"{name}:{version_family}",
|
f"{name}:{version_family}",
|
||||||
]
|
]
|
||||||
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 += [
|
||||||
@ -70,31 +70,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"shouldPush={str(should_push).lower()}", 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
|
||||||
|
20
.github/actions/setup/action.yml
vendored
20
.github/actions/setup/action.yml
vendored
@ -9,22 +9,17 @@ inputs:
|
|||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Install apt deps
|
- name: Install poetry & deps
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
pipx install poetry || true
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
||||||
- name: Install uv
|
- name: Setup python and restore poetry
|
||||||
uses: astral-sh/setup-uv@v5
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
- name: Setup python
|
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version-file: "pyproject.toml"
|
python-version-file: "pyproject.toml"
|
||||||
- name: Install Python deps
|
cache: "poetry"
|
||||||
shell: bash
|
|
||||||
run: uv sync --all-extras --dev --frozen
|
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@ -35,18 +30,15 @@ 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 install --sync
|
||||||
cd web && npm ci
|
cd web && npm ci
|
||||||
- name: Generate config
|
- name: Generate config
|
||||||
shell: uv run python {0}
|
shell: poetry run python {0}
|
||||||
run: |
|
run: |
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
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
|
||||||
|
18
.github/dependabot.yml
vendored
18
.github/dependabot.yml
vendored
@ -82,23 +82,7 @@ updates:
|
|||||||
docusaurus:
|
docusaurus:
|
||||||
patterns:
|
patterns:
|
||||||
- "@docusaurus/*"
|
- "@docusaurus/*"
|
||||||
build:
|
- package-ecosystem: pip
|
||||||
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: uv
|
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
|
@ -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
|
|
5
.github/workflows/api-ts-publish.yml
vendored
5
.github/workflows/api-ts-publish.yml
vendored
@ -36,6 +36,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||||
npm i @goauthentik/api@$VERSION
|
npm i @goauthentik/api@$VERSION
|
||||||
|
- name: Upgrade /web/packages/sfe
|
||||||
|
working-directory: web/packages/sfe
|
||||||
|
run: |
|
||||||
|
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||||
|
npm i @goauthentik/api@$VERSION
|
||||||
- uses: peter-evans/create-pull-request@v7
|
- uses: peter-evans/create-pull-request@v7
|
||||||
id: cpr
|
id: cpr
|
||||||
with:
|
with:
|
||||||
|
8
.github/workflows/ci-aws-cfn.yml
vendored
8
.github/workflows/ci-aws-cfn.yml
vendored
@ -25,15 +25,15 @@ jobs:
|
|||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: lifecycle/aws/package.json
|
node-version-file: website/package.json
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
cache-dependency-path: lifecycle/aws/package-lock.json
|
cache-dependency-path: website/package-lock.json
|
||||||
- working-directory: lifecycle/aws/
|
- working-directory: website/
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
- name: Check changes have been applied
|
- name: Check changes have been applied
|
||||||
run: |
|
run: |
|
||||||
uv run make aws-cfn
|
poetry run make aws-cfn
|
||||||
git diff --exit-code
|
git diff --exit-code
|
||||||
ci-aws-cfn-mark:
|
ci-aws-cfn-mark:
|
||||||
if: always()
|
if: always()
|
||||||
|
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
|
|
123
.github/workflows/ci-main.yml
vendored
123
.github/workflows/ci-main.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: run job
|
- name: run job
|
||||||
run: uv run make ci-${{ matrix.job }}
|
run: poetry run make ci-${{ matrix.job }}
|
||||||
test-migrations:
|
test-migrations:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@ -42,48 +42,35 @@ jobs:
|
|||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: run migrations
|
- name: run migrations
|
||||||
run: uv 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:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: checkout stable
|
- name: checkout stable
|
||||||
run: |
|
run: |
|
||||||
|
# Delete all poetry envs
|
||||||
|
rm -rf /home/runner/.cache/pypoetry
|
||||||
# Copy current, latest config to local
|
# Copy current, latest config to local
|
||||||
# Temporarly comment the .github backup while migrating to uv
|
|
||||||
cp authentik/lib/default.yml local.env.yml
|
cp authentik/lib/default.yml local.env.yml
|
||||||
# cp -R .github ..
|
cp -R .github ..
|
||||||
cp -R scripts ..
|
cp -R scripts ..
|
||||||
git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
|
git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
|
||||||
# rm -rf .github/ scripts/
|
rm -rf .github/ scripts/
|
||||||
# mv ../.github ../scripts .
|
mv ../.github ../scripts .
|
||||||
rm -rf scripts/
|
|
||||||
mv ../scripts .
|
|
||||||
- name: Setup authentik env (stable)
|
- name: Setup authentik env (stable)
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
postgresql_version: ${{ matrix.psql }}
|
postgresql_version: ${{ matrix.psql }}
|
||||||
continue-on-error: true
|
|
||||||
- name: run migrations to stable
|
- name: run migrations to stable
|
||||||
run: poetry run python -m lifecycle.migrate
|
run: poetry run python -m lifecycle.migrate
|
||||||
- name: checkout current code
|
- name: checkout current code
|
||||||
@ -93,34 +80,31 @@ jobs:
|
|||||||
git reset --hard HEAD
|
git reset --hard HEAD
|
||||||
git clean -d -fx .
|
git clean -d -fx .
|
||||||
git checkout $GITHUB_SHA
|
git checkout $GITHUB_SHA
|
||||||
|
# Delete previous poetry env
|
||||||
|
rm -rf /home/runner/.cache/pypoetry/virtualenvs/*
|
||||||
- name: Setup authentik env (ensure latest deps are installed)
|
- name: Setup authentik env (ensure latest deps are installed)
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
with:
|
||||||
postgresql_version: ${{ matrix.psql }}
|
postgresql_version: ${{ matrix.psql }}
|
||||||
- name: migrate to latest
|
- name: migrate to latest
|
||||||
run: |
|
run: |
|
||||||
uv run python -m lifecycle.migrate
|
poetry run python -m lifecycle.migrate
|
||||||
- name: run tests
|
- name: run tests
|
||||||
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: |
|
||||||
uv 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,12 +112,9 @@ 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: |
|
||||||
uv 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@v5
|
||||||
with:
|
with:
|
||||||
@ -156,8 +137,8 @@ jobs:
|
|||||||
uses: helm/kind-action@v1.12.0
|
uses: helm/kind-action@v1.12.0
|
||||||
- name: run integration
|
- name: run integration
|
||||||
run: |
|
run: |
|
||||||
uv run coverage run manage.py test tests/integration
|
poetry run coverage run manage.py test tests/integration
|
||||||
uv run coverage xml
|
poetry run coverage xml
|
||||||
- if: ${{ always() }}
|
- if: ${{ always() }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
@ -214,8 +195,8 @@ jobs:
|
|||||||
npm run build
|
npm run build
|
||||||
- name: run e2e
|
- name: run e2e
|
||||||
run: |
|
run: |
|
||||||
uv run coverage run manage.py test ${{ matrix.job.glob }}
|
poetry run coverage run manage.py test ${{ matrix.job.glob }}
|
||||||
uv run coverage xml
|
poetry run coverage xml
|
||||||
- if: ${{ always() }}
|
- if: ${{ always() }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
@ -242,18 +223,68 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
jobs: ${{ toJSON(needs) }}
|
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.3.0
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: prepare variables
|
||||||
|
uses: ./.github/actions/docker-push-variables
|
||||||
|
id: ev
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
with:
|
||||||
|
image-name: ghcr.io/goauthentik/dev-server
|
||||||
|
image-arch: ${{ matrix.arch }}
|
||||||
|
- name: Login to Container Registry
|
||||||
|
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: generate ts client
|
||||||
|
run: make gen-client-ts
|
||||||
|
- name: Build Docker Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
id: push
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
secrets: |
|
||||||
|
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
||||||
|
GEOIPUPDATE_LICENSE_KEY=${{ secrets.GEOIPUPDATE_LICENSE_KEY }}
|
||||||
|
tags: ${{ steps.ev.outputs.imageTags }}
|
||||||
|
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||||
|
build-args: |
|
||||||
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
|
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache
|
||||||
|
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-server:buildcache,mode=max' || '' }}
|
||||||
|
platforms: linux/${{ matrix.arch }}
|
||||||
|
- uses: actions/attest-build-provenance@v2
|
||||||
|
id: attest
|
||||||
|
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||||
|
with:
|
||||||
|
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||||
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
push-to-registry: true
|
||||||
pr-comment:
|
pr-comment:
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
|
6
.github/workflows/ci-outpost.yml
vendored
6
.github/workflows/ci-outpost.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
|||||||
- name: Generate API
|
- name: Generate API
|
||||||
run: make gen-client-go
|
run: make gen-client-go
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v7
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: --timeout 5000s --verbose
|
args: --timeout 5000s --verbose
|
||||||
@ -72,7 +72,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 +82,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.3.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
|
||||||
|
@ -2,7 +2,7 @@ name: authentik-gen-update-webauthn-mds
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "30 1 1,15 * *"
|
- cron: '30 1 1,15 * *'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
POSTGRES_DB: authentik
|
POSTGRES_DB: authentik
|
||||||
@ -24,7 +24,7 @@ jobs:
|
|||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- run: uv run ak update_webauthn_mds
|
- run: poetry run ak update_webauthn_mds
|
||||||
- uses: peter-evans/create-pull-request@v7
|
- uses: peter-evans/create-pull-request@v7
|
||||||
id: cpr
|
id: cpr
|
||||||
with:
|
with:
|
||||||
|
4
.github/workflows/publish-source-docs.yml
vendored
4
.github/workflows/publish-source-docs.yml
vendored
@ -21,8 +21,8 @@ jobs:
|
|||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: generate docs
|
- name: generate docs
|
||||||
run: |
|
run: |
|
||||||
uv run make migrate
|
poetry run make migrate
|
||||||
uv run ak build_source_docs
|
poetry run ak build_source_docs
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: netlify/actions/cli@master
|
uses: netlify/actions/cli@master
|
||||||
with:
|
with:
|
||||||
|
67
.github/workflows/release-publish.yml
vendored
67
.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.3.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@v2
|
||||||
|
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.3.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
|
||||||
@ -147,8 +188,8 @@ jobs:
|
|||||||
aws-region: ${{ env.AWS_REGION }}
|
aws-region: ${{ env.AWS_REGION }}
|
||||||
- name: Upload template
|
- name: Upload template
|
||||||
run: |
|
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 website/docs/install-config/install/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
|
aws s3 cp --acl=public-read website/docs/install-config/install/aws/template.yaml s3://authentik-cloudformation-templates/authentik.ecs.latest.yaml
|
||||||
test-release:
|
test-release:
|
||||||
needs:
|
needs:
|
||||||
- build-server
|
- build-server
|
||||||
@ -186,7 +227,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:
|
||||||
|
6
.github/workflows/repo-stale.yml
vendored
6
.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:
|
||||||
@ -25,7 +25,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
|
||||||
|
27
.github/workflows/semgrep.yml
vendored
27
.github/workflows/semgrep.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
name: authentik-semgrep
|
|
||||||
on:
|
|
||||||
workflow_dispatch: {}
|
|
||||||
pull_request: {}
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- master
|
|
||||||
paths:
|
|
||||||
- .github/workflows/semgrep.yml
|
|
||||||
schedule:
|
|
||||||
# random HH:MM to avoid a load spike on GitHub Actions at 00:00
|
|
||||||
- cron: '12 15 * * *'
|
|
||||||
jobs:
|
|
||||||
semgrep:
|
|
||||||
name: semgrep/ci
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
env:
|
|
||||||
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
|
|
||||||
container:
|
|
||||||
image: semgrep/semgrep
|
|
||||||
if: (github.actor != 'dependabot[bot]')
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- run: semgrep ci
|
|
@ -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,30 +15,23 @@ 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: |
|
||||||
uv run make i18n-extract
|
poetry run make i18n-extract
|
||||||
- name: run compile
|
- name: run compile
|
||||||
run: |
|
run: |
|
||||||
uv 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}"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
22
.vscode/settings.json
vendored
22
.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": [
|
||||||
|
46
.vscode/tasks.json
vendored
46
.vscode/tasks.json
vendored
@ -3,13 +3,8 @@
|
|||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"label": "authentik/core: make",
|
"label": "authentik/core: make",
|
||||||
"command": "uv",
|
"command": "poetry",
|
||||||
"args": [
|
"args": ["run", "make", "lint-fix", "lint"],
|
||||||
"run",
|
|
||||||
"make",
|
|
||||||
"lint-fix",
|
|
||||||
"lint"
|
|
||||||
],
|
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "new"
|
"panel": "new"
|
||||||
},
|
},
|
||||||
@ -17,12 +12,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/core: run",
|
"label": "authentik/core: run",
|
||||||
"command": "uv",
|
"command": "poetry",
|
||||||
"args": [
|
"args": ["run", "ak", "server"],
|
||||||
"run",
|
|
||||||
"ak",
|
|
||||||
"server"
|
|
||||||
],
|
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
@ -32,17 +23,13 @@
|
|||||||
{
|
{
|
||||||
"label": "authentik/web: make",
|
"label": "authentik/web: make",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": [
|
"args": ["web"],
|
||||||
"web"
|
|
||||||
],
|
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/web: watch",
|
"label": "authentik/web: watch",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": [
|
"args": ["web-watch"],
|
||||||
"web-watch"
|
|
||||||
],
|
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
@ -52,26 +39,19 @@
|
|||||||
{
|
{
|
||||||
"label": "authentik: install",
|
"label": "authentik: install",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": [
|
"args": ["install", "-j4"],
|
||||||
"install",
|
|
||||||
"-j4"
|
|
||||||
],
|
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/website: make",
|
"label": "authentik/website: make",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": [
|
"args": ["website"],
|
||||||
"website"
|
|
||||||
],
|
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/website: watch",
|
"label": "authentik/website: watch",
|
||||||
"command": "make",
|
"command": "make",
|
||||||
"args": [
|
"args": ["website-watch"],
|
||||||
"website-watch"
|
|
||||||
],
|
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
@ -80,12 +60,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "authentik/api: generate",
|
"label": "authentik/api: generate",
|
||||||
"command": "uv",
|
"command": "poetry",
|
||||||
"args": [
|
"args": ["run", "make", "gen"],
|
||||||
"run",
|
|
||||||
"make",
|
|
||||||
"gen"
|
|
||||||
],
|
|
||||||
"group": "build"
|
"group": "build"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -10,12 +10,11 @@ schemas/ @goauthentik/backend
|
|||||||
scripts/ @goauthentik/backend
|
scripts/ @goauthentik/backend
|
||||||
tests/ @goauthentik/backend
|
tests/ @goauthentik/backend
|
||||||
pyproject.toml @goauthentik/backend
|
pyproject.toml @goauthentik/backend
|
||||||
uv.lock @goauthentik/backend
|
poetry.lock @goauthentik/backend
|
||||||
go.mod @goauthentik/backend
|
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
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
82
Dockerfile
82
Dockerfile
@ -30,6 +30,7 @@ WORKDIR /work/web
|
|||||||
|
|
||||||
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
|
||||||
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
|
||||||
|
--mount=type=bind,target=/work/web/packages/sfe/package.json,src=./web/packages/sfe/package.json \
|
||||||
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
|
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
|
||||||
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
|
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
|
||||||
npm ci --include=dev
|
npm ci --include=dev
|
||||||
@ -42,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 3: Build go proxy
|
# Stage 3: Build go proxy
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder
|
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder
|
||||||
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@ -75,7 +76,7 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum
|
|||||||
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||||
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
--mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \
|
||||||
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \
|
||||||
CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \
|
CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \
|
||||||
go build -o /go/authentik ./cmd/server
|
go build -o /go/authentik ./cmd/server
|
||||||
|
|
||||||
# Stage 4: MaxMind GeoIP
|
# Stage 4: MaxMind GeoIP
|
||||||
@ -92,59 +93,38 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
|||||||
mkdir -p /usr/share/GeoIP && \
|
mkdir -p /usr/share/GeoIP && \
|
||||||
/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: Download uv
|
# Stage 5: Python dependencies
|
||||||
FROM ghcr.io/astral-sh/uv:0.6.12 AS uv
|
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS python-deps
|
||||||
# Stage 6: Base python image
|
|
||||||
FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base
|
|
||||||
|
|
||||||
ENV VENV_PATH="/ak-root/.venv" \
|
|
||||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
|
||||||
UV_COMPILE_BYTECODE=1 \
|
|
||||||
UV_LINK_MODE=copy \
|
|
||||||
UV_NATIVE_TLS=1 \
|
|
||||||
UV_PYTHON_DOWNLOADS=0
|
|
||||||
|
|
||||||
WORKDIR /ak-root/
|
|
||||||
|
|
||||||
COPY --from=uv /uv /uvx /bin/
|
|
||||||
|
|
||||||
# Stage 7: Python dependencies
|
|
||||||
FROM python-base AS python-deps
|
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG TARGETVARIANT
|
ARG TARGETVARIANT
|
||||||
|
|
||||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
WORKDIR /ak-root/poetry
|
||||||
|
|
||||||
ENV PATH="/root/.cargo/bin:$PATH"
|
ENV VENV_PATH="/ak-root/venv" \
|
||||||
|
POETRY_VIRTUALENVS_CREATE=false \
|
||||||
|
PATH="/ak-root/venv/bin:$PATH"
|
||||||
|
|
||||||
|
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
|
||||||
|
|
||||||
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
# Required for installing pip packages
|
# Required for installing pip packages
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev libkrb5-dev
|
||||||
# Build essentials
|
|
||||||
build-essential pkg-config libffi-dev git \
|
|
||||||
# cryptography
|
|
||||||
curl \
|
|
||||||
# libxml
|
|
||||||
libxslt-dev zlib1g-dev \
|
|
||||||
# postgresql
|
|
||||||
libpq-dev \
|
|
||||||
# python-kadmin-rs
|
|
||||||
clang libkrb5-dev sccache \
|
|
||||||
# xmlsec
|
|
||||||
libltdl-dev && \
|
|
||||||
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
|
||||||
|
|
||||||
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec"
|
RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
|
||||||
|
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
|
||||||
|
--mount=type=cache,target=/root/.cache/pip \
|
||||||
|
--mount=type=cache,target=/root/.cache/pypoetry \
|
||||||
|
python -m venv /ak-root/venv/ && \
|
||||||
|
bash -c "source ${VENV_PATH}/bin/activate && \
|
||||||
|
pip3 install --upgrade pip && \
|
||||||
|
pip3 install poetry && \
|
||||||
|
poetry install --only=main --no-ansi --no-interaction --no-root && \
|
||||||
|
pip install --force-reinstall /wheels/*"
|
||||||
|
|
||||||
RUN --mount=type=bind,target=pyproject.toml,src=pyproject.toml \
|
# Stage 6: Run
|
||||||
--mount=type=bind,target=uv.lock,src=uv.lock \
|
FROM ghcr.io/goauthentik/fips-python:3.12.7-slim-bookworm-fips-full AS final-image
|
||||||
--mount=type=cache,target=/root/.cache/uv \
|
|
||||||
uv sync --frozen --no-install-project --no-dev
|
|
||||||
|
|
||||||
# Stage 8: Run
|
|
||||||
FROM python-base AS final-image
|
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ARG GIT_BUILD_HASH
|
ARG GIT_BUILD_HASH
|
||||||
@ -160,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 && \
|
||||||
@ -176,7 +154,7 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
COPY ./authentik/ /authentik
|
COPY ./authentik/ /authentik
|
||||||
COPY ./pyproject.toml /
|
COPY ./pyproject.toml /
|
||||||
COPY ./uv.lock /
|
COPY ./poetry.lock /
|
||||||
COPY ./schemas /schemas
|
COPY ./schemas /schemas
|
||||||
COPY ./locale /locale
|
COPY ./locale /locale
|
||||||
COPY ./tests /tests
|
COPY ./tests /tests
|
||||||
@ -185,7 +163,7 @@ COPY ./blueprints /blueprints
|
|||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
|
||||||
COPY --from=go-builder /go/authentik /bin/authentik
|
COPY --from=go-builder /go/authentik /bin/authentik
|
||||||
COPY --from=python-deps /ak-root/.venv /ak-root/.venv
|
COPY --from=python-deps /ak-root/venv /ak-root/venv
|
||||||
COPY --from=web-builder /work/web/dist/ /web/dist/
|
COPY --from=web-builder /work/web/dist/ /web/dist/
|
||||||
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
||||||
COPY --from=website-builder /work/website/build/ /website/help/
|
COPY --from=website-builder /work/website/build/ /website/help/
|
||||||
@ -196,7 +174,11 @@ USER 1000
|
|||||||
ENV TMPDIR=/dev/shm/ \
|
ENV TMPDIR=/dev/shm/ \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
GOFIPS=1
|
PATH="/ak-root/venv/bin:/lifecycle:$PATH" \
|
||||||
|
VENV_PATH="/ak-root/venv" \
|
||||||
|
POETRY_VIRTUALENVS_CREATE=false
|
||||||
|
|
||||||
|
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" ]
|
||||||
|
|
||||||
|
90
Makefile
90
Makefile
@ -4,17 +4,31 @@
|
|||||||
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 website/docs/install-config/install/aws
|
||||||
DOCKER_IMAGE ?= "authentik:test"
|
DOCKER_IMAGE ?= "authentik:test"
|
||||||
|
|
||||||
GEN_API_TS = "gen-ts-api"
|
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 uv 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 uv 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 uv 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,38 +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)
|
||||||
uv run coverage run manage.py test --keepdb authentik
|
coverage run manage.py test --keepdb authentik
|
||||||
uv run coverage html
|
coverage html
|
||||||
uv 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.
|
||||||
uv run black $(PY_SOURCES)
|
black $(PY_SOURCES)
|
||||||
uv run ruff check --fix $(PY_SOURCES)
|
ruff check --fix $(PY_SOURCES)
|
||||||
|
|
||||||
lint-codespell: ## Reports spelling errors.
|
lint-codespell: ## Reports spelling errors.
|
||||||
uv run codespell -w
|
codespell -w $(CODESPELL_ARGS)
|
||||||
|
|
||||||
lint: ## Lint the python and golang sources
|
lint: ## Lint the python and golang sources
|
||||||
uv 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:
|
||||||
uv sync --frozen
|
poetry install
|
||||||
|
|
||||||
migrate: ## Run the Authentik Django server's migrations
|
migrate: ## Run the Authentik Django server's migrations
|
||||||
uv 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
|
|
||||||
|
|
||||||
run: ## Run the main authentik server process
|
|
||||||
uv run ak server
|
|
||||||
|
|
||||||
core-i18n-extract:
|
core-i18n-extract:
|
||||||
uv run ak makemessages \
|
ak makemessages \
|
||||||
--add-location file \
|
--add-location file \
|
||||||
--no-obsolete \
|
--no-obsolete \
|
||||||
--ignore web \
|
--ignore web \
|
||||||
@ -93,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 \
|
||||||
uv 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 \
|
||||||
uv 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
|
||||||
@ -132,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} \
|
||||||
@ -148,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} \
|
||||||
@ -176,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
|
||||||
uv run scripts/generate_config.py
|
python -m scripts.generate_config
|
||||||
|
|
||||||
gen: gen-build gen-client-ts
|
gen: gen-build gen-client-ts
|
||||||
|
|
||||||
@ -235,6 +252,9 @@ website-build:
|
|||||||
website-watch: ## Build and watch the documentation website, updating automatically
|
website-watch: ## Build and watch the documentation website, updating automatically
|
||||||
cd website && npm run watch
|
cd website && npm run watch
|
||||||
|
|
||||||
|
aws-cfn:
|
||||||
|
cd website && npm run aws-cfn
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
## Docker
|
## Docker
|
||||||
#########################
|
#########################
|
||||||
@ -243,9 +263,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
|
||||||
#########################
|
#########################
|
||||||
@ -257,21 +274,16 @@ ci--meta-debug:
|
|||||||
node --version
|
node --version
|
||||||
|
|
||||||
ci-black: ci--meta-debug
|
ci-black: ci--meta-debug
|
||||||
uv run black --check $(PY_SOURCES)
|
black --check $(PY_SOURCES)
|
||||||
|
|
||||||
ci-ruff: ci--meta-debug
|
ci-ruff: ci--meta-debug
|
||||||
uv run ruff check $(PY_SOURCES)
|
ruff check $(PY_SOURCES)
|
||||||
|
|
||||||
ci-codespell: ci--meta-debug
|
ci-codespell: ci--meta-debug
|
||||||
uv run codespell -s
|
codespell $(CODESPELL_ARGS) -s
|
||||||
|
|
||||||
ci-bandit: ci--meta-debug
|
ci-bandit: ci--meta-debug
|
||||||
uv run bandit -r $(PY_SOURCES)
|
bandit -r $(PY_SOURCES)
|
||||||
|
|
||||||
ci-pending-migrations: ci--meta-debug
|
ci-pending-migrations: ci--meta-debug
|
||||||
uv run ak makemigrations --check
|
ak makemigrations --check
|
||||||
|
|
||||||
ci-test: ci--meta-debug
|
|
||||||
uv run coverage run manage.py test --keepdb --randomly-seed ${CI_TEST_SEED} authentik
|
|
||||||
uv run coverage report
|
|
||||||
uv 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).
|
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 specfic audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).
|
||||||
|
|
||||||
## 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.10.x | ✅ |
|
||||||
| 2024.12.x | ✅ |
|
| 2024.12.x | ✅ |
|
||||||
| 2025.2.x | ✅ |
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from os import environ
|
from os import environ
|
||||||
|
|
||||||
__version__ = "2025.2.3"
|
__version__ = "2024.12.2"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class SystemInfoSerializer(PassiveSerializer):
|
|||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
continue
|
continue
|
||||||
actual_value = value
|
actual_value = value
|
||||||
if raw_session is not None and raw_session in actual_value:
|
if raw_session in actual_value:
|
||||||
actual_value = actual_value.replace(
|
actual_value = actual_value.replace(
|
||||||
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
raw_session, SafeExceptionReporterFilter.cleansed_substitute
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
@ -71,7 +71,6 @@ from authentik.providers.oauth2.models import (
|
|||||||
DeviceToken,
|
DeviceToken,
|
||||||
RefreshToken,
|
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
|
||||||
@ -132,7 +131,6 @@ def excluded_models() -> list[type[Model]]:
|
|||||||
EndpointDevice,
|
EndpointDevice,
|
||||||
EndpointDeviceConnection,
|
EndpointDeviceConnection,
|
||||||
DeviceToken,
|
DeviceToken,
|
||||||
StreamEvent,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,8 +49,6 @@ class BrandSerializer(ModelSerializer):
|
|||||||
"branding_title",
|
"branding_title",
|
||||||
"branding_logo",
|
"branding_logo",
|
||||||
"branding_favicon",
|
"branding_favicon",
|
||||||
"branding_custom_css",
|
|
||||||
"branding_default_flow_background",
|
|
||||||
"flow_authentication",
|
"flow_authentication",
|
||||||
"flow_invalidation",
|
"flow_invalidation",
|
||||||
"flow_recovery",
|
"flow_recovery",
|
||||||
@ -88,7 +86,6 @@ class CurrentBrandSerializer(PassiveSerializer):
|
|||||||
branding_title = CharField()
|
branding_title = CharField()
|
||||||
branding_logo = CharField(source="branding_logo_url")
|
branding_logo = CharField(source="branding_logo_url")
|
||||||
branding_favicon = CharField(source="branding_favicon_url")
|
branding_favicon = CharField(source="branding_favicon_url")
|
||||||
branding_custom_css = CharField()
|
|
||||||
ui_footer_links = ListField(
|
ui_footer_links = ListField(
|
||||||
child=FooterLinkSerializer(),
|
child=FooterLinkSerializer(),
|
||||||
read_only=True,
|
read_only=True,
|
||||||
@ -128,7 +125,6 @@ class BrandViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"branding_title",
|
"branding_title",
|
||||||
"branding_logo",
|
"branding_logo",
|
||||||
"branding_favicon",
|
"branding_favicon",
|
||||||
"branding_default_flow_background",
|
|
||||||
"flow_authentication",
|
"flow_authentication",
|
||||||
"flow_invalidation",
|
"flow_invalidation",
|
||||||
"flow_recovery",
|
"flow_recovery",
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
# Generated by Django 5.0.12 on 2025-02-22 01:51
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from django.db import migrations, models
|
|
||||||
from django.apps.registry import Apps
|
|
||||||
|
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|
||||||
Brand = apps.get_model("authentik_brands", "brand")
|
|
||||||
|
|
||||||
db_alias = schema_editor.connection.alias
|
|
||||||
|
|
||||||
path = Path("/web/dist/custom.css")
|
|
||||||
if not path.exists():
|
|
||||||
return
|
|
||||||
css = path.read_text()
|
|
||||||
Brand.objects.using(db_alias).update(branding_custom_css=css)
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_brands", "0007_brand_default_application"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="brand",
|
|
||||||
name="branding_custom_css",
|
|
||||||
field=models.TextField(blank=True, default=""),
|
|
||||||
),
|
|
||||||
migrations.RunPython(migrate_custom_css),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.0.13 on 2025-03-19 22:54
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_brands", "0008_brand_branding_custom_css"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="brand",
|
|
||||||
name="branding_default_flow_background",
|
|
||||||
field=models.TextField(default="/static/dist/assets/images/flow_background.jpg"),
|
|
||||||
),
|
|
||||||
]
|
|
@ -33,10 +33,6 @@ class Brand(SerializerModel):
|
|||||||
|
|
||||||
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
|
branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg")
|
||||||
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
|
branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png")
|
||||||
branding_custom_css = models.TextField(default="", blank=True)
|
|
||||||
branding_default_flow_background = models.TextField(
|
|
||||||
default="/static/dist/assets/images/flow_background.jpg"
|
|
||||||
)
|
|
||||||
|
|
||||||
flow_authentication = models.ForeignKey(
|
flow_authentication = models.ForeignKey(
|
||||||
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
|
Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication"
|
||||||
@ -88,12 +84,6 @@ class Brand(SerializerModel):
|
|||||||
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
|
return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon
|
||||||
return self.branding_favicon
|
return self.branding_favicon
|
||||||
|
|
||||||
def branding_default_flow_background_url(self) -> str:
|
|
||||||
"""Get branding_default_flow_background with the correct prefix"""
|
|
||||||
if self.branding_default_flow_background.startswith("/static"):
|
|
||||||
return CONFIG.get("web.path", "/")[:-1] + self.branding_default_flow_background
|
|
||||||
return self.branding_default_flow_background
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> Serializer:
|
def serializer(self) -> Serializer:
|
||||||
from authentik.brands.api import BrandSerializer
|
from authentik.brands.api import BrandSerializer
|
||||||
|
@ -24,7 +24,6 @@ class TestBrands(APITestCase):
|
|||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "authentik",
|
"branding_title": "authentik",
|
||||||
"branding_custom_css": "",
|
|
||||||
"matched_domain": brand.domain,
|
"matched_domain": brand.domain,
|
||||||
"ui_footer_links": [],
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
@ -44,7 +43,6 @@ class TestBrands(APITestCase):
|
|||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "custom",
|
"branding_title": "custom",
|
||||||
"branding_custom_css": "",
|
|
||||||
"matched_domain": "bar.baz",
|
"matched_domain": "bar.baz",
|
||||||
"ui_footer_links": [],
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
@ -61,7 +59,6 @@ class TestBrands(APITestCase):
|
|||||||
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
|
||||||
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
"branding_favicon": "/static/dist/assets/icons/icon.png",
|
||||||
"branding_title": "authentik",
|
"branding_title": "authentik",
|
||||||
"branding_custom_css": "",
|
|
||||||
"matched_domain": "fallback",
|
"matched_domain": "fallback",
|
||||||
"ui_footer_links": [],
|
"ui_footer_links": [],
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
"ui_theme": Themes.AUTOMATIC,
|
||||||
@ -124,27 +121,3 @@ class TestBrands(APITestCase):
|
|||||||
"subject": None,
|
"subject": None,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_branding_url(self):
|
|
||||||
"""Test branding attributes return correct values"""
|
|
||||||
brand = create_test_brand()
|
|
||||||
brand.branding_default_flow_background = "https://goauthentik.io/img/icon.png"
|
|
||||||
brand.branding_favicon = "https://goauthentik.io/img/icon.png"
|
|
||||||
brand.branding_logo = "https://goauthentik.io/img/icon.png"
|
|
||||||
brand.save()
|
|
||||||
self.assertEqual(
|
|
||||||
brand.branding_default_flow_background_url(), "https://goauthentik.io/img/icon.png"
|
|
||||||
)
|
|
||||||
self.assertJSONEqual(
|
|
||||||
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
|
||||||
{
|
|
||||||
"branding_logo": "https://goauthentik.io/img/icon.png",
|
|
||||||
"branding_favicon": "https://goauthentik.io/img/icon.png",
|
|
||||||
"branding_title": "authentik",
|
|
||||||
"branding_custom_css": "",
|
|
||||||
"matched_domain": brand.domain,
|
|
||||||
"ui_footer_links": [],
|
|
||||||
"ui_theme": Themes.AUTOMATIC,
|
|
||||||
"default_locale": "",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
@ -46,7 +46,7 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str:
|
||||||
"""Cache key where application list for user is saved"""
|
"""Cache key where application list for user is saved"""
|
||||||
key = f"{CACHE_PREFIX}app_access/{user_pk}"
|
key = f"{CACHE_PREFIX}/app_access/{user_pk}"
|
||||||
if page_number:
|
if page_number:
|
||||||
key += f"/{page_number}"
|
key += f"/{page_number}"
|
||||||
return key
|
return key
|
||||||
@ -209,7 +209,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="superuser_full_list",
|
name="list_rbac",
|
||||||
location=OpenApiParameter.QUERY,
|
location=OpenApiParameter.QUERY,
|
||||||
type=OpenApiTypes.BOOL,
|
type=OpenApiTypes.BOOL,
|
||||||
),
|
),
|
||||||
@ -229,10 +229,8 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""Custom list method that checks Policy based access instead of guardian"""
|
"""Custom list method that checks Policy based access instead of guardian"""
|
||||||
should_cache = request.query_params.get("search", "") == ""
|
should_cache = request.query_params.get("search", "") == ""
|
||||||
|
|
||||||
superuser_full_list = (
|
list_rbac = str(request.query_params.get("list_rbac", "false")).lower() == "true"
|
||||||
str(request.query_params.get("superuser_full_list", "false")).lower() == "true"
|
if list_rbac:
|
||||||
)
|
|
||||||
if superuser_full_list and request.user.is_superuser:
|
|
||||||
return super().list(request)
|
return super().list(request)
|
||||||
|
|
||||||
only_with_launch_url = str(
|
only_with_launch_url = str(
|
||||||
|
@ -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 = [
|
||||||
|
@ -5,7 +5,6 @@ from collections.abc import Iterable
|
|||||||
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.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
@ -86,7 +85,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,17 +154,6 @@ 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"""
|
"""User source connection"""
|
||||||
@ -179,13 +167,10 @@ class UserSourceConnectionSerializer(SourceSerializer):
|
|||||||
"user",
|
"user",
|
||||||
"source",
|
"source",
|
||||||
"source_obj",
|
"source_obj",
|
||||||
"identifier",
|
|
||||||
"created",
|
"created",
|
||||||
"last_updated",
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"created": {"read_only": True},
|
"created": {"read_only": True},
|
||||||
"last_updated": {"read_only": True},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -202,7 +187,7 @@ class UserSourceConnectionViewSet(
|
|||||||
queryset = UserSourceConnection.objects.all()
|
queryset = UserSourceConnection.objects.all()
|
||||||
serializer_class = UserSourceConnectionSerializer
|
serializer_class = UserSourceConnectionSerializer
|
||||||
filterset_fields = ["user", "source__slug"]
|
filterset_fields = ["user", "source__slug"]
|
||||||
search_fields = ["user__username", "source__slug", "identifier"]
|
search_fields = ["source__slug"]
|
||||||
ordering = ["source__slug", "pk"]
|
ordering = ["source__slug", "pk"]
|
||||||
owner_field = "user"
|
owner_field = "user"
|
||||||
|
|
||||||
@ -221,11 +206,9 @@ class GroupSourceConnectionSerializer(SourceSerializer):
|
|||||||
"source_obj",
|
"source_obj",
|
||||||
"identifier",
|
"identifier",
|
||||||
"created",
|
"created",
|
||||||
"last_updated",
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"created": {"read_only": True},
|
"created": {"read_only": True},
|
||||||
"last_updated": {"read_only": True},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -242,5 +225,6 @@ class GroupSourceConnectionViewSet(
|
|||||||
queryset = GroupSourceConnection.objects.all()
|
queryset = GroupSourceConnection.objects.all()
|
||||||
serializer_class = GroupSourceConnectionSerializer
|
serializer_class = GroupSourceConnectionSerializer
|
||||||
filterset_fields = ["group", "source__slug"]
|
filterset_fields = ["group", "source__slug"]
|
||||||
search_fields = ["group__name", "source__slug", "identifier"]
|
search_fields = ["source__slug"]
|
||||||
ordering = ["source__slug", "pk"]
|
ordering = ["source__slug", "pk"]
|
||||||
|
owner_field = "user"
|
||||||
|
@ -4,7 +4,7 @@ from typing import Any
|
|||||||
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
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
|
||||||
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
|
||||||
@ -138,13 +138,8 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
|||||||
owner_field = "user"
|
owner_field = "user"
|
||||||
rbac_allow_create_without_perm = True
|
rbac_allow_create_without_perm = True
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def perform_create(self, serializer: TokenSerializer):
|
def perform_create(self, serializer: TokenSerializer):
|
||||||
|
# TODO: better permission check
|
||||||
if not self.request.user.is_superuser:
|
if not self.request.user.is_superuser:
|
||||||
instance = serializer.save(
|
instance = serializer.save(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from importlib import import_module
|
|
||||||
from json import loads
|
from json import loads
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import update_session_auth_hash
|
from django.contrib.auth import update_session_auth_hash
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
|
from django.core.cache import cache
|
||||||
from django.db.models.functions import ExtractHour
|
from django.db.models.functions import ExtractHour
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
@ -92,7 +91,6 @@ from authentik.stages.email.tasks import send_mails
|
|||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
|
||||||
|
|
||||||
|
|
||||||
class UserGroupSerializer(ModelSerializer):
|
class UserGroupSerializer(ModelSerializer):
|
||||||
@ -238,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},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -375,7 +371,7 @@ class UsersFilter(FilterSet):
|
|||||||
method="filter_attributes",
|
method="filter_attributes",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser")
|
is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser")
|
||||||
uuid = UUIDFilter(field_name="uuid")
|
uuid = UUIDFilter(field_name="uuid")
|
||||||
|
|
||||||
path = CharFilter(field_name="path")
|
path = CharFilter(field_name="path")
|
||||||
@ -393,11 +389,6 @@ class UsersFilter(FilterSet):
|
|||||||
queryset=Group.objects.all().order_by("name"),
|
queryset=Group.objects.all().order_by("name"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def filter_is_superuser(self, queryset, name, value):
|
|
||||||
if value:
|
|
||||||
return queryset.filter(ak_groups__is_superuser=True).distinct()
|
|
||||||
return queryset.exclude(ak_groups__is_superuser=True).distinct()
|
|
||||||
|
|
||||||
def filter_attributes(self, queryset, name, value):
|
def filter_attributes(self, queryset, name, value):
|
||||||
"""Filter attributes by query args"""
|
"""Filter attributes by query args"""
|
||||||
try:
|
try:
|
||||||
@ -436,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):
|
||||||
@ -776,8 +767,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
if not instance.is_active:
|
if not instance.is_active:
|
||||||
sessions = AuthenticatedSession.objects.filter(user=instance)
|
sessions = AuthenticatedSession.objects.filter(user=instance)
|
||||||
session_ids = sessions.values_list("session_key", flat=True)
|
session_ids = sessions.values_list("session_key", flat=True)
|
||||||
for session in session_ids:
|
cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids)
|
||||||
SessionStore(session).delete()
|
|
||||||
sessions.delete()
|
sessions.delete()
|
||||||
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
LOGGER.debug("Deleted user's sessions", user=instance.username)
|
||||||
return response
|
return response
|
||||||
|
@ -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",
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
57
authentik/core/migrations/0043_alter_user_options.py
Normal file
57
authentik/core/migrations/0043_alter_user_options.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2025-01-08 17:39
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_user_debug_attribute(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
from django.apps import apps as real_apps
|
||||||
|
from django.contrib.auth.management import create_permissions
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
User = apps.get_model("authentik_core", "User")
|
||||||
|
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||||
|
|
||||||
|
# Permissions are only created _after_ migrations are run
|
||||||
|
# - https://github.com/django/django/blob/43cdfa8b20e567a801b7d0a09ec67ddd062d5ea4/django/contrib/auth/apps.py#L19
|
||||||
|
# - https://stackoverflow.com/a/72029063/1870445
|
||||||
|
create_permissions(real_apps.get_app_config("authentik_core"), using=db_alias)
|
||||||
|
|
||||||
|
Permission = apps.get_model("auth", "Permission")
|
||||||
|
|
||||||
|
new_prem = Permission.objects.using(db_alias).get(codename="user_view_debug")
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for user in User.objects.using(db_alias).filter(
|
||||||
|
**{f"attributes__{USER_ATTRIBUTE_DEBUG}": True}
|
||||||
|
):
|
||||||
|
user.permissions.add(new_prem)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0042_authenticatedsession_authentik_c_expires_08251d_idx_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="user",
|
||||||
|
options={
|
||||||
|
"permissions": [
|
||||||
|
("reset_user_password", "Reset Password"),
|
||||||
|
("impersonate", "Can impersonate other users"),
|
||||||
|
("assign_user_permissions", "Can assign permissions to users"),
|
||||||
|
("unassign_user_permissions", "Can unassign permissions from users"),
|
||||||
|
("preview_user", "Can preview user data sent to providers"),
|
||||||
|
("view_user_applications", "View applications the user has access to"),
|
||||||
|
("user_view_debug", "User receives additional details for error messages"),
|
||||||
|
],
|
||||||
|
"verbose_name": "User",
|
||||||
|
"verbose_name_plural": "Users",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate_user_debug_attribute),
|
||||||
|
]
|
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 5.0.13 on 2025-04-07 14:04
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0043_alter_group_options"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="usersourceconnection",
|
|
||||||
name="new_identifier",
|
|
||||||
field=models.TextField(default=""),
|
|
||||||
preserve_default=False,
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,30 +0,0 @@
|
|||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0044_usersourceconnection_new_identifier"),
|
|
||||||
("authentik_sources_kerberos", "0003_migrate_userkerberossourceconnection_identifier"),
|
|
||||||
("authentik_sources_oauth", "0009_migrate_useroauthsourceconnection_identifier"),
|
|
||||||
("authentik_sources_plex", "0005_migrate_userplexsourceconnection_identifier"),
|
|
||||||
("authentik_sources_saml", "0019_migrate_usersamlsourceconnection_identifier"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="usersourceconnection",
|
|
||||||
old_name="new_identifier",
|
|
||||||
new_name="identifier",
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="usersourceconnection",
|
|
||||||
index=models.Index(fields=["identifier"], name="authentik_c_identif_59226f_idx"),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name="usersourceconnection",
|
|
||||||
index=models.Index(
|
|
||||||
fields=["source", "identifier"], name="authentik_c_source__649e04_idx"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -41,7 +41,6 @@ from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGT
|
|||||||
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
|
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
|
||||||
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
|
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
|
||||||
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
|
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
|
||||||
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
|
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
|
||||||
@ -204,8 +203,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):
|
||||||
@ -284,6 +281,7 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
|
|||||||
("unassign_user_permissions", _("Can unassign permissions from users")),
|
("unassign_user_permissions", _("Can unassign permissions from users")),
|
||||||
("preview_user", _("Can preview user data sent to providers")),
|
("preview_user", _("Can preview user data sent to providers")),
|
||||||
("view_user_applications", _("View applications the user has access to")),
|
("view_user_applications", _("View applications the user has access to")),
|
||||||
|
("user_view_debug", _("User receives additional details for error messages")),
|
||||||
]
|
]
|
||||||
indexes = [
|
indexes = [
|
||||||
models.Index(fields=["last_login"]),
|
models.Index(fields=["last_login"]),
|
||||||
@ -601,14 +599,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)
|
||||||
|
|
||||||
@ -678,8 +668,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)
|
||||||
|
|
||||||
@ -761,17 +749,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
@property
|
@property
|
||||||
def component(self) -> str:
|
def component(self) -> str:
|
||||||
"""Return component used to edit this object"""
|
"""Return component used to edit this object"""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
return ""
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def property_mapping_type(self) -> "type[PropertyMapping]":
|
def property_mapping_type(self) -> "type[PropertyMapping]":
|
||||||
"""Return property mapping type used by this object"""
|
"""Return property mapping type used by this object"""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
from authentik.core.models import PropertyMapping
|
|
||||||
|
|
||||||
return PropertyMapping
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
def ui_login_button(self, request: HttpRequest) -> UILoginButton | None:
|
||||||
@ -786,14 +768,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
|
|||||||
|
|
||||||
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
"""Get base properties for a user to build final properties upon."""
|
"""Get base properties for a user to build final properties upon."""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
return {}
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]:
|
||||||
"""Get base properties for a group to build final properties upon."""
|
"""Get base properties for a group to build final properties upon."""
|
||||||
if self.managed == self.MANAGED_INBUILT:
|
|
||||||
return {}
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -824,7 +802,6 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
|||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||||
identifier = models.TextField()
|
|
||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
@ -838,10 +815,6 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = (("user", "source"),)
|
unique_together = (("user", "source"),)
|
||||||
indexes = (
|
|
||||||
models.Index(fields=("identifier",)),
|
|
||||||
models.Index(fields=("source", "identifier")),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
|
class GroupSourceConnection(SerializerModel, CreatedUpdatedModel):
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
"""authentik core signals"""
|
"""authentik core signals"""
|
||||||
|
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.signals import Signal
|
from django.core.signals import Signal
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
@ -28,7 +25,6 @@ password_changed = Signal()
|
|||||||
login_failed = Signal()
|
login_failed = Signal()
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Application)
|
@receiver(post_save, sender=Application)
|
||||||
@ -64,7 +60,8 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_):
|
|||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||||
"""Delete session when authenticated session is deleted"""
|
"""Delete session when authenticated session is deleted"""
|
||||||
SessionStore(instance.session_key).delete()
|
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||||
|
cache.delete(cache_key)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save)
|
@receiver(pre_save)
|
||||||
|
@ -35,8 +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 is_url_absolute
|
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
|
||||||
@ -47,10 +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_SOURCE_FLOW_CONTEXT = "authentik/flows/source_flow_context"
|
|
||||||
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):
|
||||||
@ -210,8 +208,6 @@ class SourceFlowManager:
|
|||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:if-user"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
if not is_url_absolute(final_redirect):
|
|
||||||
final_redirect = "authentik_core:if-user"
|
|
||||||
flow_context.update(
|
flow_context.update(
|
||||||
{
|
{
|
||||||
# Since we authenticate the user by their token, they have no backend set
|
# Since we authenticate the user by their token, they have no backend set
|
||||||
@ -223,28 +219,28 @@ class SourceFlowManager:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
flow_context.update(self.policy_context)
|
flow_context.update(self.policy_context)
|
||||||
|
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,
|
||||||
|
)
|
||||||
flow_context.setdefault(PLAN_CONTEXT_REDIRECT, final_redirect)
|
flow_context.setdefault(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."),
|
||||||
@ -263,9 +259,6 @@ 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, []):
|
|
||||||
plan.append_stage(stage)
|
|
||||||
plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {}))
|
|
||||||
return plan.to_redirect(self.request, flow)
|
return plan.to_redirect(self.request, flow)
|
||||||
|
|
||||||
def handle_auth(
|
def handle_auth(
|
||||||
@ -302,8 +295,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}")
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
build: "{{ build }}",
|
build: "{{ build }}",
|
||||||
api: {
|
api: {
|
||||||
base: "{{ base_url }}",
|
base: "{{ base_url }}",
|
||||||
relBase: "{{ base_url_rel }}",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
window.addEventListener("DOMContentLoaded", function () {
|
window.addEventListener("DOMContentLoaded", function () {
|
||||||
|
@ -8,15 +8,13 @@
|
|||||||
<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_url }}">
|
||||||
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
<link rel="shortcut icon" href="{{ brand.branding_favicon_url }}">
|
||||||
{% 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' %}">
|
||||||
<style>{{ brand.branding_custom_css }}</style>
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
||||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
|
<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("{{ request.brand.branding_default_flow_background_url }}");
|
--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);
|
||||||
|
@ -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)
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
from django.apps import apps
|
|
||||||
from django.urls import reverse
|
|
||||||
from rest_framework.test import APITestCase
|
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
|
||||||
|
|
||||||
|
|
||||||
class TestSourceAPI(APITestCase):
|
|
||||||
def setUp(self) -> None:
|
|
||||||
self.user = create_test_admin_user()
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
def test_builtin_source_used_by(self):
|
|
||||||
"""Test Providers's types endpoint"""
|
|
||||||
apps.get_app_config("authentik_core").source_inbuilt()
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:source-used-by", kwargs={"slug": "authentik-built-in"}),
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
@ -1,7 +1,6 @@
|
|||||||
"""Test Users API"""
|
"""Test Users API"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
@ -16,12 +15,7 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
UserTypes,
|
UserTypes,
|
||||||
)
|
)
|
||||||
from authentik.core.tests.utils import (
|
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
|
||||||
create_test_admin_user,
|
|
||||||
create_test_brand,
|
|
||||||
create_test_flow,
|
|
||||||
create_test_user,
|
|
||||||
)
|
|
||||||
from authentik.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
@ -32,7 +26,7 @@ class TestUsersAPI(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.admin = create_test_admin_user()
|
self.admin = create_test_admin_user()
|
||||||
self.user = create_test_user()
|
self.user = User.objects.create(username="test-user")
|
||||||
|
|
||||||
def test_filter_type(self):
|
def test_filter_type(self):
|
||||||
"""Test API filtering by type"""
|
"""Test API filtering by type"""
|
||||||
@ -47,35 +41,6 @@ class TestUsersAPI(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
def test_filter_is_superuser(self):
|
|
||||||
"""Test API filtering by superuser status"""
|
|
||||||
User.objects.all().delete()
|
|
||||||
admin = create_test_admin_user()
|
|
||||||
self.client.force_login(admin)
|
|
||||||
# Test superuser
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:user-list"),
|
|
||||||
data={
|
|
||||||
"is_superuser": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content)
|
|
||||||
self.assertEqual(len(body["results"]), 1)
|
|
||||||
self.assertEqual(body["results"][0]["username"], admin.username)
|
|
||||||
# Test non-superuser
|
|
||||||
user = create_test_user()
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_api:user-list"),
|
|
||||||
data={
|
|
||||||
"is_superuser": False,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
body = loads(response.content)
|
|
||||||
self.assertEqual(len(body["results"]), 1, body)
|
|
||||||
self.assertEqual(body["results"][0]["username"], user.username)
|
|
||||||
|
|
||||||
def test_list_with_groups(self):
|
def test_list_with_groups(self):
|
||||||
"""Test listing with groups"""
|
"""Test listing with groups"""
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
@ -134,8 +99,6 @@ class TestUsersAPI(APITestCase):
|
|||||||
def test_recovery_email_no_flow(self):
|
def test_recovery_email_no_flow(self):
|
||||||
"""Test user recovery link (no recovery flow set)"""
|
"""Test user recovery link (no recovery flow set)"""
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
self.user.email = ""
|
|
||||||
self.user.save()
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk})
|
||||||
)
|
)
|
||||||
|
@ -13,11 +13,7 @@ from authentik.core.api.devices import AdminDeviceViewSet, DeviceViewSet
|
|||||||
from authentik.core.api.groups import GroupViewSet
|
from authentik.core.api.groups import GroupViewSet
|
||||||
from authentik.core.api.property_mappings import PropertyMappingViewSet
|
from authentik.core.api.property_mappings import PropertyMappingViewSet
|
||||||
from authentik.core.api.providers import ProviderViewSet
|
from authentik.core.api.providers import ProviderViewSet
|
||||||
from authentik.core.api.sources import (
|
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||||
GroupSourceConnectionViewSet,
|
|
||||||
SourceViewSet,
|
|
||||||
UserSourceConnectionViewSet,
|
|
||||||
)
|
|
||||||
from authentik.core.api.tokens import TokenViewSet
|
from authentik.core.api.tokens import TokenViewSet
|
||||||
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
from authentik.core.api.transactional_applications import TransactionalApplicationView
|
||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
@ -85,7 +81,6 @@ api_urlpatterns = [
|
|||||||
("core/tokens", TokenViewSet),
|
("core/tokens", TokenViewSet),
|
||||||
("sources/all", SourceViewSet),
|
("sources/all", SourceViewSet),
|
||||||
("sources/user_connections/all", UserSourceConnectionViewSet),
|
("sources/user_connections/all", UserSourceConnectionViewSet),
|
||||||
("sources/group_connections/all", GroupSourceConnectionViewSet),
|
|
||||||
("providers/all", ProviderViewSet),
|
("providers/all", ProviderViewSet),
|
||||||
("propertymappings/all", PropertyMappingViewSet),
|
("propertymappings/all", PropertyMappingViewSet),
|
||||||
("authenticators/all", DeviceViewSet, "device"),
|
("authenticators/all", DeviceViewSet, "device"),
|
||||||
|
@ -55,7 +55,7 @@ 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)
|
return plan.to_redirect(request, flow)
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,7 +53,6 @@ class InterfaceView(TemplateView):
|
|||||||
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"] = 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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -36,7 +36,6 @@ class MicrosoftEntraProviderSerializer(EnterpriseRequiredMixin, ProviderSerializ
|
|||||||
"filter_group",
|
"filter_group",
|
||||||
"user_delete_action",
|
"user_delete_action",
|
||||||
"group_delete_action",
|
"group_delete_action",
|
||||||
"dry_run",
|
|
||||||
]
|
]
|
||||||
extra_kwargs = {}
|
extra_kwargs = {}
|
||||||
|
|
||||||
|
@ -3,7 +3,6 @@ from collections.abc import Coroutine
|
|||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
|
||||||
from azure.core.exceptions import (
|
from azure.core.exceptions import (
|
||||||
ClientAuthenticationError,
|
ClientAuthenticationError,
|
||||||
ServiceRequestError,
|
ServiceRequestError,
|
||||||
@ -13,7 +12,6 @@ from azure.identity.aio import ClientSecretCredential
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||||
from kiota_abstractions.api_error import APIError
|
from kiota_abstractions.api_error import APIError
|
||||||
from kiota_abstractions.request_information import RequestInformation
|
|
||||||
from kiota_authentication_azure.azure_identity_authentication_provider import (
|
from kiota_authentication_azure.azure_identity_authentication_provider import (
|
||||||
AzureIdentityAuthenticationProvider,
|
AzureIdentityAuthenticationProvider,
|
||||||
)
|
)
|
||||||
@ -23,15 +21,13 @@ from msgraph.generated.models.o_data_errors.o_data_error import ODataError
|
|||||||
from msgraph.graph_request_adapter import GraphRequestAdapter, options
|
from msgraph.graph_request_adapter import GraphRequestAdapter, options
|
||||||
from msgraph.graph_service_client import GraphServiceClient
|
from msgraph.graph_service_client import GraphServiceClient
|
||||||
from msgraph_core import GraphClientFactory
|
from msgraph_core import GraphClientFactory
|
||||||
from opentelemetry import trace
|
|
||||||
|
|
||||||
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
|
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
|
||||||
from authentik.events.utils import sanitize_item
|
from authentik.events.utils import sanitize_item
|
||||||
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,
|
||||||
@ -39,24 +35,20 @@ from authentik.lib.sync.outgoing.exceptions import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuthentikRequestAdapter(GraphRequestAdapter):
|
def get_request_adapter(
|
||||||
def __init__(self, auth_provider, provider: MicrosoftEntraProvider, client=None):
|
credentials: ClientSecretCredential, scopes: list[str] | None = None
|
||||||
super().__init__(auth_provider, client)
|
) -> GraphRequestAdapter:
|
||||||
self._provider = provider
|
if scopes:
|
||||||
|
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials, scopes=scopes)
|
||||||
|
else:
|
||||||
|
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials)
|
||||||
|
|
||||||
async def get_http_response_message(
|
return GraphRequestAdapter(
|
||||||
self,
|
auth_provider=auth_provider,
|
||||||
request_info: RequestInformation,
|
client=GraphClientFactory.create_with_default_middleware(
|
||||||
parent_span: trace.Span,
|
options=options, client=KiotaClientFactory.get_default_client()
|
||||||
claims: str = "",
|
),
|
||||||
) -> httpx.Response:
|
)
|
||||||
if self._provider.dry_run and request_info.http_method.value.upper() not in SAFE_METHODS:
|
|
||||||
raise DryRunRejected(
|
|
||||||
url=request_info.url,
|
|
||||||
method=request_info.http_method.value,
|
|
||||||
body=request_info.content.decode("utf-8"),
|
|
||||||
)
|
|
||||||
return await super().get_http_response_message(request_info, parent_span, claims=claims)
|
|
||||||
|
|
||||||
|
|
||||||
class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict](
|
class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict](
|
||||||
@ -71,27 +63,9 @@ class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict]
|
|||||||
self.credentials = provider.microsoft_credentials()
|
self.credentials = provider.microsoft_credentials()
|
||||||
self.__prefetch_domains()
|
self.__prefetch_domains()
|
||||||
|
|
||||||
def get_request_adapter(
|
|
||||||
self, credentials: ClientSecretCredential, scopes: list[str] | None = None
|
|
||||||
) -> AuthentikRequestAdapter:
|
|
||||||
if scopes:
|
|
||||||
auth_provider = AzureIdentityAuthenticationProvider(
|
|
||||||
credentials=credentials, scopes=scopes
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials)
|
|
||||||
|
|
||||||
return AuthentikRequestAdapter(
|
|
||||||
auth_provider=auth_provider,
|
|
||||||
provider=self.provider,
|
|
||||||
client=GraphClientFactory.create_with_default_middleware(
|
|
||||||
options=options, client=KiotaClientFactory.get_default_client()
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def client(self):
|
def client(self):
|
||||||
return GraphServiceClient(request_adapter=self.get_request_adapter(**self.credentials))
|
return GraphServiceClient(request_adapter=get_request_adapter(**self.credentials))
|
||||||
|
|
||||||
def _request[T](self, request: Coroutine[Any, Any, T]) -> T:
|
def _request[T](self, request: Coroutine[Any, Any, T]) -> T:
|
||||||
try:
|
try:
|
||||||
|
@ -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_microsoft_entra",
|
|
||||||
"0002_microsoftentraprovidergroup_attributes_and_more",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="microsoftentraprovider",
|
|
||||||
name="dry_run",
|
|
||||||
field=models.BooleanField(
|
|
||||||
default=False,
|
|
||||||
help_text="When enabled, provider will not modify or create objects in the remote system.",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -32,6 +32,7 @@ class MicrosoftEntraUserTests(APITestCase):
|
|||||||
|
|
||||||
@apply_blueprint("system/providers-microsoft-entra.yaml")
|
@apply_blueprint("system/providers-microsoft-entra.yaml")
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|
||||||
# Delete all users and groups as the mocked HTTP responses only return one ID
|
# Delete all users and groups as the mocked HTTP responses only return one ID
|
||||||
# which will cause errors with multiple users
|
# which will cause errors with multiple users
|
||||||
Tenant.objects.update(avatars="none")
|
Tenant.objects.update(avatars="none")
|
||||||
@ -96,38 +97,6 @@ class MicrosoftEntraUserTests(APITestCase):
|
|||||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||||
user_create.assert_called_once()
|
user_create.assert_called_once()
|
||||||
|
|
||||||
def test_user_create_dry_run(self):
|
|
||||||
"""Test user creation (dry run)"""
|
|
||||||
self.provider.dry_run = True
|
|
||||||
self.provider.save()
|
|
||||||
uid = generate_id()
|
|
||||||
with (
|
|
||||||
patch(
|
|
||||||
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
|
|
||||||
MagicMock(return_value={"credentials": self.creds}),
|
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
|
|
||||||
AsyncMock(
|
|
||||||
return_value=OrganizationCollectionResponse(
|
|
||||||
value=[
|
|
||||||
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
):
|
|
||||||
user = User.objects.create(
|
|
||||||
username=uid,
|
|
||||||
name=f"{uid} {uid}",
|
|
||||||
email=f"{uid}@goauthentik.io",
|
|
||||||
)
|
|
||||||
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
|
|
||||||
provider=self.provider, user=user
|
|
||||||
).first()
|
|
||||||
self.assertIsNone(microsoft_user)
|
|
||||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
|
||||||
|
|
||||||
def test_user_not_created(self):
|
def test_user_not_created(self):
|
||||||
"""Test without property mappings, no group is created"""
|
"""Test without property mappings, no group is created"""
|
||||||
self.provider.property_mappings.clear()
|
self.provider.property_mappings.clear()
|
||||||
|
@ -6,12 +6,13 @@ from rest_framework.viewsets import GenericViewSet
|
|||||||
from authentik.core.api.groups import GroupMemberSerializer
|
from authentik.core.api.groups import GroupMemberSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import ModelSerializer
|
from authentik.core.api.utils import ModelSerializer
|
||||||
from authentik.providers.rac.api.endpoints import EndpointSerializer
|
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||||
from authentik.providers.rac.api.providers import RACProviderSerializer
|
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
|
||||||
from authentik.providers.rac.models import ConnectionToken
|
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
|
||||||
|
from authentik.enterprise.providers.rac.models import ConnectionToken
|
||||||
|
|
||||||
|
|
||||||
class ConnectionTokenSerializer(ModelSerializer):
|
class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||||
"""ConnectionToken Serializer"""
|
"""ConnectionToken Serializer"""
|
||||||
|
|
||||||
provider_obj = RACProviderSerializer(source="provider", read_only=True)
|
provider_obj = RACProviderSerializer(source="provider", read_only=True)
|
@ -14,9 +14,10 @@ from structlog.stdlib import get_logger
|
|||||||
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 Provider
|
from authentik.core.models import Provider
|
||||||
|
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||||
|
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
|
||||||
|
from authentik.enterprise.providers.rac.models import Endpoint
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.providers.rac.api.providers import RACProviderSerializer
|
|
||||||
from authentik.providers.rac.models import Endpoint
|
|
||||||
from authentik.rbac.filters import ObjectFilter
|
from authentik.rbac.filters import ObjectFilter
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -27,7 +28,7 @@ def user_endpoint_cache_key(user_pk: str) -> str:
|
|||||||
return f"goauthentik.io/providers/rac/endpoint_access/{user_pk}"
|
return f"goauthentik.io/providers/rac/endpoint_access/{user_pk}"
|
||||||
|
|
||||||
|
|
||||||
class EndpointSerializer(ModelSerializer):
|
class EndpointSerializer(EnterpriseRequiredMixin, ModelSerializer):
|
||||||
"""Endpoint Serializer"""
|
"""Endpoint Serializer"""
|
||||||
|
|
||||||
provider_obj = RACProviderSerializer(source="provider", read_only=True)
|
provider_obj = RACProviderSerializer(source="provider", read_only=True)
|
||||||
@ -95,7 +96,7 @@ class EndpointViewSet(UsedByMixin, ModelViewSet):
|
|||||||
OpenApiTypes.STR,
|
OpenApiTypes.STR,
|
||||||
),
|
),
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="superuser_full_list",
|
name="list_rbac",
|
||||||
location=OpenApiParameter.QUERY,
|
location=OpenApiParameter.QUERY,
|
||||||
type=OpenApiTypes.BOOL,
|
type=OpenApiTypes.BOOL,
|
||||||
),
|
),
|
||||||
@ -109,8 +110,8 @@ class EndpointViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""List accessible endpoints"""
|
"""List accessible endpoints"""
|
||||||
should_cache = request.GET.get("search", "") == ""
|
should_cache = request.GET.get("search", "") == ""
|
||||||
|
|
||||||
superuser_full_list = str(request.GET.get("superuser_full_list", "false")).lower() == "true"
|
list_rbac = str(request.GET.get("list_rbac", "false")).lower() == "true"
|
||||||
if superuser_full_list and request.user.is_superuser:
|
if list_rbac:
|
||||||
return super().list(request)
|
return super().list(request)
|
||||||
|
|
||||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
queryset = self._filter_queryset_for_list(self.get_queryset())
|
@ -10,7 +10,7 @@ from rest_framework.viewsets import ModelViewSet
|
|||||||
from authentik.core.api.property_mappings import PropertyMappingSerializer
|
from authentik.core.api.property_mappings import PropertyMappingSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import JSONDictField
|
from authentik.core.api.utils import JSONDictField
|
||||||
from authentik.providers.rac.models import RACPropertyMapping
|
from authentik.enterprise.providers.rac.models import RACPropertyMapping
|
||||||
|
|
||||||
|
|
||||||
class RACPropertyMappingSerializer(PropertyMappingSerializer):
|
class RACPropertyMappingSerializer(PropertyMappingSerializer):
|
@ -5,10 +5,11 @@ from rest_framework.viewsets import ModelViewSet
|
|||||||
|
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.providers.rac.models import RACProvider
|
from authentik.enterprise.api import EnterpriseRequiredMixin
|
||||||
|
from authentik.enterprise.providers.rac.models import RACProvider
|
||||||
|
|
||||||
|
|
||||||
class RACProviderSerializer(ProviderSerializer):
|
class RACProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
|
||||||
"""RACProvider Serializer"""
|
"""RACProvider Serializer"""
|
||||||
|
|
||||||
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
14
authentik/enterprise/providers/rac/apps.py
Normal file
14
authentik/enterprise/providers/rac/apps.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""RAC app config"""
|
||||||
|
|
||||||
|
from authentik.enterprise.apps import EnterpriseConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthentikEnterpriseProviderRAC(EnterpriseConfig):
|
||||||
|
"""authentik enterprise rac app config"""
|
||||||
|
|
||||||
|
name = "authentik.enterprise.providers.rac"
|
||||||
|
label = "authentik_providers_rac"
|
||||||
|
verbose_name = "authentik Enterprise.Providers.RAC"
|
||||||
|
default = True
|
||||||
|
mountpoint = ""
|
||||||
|
ws_mountpoint = "authentik.enterprise.providers.rac.urls"
|
@ -7,22 +7,22 @@ from channels.generic.websocket import AsyncWebsocketConsumer
|
|||||||
from django.http.request import QueryDict
|
from django.http.request import QueryDict
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
|
|
||||||
|
from authentik.enterprise.providers.rac.models import ConnectionToken, RACProvider
|
||||||
from authentik.outposts.consumer import OUTPOST_GROUP_INSTANCE
|
from authentik.outposts.consumer import OUTPOST_GROUP_INSTANCE
|
||||||
from authentik.outposts.models import Outpost, OutpostState, OutpostType
|
from authentik.outposts.models import Outpost, OutpostState, OutpostType
|
||||||
from authentik.providers.rac.models import ConnectionToken, RACProvider
|
|
||||||
|
|
||||||
# Global broadcast group, which messages are sent to when the outpost connects back
|
# Global broadcast group, which messages are sent to when the outpost connects back
|
||||||
# to authentik for a specific connection
|
# to authentik for a specific connection
|
||||||
# The `RACClientConsumer` consumer adds itself to this group on connection,
|
# The `RACClientConsumer` consumer adds itself to this group on connection,
|
||||||
# and removes itself once it has been assigned a specific outpost channel
|
# and removes itself once it has been assigned a specific outpost channel
|
||||||
RAC_CLIENT_GROUP = "group_rac_client"
|
RAC_CLIENT_GROUP = "group_enterprise_rac_client"
|
||||||
# A group for all connections in a given authentik session ID
|
# A group for all connections in a given authentik session ID
|
||||||
# A disconnect message is sent to this group when the session expires/is deleted
|
# A disconnect message is sent to this group when the session expires/is deleted
|
||||||
RAC_CLIENT_GROUP_SESSION = "group_rac_client_%(session)s"
|
RAC_CLIENT_GROUP_SESSION = "group_enterprise_rac_client_%(session)s"
|
||||||
# A group for all connections with a specific token, which in almost all cases
|
# A group for all connections with a specific token, which in almost all cases
|
||||||
# is just one connection, however this is used to disconnect the connection
|
# is just one connection, however this is used to disconnect the connection
|
||||||
# when the token is deleted
|
# when the token is deleted
|
||||||
RAC_CLIENT_GROUP_TOKEN = "group_rac_token_%(token)s" # nosec
|
RAC_CLIENT_GROUP_TOKEN = "group_enterprise_rac_token_%(token)s" # nosec
|
||||||
|
|
||||||
# Step 1: Client connects to this websocket endpoint
|
# Step 1: Client connects to this websocket endpoint
|
||||||
# Step 2: We prepare all the connection args for Guac
|
# Step 2: We prepare all the connection args for Guac
|
@ -3,7 +3,7 @@
|
|||||||
from channels.exceptions import ChannelFull
|
from channels.exceptions import ChannelFull
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
|
||||||
from authentik.providers.rac.consumer_client import RAC_CLIENT_GROUP
|
from authentik.enterprise.providers.rac.consumer_client import RAC_CLIENT_GROUP
|
||||||
|
|
||||||
|
|
||||||
class RACOutpostConsumer(AsyncWebsocketConsumer):
|
class RACOutpostConsumer(AsyncWebsocketConsumer):
|
@ -74,7 +74,7 @@ class RACProvider(Provider):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
from authentik.providers.rac.api.providers import RACProviderSerializer
|
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
|
||||||
|
|
||||||
return RACProviderSerializer
|
return RACProviderSerializer
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ class Endpoint(SerializerModel, PolicyBindingModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
from authentik.providers.rac.api.endpoints import EndpointSerializer
|
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
|
||||||
|
|
||||||
return EndpointSerializer
|
return EndpointSerializer
|
||||||
|
|
||||||
@ -129,7 +129,7 @@ class RACPropertyMapping(PropertyMapping):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> type[Serializer]:
|
def serializer(self) -> type[Serializer]:
|
||||||
from authentik.providers.rac.api.property_mappings import (
|
from authentik.enterprise.providers.rac.api.property_mappings import (
|
||||||
RACPropertyMappingSerializer,
|
RACPropertyMappingSerializer,
|
||||||
)
|
)
|
||||||
|
|
@ -4,17 +4,18 @@ from asgiref.sync import async_to_sync
|
|||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
from django.contrib.auth.signals import user_logged_out
|
from django.contrib.auth.signals import user_logged_out
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models.signals import post_delete, post_save, pre_delete
|
from django.db.models import Model
|
||||||
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.providers.rac.api.endpoints import user_endpoint_cache_key
|
from authentik.enterprise.providers.rac.api.endpoints import user_endpoint_cache_key
|
||||||
from authentik.providers.rac.consumer_client import (
|
from authentik.enterprise.providers.rac.consumer_client import (
|
||||||
RAC_CLIENT_GROUP_SESSION,
|
RAC_CLIENT_GROUP_SESSION,
|
||||||
RAC_CLIENT_GROUP_TOKEN,
|
RAC_CLIENT_GROUP_TOKEN,
|
||||||
)
|
)
|
||||||
from authentik.providers.rac.models import ConnectionToken, Endpoint
|
from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint
|
||||||
|
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
@receiver(user_logged_out)
|
||||||
@ -45,8 +46,12 @@ def pre_delete_connection_token_disconnect(sender, instance: ConnectionToken, **
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver([post_save, post_delete], sender=Endpoint)
|
@receiver(post_save, sender=Endpoint)
|
||||||
def post_save_post_delete_endpoint(**_):
|
def post_save_endpoint(sender: type[Model], instance, created: bool, **_):
|
||||||
"""Clear user's endpoint cache upon endpoint creation or deletion"""
|
"""Clear user's endpoint cache upon endpoint creation"""
|
||||||
|
if not created: # pragma: no cover
|
||||||
|
return
|
||||||
|
|
||||||
|
# Delete user endpoint cache
|
||||||
keys = cache.keys(user_endpoint_cache_key("*"))
|
keys = cache.keys(user_endpoint_cache_key("*"))
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user