Compare commits

..

5 Commits

Author SHA1 Message Date
Marcelo Elizeche Landó
42afef2bb2 remove placeholder section 2025-08-14 15:10:38 -04:00
Marcelo Elizeche Landó
b3ba6f9134 fix linting 2025-08-14 15:08:23 -04:00
Marcelo Elizeche Landó
22971942c1 Merge branch 'main' into pr/16059 2025-08-14 14:52:35 -04:00
Brian Begun
907d311339 Update website/integrations/media/tautulli/index.md
Co-authored-by: Dominic R <dominic@sdko.org>
Signed-off-by: Brian Begun <begunfx@usa.net>
2025-08-10 12:32:17 -07:00
Brian Begun
9890ea8275 Update index.md
Revised tutorial using new template.  Sorry for the delay on this.  

Signed-off-by: Brian Begun <begunfx@usa.net>
2025-08-10 07:31:51 -07:00
1945 changed files with 248698 additions and 251554 deletions

View File

@@ -1,81 +0,0 @@
name: Bug report
description: Create a report to help us improve
labels: ["bug", "triage"]
type: bug
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this bug report!
- type: textarea
id: describe-the-bug
attributes:
label: Describe the bug
description: "A clear and concise description of what the bug is."
placeholder: "Describe the issue"
validations:
required: true
- type: textarea
id: how-to-reproduce
attributes:
label: How to reproduce
description: "Steps to reproduce the behavior."
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: "A clear and concise description of what you expected to happen."
placeholder: "The behavior that I expect to see is [...]"
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: "If applicable, add screenshots to help explain your problem."
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional context
description: "Add any other context about the problem here."
placeholder: "Also note that [...]"
validations:
required: false
- type: dropdown
id: deployment-method
attributes:
label: Deployment Method
description: "What deployment method are you using for authentik? Only Docker, Kubernetes and AWS CloudFormation are supported."
options:
- Docker
- Kubernetes
- AWS CloudFormation
- Other (please specify)
default: 0
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: "What version of authentik are you using?"
placeholder: "[e.g. 2025.10.1]"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: "Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks."
render: shell
validations:
required: false

View File

@@ -1,49 +0,0 @@
name: Documentation suggestion/problem
description: Suggest an improvement or report a problem in our docs
labels: ["area: docs", "triage"]
type: task
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this documentation issue!
- type: markdown
attributes:
value: |
**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).
- type: textarea
id: issue
attributes:
label: Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link?
description: "A clear and concise description of what the problem is, or where the document can be improved."
placeholder: "I believe we need more details about [...]"
validations:
required: true
- type: input
id: link
attributes:
label: Link
description: "Provide the URL or link to the exact page in the documentation to which you are referring."
placeholder: "If there are multiple pages, list them all"
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution
description: "A clear and concise description of what you suggest as a solution"
placeholder: "This issue could be resolved by [...]"
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: "Add any other context or screenshots about the documentation issue here."
placeholder: "Also note that [...]"
validations:
required: false

View File

@@ -1,41 +0,0 @@
name: Feature request
description: Suggest an idea for a feature
labels: ["enhancement", "triage"]
type: feature
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this feature request!
- type: textarea
id: related-to-problem
attributes:
label: Is your feature request related to a problem?
description: "A clear and concise description of what the problem is."
placeholder: "I'm always frustrated when [...]"
validations:
required: true
- type: textarea
id: feature
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
placeholder: "I'd like authentik to have [...]"
validations:
required: false
- type: textarea
id: alternatives
attributes:
label: Describe alternatives that you've considered
description: "A clear and concise description of any alternative solutions or features you've considered."
placeholder: "I've tried this but [...]"
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: "Add any other context or screenshots about the feature request here."
placeholder: "Also note that [...]"
validations:
required: false

39
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,39 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Output of docker-compose logs or kubectl logs respectively
**Version and Deployment (please complete the following information):**
<!--
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]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://github.com/goauthentik/authentik/discussions
about: Please ask questions via GitHub Discussions rather than creating issues.
- name: authentik Discord
url: https://discord.com/invite/jg33eMhnj6
about: For community support, visit our Discord server.

22
.github/ISSUE_TEMPLATE/docs_issue.md vendored Normal file
View File

@@ -0,0 +1,22 @@
---
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).

View File

@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: enhancement
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,17 @@
---
name: Hackathon Idea
about: Propose an idea for the hackathon
title: ""
labels: hackathon
assignees: ""
---
**Describe the idea**
A clear concise description of the idea you want to implement
You're also free to work on existing GitHub issues, whether they be feature requests or bugs, just link the existing GitHub issue here.
<!-- Don't modify below here -->
If you want to help working on this idea or want to contribute in any other way, react to this issue with a :rocket:

View File

@@ -1,7 +0,0 @@
---
name: Blank issue
about: This issue type is only for internal use
title:
labels:
assignees:
---

32
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,32 @@
---
name: Question
about: Ask a question about a feature or specific configuration
title: ""
labels: question
assignees: ""
---
**Describe your question/**
A clear and concise description of what you're trying to do.
**Relevant info**
i.e. Version of other software you're using, specifics of your setup
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Output of docker-compose logs or kubectl logs respectively
**Version and Deployment (please complete the following information):**
<!--
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]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,267 +0,0 @@
name: "Cherry-picker"
description: "Cherry-pick PRs based on their labels"
inputs:
token:
description: "GitHub Token"
required: true
git_user:
description: "Git user for pushing the cherry-pick PR"
required: true
git_user_email:
description: "Git user email for pushing the cherry-pick PR"
required: true
runs:
using: "composite"
steps:
- name: Check if workflow should run
id: should_run
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
run: |
set -e -o pipefail
# For issues events, check if it's actually a PR
if [ "${{ github.event_name }}" = "issues" ]; then
# Check if this issue is actually a PR
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} 2>/dev/null || echo "null")
if [ "$PR_DATA" = "null" ]; then
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=not_a_pr" >> $GITHUB_OUTPUT
echo "This is an issue, not a PR. Skipping."
exit 0
fi
# Get PR data
PR_MERGED=$(echo "$PR_DATA" | jq -r '.merged')
PR_NUMBER="${{ github.event.issue.number }}"
MERGE_COMMIT_SHA=$(echo "$PR_DATA" | jq -r '.merge_commit_sha')
# Check if it's a backport label
LABEL_NAME="${{ github.event.label.name }}"
if [[ "$LABEL_NAME" =~ ^backport/(.+)$ ]]; then
if [ "$PR_MERGED" = "true" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "reason=label_added_to_merged_pr" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "merge_commit_sha=$MERGE_COMMIT_SHA" >> $GITHUB_OUTPUT
exit 0
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=label_added_to_open_pr" >> $GITHUB_OUTPUT
echo "Backport label added to open PR. Will run after PR is merged."
exit 0
fi
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=non_backport_label" >> $GITHUB_OUTPUT
exit 0
fi
fi
# For pull_request and pull_request_target events
PR_NUMBER="${{ github.event.pull_request.number }}"
MERGE_COMMIT_SHA="${{ github.event.pull_request.merge_commit_sha }}"
# Case 1: PR was just merged (closed + merged = true)
if [ "${{ github.event.action }}" = "closed" ] && [ "${{ github.event.pull_request.merged }}" = "true" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "reason=pr_merged" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "merge_commit_sha=$MERGE_COMMIT_SHA" >> $GITHUB_OUTPUT
exit 0
fi
# Case 2: Label was added
if [ "${{ github.event.action }}" = "labeled" ]; then
LABEL_NAME="${{ github.event.label.name }}"
# Check if it's a backport label
if [[ "$LABEL_NAME" =~ ^backport/(.+)$ ]]; then
# Check if PR is already merged
if [ "${{ github.event.pull_request.merged }}" = "true" ]; then
echo "should_run=true" >> $GITHUB_OUTPUT
echo "reason=label_added_to_merged_pr" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "merge_commit_sha=$MERGE_COMMIT_SHA" >> $GITHUB_OUTPUT
exit 0
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=label_added_to_open_pr" >> $GITHUB_OUTPUT
echo "Backport label added to open PR. Will run after PR is merged."
exit 0
fi
else
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=non_backport_label" >> $GITHUB_OUTPUT
exit 0
fi
fi
echo "should_run=false" >> $GITHUB_OUTPUT
echo "reason=unknown" >> $GITHUB_OUTPUT
- name: Configure Git
if: steps.should_run.outputs.should_run == 'true'
shell: bash
env:
user: ${{ inputs.git_user }}
email: ${{ inputs.git_user_email }}
run: |
git config --global user.name "${user}"
git config --global user.email "${email}"
- name: Get PR details and extract backport labels
if: steps.should_run.outputs.should_run == 'true'
id: pr_details
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
run: |
set -e -o pipefail
PR_NUMBER="${{ steps.should_run.outputs.pr_number }}"
# Get PR details
PR_DATA=$(gh api repos/${{ github.repository }}/pulls/$PR_NUMBER)
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.user.login')
echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
echo "pr_author=$PR_AUTHOR" >> $GITHUB_OUTPUT
# Determine which labels to process
if [ "${{ steps.should_run.outputs.reason }}" = "label_added_to_merged_pr" ]; then
# Only process the specific label that was just added
if [ "${{ github.event_name }}" = "issues" ]; then
LABEL_NAME="${{ github.event.label.name }}"
else
LABEL_NAME="${{ github.event.label.name }}"
fi
if [[ "$LABEL_NAME" =~ ^backport/(.+)$ ]]; then
echo "labels=$LABEL_NAME" >> $GITHUB_OUTPUT
else
echo "Label $LABEL_NAME does not match backport pattern"
echo "labels=" >> $GITHUB_OUTPUT
fi
else
# PR was just merged, process all backport labels
LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name' | grep '^backport/' | tr '\n' ' ' || true)
echo "labels=$LABELS" >> $GITHUB_OUTPUT
fi
- name: Cherry-pick to target branches
if: steps.should_run.outputs.should_run == 'true' && steps.pr_details.outputs.labels != ''
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.token }}
run: |
set -e -o pipefail
PR_NUMBER='${{ steps.should_run.outputs.pr_number }}'
COMMIT_SHA='${{ steps.should_run.outputs.merge_commit_sha }}'
PR_TITLE='${{ steps.pr_details.outputs.pr_title }}'
PR_AUTHOR='${{ steps.pr_details.outputs.pr_author }}'
LABELS='${{ steps.pr_details.outputs.labels }}'
echo "Processing PR #$PR_NUMBER (reason: ${{ steps.should_run.outputs.reason }})"
echo "Found backport labels: $LABELS"
# Process each backport label
for label in $LABELS; do
if [[ "$label" =~ ^backport/(.+)$ ]]; then
TARGET_BRANCH="${BASH_REMATCH[1]}"
echo "Processing backport to branch: $TARGET_BRANCH"
# Check if target branch exists
if ! git ls-remote --heads origin "$TARGET_BRANCH" | grep -q "$TARGET_BRANCH"; then
echo "❌ Target branch $TARGET_BRANCH does not exist, skipping"
# Comment on the original PR about the missing branch
gh pr comment $PR_NUMBER --body "⚠️ Cannot backport to \`$TARGET_BRANCH\`: branch does not exist."
continue
fi
# Create a unique branch name for the cherry-pick
CHERRY_PICK_BRANCH="cherry-pick/${PR_NUMBER}-to-${TARGET_BRANCH}"
# Check if a cherry-pick PR already exists
EXISTING_PR=$(gh pr list --head "$CHERRY_PICK_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING_PR" ]; then
echo "⚠️ Cherry-pick PR already exists: #$EXISTING_PR"
gh pr comment $PR_NUMBER --body "Cherry-pick to \`$TARGET_BRANCH\` already exists: #$EXISTING_PR"
continue
fi
# Fetch and checkout target branch
git fetch origin "$TARGET_BRANCH"
git checkout -b "$CHERRY_PICK_BRANCH" "origin/$TARGET_BRANCH"
# Attempt cherry-pick
if git cherry-pick "$COMMIT_SHA"; then
echo "✅ Cherry-pick successful for $TARGET_BRANCH"
# Push the cherry-pick branch
git push origin "$CHERRY_PICK_BRANCH"
# Create PR for the cherry-pick
CHERRY_PICK_TITLE="$PR_TITLE (cherry-pick #$PR_NUMBER to $TARGET_BRANCH)"
CHERRY_PICK_BODY="Cherry-pick of #$PR_NUMBER to \`$TARGET_BRANCH\` branch.
**Original PR:** #$PR_NUMBER
**Original Author:** @$PR_AUTHOR
**Cherry-picked commit:** $COMMIT_SHA"
NEW_PR=$(gh pr create \
--title "$CHERRY_PICK_TITLE" \
--body "$CHERRY_PICK_BODY" \
--base "$TARGET_BRANCH" \
--head "$CHERRY_PICK_BRANCH" \
--label "cherry-pick")
echo "✅ Created cherry-pick PR $NEW_PR for $TARGET_BRANCH"
# Comment on original PR
gh pr comment $PR_NUMBER --body "🍒 Cherry-pick to \`$TARGET_BRANCH\` created: $NEW_PR"
else
echo "⚠️ Cherry-pick failed for $TARGET_BRANCH, creating conflict resolution PR"
# Add conflicted files and commit
git add .
git commit -m "Cherry-pick #$PR_NUMBER to $TARGET_BRANCH (with conflicts)
This cherry-pick has conflicts that need manual resolution.
Original PR: #$PR_NUMBER
Original commit: $COMMIT_SHA"
# Push the branch with conflicts
git push origin "$CHERRY_PICK_BRANCH"
# Create PR with conflict notice
CONFLICT_TITLE="$PR_TITLE (cherry-pick #$PR_NUMBER to $TARGET_BRANCH)"
CONFLICT_BODY="⚠️ **This cherry-pick has conflicts that require manual resolution.**
Cherry-pick of #$PR_NUMBER to \`$TARGET_BRANCH\` branch.
**Original PR:** #$PR_NUMBER
**Original Author:** @$PR_AUTHOR
**Cherry-picked commit:** $COMMIT_SHA
**Please resolve the conflicts in this PR before merging.**"
NEW_PR=$(gh pr create \
--title "$CONFLICT_TITLE" \
--body "$CONFLICT_BODY" \
--base "$TARGET_BRANCH" \
--head "$CHERRY_PICK_BRANCH" \
--label "cherry-pick")
echo "⚠️ Created conflict resolution PR $NEW_PR for $TARGET_BRANCH"
# Comment on original PR
gh pr comment $PR_NUMBER --body "⚠️ Cherry-pick to \`$TARGET_BRANCH\` has conflicts: $NEW_PR"
fi
# Clean up - go back to main branch
git checkout main
git branch -D "$CHERRY_PICK_BRANCH" 2>/dev/null || true
fi
done

View File

@@ -10,14 +10,14 @@ runs:
using: "composite"
steps:
- name: Find Comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v2
uses: peter-evans/find-comment@v2
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: authentik PR Installation instructions
- name: Create or update comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v2
uses: peter-evans/create-or-update-comment@v2
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}

View File

@@ -2,28 +2,16 @@
import os
from json import dumps
from sys import exit as sysexit
from time import time
from authentik import authentik_version
def must_or_fail(input: str | None, error: str) -> str:
if not input:
print(f"::error::{error}")
sysexit(1)
return input
# Decide if we should push the image or not
should_push = True
if len(os.environ.get("DOCKER_USERNAME", "")) < 1:
# Don't push if we don't have DOCKER_USERNAME, i.e. no secrets are available
should_push = False
if (
must_or_fail(os.environ.get("GITHUB_REPOSITORY"), "Repo required").lower()
== "goauthentik/authentik-internal"
):
if os.environ.get("GITHUB_REPOSITORY").lower() == "goauthentik/authentik-internal":
# Don't push on the internal repo
should_push = False
@@ -32,16 +20,13 @@ if os.environ.get("GITHUB_HEAD_REF", "") != "":
branch_name = os.environ["GITHUB_HEAD_REF"]
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-").replace("'", "-")
image_names = must_or_fail(os.getenv("IMAGE_NAME"), "Image name required").split(",")
image_names = os.getenv("IMAGE_NAME").split(",")
image_arch = os.getenv("IMAGE_ARCH") or None
is_pull_request = bool(os.getenv("PR_HEAD_SHA"))
is_release = "dev" not in image_names[0]
sha = must_or_fail(
os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA"),
"could not determine SHA",
)
sha = os.environ["GITHUB_SHA"] if not is_pull_request else os.getenv("PR_HEAD_SHA")
# 2042.1.0 or 2042.1.0-rc1
version = authentik_version()
@@ -73,7 +58,7 @@ else:
image_main_tag = image_tags[0].split(":")[-1]
def get_attest_image_names(image_with_tags: list[str]) -> str:
def get_attest_image_names(image_with_tags: list[str]):
"""Attestation only for GHCR"""
image_tags = []
for image_name in set(name.split(":")[0] for name in image_with_tags):
@@ -97,6 +82,7 @@ if os.getenv("RELEASE", "false").lower() == "true":
image_build_args = [f"VERSION={os.getenv('REF')}"]
else:
image_build_args = [f"GIT_BUILD_HASH={sha}"]
image_build_args = "\n".join(image_build_args)
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"shouldPush={str(should_push).lower()}", file=_output)
@@ -109,4 +95,4 @@ with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print(f"imageMainTag={image_main_tag}", file=_output)
print(f"imageMainName={image_tags[0]}", file=_output)
print(f"cacheTo={cache_to}", file=_output)
print(f"imageBuildArgs={"\n".join(image_build_args)}", file=_output)
print(f"imageBuildArgs={image_build_args}", file=_output)

View File

@@ -21,12 +21,12 @@ runs:
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
if: ${{ contains(inputs.dependencies, 'python') }}
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v5
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
- name: Setup python
if: ${{ contains(inputs.dependencies, 'python') }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"
- name: Install Python deps
@@ -35,15 +35,14 @@ runs:
run: uv sync --all-extras --dev --frozen
- name: Setup node
if: ${{ contains(inputs.dependencies, 'node') }}
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v4
uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
registry-url: 'https://registry.npmjs.org'
- name: Setup go
if: ${{ contains(inputs.dependencies, 'go') }}
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v5
uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Setup docker cache
@@ -57,7 +56,7 @@ runs:
run: |
export PSQL_TAG=${{ inputs.postgresql_version }}
docker compose -f .github/actions/setup/docker-compose.yml up -d
cd web && npm i
cd web && npm ci
- name: Generate config
if: ${{ contains(inputs.dependencies, 'python') }}
shell: uv run python {0}

View File

@@ -3,7 +3,6 @@ services:
image: docker.io/library/postgres:${PSQL_TAG:-16}
volumes:
- db-data:/var/lib/postgresql/data
command: "-c log_statement=all"
environment:
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"

View File

@@ -1,28 +0,0 @@
name: "Process test results"
description: Convert test results to JUnit, add them to GitHub Actions and codecov
inputs:
flags:
description: Codecov flags
runs:
using: "composite"
steps:
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
with:
flags: ${{ inputs.flags }}
use_oidc: true
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1
with:
flags: ${{ inputs.flags }}
file: unittest.xml
use_oidc: true
- name: PostgreSQL Logs
shell: bash
run: |
if [[ $ACTIONS_RUNNER_DEBUG == 'true' || $ACTIONS_STEP_DEBUG == 'true' ]]; then
docker stop setup-postgresql-1
echo "::group::PostgreSQL Logs"
docker logs setup-postgresql-1
echo "::endgroup::"
fi

2
.github/cherry-pick-bot.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
enabled: true
preservePullRequestTitle: true

View File

@@ -1,15 +1,7 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directories:
- /
# Required to update composite actions
# https://github.com/dependabot/dependabot-core/issues/6704
- /.github/actions/cherry-pick
- /.github/actions/setup
- /.github/actions/docker-push-variables
- /.github/actions/comment-pr-instructions
- /.github/actions/test-results
directory: "/"
schedule:
interval: daily
time: "04:00"
@@ -85,12 +77,6 @@ updates:
goauthentik:
patterns:
- "@goauthentik/*"
react:
patterns:
- "react"
- "react-dom"
- "@types/react"
- "@types/react-dom"
- package-ecosystem: npm
directory: "/website"
schedule:
@@ -142,9 +128,7 @@ updates:
labels:
- dependencies
- package-ecosystem: docker
directories:
- /
- /website
directory: "/"
schedule:
interval: daily
time: "04:00"
@@ -156,7 +140,6 @@ updates:
- package-ecosystem: docker-compose
directories:
# - /scripts # Maybe
- /scripts/api
- /tests/e2e
schedule:
interval: daily

View File

@@ -1,4 +1,3 @@
---
git:
filters:
- filter_type: file

View File

@@ -42,9 +42,9 @@ jobs:
# Needed for checkout
contents: read
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
- uses: actions/checkout@v5
- 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
@@ -56,13 +56,13 @@ jobs:
release: ${{ inputs.release }}
- name: Login to Docker Hub
if: ${{ inputs.registry_dockerhub }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_CORP_USERNAME }}
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ inputs.registry_ghcr }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -72,18 +72,11 @@ jobs:
run: |
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: Setup node
if: ${{ !inputs.release }}
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: generate ts client
if: ${{ !inputs.release }}
run: make gen-client-ts
- name: Build Docker Image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@v6
id: push
with:
context: .
@@ -97,7 +90,7 @@ jobs:
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@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:

View File

@@ -21,7 +21,7 @@ on:
jobs:
build-server-amd64:
uses: ./.github/workflows/_reusable-docker-build-single.yml
uses: ./.github/workflows/_reusable-docker-build-single.yaml
secrets: inherit
with:
image_name: ${{ inputs.image_name }}
@@ -31,7 +31,7 @@ jobs:
registry_ghcr: ${{ inputs.registry_ghcr }}
release: ${{ inputs.release }}
build-server-arm64:
uses: ./.github/workflows/_reusable-docker-build-single.yml
uses: ./.github/workflows/_reusable-docker-build-single.yaml
secrets: inherit
with:
image_name: ${{ inputs.image_name }}
@@ -49,7 +49,7 @@ jobs:
tags: ${{ steps.ev.outputs.imageTagsJSON }}
shouldPush: ${{ steps.ev.outputs.shouldPush }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -69,7 +69,7 @@ jobs:
matrix:
tag: ${{ fromJson(needs.get-tags.outputs.tags) }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -79,25 +79,25 @@ jobs:
image-name: ${{ inputs.image_name }}
- name: Login to Docker Hub
if: ${{ inputs.registry_dockerhub }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_CORP_USERNAME }}
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
- name: Login to GitHub Container Registry
if: ${{ inputs.registry_ghcr }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: int128/docker-manifest-create-action@b60433fd4312d7a64a56d769b76ebe3f45cf36b4 # v2
- 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@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
- uses: actions/attest-build-provenance@v2
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}

68
.github/workflows/api-py-publish.yml vendored Normal file
View File

@@ -0,0 +1,68 @@
---
name: API - Publish Python client
on:
push:
branches: [main]
paths:
- "schema.yml"
workflow_dispatch:
jobs:
build:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
permissions:
id-token: write
steps:
- id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v5
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Install poetry & deps
shell: bash
run: |
pipx install poetry || true
sudo apt-get update
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
- name: Setup python and restore poetry
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"
- name: Generate API Client
run: make gen-client-py
- name: Publish package
working-directory: gen-py-api/
run: |
poetry build
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: gen-py-api/dist/
# We can't easily upgrade the API client being used due to poetry being poetry
# so we'll have to rely on dependabot
# - name: Upgrade /
# run: |
# export VERSION=$(cd gen-py-api && poetry version -s)
# poetry add "authentik_client=$VERSION" --allow-prereleases --lock
# - uses: peter-evans/create-pull-request@v6
# id: cpr
# with:
# token: ${{ steps.generate_token.outputs.token }}
# branch: update-root-api-client
# commit-message: "root: bump API Client version"
# title: "root: bump API Client version"
# body: "root: bump API Client version"
# delete-branch: true
# signoff: true
# # ID from https://api.github.com/users/authentik-automation[bot]
# author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
# - uses: peter-evans/enable-pull-request-automerge@v3
# with:
# token: ${{ steps.generate_token.outputs.token }}
# pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
# merge-method: squash

View File

@@ -8,24 +8,20 @@ on:
- "schema.yml"
workflow_dispatch:
permissions:
# Required for NPM OIDC trusted publisher
id-token: write
contents: read
jobs:
build:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: tibdex/github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v5
with:
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
registry-url: "https://registry.npmjs.org"
@@ -36,6 +32,8 @@ jobs:
run: |
npm i
npm publish --tag generated
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
- name: Upgrade /web
working-directory: web
run: |
@@ -46,7 +44,7 @@ jobs:
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@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
- uses: peter-evans/create-pull-request@v7
id: cpr
with:
token: ${{ steps.generate_token.outputs.token }}
@@ -59,7 +57,7 @@ jobs:
# ID from https://api.github.com/users/authentik-automation[bot]
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
labels: dependencies
- uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3
- uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ steps.generate_token.outputs.token }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}

View File

@@ -21,7 +21,7 @@ jobs:
command:
- prettier-check
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Install Dependencies
working-directory: website/
run: npm ci
@@ -32,8 +32,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"
@@ -41,7 +41,7 @@ jobs:
- working-directory: website/
name: Install Dependencies
run: npm ci
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
- uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/website/api/.docusaurus
@@ -55,7 +55,7 @@ jobs:
env:
NODE_ENV: production
run: npm run build -w api
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v4
- uses: actions/upload-artifact@v4
with:
name: api-docs
path: website/api/build
@@ -66,12 +66,12 @@ jobs:
- lint
- build
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5
- uses: actions/checkout@v5
- uses: actions/download-artifact@v5
with:
name: api-docs
path: website/api/build
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"

View File

@@ -21,10 +21,10 @@ jobs:
check-changes-applied:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Setup authentik env
uses: ./.github/actions/setup
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/setup-node@v4
with:
node-version-file: lifecycle/aws/package.json
cache: "npm"
@@ -35,13 +35,13 @@ jobs:
- name: Check changes have been applied
run: |
uv run make aws-cfn
git diff --exit-code lifecycle/aws/template.yaml
git diff --exit-code
ci-aws-cfn-mark:
if: always()
needs:
- check-changes-applied
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

@@ -13,10 +13,11 @@ env:
jobs:
publish-source-docs:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: generate docs
@@ -24,9 +25,9 @@ jobs:
uv run make migrate
uv run ak build_source_docs
- name: Publish
uses: netlify/actions/cli@master
with:
args: deploy --dir=source_docs --prod
env:
NETLIFY_SITE_ID: eb246b7b-1d83-4f69-89f7-01a936b4ca59
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
run: |
npm install -g netlify-cli
netlify deploy --dir=source_docs --prod

View File

@@ -21,7 +21,7 @@ jobs:
command:
- prettier-check
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Install dependencies
working-directory: website/
run: npm ci
@@ -32,8 +32,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"
@@ -48,8 +48,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"
@@ -61,6 +61,7 @@ jobs:
working-directory: website/
run: npm run build -w integrations
build-container:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
permissions:
# Needed to upload container images to ghcr.io
@@ -69,13 +70,13 @@ jobs:
id-token: write
attestations: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -85,14 +86,14 @@ jobs:
image-name: ghcr.io/goauthentik/dev-docs
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
@@ -101,7 +102,7 @@ jobs:
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && 'type=registry,ref=ghcr.io/goauthentik/dev-docs:buildcache,mode=max' || '' }}
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:
@@ -117,6 +118,7 @@ jobs:
- build-container
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
allowed-skips: ${{ github.repository == 'goauthentik/authentik-internal' && 'build-container' || '[]' }}

View File

@@ -9,6 +9,7 @@ on:
jobs:
test-container:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -18,7 +19,7 @@ jobs:
- version-2025-4
- version-2025-2
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- run: |
current="$(pwd)"
dir="/tmp/authentik/${{ matrix.version }}"

View File

@@ -34,10 +34,9 @@ jobs:
- codespell
- pending-migrations
- ruff
- mypy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run job
@@ -45,7 +44,7 @@ jobs:
test-migrations:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run migrations
@@ -61,33 +60,27 @@ jobs:
test-migrations-from-stable:
name: test-migrations-from-stable - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 20
needs: test-make-seed
strategy:
fail-fast: false
matrix:
psql:
- 14-alpine
- 18-alpine
- 15-alpine
- 16-alpine
- 17-alpine
run_id: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
fetch-depth: 0
- name: checkout stable
run: |
set -e -o pipefail
# Copy current, latest config to local
cp authentik/lib/default.yml local.env.yml
cp -R .github ..
cp -R scripts ..
# Previous stable tag
prev_stable=$(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
# Current version family based on
current_version_family=$(cat internal/constants/VERSION | grep -vE -- 'rc[0-9]+$' || true)
if [[ -n $current_version_family ]]; then
prev_stable=$current_version_family
fi
echo "::notice::Checking out ${prev_stable} as stable version..."
git checkout ${prev_stable}
git checkout $(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
rm -rf .github/ scripts/
mv ../.github ../scripts .
- name: Setup authentik env (stable)
@@ -119,24 +112,21 @@ jobs:
CI_TOTAL_RUNS: "5"
run: |
uv run make ci-test
- uses: ./.github/actions/test-results
if: ${{ always() }}
with:
flags: unit-migrate
test-unittest:
name: test-unittest - PostgreSQL ${{ matrix.psql }} - Run ${{ matrix.run_id }}/5
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 20
needs: test-make-seed
strategy:
fail-fast: false
matrix:
psql:
- 14-alpine
- 18-alpine
- 15-alpine
- 16-alpine
- 17-alpine
run_id: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Setup authentik env
uses: ./.github/actions/setup
with:
@@ -148,27 +138,41 @@ jobs:
CI_TOTAL_RUNS: "5"
run: |
uv run make ci-test
- uses: ./.github/actions/test-results
if: ${{ always() }}
- if: ${{ always() }}
uses: codecov/codecov-action@v5
with:
flags: unit
use_oidc: true
- if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
flags: unit
file: unittest.xml
use_oidc: true
test-integration:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Create k8s Kind Cluster
uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0
uses: helm/kind-action@v1.12.0
- name: run integration
run: |
uv run coverage run manage.py test tests/integration
uv run coverage xml
- uses: ./.github/actions/test-results
if: ${{ always() }}
- if: ${{ always() }}
uses: codecov/codecov-action@v5
with:
flags: integration
use_oidc: true
- if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
flags: integration
file: unittest.xml
use_oidc: true
test-e2e:
name: test-e2e (${{ matrix.job.name }})
runs-on: ubuntu-latest
@@ -194,14 +198,14 @@ jobs:
- name: flows
glob: tests/e2e/test_flows*
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Setup e2e env (chrome, etc)
run: |
docker compose -f tests/e2e/docker-compose.yml up -d --quiet-pull
- id: cache-web
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache@v4
with:
path: web/dist
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'package-lock.json', 'web/src/**', 'web/packages/sfe/src/**') }}-b
@@ -217,10 +221,17 @@ jobs:
run: |
uv run coverage run manage.py test ${{ matrix.job.glob }}
uv run coverage xml
- uses: ./.github/actions/test-results
if: ${{ always() }}
- if: ${{ always() }}
uses: codecov/codecov-action@v5
with:
flags: e2e
use_oidc: true
- if: ${{ !cancelled() }}
uses: codecov/test-results-action@v1
with:
flags: e2e
file: unittest.xml
use_oidc: true
ci-core-mark:
if: always()
needs:
@@ -232,7 +243,7 @@ jobs:
- test-e2e
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
build:
@@ -245,7 +256,7 @@ jobs:
# Needed for checkout
contents: read
needs: ci-core-mark
uses: ./.github/workflows/_reusable-docker-build.yml
uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit
with:
image_name: ${{ github.repository == 'goauthentik/authentik-internal' && 'ghcr.io/goauthentik/internal-server' || 'ghcr.io/goauthentik/dev-server' }}
@@ -260,7 +271,7 @@ jobs:
pull-requests: write
timeout-minutes: 120
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: prepare variables

View File

@@ -12,17 +12,12 @@ on:
- main
- version-*
env:
POSTGRES_DB: authentik
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
jobs:
lint-golint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Prepare and generate API
@@ -34,7 +29,7 @@ jobs:
- name: Generate API
run: make gen-client-go
- name: golangci-lint
uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v8
uses: golangci/golangci-lint-action@v8
with:
version: latest
args: --timeout 5000s --verbose
@@ -42,17 +37,14 @@ jobs:
test-unittest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Generate API
run: make gen-client-go
- name: prepare database
run: |
uv run make migrate
- name: Go unittests
run: |
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./...
@@ -63,10 +55,11 @@ jobs:
- test-unittest
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
build-container:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
timeout-minutes: 120
needs:
- ci-outpost-mark
@@ -86,13 +79,13 @@ jobs:
id-token: write
attestations: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -102,7 +95,7 @@ jobs:
image-name: ghcr.io/goauthentik/dev-${{ matrix.type }}
- name: Login to Container Registry
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -111,7 +104,7 @@ jobs:
run: make gen-client-go
- name: Build Docker Image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: ${{ matrix.type }}.Dockerfile
@@ -122,7 +115,7 @@ jobs:
context: .
cache-from: type=registry,ref=ghcr.io/goauthentik/dev-${{ matrix.type }}:buildcache
cache-to: ${{ steps.ev.outputs.shouldPush == 'true' && format('type=registry,ref=ghcr.io/goauthentik/dev-{0}:buildcache,mode=max', matrix.type) || '' }}
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
- uses: actions/attest-build-provenance@v2
id: attest
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
with:
@@ -145,13 +138,13 @@ jobs:
goos: [linux]
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"

View File

@@ -31,8 +31,8 @@ jobs:
- command: lit-analyse
project: web
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
@@ -48,8 +48,8 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
@@ -68,7 +68,7 @@ jobs:
- lint
runs-on: ubuntu-latest
steps:
- uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
test:
@@ -76,8 +76,8 @@ jobs:
- ci-web-mark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"

View File

@@ -29,32 +29,32 @@ jobs:
github.event.pull_request.head.repo.full_name == github.repository)
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: tibdex/github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v5
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Compress images
id: compress
uses: calibreapp/image-actions@420075c115b26f8785e293c5bd5bef0911c506e5 # main
uses: calibreapp/image-actions@main
with:
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
githubToken: ${{ steps.generate_token.outputs.token }}
compressOnly: ${{ github.event_name != 'pull_request' }}
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
- uses: peter-evans/create-pull-request@v7
if: "${{ github.event_name != 'pull_request' && steps.compress.outputs.markdown != '' }}"
id: cpr
with:
token: ${{ steps.generate_token.outputs.token }}
title: "*: Auto compress images"
branch-suffix: timestamp
commit-message: "*: compress images"
commit-messsage: "*: compress images"
body: ${{ steps.compress.outputs.markdown }}
delete-branch: true
signoff: true
labels: dependencies
- uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3
- uses: peter-evans/enable-pull-request-automerge@v3
if: "${{ github.event_name != 'pull_request' && steps.compress.outputs.markdown != '' }}"
with:
token: ${{ steps.generate_token.outputs.token }}

View File

@@ -13,20 +13,21 @@ env:
jobs:
build:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: tibdex/github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v5
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Setup authentik env
uses: ./.github/actions/setup
- run: uv run ak update_webauthn_mds
- uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
- uses: peter-evans/create-pull-request@v7
id: cpr
with:
token: ${{ steps.generate_token.outputs.token }}
@@ -39,7 +40,7 @@ jobs:
# ID from https://api.github.com/users/authentik-automation[bot]
author: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
labels: dependencies
- uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3
- uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ steps.generate_token.outputs.token }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}

View File

@@ -1,36 +0,0 @@
name: GH - Cherry-pick
on:
pull_request_target:
types: [closed, labeled]
jobs:
cherry-pick:
runs-on: ubuntu-latest
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
if: ${{ env.GH_APP_ID != '' }}
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
env:
GH_APP_ID: ${{ secrets.GH_APP_ID }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
if: ${{ steps.app-token.outcome != 'skipped' }}
with:
fetch-depth: 0
token: "${{ steps.app-token.outputs.token }}"
- id: get-user-id
if: ${{ steps.app-token.outcome != 'skipped' }}
name: Get GitHub app user ID
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
- uses: ./.github/actions/cherry-pick
if: ${{ steps.app-token.outcome != 'skipped' }}
with:
token: ${{ steps.app-token.outputs.token }}
git_user: ${{ steps.app-token.outputs.app-slug }}[bot]
git_user_email: '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com'

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
uses: actions/checkout@v5
- name: Cleanup
run: |

View File

@@ -5,28 +5,25 @@ on:
# schedule:
# - cron: "0 0 * * *" # every day at midnight
workflow_dispatch:
inputs:
dry-run:
type: boolean
description: Enable dry-run mode
jobs:
clean-ghcr:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
name: Delete old unused container images
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: tibdex/github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Delete 'dev' containers older than a week
uses: snok/container-retention-policy@3b0972b2276b171b212f8c4efbca59ebba26eceb # v3.0.1
uses: snok/container-retention-policy@v2
with:
image-names: dev-server,dev-ldap,dev-proxy
image-tags: "!gh-next,!gh-main"
cut-off: One week ago UTC
account: goauthentik
tag-selection: untagged
account-type: org
org-name: goauthentik
untagged-only: false
token: ${{ steps.generate_token.outputs.token }}
dry-run: ${{ inputs.dry-run }}
skip-tags: gh-next,gh-main

View File

@@ -5,47 +5,40 @@ on:
push:
branches: [main]
paths:
- packages/tsconfig/**
- packages/docusaurus-config/**
- packages/eslint-config/**
- packages/prettier-config/**
- packages/docusaurus-config/**
- packages/tsconfig/**
- packages/esbuild-plugin-live-reload/**
workflow_dispatch:
permissions:
# Required for NPM OIDC trusted publisher
id-token: write
contents: read
jobs:
publish:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package:
# The order of the `*config` packages should not be changed, as they depend on each other.
- packages/tsconfig
- packages/docusaurus-config
- packages/eslint-config
- packages/prettier-config
- packages/docusaurus-config
- packages/tsconfig
- packages/esbuild-plugin-live-reload
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
fetch-depth: 2
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/setup-node@v4
with:
node-version-file: ${{ matrix.package }}/package.json
registry-url: "https://registry.npmjs.org"
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # 24d32ffd492484c1d75e0c0b894501ddb9d30d62
uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c
with:
files: |
${{ matrix.package }}/package.json
- name: Install Dependencies
run: npm ci
- name: Publish package
if: steps.changed-files.outputs.any_changed == 'true'
working-directory: ${{ matrix.package }}
@@ -53,3 +46,5 @@ jobs:
npm ci
npm run build
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

View File

@@ -24,14 +24,14 @@ jobs:
language: ["go", "javascript", "python"]
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
uses: actions/checkout@v5
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3

View File

@@ -26,5 +26,5 @@ jobs:
image: semgrep/semgrep
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- run: semgrep ci

View File

@@ -29,12 +29,12 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout main
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
uses: actions/checkout@v5
with:
ref: main
token: "${{ steps.app-token.outputs.token }}"
@@ -43,13 +43,10 @@ jobs:
with:
dependencies: python
- name: Create version branch
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
run: |
current_major_version="$(uv version --short | grep -oE "^[0-9]{4}\.[0-9]{1,2}")"
git checkout -b "version-${current_major_version}"
git push origin "version-${current_major_version}"
gh label create "backport/version-${current_major_version}" --description "Add this label to PRs to backport changes to version-${current_major_version}" --color "fbca04"
bump-version-pr:
name: Open version bump PR
needs:
@@ -57,12 +54,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: tibdex/github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout main
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
uses: actions/checkout@v5
with:
ref: main
token: ${{ steps.generate_token.outputs.token }}
@@ -73,7 +70,7 @@ jobs:
- name: Bump version
run: "make bump version=${{ inputs.next_version }}.0-rc1"
- name: Create pull request
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.generate_token.outputs.token }}
branch: release-bump-${{ inputs.next_version }}

View File

@@ -12,10 +12,11 @@ permissions:
jobs:
update-next:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
environment: internal-production
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
ref: main
- run: |

View File

@@ -7,7 +7,7 @@ on:
jobs:
build-server:
uses: ./.github/workflows/_reusable-docker-build.yml
uses: ./.github/workflows/_reusable-docker-build.yaml
secrets: inherit
permissions:
contents: read
@@ -31,11 +31,11 @@ jobs:
id-token: write
attestations: write
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -44,21 +44,21 @@ jobs:
with:
image-name: ghcr.io/goauthentik/docs
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
id: push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@v6
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: website/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
- uses: actions/attest-build-provenance@v2
id: attest
if: true
with:
@@ -83,14 +83,14 @@ jobs:
- radius
- rac
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
uses: docker/setup-qemu-action@v3.6.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
uses: docker/setup-buildx-action@v3
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -103,18 +103,18 @@ jobs:
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: Docker Login Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_CORP_USERNAME }}
password: ${{ secrets.DOCKER_CORP_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
uses: docker/build-push-action@v6
id: push
with:
push: true
@@ -124,7 +124,7 @@ jobs:
file: ${{ matrix.type }}.Dockerfile
platforms: linux/amd64,linux/arm64
context: .
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
- uses: actions/attest-build-provenance@v2
id: attest
with:
subject-name: ${{ steps.ev.outputs.attestImageNames }}
@@ -146,11 +146,11 @@ jobs:
goos: [linux, darwin]
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6
- uses: actions/checkout@v5
- uses: actions/setup-go@v5
with:
go-version-file: "go.mod"
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
@@ -168,7 +168,7 @@ jobs:
export CGO_ENABLED=0
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
- name: Upload binaries to release
uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # v2
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
@@ -186,8 +186,8 @@ jobs:
AWS_REGION: eu-central-1
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5
- uses: actions/checkout@v5
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: "arn:aws:iam::016170277896:role/github_goauthentik_authentik"
aws-region: ${{ env.AWS_REGION }}
@@ -202,14 +202,14 @@ jobs:
- build-outpost-binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: Run test suite in final docker images
run: |
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql
docker compose start postgresql redis
docker compose run -u root server test-all
sentry-release:
needs:
@@ -218,7 +218,7 @@ jobs:
- build-outpost-binary
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
@@ -232,7 +232,7 @@ jobs:
container=$(docker container create ${{ steps.ev.outputs.imageMainName }})
docker cp ${container}:web/ .
- name: Create a Sentry.io release
uses: getsentry/action-release@128c5058bbbe93c8e02147fe0a9c713f166259a6 # v3
uses: getsentry/action-release@v3
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

View File

@@ -35,10 +35,8 @@ jobs:
echo "major_version=${{ inputs.version }}" | grep -oE "^major_version=[0-9]{4}\.[0-9]{1,2}" >> "$GITHUB_OUTPUT"
- id: changelog-url
run: |
if [ "${{ inputs.release_reason }}" = "feature" ]; then
if [ "${{ inputs.release_reason }}" = "feature" ] || [ "${{ inputs.release_reason }}" = "prerelease" ]; then
changelog_url="https://docs.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}"
elif [ "${{ inputs.release_reason }}" = "prerelease" ]; then
changelog_url="https://next.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}"
else
changelog_url="https://docs.goauthentik.io/docs/releases/${{ steps.check.outputs.major_version }}#fixed-in-$(echo -n ${{ inputs.version }} | sed 's/\.//g')"
fi
@@ -50,7 +48,7 @@ jobs:
name: Pre-release test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
- run: make test-docker
bump-authentik:
name: Bump authentik version
@@ -61,7 +59,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@@ -70,7 +68,7 @@ jobs:
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
ref: "version-${{ needs.check-inputs.outputs.major_version }}"
token: "${{ steps.app-token.outputs.token }}"
@@ -89,7 +87,7 @@ jobs:
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
git push --follow-tags
- name: Create Release
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@v2
with:
token: "${{ steps.app-token.outputs.token }}"
tag_name: "version/${{ inputs.version }}"
@@ -108,7 +106,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@@ -118,7 +116,7 @@ jobs:
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
repository: "${{ github.repository_owner }}/helm"
token: "${{ steps.app-token.outputs.token }}"
@@ -130,7 +128,7 @@ jobs:
sed -E -i 's/[0-9]{4}\.[0-9]{1,2}\.[0-9]+$/${{ inputs.version }}/' charts/authentik/Chart.yaml
./scripts/helm-docs.sh
- name: Create pull request
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
uses: peter-evans/create-pull-request@v7
with:
token: "${{ steps.app-token.outputs.token }}"
branch: bump-${{ inputs.version }}
@@ -150,7 +148,7 @@ jobs:
steps:
- id: app-token
name: Generate app token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@@ -160,7 +158,7 @@ jobs:
run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT"
env:
GH_TOKEN: "${{ steps.app-token.outputs.token }}"
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
with:
repository: "${{ github.repository_owner }}/version"
token: "${{ steps.app-token.outputs.token }}"
@@ -185,7 +183,7 @@ jobs:
'.stable.version = $version | .stable.changelog = $changelog | .stable.changelog_url = $changelog_url' version.json > version.new.json
mv version.new.json version.json
- name: Create pull request
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
uses: peter-evans/create-pull-request@v7
with:
token: "${{ steps.app-token.outputs.token }}"
branch: bump-${{ inputs.version }}

View File

@@ -0,0 +1,22 @@
---
name: Repo - Cleanup internal mirror
on:
workflow_dispatch:
jobs:
to_internal:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- if: ${{ env.MIRROR_KEY != '' }}
uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb
with:
target_repo_url: git@github.com:goauthentik/authentik-internal.git
ssh_private_key: ${{ secrets.GH_MIRROR_KEY }}
args: --tags --force --prune
env:
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}

21
.github/workflows/repo-mirror.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
---
name: Repo - Mirror to internal
on: [push, delete]
jobs:
to_internal:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- if: ${{ env.MIRROR_KEY != '' }}
uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb
with:
target_repo_url: git@github.com:goauthentik/authentik-internal.git
ssh_private_key: ${{ secrets.GH_MIRROR_KEY }}
args: --tags --force
env:
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}

View File

@@ -12,14 +12,15 @@ permissions:
jobs:
stale:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: tibdex/github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/stale@v9
with:
repo-token: ${{ steps.generate_token.outputs.token }}
days-before-stale: 60

View File

@@ -20,14 +20,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Find Comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: authentik translations instructions
- name: Create or update comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}

View File

@@ -17,19 +17,20 @@ env:
jobs:
compile:
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
runs-on: ubuntu-latest
steps:
- id: generate_token
if: ${{ github.event_name != 'pull_request' }}
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
uses: tibdex/github-app-token@v2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v5
if: ${{ github.event_name != 'pull_request' }}
with:
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v5
- uses: actions/checkout@v5
if: ${{ github.event_name == 'pull_request' }}
- name: Setup authentik env
uses: ./.github/actions/setup
@@ -44,7 +45,7 @@ jobs:
make web-check-compile
- name: Create Pull Request
if: ${{ github.event_name != 'pull_request' }}
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7
uses: peter-evans/create-pull-request@v7
with:
token: ${{ steps.generate_token.outputs.token }}
branch: extract-compile-backend-translation

View File

@@ -0,0 +1,41 @@
---
# Rename transifex pull requests to have a correct naming
# Also enables auto squash-merge
name: Translation - Auto-rename Transifex PRs
on:
pull_request:
types: [opened, reopened]
permissions:
# Permission to rename PR
pull-requests: write
jobs:
rename_pr:
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
steps:
- uses: actions/checkout@v5
- id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Get current title
id: title
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
title=$(gh pr view ${{ github.event.pull_request.number }} --json "title" -q ".title")
echo "title=${title}" >> "$GITHUB_OUTPUT"
- name: Rename
env:
GH_TOKEN: ${{ steps.generate_token.outputs.token }}
run: |
gh pr edit ${{ github.event.pull_request.number }} -t "translate: ${{ steps.title.outputs.title }}" --add-label dependencies
- uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ steps.generate_token.outputs.token }}
pull-request-number: ${{ github.event.pull_request.number }}
merge-method: squash

2
.gitignore vendored
View File

@@ -72,7 +72,7 @@ unittest.xml
# Translations
# Have to include binary mo files as they are annoying to compile at build time
# since a full postgres instance is required
# since a full postgres and redis instance are required
# *.mo
# Django stuff:

View File

@@ -26,10 +26,6 @@ website/api/reference
node_modules
coverage
## Vendored files
vendored
*.min.js
## Configs
*.log
*.yaml

15
.vscode/settings.json vendored
View File

@@ -1,16 +1,4 @@
{
"[css]": {
"editor.minimap.markSectionHeaderRegex": "#\\bregion\\s*(?<separator>-?)\\s*(?<label>.*)\\*/$"
},
"[makefile]": {
"editor.minimap.markSectionHeaderRegex": "^#{25}\n##\\s\\s*(?<separator>-?)\\s*(?<label>[^\n]*)\n#{25}$"
},
"[dockerfile]": {
"editor.minimap.markSectionHeaderRegex": "\\bStage\\s*\\d:(?<separator>-?)\\s*(?<label>.*)$"
},
"[jsonc]": {
"editor.minimap.markSectionHeaderRegex": "#\\bregion\\s*(?<separator>-?)\\s*(?<label>.*)$"
},
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,
"yaml.customTags": [
@@ -49,9 +37,6 @@
"go.testFlags": [
"-count=1"
],
"go.testEnvVars": {
"WORKSPACE_DIR": "${workspaceFolder}"
},
"github-actions.workflows.pinned.workflows": [
".github/workflows/ci-main.yml"
]

View File

@@ -24,8 +24,6 @@ Makefile @goauthentik/infrastructure
.editorconfig @goauthentik/infrastructure
CODEOWNERS @goauthentik/infrastructure
# Backend packages
packages/django-channels-postgres @goauthentik/backend
packages/django-postgres-cache @goauthentik/backend
packages/django-dramatiq-postgres @goauthentik/backend
# Web packages
packages/docusaurus-config @goauthentik/frontend
@@ -35,12 +33,17 @@ packages/prettier-config @goauthentik/frontend
packages/tsconfig @goauthentik/frontend
# Web
web/ @goauthentik/frontend
tests/wdio/ @goauthentik/frontend
# Locale
locale/ @goauthentik/backend @goauthentik/frontend
web/xliff/ @goauthentik/backend @goauthentik/frontend
# Docs
# Docs & Website
docs/ @goauthentik/docs
# TODO Remove after moving website to docs
website/ @goauthentik/docs
CODE_OF_CONDUCT.md @goauthentik/docs
# Security
SECURITY.md @goauthentik/security @goauthentik/docs
# TODO Remove after moving website to docs
website/security/ @goauthentik/security @goauthentik/docs
docs/security/ @goauthentik/security @goauthentik/docs

View File

@@ -1,4 +0,0 @@
# Contributing to authentik
Thanks for your interest in contributing! Please see our [contributing guide](https://docs.goauthentik.io/docs/developer-docs/?utm_source=github) for more information.

1
CONTRIBUTING.md Symbolic link
View File

@@ -0,0 +1 @@
website/docs/developer-docs/index.md

View File

@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24-trixie-slim@sha256:45babd1b4ce0349fb12c4e24bf017b90b96d52806db32e001e3013f341bef0fe AS node-builder
FROM --platform=${BUILDPLATFORM} docker.io/library/node:24-slim AS node-builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
@@ -26,7 +26,7 @@ RUN npm run build && \
npm run build:sfe
# Stage 2: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:a02d35efc036053fdf0da8c15919276bf777a80cbfda6a35c5e9f087e652adfc AS go-builder
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25-bookworm AS go-builder
ARG TARGETOS
ARG TARGETARCH
@@ -63,7 +63,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
go build -o /go/authentik ./cmd/server
# Stage 3: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.1@sha256:faecdca22579730ab0b7dea5aa9af350bb3c93cb9d39845c173639ead30346d2 AS geoip
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.1 AS geoip
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
ENV GEOIPUPDATE_VERBOSE="1"
@@ -76,9 +76,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
# Stage 4: Download uv
FROM ghcr.io/astral-sh/uv:0.9.14@sha256:fef8e5fb8809f4b57069e919ffcd1529c92b432a2c8d8ad1768087b0b018d840 AS uv
FROM ghcr.io/astral-sh/uv:0.8.10 AS uv
# Stage 5: Base python image
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff AS python-base
FROM ghcr.io/goauthentik/fips-python:3.13.6-slim-bookworm-fips AS python-base
ENV VENV_PATH="/ak-root/.venv" \
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
@@ -119,11 +119,7 @@ RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/v
libltdl-dev && \
curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec" \
# https://github.com/rust-lang/rustup/issues/2949
# Fixes issues where the rust version in the build cache is older than latest
# and rustup tries to update it, which fails
RUSTUP_PERMIT_COPY_RENAME="true"
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=uv.lock,src=uv.lock \
@@ -139,7 +135,6 @@ ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
LABEL org.opencontainers.image.authors="Authentik Security Inc." \
org.opencontainers.image.source="https://github.com/goauthentik/authentik" \
org.opencontainers.image.description="goauthentik.io Main server image, see https://goauthentik.io for more info." \
org.opencontainers.image.documentation="https://docs.goauthentik.io" \
org.opencontainers.image.licenses="https://github.com/goauthentik/authentik/blob/main/LICENSE" \

129
Makefile
View File

@@ -16,24 +16,9 @@ GEN_API_GO = gen-go-api
pg_user := $(shell uv run 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_name := $(shell uv run python -m authentik.lib.config postgresql.name 2>/dev/null)
redis_db := $(shell uv run python -m authentik.lib.config redis.db 2>/dev/null)
# For macOS users, add the libxml2 installed from brew libxmlsec1 to the build path
# to prevent SAML-related tests from failing and ensure correct pip dependency compilation
# These functions are only evaluated when called in specific targets
LIBXML2_EXISTS = $(shell brew list libxml2 2> /dev/null)
KRB5_EXISTS = $(shell brew list krb5 2> /dev/null)
LIBXML2_LDFLAGS = -L$(shell brew --prefix libxml2)/lib $(LDFLAGS)
LIBXML2_CPPFLAGS = -I$(shell brew --prefix libxml2)/include $(CPPFLAGS)
LIBXML2_PKG_CONFIG = $(shell brew --prefix libxml2)/lib/pkgconfig:$(PKG_CONFIG_PATH)
KRB_PATH =
ifneq ($(KRB5_EXISTS),)
KRB_PATH = PATH="$(shell brew --prefix krb5)/sbin:$(shell brew --prefix krb5)/bin:$$PATH"
endif
all: lint-fix lint gen web test ## Lint, build, and test everything
all: lint-fix lint test gen web ## Lint, build, and test everything
HELP_WIDTH := $(shell grep -h '^[a-z][^ ]*:.*\#\#' $(MAKEFILE_LIST) 2>/dev/null | \
cut -d':' -f1 | awk '{printf "%d\n", length}' | sort -rn | head -1)
@@ -49,7 +34,7 @@ go-test:
go test -timeout 0 -v -race -cover ./...
test: ## Run the server tests and produce a coverage report (locally)
$(KRB_PATH) uv run coverage run manage.py test --keepdb $(or $(filter-out $@,$(MAKECMDGOALS)),authentik)
uv run coverage run manage.py test --keepdb authentik
uv run coverage html
uv run coverage report
@@ -65,14 +50,7 @@ lint: ## Lint the python and golang sources
golangci-lint run -v
core-install:
ifneq ($(LIBXML2_EXISTS),)
# Clear cache to ensure fresh compilation
uv cache clean
# Force compilation from source for lxml and xmlsec with correct environment
LDFLAGS="$(LIBXML2_LDFLAGS)" CPPFLAGS="$(LIBXML2_CPPFLAGS)" PKG_CONFIG_PATH="$(LIBXML2_PKG_CONFIG)" uv sync --frozen --reinstall-package lxml --reinstall-package xmlsec --no-binary-package lxml --no-binary-package xmlsec
else
uv sync --frozen
endif
migrate: ## Run the Authentik Django server's migrations
uv run python -m lifecycle.migrate
@@ -105,6 +83,7 @@ dev-drop-db:
dropdb -U ${pg_user} -h ${pg_host} ${pg_name} || true
# Also remove the test-db if it exists
dropdb -U ${pg_user} -h ${pg_host} test_${pg_name} || true
redis-cli -n ${redis_db} flushall
dev-create-db:
createdb -U ${pg_user} -h ${pg_host} ${pg_name}
@@ -148,13 +127,14 @@ gen-changelog: ## (Release) generate the changelog based from the commits since
npx prettier --write changelog.md
gen-diff: ## (Release) generate the changelog diff between the current schema and the last tag
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > schema-old.yml
docker compose -f scripts/api/docker-compose.yml run --rm --user "${UID}:${GID}" diff \
--markdown \
/local/diff.md \
/local/schema-old.yml \
/local/schema.yml
rm schema-old.yml
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > old_schema.yml
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-diff:2.1.0-beta.8 \
--markdown /local/diff.md \
/local/old_schema.yml /local/schema.yml
rm old_schema.yml
sed -i 's/{/&#123;/g' diff.md
sed -i 's/}/&#125;/g' diff.md
npx prettier --write diff.md
@@ -163,45 +143,53 @@ gen-clean-ts: ## Remove generated API client for TypeScript
rm -rf ${PWD}/${GEN_API_TS}/
rm -rf ${PWD}/web/node_modules/@goauthentik/api/
gen-clean-py: ## Remove generated API client for Python
rm -rf ${PWD}/${GEN_API_PY}
gen-clean-go: ## Remove generated API client for Go
mkdir -p ${PWD}/${GEN_API_GO}
ifneq ($(wildcard ${PWD}/${GEN_API_GO}/.*),)
make -C ${PWD}/${GEN_API_GO} clean
else
rm -rf ${PWD}/${GEN_API_GO}
endif
gen-clean-py: ## Remove generated API client for Python
rm -rf ${PWD}/${GEN_API_PY}/
gen-clean: gen-clean-ts gen-clean-go gen-clean-py ## Remove generated API clients
gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescript into the authentik UI Application
docker compose -f scripts/api/docker-compose.yml run --rm --user "${UID}:${GID}" gen \
generate \
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \
-g typescript-fetch \
-o /local/${GEN_API_TS} \
-c /local/scripts/api/ts-config.yaml \
-c /local/scripts/api-ts-config.yaml \
--additional-properties=npmVersion=${NPM_VERSION} \
--git-repo-id authentik \
--git-user-id goauthentik
cd ${PWD}/${GEN_API_TS} && npm i
cd ${PWD}/${GEN_API_TS} && npm link
cd ${PWD}/web && npm link @goauthentik/api
gen-client-py: gen-clean-py ## Build and install the authentik API for Python
mkdir -p ${PWD}/${GEN_API_PY}
ifeq ($(wildcard ${PWD}/${GEN_API_PY}/.*),)
git clone --depth 1 https://github.com/goauthentik/client-python.git ${PWD}/${GEN_API_PY}
else
cd ${PWD}/${GEN_API_PY} && git pull
endif
cp ${PWD}/schema.yml ${PWD}/${GEN_API_PY}
make -C ${PWD}/${GEN_API_PY} build version=${NPM_VERSION}
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
docker.io/openapitools/openapi-generator-cli:v7.11.0 generate \
-i /local/schema.yml \
-g python \
-o /local/${GEN_API_PY} \
-c /local/scripts/api-py-config.yaml \
--additional-properties=packageVersion=${NPM_VERSION} \
--git-repo-id authentik \
--git-user-id goauthentik
gen-client-go: ## Build and install the authentik API for Golang
gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
mkdir -p ${PWD}/${GEN_API_GO}
ifeq ($(wildcard ${PWD}/${GEN_API_GO}/.*),)
git clone --depth 1 https://github.com/goauthentik/client-go.git ${PWD}/${GEN_API_GO}
else
cd ${PWD}/${GEN_API_GO} && git reset --hard
cd ${PWD}/${GEN_API_GO} && git pull
endif
cp ${PWD}/schema.yml ${PWD}/${GEN_API_GO}
@@ -226,30 +214,34 @@ node-install: ## Install the necessary libraries to build Node.js packages
#########################
web-build: node-install ## Build the Authentik UI
npm run --prefix web build
cd web && npm run build
web: web-lint-fix web-lint web-check-compile ## Automatically fix formatting issues in the Authentik UI source code, lint the code, and compile it
web-test: ## Run tests for the Authentik UI
npm run --prefix web test
cd web && npm run test
web-watch: ## Build and watch the Authentik UI for changes, updating automatically
npm run --prefix web watch
rm -rf web/dist/
mkdir web/dist/
touch web/dist/.gitkeep
cd web && npm run watch
web-storybook-watch: ## Build and run the storybook documentation server
npm run --prefix web storybook
cd web && npm run storybook
web-lint-fix:
npm run --prefix web prettier
cd web && npm run prettier
web-lint:
npm run --prefix web lint
npm run --prefix web lit-analyse
cd web && npm run lint
cd web && npm run lit-analyse
web-check-compile:
npm run --prefix web tsc
cd web && npm run tsc
web-i18n-extract:
npm run --prefix web extract-locales
cd web && npm run extract-locales
#########################
## Docs
@@ -261,31 +253,31 @@ docs-install:
npm ci --prefix website
docs-lint-fix: lint-codespell
npm run --prefix website prettier
npm run prettier --prefix website
docs-build:
npm run --prefix website build
npm run build --prefix website
docs-watch: ## Build and watch the topics documentation
npm run --prefix website start
npm run start --prefix website
integrations: docs-lint-fix integrations-build ## Fix formatting issues in the integrations source code, lint the code, and compile it
integrations-build:
npm run --prefix website -w integrations build
npm run build --prefix website -w integrations
integrations-watch: ## Build and watch the Integrations documentation
npm run --prefix website -w integrations start
npm run start --prefix website -w integrations
docs-api-build:
npm run --prefix website -w api build
npm run build --prefix website -w api
docs-api-watch: ## Build and watch the API documentation
npm run --prefix website -w api build:api
npm run --prefix website -w api start
npm run build:api --prefix website -w api
npm run start --prefix website -w api
docs-api-clean: ## Clean generated API documentation
npm run --prefix website -w api build:api:clean
npm run build:api:clean --prefix website -w api
#########################
## Docker
@@ -308,9 +300,6 @@ ci--meta-debug:
python -V
node --version
ci-mypy: ci--meta-debug
uv run mypy --strict $(PY_SOURCES)
ci-black: ci--meta-debug
uv run black --check $(PY_SOURCES)

View File

@@ -9,21 +9,21 @@
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/goauthentik/authentik/ci-outpost.yml?branch=main&label=outpost%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/goauthentik/authentik/ci-web.yml?branch=main&label=web%20build&style=for-the-badge)](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
[![Code Coverage](https://img.shields.io/codecov/c/gh/goauthentik/authentik?style=for-the-badge)](https://codecov.io/gh/goauthentik/authentik)
![Docker pulls](https://img.shields.io/docker/pulls/authentik/server.svg?style=for-the-badge)
![Latest version](https://img.shields.io/docker/v/authentik/server?sort=semver&style=for-the-badge)
[![](https://img.shields.io/badge/Help%20translate-transifex-blue?style=for-the-badge)](https://explore.transifex.com/authentik/authentik/)
[![](https://img.shields.io/badge/Help%20translate-transifex-blue?style=for-the-badge)](https://www.transifex.com/authentik/authentik/)
## What is authentik?
authentik is an open-source Identity Provider (IdP) for modern SSO. It supports SAML, OAuth2/OIDC, LDAP, RADIUS, and more, designed for self-hosting from small labs to large production clusters.
authentik is an open-source Identity Provider that emphasizes flexibility and versatility, with support for a wide set of protocols.
Our [enterprise offering](https://goauthentik.io/pricing) is available for organizations to securely replace existing IdPs such as Okta, Auth0, Entra ID, and Ping Identity for robust, large-scale identity management.
Our [enterprise offer](https://goauthentik.io/pricing) can also be used as a self-hosted replacement for large-scale deployments of Okta/Auth0, Entra ID, Ping Identity, or other legacy IdPs for employees and B2B2C use.
## Installation
- Docker Compose: recommended for small/test setups. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/docker-compose/).
- Kubernetes (Helm Chart): recommended for larger setups. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/kubernetes/) and the Helm chart [repository](https://github.com/goauthentik/helm).
- AWS CloudFormation: deploy on AWS using our official templates. See the [documentation](https://docs.goauthentik.io/docs/install-config/install/aws/).
- DigitalOcean Marketplace: one-click deployment via the official Marketplace app. See the [app listing](https://marketplace.digitalocean.com/apps/authentik).
For small/test setups it is recommended to use Docker Compose; refer to the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github).
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github).
## Screenshots
@@ -32,20 +32,14 @@ Our [enterprise offering](https://goauthentik.io/pricing) is available for organ
| ![](https://docs.goauthentik.io/img/screen_apps_light.jpg) | ![](https://docs.goauthentik.io/img/screen_apps_dark.jpg) |
| ![](https://docs.goauthentik.io/img/screen_admin_light.jpg) | ![](https://docs.goauthentik.io/img/screen_admin_dark.jpg) |
## Development and contributions
## Development
See the [Developer Documentation](https://docs.goauthentik.io/docs/developer-docs/) for information about setting up local build environments, testing your contributions, and our contribution process.
See [Developer Documentation](https://docs.goauthentik.io/docs/developer-docs/?utm_source=github)
## Security
Please see [SECURITY.md](SECURITY.md).
See [SECURITY.md](SECURITY.md)
## Adoption
## Adoption and Contributions
Using authentik? We'd love to hear your story and feature your logo. Email us at [hello@goauthentik.io](mailto:hello@goauthentik.io) or open a GitHub Issue/PR!
## License
[![MIT License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)](LICENSE)
[![CC BY-SA 4.0](https://img.shields.io/badge/License-CC%20BY--SA%204.0-lightgrey?style=for-the-badge)](website/LICENSE)
[![authentik EE License](https://img.shields.io/badge/License-EE-orange?style=for-the-badge)](authentik/enterprise/LICENSE)
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [contribution guide](https://docs.goauthentik.io/docs/developer-docs?utm_source=github).

View File

@@ -18,35 +18,14 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
(.x being the latest patch release for each version)
| Version | Supported |
| ---------- | ---------- |
| 2025.8.x | ✅ |
| 2025.10.x | ✅ |
| Version | Supported |
| --------- | --------- |
| 2025.4.x | ✅ |
| 2025.6.x | ✅ |
## Reporting a Vulnerability
If you discover a potential vulnerability, please report it responsibly through one of the following channels:
- **Email**: [security@goauthentik.io](mailto:security@goauthentik.io)
- **GitHub**: Submit a private security advisory via our [repositorys advisory portal](https://github.com/goauthentik/authentik/security/advisories/new)
When submitting a report, please include as much detail as possible, such as:
- **Affected version(s)**: The version of authentik where the issue was identified.
- **Steps to reproduce**: A clear description or proof of concept to help us verify the issue.
- **Impact assessment**: How the vulnerability could be exploited and its potential effect.
- **Additional information**: Logs, configuration details (if relevant), or any suggested mitigations.
We kindly ask that you do not disclose the vulnerability publicly until we have confirmed and addressed the issue.
Our team will:
- Acknowledge receipt of your report as quickly as possible.
- Keep you updated on the investigation and resolution progress.
## Researcher Recognition
We value contributions from the security community. For each valid report, we will publish a dedicated entry on our Security Advisory page that optionally includes the reporters name (or preferred alias). Please note that while we do not currently offer monetary bounties, we are committed to giving researchers appropriate credit for their efforts in keeping authentik secure.
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io). Be sure to include relevant information like which version you've found the issue in, instructions on how to reproduce the issue, and anything else that might make it easier for us to find the issue.
## Severity levels

View File

@@ -3,7 +3,7 @@
from functools import lru_cache
from os import environ
VERSION = "2025.12.0-rc1"
VERSION = "2025.10.0-rc1"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@@ -1,9 +0,0 @@
from django.dispatch import receiver
from authentik.admin.tasks import _set_prom_info
from authentik.root.signals import post_startup
@receiver(post_startup)
def post_startup_admin_metrics(sender, **_):
_set_prom_info()

View File

@@ -2,6 +2,7 @@
from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTask
from dramatiq import actor
from packaging.version import parse
from requests import RequestException
@@ -12,7 +13,7 @@ from authentik.admin.apps import PROM_INFO
from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG
from authentik.lib.utils.http import get_http_session
from authentik.tasks.middleware import CurrentTask
from authentik.tasks.models import Task
LOGGER = get_logger()
VERSION_NULL = "0.0.0"
@@ -34,7 +35,7 @@ def _set_prom_info():
@actor(description=_("Update latest version info."))
def update_latest_version():
self = CurrentTask.get_task()
self: Task = CurrentTask.get_task()
if CONFIG.get_bool("disable_update_check"):
cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT)
self.info("Version check disabled.")
@@ -71,3 +72,6 @@ def update_latest_version():
except (RequestException, IndexError) as exc:
cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT)
raise exc
_set_prom_info()

View File

@@ -27,16 +27,16 @@ except OSError:
ipc_key = None
def validate_auth(header: bytes, format="bearer") -> str | None:
def validate_auth(header: bytes) -> str | None:
"""Validate that the header is in a correct format,
returns type and credentials"""
auth_credentials = header.decode().strip()
if auth_credentials == "" or " " not in auth_credentials:
return None
auth_type, _, auth_credentials = auth_credentials.partition(" ")
if not compare_digest(auth_type.lower(), format):
if auth_type.lower() != "bearer":
LOGGER.debug("Unsupported authentication type, denying", type=auth_type.lower())
return None
raise AuthenticationFailed("Unsupported authentication type")
if auth_credentials == "": # nosec # noqa
raise AuthenticationFailed("Malformed header")
return auth_credentials

View File

@@ -1,10 +1,44 @@
"""Pagination which includes total pages and current page"""
from drf_spectacular.plumbing import build_object_type
from rest_framework import pagination
from rest_framework.response import Response
from authentik.api.v3.schema.response import PAGINATION
PAGINATION_COMPONENT_NAME = "Pagination"
PAGINATION_SCHEMA = {
"type": "object",
"properties": {
"next": {
"type": "number",
},
"previous": {
"type": "number",
},
"count": {
"type": "number",
},
"current": {
"type": "number",
},
"total_pages": {
"type": "number",
},
"start_index": {
"type": "number",
},
"end_index": {
"type": "number",
},
},
"required": [
"next",
"previous",
"count",
"current",
"total_pages",
"start_index",
"end_index",
],
}
class Pagination(pagination.PageNumberPagination):
@@ -36,13 +70,14 @@ class Pagination(pagination.PageNumberPagination):
)
def get_paginated_response_schema(self, schema):
return build_object_type(
properties={
"pagination": PAGINATION.ref,
return {
"type": "object",
"properties": {
"pagination": {"$ref": f"#/components/schemas/{PAGINATION_COMPONENT_NAME}"},
"results": schema,
},
required=["pagination", "results"],
)
"required": ["pagination", "results"],
}
class SmallerPagination(Pagination):

View File

@@ -1,60 +1,96 @@
"""Error Response schema, from https://github.com/axnsan12/drf-yasg/issues/224"""
from collections.abc import Callable
from typing import Any
from django.utils.translation import gettext_lazy as _
from drf_spectacular.generators import SchemaGenerator
from drf_spectacular.plumbing import ResolvedComponent
from drf_spectacular.renderers import OpenApiJsonRenderer
from drf_spectacular.plumbing import (
ResolvedComponent,
build_array_type,
build_basic_type,
build_object_type,
)
from drf_spectacular.settings import spectacular_settings
from structlog.stdlib import get_logger
from drf_spectacular.types import OpenApiTypes
from rest_framework.settings import api_settings
from authentik.api.apps import AuthentikAPIConfig
from authentik.api.v3.schema.query import QUERY_PARAMS
from authentik.api.v3.schema.response import (
GENERIC_ERROR,
GENERIC_ERROR_RESPONSE,
PAGINATION,
VALIDATION_ERROR,
VALIDATION_ERROR_RESPONSE,
from authentik.api.pagination import PAGINATION_COMPONENT_NAME, PAGINATION_SCHEMA
def build_standard_type(obj, **kwargs):
"""Build a basic type with optional add owns."""
schema = build_basic_type(obj)
schema.update(kwargs)
return schema
GENERIC_ERROR = build_object_type(
description=_("Generic API Error"),
properties={
"detail": build_standard_type(OpenApiTypes.STR),
"code": build_standard_type(OpenApiTypes.STR),
},
required=["detail"],
)
VALIDATION_ERROR = build_object_type(
description=_("Validation Error"),
properties={
api_settings.NON_FIELD_ERRORS_KEY: build_array_type(build_standard_type(OpenApiTypes.STR)),
"code": build_standard_type(OpenApiTypes.STR),
},
required=[],
additionalProperties={},
)
LOGGER = get_logger()
def create_component(generator: SchemaGenerator, name, schema, type_=ResolvedComponent.SCHEMA):
"""Register a component and return a reference to it."""
component = ResolvedComponent(
name=name,
type=type_,
schema=schema,
object=name,
)
generator.registry.register_on_missing(component)
return component
def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Callable]], **kwargs):
"""Filter out all API Views which are not mounted under /api"""
return [
(path, path_regex, method, callback)
for path, path_regex, method, callback in endpoints
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
]
def postprocess_schema_responses(result, generator: SchemaGenerator, **kwargs):
"""Workaround to set a default response for endpoints.
Workaround suggested at
<https://github.com/tfranzel/drf-spectacular/issues/119#issuecomment-656970357>
for the missing drf-spectacular feature discussed in
<https://github.com/tfranzel/drf-spectacular/issues/101>.
"""
create_component(generator, PAGINATION_COMPONENT_NAME, PAGINATION_SCHEMA)
def postprocess_schema_register(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Register custom schema components"""
LOGGER.debug("Registering custom schemas")
generator.registry.register_on_missing(PAGINATION)
generator.registry.register_on_missing(GENERIC_ERROR)
generator.registry.register_on_missing(GENERIC_ERROR_RESPONSE)
generator.registry.register_on_missing(VALIDATION_ERROR)
generator.registry.register_on_missing(VALIDATION_ERROR_RESPONSE)
for query in QUERY_PARAMS.values():
generator.registry.register_on_missing(query)
return result
generic_error = create_component(generator, "GenericError", GENERIC_ERROR)
validation_error = create_component(generator, "ValidationError", VALIDATION_ERROR)
def postprocess_schema_responses(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Default error responses"""
LOGGER.debug("Adding default error responses")
for path in result["paths"].values():
for method in path.values():
method["responses"].setdefault("400", VALIDATION_ERROR_RESPONSE.ref)
method["responses"].setdefault("403", GENERIC_ERROR_RESPONSE.ref)
method["responses"].setdefault(
"400",
{
"content": {
"application/json": {
"schema": validation_error.ref,
}
},
"description": "",
},
)
method["responses"].setdefault(
"403",
{
"content": {
"application/json": {
"schema": generic_error.ref,
}
},
"description": "",
},
)
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
@@ -68,36 +104,10 @@ def postprocess_schema_responses(
return result
def postprocess_schema_query_params(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Optimise pagination parameters, instead of redeclaring parameters for each endpoint
declare them globally and refer to them"""
LOGGER.debug("Deduplicating query parameters")
for path in result["paths"].values():
for method in path.values():
for idx, param in enumerate(method.get("parameters", [])):
if param["name"] not in QUERY_PARAMS:
continue
method["parameters"][idx] = QUERY_PARAMS[param["name"]].ref
return result
def postprocess_schema_remove_unused(
result: dict[str, Any], generator: SchemaGenerator, **kwargs
) -> dict[str, Any]:
"""Remove unused components"""
# To check if the schema is used, render it to JSON and then substring check that
# less efficient than walking through the tree but a lot simpler and no
# possibility that we miss something
raw = OpenApiJsonRenderer().render(result, renderer_context={}).decode()
count = 0
for key in result["components"][ResolvedComponent.SCHEMA].keys():
schema_usages = raw.count(f"#/components/{ResolvedComponent.SCHEMA}/{key}")
if schema_usages >= 1:
continue
del generator.registry[(key, ResolvedComponent.SCHEMA)]
count += 1
LOGGER.debug("Removing unused components", count=count)
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
return result
def preprocess_schema_exclude_non_api(endpoints, **kwargs):
"""Filter out all API Views which are not mounted under /api"""
return [
(path, path_regex, method, callback)
for path, path_regex, method, callback in endpoints
if path.startswith("/" + AuthentikAPIConfig.mountpoint)
]

View File

@@ -24,7 +24,8 @@ class TestAPIAuth(TestCase):
def test_invalid_type(self):
"""Test invalid type"""
self.assertIsNone(bearer_auth(b"foo bar"))
with self.assertRaises(AuthenticationFailed):
bearer_auth(b"foo bar")
def test_invalid_empty(self):
"""Test invalid type"""
@@ -33,8 +34,9 @@ class TestAPIAuth(TestCase):
def test_invalid_no_token(self):
"""Test invalid with no token"""
auth = b64encode(b":abc").decode()
self.assertIsNone(bearer_auth(f"Basic :{auth}".encode()))
with self.assertRaises(AuthenticationFailed):
auth = b64encode(b":abc").decode()
self.assertIsNone(bearer_auth(f"Basic :{auth}".encode()))
def test_bearer_valid(self):
"""Test valid token"""

View File

@@ -5,7 +5,6 @@ from pathlib import Path
from django.conf import settings
from django.db import models
from django.dispatch import Signal
from django.http import HttpRequest
from drf_spectacular.utils import extend_schema
from rest_framework.fields import (
BooleanField,
@@ -57,6 +56,7 @@ class ConfigSerializer(PassiveSerializer):
cache_timeout = IntegerField(required=True)
cache_timeout_flows = IntegerField(required=True)
cache_timeout_policies = IntegerField(required=True)
cache_timeout_reputation = IntegerField(required=True)
class ConfigView(APIView):
@@ -64,8 +64,7 @@ class ConfigView(APIView):
permission_classes = [AllowAny]
@staticmethod
def get_capabilities(request: HttpRequest) -> list[Capabilities]:
def get_capabilities(self) -> list[Capabilities]:
"""Get all capabilities this server instance supports"""
caps = []
deb_test = settings.DEBUG or settings.TEST
@@ -78,19 +77,18 @@ class ConfigView(APIView):
for processor in get_context_processors():
if cap := processor.capability():
caps.append(cap)
if request.tenant.impersonation:
if self.request.tenant.impersonation:
caps.append(Capabilities.CAN_IMPERSONATE)
if settings.DEBUG: # pragma: no cover
caps.append(Capabilities.CAN_DEBUG)
if "authentik.enterprise" in settings.INSTALLED_APPS:
caps.append(Capabilities.IS_ENTERPRISE)
for _, result in capabilities.send(sender=ConfigView):
for _, result in capabilities.send(sender=self):
if result:
caps.append(result)
return caps
@staticmethod
def get_config(request: HttpRequest) -> ConfigSerializer:
def get_config(self) -> ConfigSerializer:
"""Get Config"""
return ConfigSerializer(
{
@@ -101,14 +99,15 @@ class ConfigView(APIView):
"send_pii": CONFIG.get("error_reporting.send_pii"),
"traces_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.4)),
},
"capabilities": ConfigView.get_capabilities(request),
"capabilities": self.get_capabilities(),
"cache_timeout": CONFIG.get_int("cache.timeout"),
"cache_timeout_flows": CONFIG.get_int("cache.timeout_flows"),
"cache_timeout_policies": CONFIG.get_int("cache.timeout_policies"),
"cache_timeout_reputation": CONFIG.get_int("cache.timeout_reputation"),
}
)
@extend_schema(responses={200: ConfigSerializer(many=False)})
def get(self, request: Request) -> Response:
"""Retrieve public configuration options"""
return Response(ConfigView.get_config(request).data)
return Response(self.get_config().data)

View File

@@ -1,65 +0,0 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.plumbing import (
ResolvedComponent,
build_basic_type,
build_parameter_type,
)
from drf_spectacular.types import OpenApiTypes
QUERY_PARAMS = {
"ordering": ResolvedComponent(
name="QueryPaginationOrdering",
type=ResolvedComponent.PARAMETER,
object="QueryPaginationOrdering",
schema=build_parameter_type(
name="ordering",
schema=build_basic_type(OpenApiTypes.STR),
location="query",
description=_("Which field to use when ordering the results."),
),
),
"page": ResolvedComponent(
name="QueryPaginationPage",
type=ResolvedComponent.PARAMETER,
object="QueryPaginationPage",
schema=build_parameter_type(
name="page",
schema=build_basic_type(OpenApiTypes.INT),
location="query",
description=_("A page number within the paginated result set."),
),
),
"page_size": ResolvedComponent(
name="QueryPaginationPageSize",
type=ResolvedComponent.PARAMETER,
object="QueryPaginationPageSize",
schema=build_parameter_type(
name="page_size",
schema=build_basic_type(OpenApiTypes.INT),
location="query",
description=_("Number of results to return per page."),
),
),
"search": ResolvedComponent(
name="QuerySearch",
type=ResolvedComponent.PARAMETER,
object="QuerySearch",
schema=build_parameter_type(
name="search",
schema=build_basic_type(OpenApiTypes.STR),
location="query",
description=_("A search term."),
),
),
# Not related to pagination but a very common query param
"name": ResolvedComponent(
name="QueryName",
type=ResolvedComponent.PARAMETER,
object="QueryName",
schema=build_parameter_type(
name="name",
schema=build_basic_type(OpenApiTypes.STR),
location="query",
),
),
}

View File

@@ -1,84 +0,0 @@
from django.utils.translation import gettext_lazy as _
from drf_spectacular.plumbing import (
ResolvedComponent,
build_array_type,
build_basic_type,
build_object_type,
)
from drf_spectacular.types import OpenApiTypes
from rest_framework.settings import api_settings
GENERIC_ERROR = ResolvedComponent(
name="GenericError",
type=ResolvedComponent.SCHEMA,
object="GenericError",
schema=build_object_type(
description=_("Generic API Error"),
properties={
"detail": build_basic_type(OpenApiTypes.STR),
"code": build_basic_type(OpenApiTypes.STR),
},
required=["detail"],
),
)
GENERIC_ERROR_RESPONSE = ResolvedComponent(
name="GenericErrorResponse",
type=ResolvedComponent.RESPONSE,
object="GenericErrorResponse",
schema={
"content": {"application/json": {"schema": GENERIC_ERROR.ref}},
"description": "",
},
)
VALIDATION_ERROR = ResolvedComponent(
"ValidationError",
object="ValidationError",
type=ResolvedComponent.SCHEMA,
schema=build_object_type(
description=_("Validation Error"),
properties={
api_settings.NON_FIELD_ERRORS_KEY: build_array_type(build_basic_type(OpenApiTypes.STR)),
"code": build_basic_type(OpenApiTypes.STR),
},
required=[],
additionalProperties={},
),
)
VALIDATION_ERROR_RESPONSE = ResolvedComponent(
name="ValidationErrorResponse",
type=ResolvedComponent.RESPONSE,
object="ValidationErrorResponse",
schema={
"content": {
"application/json": {
"schema": VALIDATION_ERROR.ref,
}
},
"description": "",
},
)
PAGINATION = ResolvedComponent(
name="Pagination",
type=ResolvedComponent.SCHEMA,
object="Pagination",
schema=build_object_type(
properties={
"next": build_basic_type(OpenApiTypes.NUMBER),
"previous": build_basic_type(OpenApiTypes.NUMBER),
"count": build_basic_type(OpenApiTypes.NUMBER),
"current": build_basic_type(OpenApiTypes.NUMBER),
"total_pages": build_basic_type(OpenApiTypes.NUMBER),
"start_index": build_basic_type(OpenApiTypes.NUMBER),
"end_index": build_basic_type(OpenApiTypes.NUMBER),
},
required=[
"next",
"previous",
"count",
"current",
"total_pages",
"start_index",
"end_index",
],
),
)

View File

@@ -1,50 +0,0 @@
from collections.abc import Callable
from functools import wraps
from typing import Literal
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.viewsets import ViewSet
def validate(serializer_type: type[Serializer], location: Literal["body", "query"] = "body"):
"""Validate incoming data with the specified serializer. Raw data can either be taken
from request body or query string, defaulting to body.
Validated data is added to the function this decorator is used on with a named parameter
based on the location of the data.
Example:
@validate(MySerializer)
@validate(MyQuerySerializer, location="query")
def my_action(self, request, *, body: MySerializer, query: MyQuerySerializer):
...
"""
def wrapper_outer(func: Callable):
@wraps(func)
def wrapper(self: ViewSet, request: Request, *args, **kwargs) -> Response:
data = {}
if location == "body":
data = request.data
elif location == "query":
data = request.query_params
else:
raise ValueError(f"Invalid data location '{location}'")
instance = serializer_type(
data=data,
context={
"request": request,
},
)
instance.is_valid(raise_exception=True)
kwargs[location] = instance
return func(self, request, *args, **kwargs)
return wrapper
return wrapper_outer

View File

@@ -118,10 +118,7 @@ class Command(BaseCommand):
model_instance: Model = model()
if not isinstance(model_instance, SerializerModel):
continue
try:
serializer_class = model_instance.serializer
except NotImplementedError as exc:
raise NotImplementedError(model_instance) from exc
serializer_class = model_instance.serializer
serializer = serializer_class(
context={
SERIALIZER_CONTEXT_BLUEPRINT: False,

View File

@@ -15,7 +15,6 @@ from django.db.models import Model
from django.db.models.query_utils import Q
from django.db.transaction import atomic
from django.db.utils import IntegrityError
from django_channels_postgres.models import GroupChannel, Message
from guardian.models import UserObjectPermission
from guardian.shortcuts import assign_perm
from rest_framework.exceptions import ValidationError
@@ -42,15 +41,6 @@ from authentik.core.models import (
User,
UserSourceConnection,
)
from authentik.endpoints.connectors.agent.models import (
AgentDeviceConnection,
AppleNonce,
DeviceAuthenticationToken,
)
from authentik.endpoints.connectors.agent.models import (
DeviceToken as EndpointDeviceToken,
)
from authentik.endpoints.models import Connector, Device, DeviceConnection, DeviceFactSnapshot
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import LicenseUsage
from authentik.enterprise.providers.google_workspace.models import (
@@ -81,15 +71,12 @@ from authentik.providers.oauth2.models import (
DeviceToken,
RefreshToken,
)
from authentik.providers.proxy.models import ProxySession
from authentik.providers.rac.models import ConnectionToken
from authentik.providers.saml.models import SAMLSession
from authentik.providers.scim.models import SCIMProviderGroup, SCIMProviderUser
from authentik.rbac.models import Role
from authentik.sources.scim.models import SCIMSourceGroup, SCIMSourceUser
from authentik.stages.authenticator_webauthn.models import WebAuthnDeviceType
from authentik.stages.consent.models import UserConsent
from authentik.tasks.models import Task, TaskLog
from authentik.tasks.models import Task
from authentik.tenants.models import Tenant
# Context set when the serializer is created in a blueprint context
@@ -121,7 +108,6 @@ def excluded_models() -> list[type[Model]]:
OutpostServiceConnection,
Policy,
PolicyBindingModel,
Connector,
# Classes that have other dependencies
Session,
AuthenticatedSession,
@@ -133,12 +119,10 @@ def excluded_models() -> list[type[Model]]:
SCIMProviderUser,
Tenant,
Task,
TaskLog,
ConnectionToken,
AuthorizationCode,
AccessToken,
RefreshToken,
ProxySession,
Reputation,
WebAuthnDeviceType,
SCIMSourceUser,
@@ -149,19 +133,8 @@ def excluded_models() -> list[type[Model]]:
MicrosoftEntraProviderGroup,
EndpointDevice,
EndpointDeviceConnection,
EndpointDeviceToken,
Device,
DeviceConnection,
DeviceAuthenticationToken,
AppleNonce,
AgentDeviceConnection,
DeviceFactSnapshot,
DeviceToken,
StreamEvent,
UserConsent,
SAMLSession,
Message,
GroupChannel,
)
@@ -330,7 +303,6 @@ class Importer:
serializer_kwargs = {}
model_instance = existing_models.first()
override_serializer_instance = False
if (
not isinstance(model(), BaseMetaModel)
and model_instance
@@ -359,7 +331,11 @@ class Importer:
model=model,
**cleanse_dict(updated_identifiers),
)
override_serializer_instance = True
model_instance = model()
# pk needs to be set on the model instance otherwise a new one will be generated
if "pk" in updated_identifiers:
model_instance.pk = updated_identifiers["pk"]
serializer_kwargs["instance"] = model_instance
try:
full_data = self.__update_pks_for_attrs(entry.get_attrs(self._import))
except ValueError as exc:
@@ -382,12 +358,6 @@ class Importer:
entry=entry,
serializer=serializer,
) from exc
if override_serializer_instance:
model_instance = model()
# pk needs to be set on the model instance otherwise a new one will be generated
if "pk" in updated_identifiers:
model_instance.pk = updated_identifiers["pk"]
serializer.instance = model_instance
return serializer
def _apply_permissions(self, instance: Model, entry: BlueprintEntry):
@@ -466,7 +436,7 @@ class Importer:
self._apply_permissions(instance, entry)
elif state == BlueprintEntryDesiredState.ABSENT:
instance: Model | None = serializer.instance
if instance and instance.pk:
if instance.pk:
instance.delete()
self.logger.debug("Deleted model", mode=instance)
continue

View File

@@ -12,7 +12,7 @@ from django.db import DatabaseError, InternalError, ProgrammingError
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_dramatiq_postgres.middleware import CurrentTaskNotFound
from django_dramatiq_postgres.middleware import CurrentTask, CurrentTaskNotFound
from dramatiq.actor import actor
from dramatiq.middleware import Middleware
from structlog.stdlib import get_logger
@@ -38,8 +38,6 @@ from authentik.blueprints.v1.oci import OCI_PREFIX
from authentik.events.logs import capture_logs
from authentik.events.utils import sanitize_dict
from authentik.lib.config import CONFIG
from authentik.tasks.apps import PRIORITY_HIGH
from authentik.tasks.middleware import CurrentTask
from authentik.tasks.models import Task
from authentik.tasks.schedules.models import Schedule
from authentik.tenants.models import Tenant
@@ -112,7 +110,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
@actor(
description=_("Find blueprints as `blueprints_find` does, but return a safe dict."),
priority=PRIORITY_HIGH,
throws=(DatabaseError, ProgrammingError, InternalError),
)
def blueprints_find_dict():
blueprints = []
@@ -150,9 +148,12 @@ def blueprints_find() -> list[BlueprintFile]:
return blueprints
@actor(description=_("Find blueprints and check if they need to be created in the database."))
@actor(
description=_("Find blueprints and check if they need to be created in the database."),
throws=(DatabaseError, ProgrammingError, InternalError),
)
def blueprints_discovery(path: str | None = None):
self = CurrentTask.get_task()
self: Task = CurrentTask.get_task()
count = 0
for blueprint in blueprints_find():
if path and blueprint.path != path:
@@ -192,7 +193,7 @@ def check_blueprint_v1_file(blueprint: BlueprintFile):
@actor(description=_("Apply single blueprint."))
def apply_blueprint(instance_pk: UUID):
try:
self = CurrentTask.get_task()
self: Task = CurrentTask.get_task()
except CurrentTaskNotFound:
self = Task()
self.set_uid(str(instance_pk))

View File

@@ -113,7 +113,7 @@ class Brand(SerializerModel):
try:
return self.attributes.get("settings", {}).get("locale", "")
except Exception as exc: # noqa
except Exception as exc:
LOGGER.warning("Failed to get default locale", exc=exc)
return ""

View File

@@ -1,11 +1,8 @@
"""Test brands"""
from json import loads
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.brands.api import Themes
from authentik.brands.models import Brand
from authentik.core.models import Application
@@ -26,7 +23,6 @@ class TestBrands(APITestCase):
_flag = flag()
if _flag.visibility == "public":
self.default_flags[_flag.key] = _flag.get()
Brand.objects.all().delete()
def test_current_brand(self):
"""Test Current brand API"""
@@ -48,6 +44,7 @@ class TestBrands(APITestCase):
def test_brand_subdomain(self):
"""Test Current brand API"""
Brand.objects.all().delete()
Brand.objects.create(domain="bar.baz", branding_title="custom")
self.assertJSONEqual(
self.client.get(
@@ -66,88 +63,11 @@ class TestBrands(APITestCase):
},
)
def test_fallback(self):
"""Test fallback brand"""
self.assertJSONEqual(
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
{
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": "fallback",
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
"default_locale": "",
"flags": self.default_flags,
},
)
@apply_blueprint("default/default-brand.yaml")
def test_blueprint(self):
"""Test Current brand API"""
response = loads(self.client.get(reverse("authentik_api:brand-current")).content.decode())
response.pop("flow_authentication", None)
response.pop("flow_invalidation", None)
response.pop("flow_user_settings", None)
self.assertEqual(
response,
{
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": "authentik-default",
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
"default_locale": "",
"flags": self.default_flags,
},
)
@apply_blueprint("default/default-brand.yaml")
def test_blueprint_with_other_brand(self):
"""Test Current brand API"""
Brand.objects.create(domain="bar.baz", branding_title="custom")
response = loads(self.client.get(reverse("authentik_api:brand-current")).content.decode())
response.pop("flow_authentication", None)
response.pop("flow_invalidation", None)
response.pop("flow_user_settings", None)
self.assertEqual(
response,
{
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": "authentik-default",
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
"default_locale": "",
"flags": self.default_flags,
},
)
self.assertJSONEqual(
self.client.get(
reverse("authentik_api:brand-current"), HTTP_HOST="foo.bar.baz"
).content.decode(),
{
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "custom",
"branding_custom_css": "",
"matched_domain": "bar.baz",
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
"default_locale": "",
"flags": self.default_flags,
},
)
def test_brand_subdomain_same_suffix(self):
"""Test Current brand API"""
Brand.objects.create(domain="bar.baz", branding_title="custom-weak")
Brand.objects.create(domain="foo.bar.baz", branding_title="custom-strong")
Brand.objects.all().delete()
Brand.objects.create(domain="bar.baz", branding_title="custom")
Brand.objects.create(domain="foo.bar.baz", branding_title="custom")
self.assertJSONEqual(
self.client.get(
reverse("authentik_api:brand-current"), HTTP_HOST="foo.bar.baz"
@@ -155,7 +75,7 @@ class TestBrands(APITestCase):
{
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "custom-strong",
"branding_title": "custom",
"branding_custom_css": "",
"matched_domain": "foo.bar.baz",
"ui_footer_links": [],
@@ -165,20 +85,17 @@ class TestBrands(APITestCase):
},
)
def test_brand_subdomain_other_suffix(self):
"""Test Current brand API"""
Brand.objects.create(domain="bar.baz", branding_title="custom-weak")
Brand.objects.create(domain="foo.bar.baz", branding_title="custom-strong")
def test_fallback(self):
"""Test fallback brand"""
Brand.objects.all().delete()
self.assertJSONEqual(
self.client.get(
reverse("authentik_api:brand-current"), HTTP_HOST="other.bar.baz"
).content.decode(),
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
{
"branding_logo": "/static/dist/assets/icons/icon_left_brand.svg",
"branding_favicon": "/static/dist/assets/icons/icon.png",
"branding_title": "custom-weak",
"branding_title": "authentik",
"branding_custom_css": "",
"matched_domain": "bar.baz",
"matched_domain": "fallback",
"ui_footer_links": [],
"ui_theme": Themes.AUTOMATIC,
"default_locale": "",

View File

@@ -2,7 +2,8 @@
from typing import Any
from django.db.models import Case, F, IntegerField, Q, Value, When
from django.db.models import F, Q
from django.db.models import Value as V
from django.db.models.functions import Length
from django.http.request import HttpRequest
from django.utils.html import _json_script_escapes
@@ -19,36 +20,15 @@ DEFAULT_BRAND = Brand(domain="fallback")
def get_brand_for_request(request: HttpRequest) -> Brand:
"""Get brand object for current request"""
brand = (
Brand.objects.annotate(
host_domain=Value(request.get_host()),
domain_length=Length("domain"),
match_priority=Case(
When(
condition=Q(host_domain__iendswith=F("domain")),
then=F("domain_length"),
),
default=Value(-1),
output_field=IntegerField(),
),
is_default_fallback=Case(
When(
condition=Q(default=True),
then=Value(0),
),
default=Value(-2),
output_field=IntegerField(),
),
)
.filter(Q(match_priority__gt=-1) | Q(default=True))
.order_by("-match_priority", "-is_default_fallback")
.first()
db_brands = (
Brand.objects.annotate(host_domain=V(request.get_host()), match_length=Length("domain"))
.filter(Q(host_domain__iendswith=F("domain")) | _q_default)
.order_by("-match_length", "default")
)
if brand is None:
brands = list(db_brands.all())
if len(brands) < 1:
return DEFAULT_BRAND
return brand
return brands[0]
def context_processor(request: HttpRequest) -> dict[str, Any]:

View File

@@ -1,8 +0,0 @@
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikCommandsConfig(ManagedAppConfig):
name = "authentik.commands"
label = "authentik_commands"
verbose_name = "authentik Commands"
default = True

View File

@@ -1,8 +0,0 @@
from django.db.migrations.autodetector import MigrationAutodetector as BaseMigrationAutodetector
from pgtrigger.migrations import MigrationAutodetectorMixin
MigrationAutodetector = type(
"MigrationAutodetector",
(MigrationAutodetectorMixin, BaseMigrationAutodetector),
{},
)

View File

@@ -1,7 +0,0 @@
from django.core.management.commands.makemigrations import Command as BaseCommand
from authentik.commands.management.commands import MigrationAutodetector
class Command(BaseCommand):
autodetector = MigrationAutodetector

View File

@@ -1,7 +0,0 @@
from django_tenants.management.commands.migrate import Command as BaseCommand
from authentik.commands.management.commands import MigrationAutodetector
class Command(BaseCommand):
autodetector = MigrationAutodetector # type: ignore[assignment]

View File

@@ -1,7 +0,0 @@
from django_tenants.management.commands.migrate_schemas import Command as BaseCommand
from authentik.commands.management.commands import MigrationAutodetector
class Command(BaseCommand):
autodetector = MigrationAutodetector # type: ignore[assignment]

View File

@@ -13,7 +13,6 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from authentik.api.validation import validate
from authentik.core.api.users import ParamUserSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.enterprise.stages.authenticator_endpoint_gdtc.models import EndpointDevice
@@ -86,7 +85,8 @@ class AdminDeviceViewSet(ViewSet):
parameters=[ParamUserSerializer],
responses={200: DeviceSerializer(many=True)},
)
@validate(ParamUserSerializer, "query")
def list(self, request: Request, query: ParamUserSerializer) -> Response:
def list(self, request: Request) -> Response:
"""Get all devices for current user"""
return Response(DeviceSerializer(self.get_devices(**query.validated_data), many=True).data)
args = ParamUserSerializer(data=request.query_params)
args.is_valid(raise_exception=True)
return Response(DeviceSerializer(self.get_devices(**args.validated_data), many=True).data)

View File

@@ -14,7 +14,6 @@ from drf_spectacular.utils import (
extend_schema_field,
)
from guardian.shortcuts import get_objects_for_user
from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.request import Request
@@ -23,18 +22,15 @@ from rest_framework.serializers import ListSerializer, ValidationError
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from authentik.api.authentication import TokenAuthentication
from authentik.api.validation import validate
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
from authentik.core.models import Group, User
from authentik.endpoints.connectors.agent.auth import AgentAuth
from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required
class PartialUserSerializer(ModelSerializer):
"""Partial User Serializer, does not include child relations."""
class GroupMemberSerializer(ModelSerializer):
"""Stripped down user serializer to show relevant users for groups"""
attributes = JSONDictField(required=False)
uid = CharField(read_only=True)
@@ -98,11 +94,11 @@ class GroupSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_children", "false")).lower() == "true"
@extend_schema_field(PartialUserSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[PartialUserSerializer] | None:
@extend_schema_field(GroupMemberSerializer(many=True))
def get_users_obj(self, instance: Group) -> list[GroupMemberSerializer] | None:
if not self._should_include_users:
return None
return PartialUserSerializer(instance.users, many=True).data
return GroupMemberSerializer(instance.users, many=True).data
@extend_schema_field(GroupChildSerializer(many=True))
def get_children_obj(self, instance: Group) -> list[GroupChildSerializer] | None:
@@ -231,24 +227,6 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
search_fields = ["name", "is_superuser"]
filterset_class = GroupFilter
ordering = ["name"]
authentication_classes = [
TokenAuthentication,
SessionAuthentication,
AgentAuth,
]
def get_ql_fields(self):
from djangoql.schema import BoolField, StrField
from authentik.enterprise.search.fields import (
JSONSearchField,
)
return [
StrField(Group, "name"),
BoolField(Group, "is_superuser", nullable=True),
JSONSearchField(Group, "attributes", suggest_nested=False),
]
def get_queryset(self):
base_qs = Group.objects.all().select_related("parent").prefetch_related("roles")
@@ -298,14 +276,13 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
filter_backends=[],
permission_classes=[],
)
@validate(UserAccountSerializer)
def add_user(self, request: Request, body: UserAccountSerializer, pk: str) -> Response:
def add_user(self, request: Request, pk: str) -> Response:
"""Add user to group"""
group: Group = self.get_object()
user: User = (
get_objects_for_user(request.user, "authentik_core.view_user")
.filter(
pk=body.validated_data.get("pk"),
pk=request.data.get("pk"),
)
.first()
)
@@ -318,7 +295,7 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
@extend_schema(
request=UserAccountSerializer,
responses={
204: OpenApiResponse(description="User removed"),
204: OpenApiResponse(description="User added"),
404: OpenApiResponse(description="User not found"),
},
)
@@ -329,14 +306,13 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
filter_backends=[],
permission_classes=[],
)
@validate(UserAccountSerializer)
def remove_user(self, request: Request, body: UserAccountSerializer, pk: str) -> Response:
"""Remove user from group"""
def remove_user(self, request: Request, pk: str) -> Response:
"""Add user to group"""
group: Group = self.get_object()
user: User = (
get_objects_for_user(request.user, "authentik_core.view_user")
.filter(
pk=body.validated_data.get("pk"),
pk=request.data.get("pk"),
)
.first()
)

View File

@@ -21,7 +21,6 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from authentik.api.validation import validate
from authentik.blueprints.api import ManagedSerializer
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
@@ -129,20 +128,23 @@ class PropertyMappingViewSet(
],
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
@validate(PropertyMappingTestSerializer)
def test(self, request: Request, pk: str, body: PropertyMappingTestSerializer) -> Response:
def test(self, request: Request, pk: str) -> Response:
"""Test Property Mapping"""
_mapping: PropertyMapping = self.get_object()
# Use `get_subclass` to get correct class and correct `.evaluate` implementation
mapping: PropertyMapping = PropertyMapping.objects.get_subclass(pk=_mapping.pk)
# FIXME: when we separate policy mappings between ones for sources
# and ones for providers, we need to make the user field optional for the source mapping
test_params = self.PropertyMappingTestSerializer(data=request.data)
if not test_params.is_valid():
return Response(test_params.errors, status=400)
format_result = str(request.GET.get("format_result", "false")).lower() == "true"
context: dict = body.validated_data.get("context", {})
context: dict = test_params.validated_data.get("context", {})
context.setdefault("user", None)
if user := body.validated_data.get("user"):
if user := test_params.validated_data.get("user"):
# User permission check, only allow mapping testing for users that are readable
users = get_objects_for_user(request.user, "authentik_core.view_user").filter(
pk=user.pk
@@ -150,7 +152,7 @@ class PropertyMappingViewSet(
if not users.exists():
raise PermissionDenied()
context["user"] = user
if group := body.validated_data.get("group"):
if group := test_params.validated_data.get("group"):
# Group permission check, only allow mapping testing for groups that are readable
groups = get_objects_for_user(request.user, "authentik_core.view_group").filter(
pk=group.pk
@@ -169,7 +171,7 @@ class PropertyMappingViewSet(
except PropertyMappingExpressionException as exc:
response_data["result"] = exception_to_string(exc.exc)
response_data["successful"] = False
except Exception as exc: # noqa
except Exception as exc:
response_data["result"] = exception_to_string(exc)
response_data["successful"] = False
response = PropertyMappingTestResultSerializer(response_data)

View File

@@ -56,7 +56,6 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
"name",
"slug",
"enabled",
"promoted",
"authentication_flow",
"enrollment_flow",
"user_property_mappings",

View File

@@ -3,7 +3,7 @@
from typing import Any
from django.utils.timezone import now
from drf_spectacular.utils import OpenApiResponse, extend_schema
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
@@ -12,7 +12,6 @@ from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.api.validation import validate
from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
@@ -108,12 +107,6 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
}
class TokenSetKeySerializer(PassiveSerializer):
"""Set token's key"""
key = CharField()
class TokenViewSerializer(PassiveSerializer):
"""Show token's current key"""
@@ -177,7 +170,12 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
@permission_required("authentik_core.set_token_key")
@extend_schema(
request=TokenSetKeySerializer(),
request=inline_serializer(
"TokenSetKey",
{
"key": CharField(),
},
),
responses={
204: OpenApiResponse(description="Successfully changed key"),
400: OpenApiResponse(description="Missing key"),
@@ -185,12 +183,11 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
},
)
@action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"])
@validate(TokenSetKeySerializer)
def set_key(self, request: Request, identifier: str, body: TokenSetKeySerializer) -> Response:
def set_key(self, request: Request, identifier: str) -> Response:
"""Set token key. Action is logged as event. `authentik_core.set_token_key` permission
is required."""
token: Token = self.get_object()
key = body.validated_data.get("key")
key = request.data.get("key")
if not key:
return Response(status=400)
token.key = key

View File

@@ -12,7 +12,6 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from yaml import ScalarNode
from authentik.api.validation import validate
from authentik.blueprints.v1.common import (
Blueprint,
BlueprintEntry,
@@ -161,10 +160,11 @@ class TransactionalApplicationView(APIView):
200: TransactionApplicationResponseSerializer(),
},
)
@validate(TransactionApplicationSerializer)
def put(self, request: Request, body: TransactionApplicationSerializer) -> Response:
def put(self, request: Request) -> Response:
"""Convert data into a blueprint, validate it and apply it"""
blueprint: Blueprint = body.validated_data
data = TransactionApplicationSerializer(data=request.data)
data.is_valid(raise_exception=True)
blueprint: Blueprint = data.validated_data
for entry in blueprint.entries:
full_model = entry.get_model(blueprint)
app, __, model = full_model.partition(".")

View File

@@ -31,7 +31,6 @@ from drf_spectacular.utils import (
inline_serializer,
)
from guardian.shortcuts import get_objects_for_user
from rest_framework.authentication import SessionAuthentication
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import (
@@ -53,8 +52,6 @@ from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.authentication import TokenAuthentication
from authentik.api.validation import validate
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin
@@ -78,7 +75,6 @@ from authentik.core.models import (
User,
UserTypes,
)
from authentik.endpoints.connectors.agent.auth import AgentAuth
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
@@ -101,8 +97,8 @@ class ParamUserSerializer(PassiveSerializer):
user = PrimaryKeyRelatedField(queryset=User.objects.all().exclude_anonymous(), required=False)
class PartialGroupSerializer(ModelSerializer):
"""Partial Group Serializer, does not include child relations."""
class UserGroupSerializer(ModelSerializer):
"""Simplified Group Serializer for user's groups"""
attributes = JSONDictField(required=False)
parent_name = CharField(source="parent.name", read_only=True, allow_null=True)
@@ -147,11 +143,11 @@ class UserSerializer(ModelSerializer):
return True
return str(request.query_params.get("include_groups", "true")).lower() == "true"
@extend_schema_field(PartialGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[PartialGroupSerializer] | None:
@extend_schema_field(UserGroupSerializer(many=True))
def get_groups_obj(self, instance: User) -> list[UserGroupSerializer] | None:
if not self._should_include_groups:
return None
return PartialGroupSerializer(instance.ak_groups, many=True).data
return UserGroupSerializer(instance.ak_groups, many=True).data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -332,27 +328,6 @@ class SessionUserSerializer(PassiveSerializer):
original = UserSelfSerializer(required=False)
class UserPasswordSetSerializer(PassiveSerializer):
"""Payload to set a users' password directly"""
password = CharField(required=True)
class UserServiceAccountSerializer(PassiveSerializer):
"""Payload to create a service account"""
name = CharField(
required=True,
validators=[UniqueValidator(queryset=User.objects.all().order_by("username"))],
)
create_group = BooleanField(default=False)
expiring = BooleanField(default=True)
expires = DateTimeField(
required=False,
help_text="If not provided, valid for 360 days",
)
class UsersFilter(FilterSet):
"""Filter for users"""
@@ -436,11 +411,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
serializer_class = UserSerializer
filterset_class = UsersFilter
search_fields = ["email", "name", "uuid", "username"]
authentication_classes = [
TokenAuthentication,
SessionAuthentication,
AgentAuth,
]
def get_ql_fields(self):
from djangoql.schema import BoolField, StrField
@@ -518,7 +488,18 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@permission_required(None, ["authentik_core.add_user", "authentik_core.add_token"])
@extend_schema(
request=UserServiceAccountSerializer,
request=inline_serializer(
"UserServiceAccountSerializer",
{
"name": CharField(required=True),
"create_group": BooleanField(default=False),
"expiring": BooleanField(default=True),
"expires": DateTimeField(
required=False,
help_text="If not provided, valid for 360 days",
),
},
),
responses={
200: inline_serializer(
"UserServiceAccountResponse",
@@ -538,13 +519,13 @@ class UserViewSet(UsedByMixin, ModelViewSet):
pagination_class=None,
filter_backends=[],
)
@validate(UserServiceAccountSerializer)
def service_account(self, request: Request, body: UserServiceAccountSerializer) -> Response:
def service_account(self, request: Request) -> Response:
"""Create a new user account that is marked as a service account"""
expires = body.validated_data.get("expires", now() + timedelta(days=360))
username = request.data.get("name")
create_group = request.data.get("create_group", False)
expiring = request.data.get("expiring", True)
expires = request.data.get("expires", now() + timedelta(days=360))
username = body.validated_data["name"]
expiring = body.validated_data["expiring"]
with atomic():
try:
user: User = User.objects.create(
@@ -562,10 +543,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"user_uid": user.uid,
"user_pk": user.pk,
}
if body.validated_data["create_group"] and self.request.user.has_perm(
"authentik_core.add_group"
):
group = Group.objects.create(name=username)
if create_group and self.request.user.has_perm("authentik_core.add_group"):
group = Group.objects.create(
name=username,
)
group.users.add(user)
response["group_pk"] = str(group.pk)
token = Token.objects.create(
@@ -578,29 +559,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
response["token"] = token.key
return Response(response)
except IntegrityError as exc:
error_msg = str(exc).lower()
if "unique" in error_msg:
return Response(
data={
"non_field_errors": [
_("A user/group with these details already exists")
]
},
status=400,
)
else:
LOGGER.warning("Service account creation failed", exc=exc)
return Response(
data={"non_field_errors": [_("Unable to create user")]},
status=400,
)
except (ValueError, TypeError) as exc:
LOGGER.error("Unexpected error during service account creation", exc=exc)
return Response(
data={"non_field_errors": [_("Unknown error occurred")]},
status=500,
)
return Response(data={"non_field_errors": [str(exc)]}, status=400)
@extend_schema(responses={200: SessionUserSerializer(many=False)})
@action(
@@ -626,19 +585,23 @@ class UserViewSet(UsedByMixin, ModelViewSet):
@permission_required("authentik_core.reset_user_password")
@extend_schema(
request=UserPasswordSetSerializer,
request=inline_serializer(
"UserPasswordSetSerializer",
{
"password": CharField(required=True),
},
),
responses={
204: OpenApiResponse(description="Successfully changed password"),
400: OpenApiResponse(description="Bad request"),
},
)
@action(detail=True, methods=["POST"], permission_classes=[])
@validate(UserPasswordSetSerializer)
def set_password(self, request: Request, pk: int, body: UserPasswordSetSerializer) -> Response:
def set_password(self, request: Request, pk: int) -> Response:
"""Set password for user"""
user: User = self.get_object()
try:
user.set_password(body.validated_data["password"], request=request)
user.set_password(request.data.get("password"), request=request)
user.save()
except (ValidationError, IntegrityError) as exc:
LOGGER.debug("Failed to set password", exc=exc)
@@ -715,7 +678,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
},
),
responses={
204: OpenApiResponse(description="Successfully started impersonation"),
"204": OpenApiResponse(description="Successfully started impersonation"),
"401": OpenApiResponse(description="Access denied"),
},
)
@action(detail=True, methods=["POST"], permission_classes=[])
@@ -734,7 +698,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"User attempted to impersonate without permissions",
user=request.user,
)
return Response(status=403)
return Response(status=401)
if user_to_be.pk == self.request.user.pk:
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
return Response(status=401)
@@ -743,19 +707,19 @@ class UserViewSet(UsedByMixin, ModelViewSet):
"User attempted to impersonate without providing a reason",
user=request.user,
)
raise ValidationError({"reason": _("This field is required.")})
return Response(status=401)
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED, reason=reason).from_http(request, user_to_be)
return Response(status=204)
return Response(status=201)
@extend_schema(
request=None,
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully ended impersonation"),
"204": OpenApiResponse(description="Successfully started impersonation"),
},
)
@action(detail=False, methods=["GET"])

View File

@@ -21,6 +21,8 @@ from rest_framework.serializers import (
raise_errors_on_nested_writes,
)
from authentik.rbac.permissions import assign_initial_permissions
def is_dict(value: Any):
"""Ensure a value is a dictionary, useful for JSONFields"""
@@ -50,6 +52,15 @@ class ModelSerializer(BaseModelSerializer):
serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy()
serializer_field_mapping[models.JSONField] = JSONDictField
def create(self, validated_data):
instance = super().create(validated_data)
request = self.context.get("request")
if request and hasattr(request, "user") and not request.user.is_anonymous:
assign_initial_permissions(request.user, instance)
return instance
def update(self, instance: Model, validated_data):
raise_errors_on_nested_writes("update", self, validated_data)
info = model_meta.get_field_info(instance)

View File

@@ -1,6 +1,6 @@
"""custom runserver command"""
from io import StringIO
from typing import TextIO
from daphne.management.commands.runserver import Command as RunServer
from daphne.server import Server
@@ -33,4 +33,4 @@ class Command(RunServer):
super().__init__(*args, **kwargs)
# Redirect standard stdout banner from Daphne into the void
# as there are a couple more steps that happen before startup is fully done
self.stdout = StringIO()
self.stdout = TextIO()

View File

@@ -1,9 +1,13 @@
"""authentik shell command"""
import code
import platform
import sys
import traceback
from pprint import pprint
from django.core.management.commands.shell import Command as BaseCommand
from django.apps import apps
from django.core.management.base import BaseCommand
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
@@ -22,12 +26,29 @@ def get_banner_text(shell_type="shell") -> str:
class Command(BaseCommand):
"""Start the Django shell with all authentik models already imported"""
def get_namespace(self, **options):
return {
**super().get_namespace(**options),
django_models = {}
def add_arguments(self, parser):
parser.add_argument(
"-c",
"--command",
help="Python code to execute (instead of starting an interactive shell)",
)
def get_namespace(self):
"""Prepare namespace with all models"""
namespace = {
"pprint": pprint,
}
# Gather Django models and constants from each app
for app in apps.get_app_configs():
# Load models from each app
for model in app.get_models():
namespace[model.__name__] = model
return namespace
@staticmethod
def post_save_handler(sender, instance: Model, created: bool, **_):
"""Signal handler for all object's post_save"""
@@ -58,9 +79,41 @@ class Command(BaseCommand):
).save()
def handle(self, **options):
namespace = self.get_namespace()
post_save.connect(Command.post_save_handler)
pre_delete.connect(Command.pre_delete_handler)
print(get_banner_text())
# If Python code has been passed, execute it and exit.
if options["command"]:
super().handle(**options)
exec(options["command"], namespace) # nosec # noqa
return
try:
hook = sys.__interactivehook__
except AttributeError:
# Match the behavior of the cpython shell where a missing
# sys.__interactivehook__ is ignored.
pass
else:
try:
hook()
except Exception:
# Match the behavior of the cpython shell where an error in
# sys.__interactivehook__ prints a warning and the exception
# and continues.
print("Failed calling sys.__interactivehook__")
traceback.print_exc()
# Try to enable tab-complete
try:
import readline
import rlcompleter
except ModuleNotFoundError:
pass
else:
readline.set_completer(rlcompleter.Completer(namespace).complete)
readline.parse_and_bind("tab: complete")
# Run interactive shell
code.interact(banner=get_banner_text(), local=namespace)

View File

@@ -13,6 +13,14 @@ import authentik.core.models
import authentik.lib.models
def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core.cache import cache
session_keys = cache.keys(KEY_PREFIX + "*")
cache.delete_many(session_keys)
def fix_duplicates(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
db_alias = schema_editor.connection.alias
Token = apps.get_model("authentik_core", "token")
@@ -143,6 +151,9 @@ class Migration(migrations.Migration):
"abstract": False,
},
),
migrations.RunPython(
code=migrate_sessions,
),
migrations.AlterField(
model_name="application",
name="meta_launch_url",

View File

@@ -7,10 +7,15 @@ from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_K
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.utils.timezone import now, timedelta
from authentik.lib.migrations import progress_bar
from authentik.root.middleware import ClientIPMiddleware
SESSION_CACHE_ALIAS = "default"
class PickleSerializer:
"""
Simple wrapper around pickle to be used in signing.dumps()/loads() and
@@ -78,6 +83,27 @@ def _migrate_session(
)
def migrate_redis_sessions(apps, schema_editor):
from django.core.cache import caches
db_alias = schema_editor.connection.alias
cache = caches[SESSION_CACHE_ALIAS]
# Not a redis cache, skipping
if not hasattr(cache, "keys"):
return
print("\nMigrating Redis sessions to database, this might take a couple of minutes...")
for key, session_data in progress_bar(cache.get_many(cache.keys(f"{KEY_PREFIX}*")).items()):
_migrate_session(
apps=apps,
db_alias=db_alias,
session_key=key.removeprefix(KEY_PREFIX),
session_data=session_data,
expires=now() + timedelta(seconds=cache.ttl(key)),
)
def migrate_database_sessions(apps, schema_editor):
DjangoSession = apps.get_model("sessions", "Session")
db_alias = schema_editor.connection.alias
@@ -205,6 +231,10 @@ class Migration(migrations.Migration):
"verbose_name_plural": "Authenticated Sessions",
},
),
migrations.RunPython(
code=migrate_redis_sessions,
reverse_code=migrations.RunPython.noop,
),
migrations.RunPython(
code=migrate_database_sessions,
reverse_code=migrations.RunPython.noop,

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.12 on 2025-09-25 13:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0050_user_last_updated_and_more"),
("authentik_rbac", "0006_alter_role_options"),
]
operations = [
migrations.AddIndex(
model_name="group",
index=models.Index(fields=["is_superuser"], name="authentik_c_is_supe_1e5a97_idx"),
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-23 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0051_group_authentik_c_is_supe_1e5a97_idx"),
]
operations = [
migrations.AddField(
model_name="source",
name="promoted",
field=models.BooleanField(
default=False,
help_text="When enabled, this source will be displayed as a prominent button on the login page, instead of a small icon.",
),
),
]

View File

@@ -1,45 +0,0 @@
# Generated by Django 5.2.8 on 2025-11-25 16:36
import django.core.validators
import re
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0052_source_promoted"),
]
operations = [
migrations.AlterField(
model_name="application",
name="slug",
field=models.TextField(
help_text="Internal application name, used in URLs.",
unique=True,
validators=[
django.core.validators.RegexValidator(
re.compile("^[-a-zA-Z0-9_]+\\Z"),
"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.",
"invalid",
)
],
),
),
migrations.AlterField(
model_name="source",
name="slug",
field=models.TextField(
help_text="Internal source name, used in URLs.",
unique=True,
validators=[
django.core.validators.RegexValidator(
re.compile("^[-a-zA-Z0-9_]+\\Z"),
"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.",
"invalid",
)
],
),
),
]

View File

@@ -11,12 +11,11 @@ from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import UserManager as DjangoUserManager
from django.contrib.sessions.base_session import AbstractBaseSession
from django.core.validators import validate_slug
from django.db import models
from django.db.models import Q, QuerySet, options
from django.db.models.constants import LOOKUP_SEP
from django.http import HttpRequest
from django.utils.functional import cached_property
from django.utils.functional import SimpleLazyObject, cached_property
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django_cte import CTE, with_cte
@@ -30,7 +29,6 @@ from authentik.blueprints.models import ManagedModel
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.avatars import get_avatar
from authentik.lib.config import CONFIG
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.generators import generate_id
from authentik.lib.merge import MERGE_LIST_UNIQUE
@@ -45,19 +43,18 @@ from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGT
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
LOGGER = get_logger()
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME = "goauthentik.io/user/token-maximum-lifetime" # nosec
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
_USER_ATTR_PREFIX = f"{USER_PATH_SYSTEM_PREFIX}/user"
USER_ATTRIBUTE_DEBUG = f"{_USER_ATTR_PREFIX}/debug"
USER_ATTRIBUTE_GENERATED = f"{_USER_ATTR_PREFIX}/generated"
USER_ATTRIBUTE_EXPIRES = f"{_USER_ATTR_PREFIX}/expires"
USER_ATTRIBUTE_DELETE_ON_LOGOUT = f"{_USER_ATTR_PREFIX}/delete-on-logout"
USER_ATTRIBUTE_SOURCES = f"{_USER_ATTR_PREFIX}/sources"
USER_ATTRIBUTE_TOKEN_EXPIRING = f"{_USER_ATTR_PREFIX}/token-expires" # nosec
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME = f"{_USER_ATTR_PREFIX}/token-maximum-lifetime" # nosec
USER_ATTRIBUTE_CHANGE_USERNAME = f"{_USER_ATTR_PREFIX}/can-change-username"
USER_ATTRIBUTE_CHANGE_NAME = f"{_USER_ATTR_PREFIX}/can-change-name"
USER_ATTRIBUTE_CHANGE_EMAIL = f"{_USER_ATTR_PREFIX}/can-change-email"
USER_PATH_SERVICE_ACCOUNT = f"{USER_PATH_SYSTEM_PREFIX}/service-accounts"
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
# used_by API that allows models to specify if they shadow an object
@@ -117,21 +114,15 @@ class AttributesMixin(models.Model):
def update_attributes(self, properties: dict[str, Any]):
"""Update fields and attributes, but correctly by merging dicts"""
needs_update = False
for key, value in properties.items():
if key == "attributes":
continue
if getattr(self, key, None) != value:
setattr(self, key, value)
needs_update = True
setattr(self, key, value)
final_attributes = {}
MERGE_LIST_UNIQUE.merge(final_attributes, self.attributes)
MERGE_LIST_UNIQUE.merge(final_attributes, properties.get("attributes", {}))
if self.attributes != final_attributes:
self.attributes = final_attributes
needs_update = True
if needs_update:
self.save()
self.attributes = final_attributes
self.save()
@classmethod
def update_or_create_attributes(
@@ -209,10 +200,7 @@ class Group(SerializerModel, AttributesMixin):
"parent",
),
)
indexes = (
models.Index(fields=["name"]),
models.Index(fields=["is_superuser"]),
)
indexes = [models.Index(fields=["name"])]
verbose_name = _("Group")
verbose_name_plural = _("Groups")
permissions = [
@@ -409,12 +397,10 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
def locale(self, request: HttpRequest | None = None) -> str:
"""Get the locale the user has configured"""
if request and hasattr(request, "LANGUAGE_CODE"):
return request.LANGUAGE_CODE
try:
return self.attributes.get("settings", {}).get("locale", "")
except Exception as exc: # noqa
except Exception as exc:
LOGGER.warning("Failed to get default locale", exc=exc)
if request:
return request.brand.locale
@@ -535,11 +521,7 @@ class Application(SerializerModel, PolicyBindingModel):
add custom fields and other properties"""
name = models.TextField(help_text=_("Application's display Name."))
slug = models.TextField(
validators=[validate_slug],
help_text=_("Internal application name, used in URLs."),
unique=True,
)
slug = models.SlugField(help_text=_("Internal application name, used in URLs."), unique=True)
group = models.TextField(blank=True, default="")
provider = models.OneToOneField(
@@ -581,27 +563,25 @@ class Application(SerializerModel, PolicyBindingModel):
it is returned as-is"""
if not self.meta_icon:
return None
if self.meta_icon.name.startswith("http"):
if "://" in self.meta_icon.name or self.meta_icon.name.startswith("/static"):
return self.meta_icon.name
if self.meta_icon.name.startswith("fa://"):
return self.meta_icon.name
if self.meta_icon.name.startswith("/"):
return CONFIG.get("web.path", "/")[:-1] + self.meta_icon.name
return self.meta_icon.url
def get_launch_url(self, user: Optional["User"] = None) -> str | None:
"""Get launch URL if set, otherwise attempt to get launch URL based on provider."""
from authentik.core.api.users import UserSerializer
url = None
if self.meta_launch_url:
url = self.meta_launch_url
elif provider := self.get_provider():
url = provider.launch_url
if user and url:
if isinstance(user, SimpleLazyObject):
user._setup()
user = user._wrapped
try:
return url % UserSerializer(instance=user).data
except Exception as exc: # noqa
return url % user.__dict__
except Exception as exc:
LOGGER.warning("Failed to format launch url", exc=exc)
return url
return url
@@ -725,22 +705,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
MANAGED_INBUILT = "goauthentik.io/sources/inbuilt"
name = models.TextField(help_text=_("Source's display Name."))
slug = models.TextField(
validators=[validate_slug],
help_text=_("Internal source name, used in URLs."),
unique=True,
)
slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True)
user_path_template = models.TextField(default="goauthentik.io/sources/%(slug)s")
enabled = models.BooleanField(default=True)
promoted = models.BooleanField(
default=False,
help_text=_(
"When enabled, this source will be displayed as a prominent button on the "
"login page, instead of a small icon."
),
)
user_property_mappings = models.ManyToManyField(
"PropertyMapping", default=None, blank=True, related_name="source_userpropertymappings_set"
)
@@ -797,12 +766,8 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
starts with http it is returned as-is"""
if not self.icon:
return None
if self.icon.name.startswith("http"):
if "://" in self.icon.name or self.icon.name.startswith("/static"):
return self.icon.name
if self.icon.name.startswith("fa://"):
return self.icon.name
if self.icon.name.startswith("/"):
return CONFIG.get("web.path", "/")[:-1] + self.icon.name
return self.icon.url
def get_user_path(self) -> str:
@@ -812,7 +777,7 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"slug": self.slug,
}
except Exception as exc: # noqa
except Exception as exc:
LOGGER.warning("Failed to template user path", exc=exc, source=self)
return User.default_path()
@@ -945,7 +910,7 @@ class ExpiringModel(models.Model):
return self.delete(*args, **kwargs)
@classmethod
def filter_not_expired(cls, **kwargs) -> QuerySet["Self"]:
def filter_not_expired(cls, **kwargs) -> QuerySet["Token"]:
"""Filer for tokens which are not expired yet or are not expiring,
and match filters in `kwargs`"""
for obj in cls.objects.filter(**kwargs).filter(Q(expires__lt=now(), expiring=True)):

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