mirror of
https://github.com/goauthentik/authentik
synced 2026-05-06 15:12:13 +02:00
Compare commits
1 Commits
patternfly
...
flows/conc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43628f308d |
81
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
81
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
@@ -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
|
||||
49
.github/ISSUE_TEMPLATE/2-docs-issue.yml
vendored
49
.github/ISSUE_TEMPLATE/2-docs-issue.yml
vendored
@@ -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
|
||||
41
.github/ISSUE_TEMPLATE/3-feature-request.yml
vendored
41
.github/ISSUE_TEMPLATE/3-feature-request.yml
vendored
@@ -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
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
Normal 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).
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||
17
.github/ISSUE_TEMPLATE/hackathon_idea.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/hackathon_idea.md
vendored
Normal 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:
|
||||
7
.github/ISSUE_TEMPLATE/issue_template.md
vendored
7
.github/ISSUE_TEMPLATE/issue_template.md
vendored
@@ -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
32
.github/ISSUE_TEMPLATE/question.md
vendored
Normal 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.
|
||||
@@ -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 }}
|
||||
|
||||
11
.github/actions/setup/action.yml
vendored
11
.github/actions/setup/action.yml
vendored
@@ -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@5a7eac68fb9809dea845d802897dc5c723910fa3 # v5
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Setup python
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # 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@44694675825211faa026b3c33043df3e48a5fa00 # 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}
|
||||
|
||||
4
.github/actions/test-results/action.yml
vendored
4
.github/actions/test-results/action.yml
vendored
@@ -8,11 +8,11 @@ inputs:
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
flags: ${{ inputs.flags }}
|
||||
use_oidc: true
|
||||
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1
|
||||
- uses: codecov/test-results-action@v1
|
||||
with:
|
||||
flags: ${{ inputs.flags }}
|
||||
file: unittest.xml
|
||||
|
||||
15
.github/dependabot.yml
vendored
15
.github/dependabot.yml
vendored
@@ -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"
|
||||
@@ -142,9 +134,7 @@ updates:
|
||||
labels:
|
||||
- dependencies
|
||||
- package-ecosystem: docker
|
||||
directories:
|
||||
- /
|
||||
- /website
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
@@ -156,7 +146,6 @@ updates:
|
||||
- package-ecosystem: docker-compose
|
||||
directories:
|
||||
# - /scripts # Maybe
|
||||
- /scripts/api
|
||||
- /tests/e2e
|
||||
schedule:
|
||||
interval: daily
|
||||
|
||||
@@ -42,9 +42,9 @@ jobs:
|
||||
# Needed for checkout
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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 }}
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
mkdir -p ./gen-go-api
|
||||
- name: Setup node
|
||||
if: ${{ !inputs.release }}
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
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 +97,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@v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
|
||||
12
.github/workflows/_reusable-docker-build.yml
vendored
12
.github/workflows/_reusable-docker-build.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
tags: ${{ steps.ev.outputs.imageTagsJSON }}
|
||||
shouldPush: ${{ steps.ev.outputs.shouldPush }}
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@v3
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
|
||||
22
.github/workflows/api-ts-publish.yml
vendored
22
.github/workflows/api-ts-publish.yml
vendored
@@ -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@67018539274d69449ef7c02e8e71183d1719ab42 # v2
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@v5
|
||||
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@271a8d0340265f705b14b6d32b9829c1cb33d45e # 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 }}
|
||||
|
||||
16
.github/workflows/ci-api-docs.yml
vendored
16
.github/workflows/ci-api-docs.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
command:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
|
||||
8
.github/workflows/ci-aws-cfn.yml
vendored
8
.github/workflows/ci-aws-cfn.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
check-changes-applied:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@v5
|
||||
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) }}
|
||||
|
||||
9
.github/workflows/ci-docs-source.yml
vendored
9
.github/workflows/ci-docs-source.yml
vendored
@@ -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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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
|
||||
|
||||
26
.github/workflows/ci-docs.yml
vendored
26
.github/workflows/ci-docs.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
command:
|
||||
- prettier-check
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: website/package.json
|
||||
cache: "npm"
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@v3
|
||||
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' || '[]' }}
|
||||
|
||||
3
.github/workflows/ci-main-daily.yml
vendored
3
.github/workflows/ci-main-daily.yml
vendored
@@ -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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
- run: |
|
||||
current="$(pwd)"
|
||||
dir="/tmp/authentik/${{ matrix.version }}"
|
||||
|
||||
45
.github/workflows/ci-main.yml
vendored
45
.github/workflows/ci-main.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
- mypy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run job
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run migrations
|
||||
@@ -61,35 +61,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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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)
|
||||
@@ -128,17 +120,18 @@ jobs:
|
||||
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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
@@ -158,11 +151,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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
|
||||
@@ -196,14 +189,14 @@ jobs:
|
||||
- name: flows
|
||||
glob: tests/e2e/test_flows*
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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
|
||||
@@ -234,7 +227,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:
|
||||
@@ -262,7 +255,7 @@ jobs:
|
||||
pull-requests: write
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: prepare variables
|
||||
|
||||
39
.github/workflows/ci-outpost.yml
vendored
39
.github/workflows/ci-outpost.yml
vendored
@@ -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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
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@0a35821d5c230e903fcfe077583637dea1b27b47 # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@v3
|
||||
id: attest
|
||||
if: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
with:
|
||||
@@ -145,13 +138,13 @@ jobs:
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
|
||||
14
.github/workflows/ci-web.yml
vendored
14
.github/workflows/ci-web.yml
vendored
@@ -31,8 +31,8 @@ jobs:
|
||||
- command: lit-analyse
|
||||
project: web
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: ${{ matrix.project }}/package.json
|
||||
cache: "npm"
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version-file: web/package.json
|
||||
cache: "npm"
|
||||
|
||||
18
.github/workflows/gen-image-compress.yml
vendored
18
.github/workflows/gen-image-compress.yml
vendored
@@ -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@67018539274d69449ef7c02e8e71183d1719ab42 # v2
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@271a8d0340265f705b14b6d32b9829c1cb33d45e # 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 }}
|
||||
|
||||
13
.github/workflows/gen-update-webauthn-mds.yml
vendored
13
.github/workflows/gen-update-webauthn-mds.yml
vendored
@@ -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@67018539274d69449ef7c02e8e71183d1719ab42 # v2
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@271a8d0340265f705b14b6d32b9829c1cb33d45e # 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 }}
|
||||
|
||||
4
.github/workflows/gh-cherry-pick.yml
vendored
4
.github/workflows/gh-cherry-pick.yml
vendored
@@ -10,14 +10,14 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
|
||||
uses: actions/create-github-app-token@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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
if: ${{ steps.app-token.outcome != 'skipped' }}
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
2
.github/workflows/gh-gha-cache-cleanup.yml
vendored
2
.github/workflows/gh-gha-cache-cleanup.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
|
||||
21
.github/workflows/gh-ghcr-retention.yml
vendored
21
.github/workflows/gh-ghcr-retention.yml
vendored
@@ -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@67018539274d69449ef7c02e8e71183d1719ab42 # 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
|
||||
|
||||
14
.github/workflows/packages-npm-publish.yml
vendored
14
.github/workflows/packages-npm-publish.yml
vendored
@@ -12,13 +12,9 @@ on:
|
||||
- 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
|
||||
@@ -30,16 +26,16 @@ jobs:
|
||||
- packages/tsconfig
|
||||
- packages/esbuild-plugin-live-reload
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@v5
|
||||
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@24d32ffd492484c1d75e0c0b894501ddb9d30d62
|
||||
with:
|
||||
files: |
|
||||
${{ matrix.package }}/package.json
|
||||
@@ -50,3 +46,5 @@ jobs:
|
||||
npm ci
|
||||
npm run build
|
||||
npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
|
||||
8
.github/workflows/qa-codeql.yml
vendored
8
.github/workflows/qa-codeql.yml
vendored
@@ -24,14 +24,14 @@ jobs:
|
||||
language: ["go", "javascript", "python"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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
|
||||
|
||||
2
.github/workflows/qa-semgrep.yml
vendored
2
.github/workflows/qa-semgrep.yml
vendored
@@ -26,5 +26,5 @@ jobs:
|
||||
image: semgrep/semgrep
|
||||
if: (github.actor != 'dependabot[bot]')
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
- run: semgrep ci
|
||||
|
||||
14
.github/workflows/release-branch-off.yml
vendored
14
.github/workflows/release-branch-off.yml
vendored
@@ -29,12 +29,12 @@ jobs:
|
||||
steps:
|
||||
- id: app-token
|
||||
name: Generate app token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
@@ -57,12 +57,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
- name: Bump version
|
||||
run: "make bump version=${{ inputs.next_version }}.0-rc1"
|
||||
- name: Create pull request
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: release-bump-${{ inputs.next_version }}
|
||||
|
||||
3
.github/workflows/release-next-branch.yml
vendored
3
.github/workflows/release-next-branch.yml
vendored
@@ -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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
ref: main
|
||||
- run: |
|
||||
|
||||
48
.github/workflows/release-publish.yml
vendored
48
.github/workflows/release-publish.yml
vendored
@@ -31,11 +31,11 @@ jobs:
|
||||
id-token: write
|
||||
attestations: write
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@v3
|
||||
id: attest
|
||||
if: true
|
||||
with:
|
||||
@@ -83,14 +83,14 @@ jobs:
|
||||
- radius
|
||||
- rac
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
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@v3
|
||||
id: attest
|
||||
with:
|
||||
subject-name: ${{ steps.ev.outputs.attestImageNames }}
|
||||
@@ -146,11 +146,11 @@ jobs:
|
||||
goos: [linux, darwin]
|
||||
goarch: [amd64, arm64]
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v5
|
||||
- uses: actions/setup-node@v5
|
||||
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@81c65b7cd4de9b2570615ce3aad67a41de5b1a13 # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: aws-actions/configure-aws-credentials@00943011d9042930efac3dcd3a170e4273319bc8 # v5
|
||||
- uses: actions/checkout@v5
|
||||
- uses: aws-actions/configure-aws-credentials@v5
|
||||
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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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 }}
|
||||
|
||||
24
.github/workflows/release-tag.yml
vendored
24
.github/workflows/release-tag.yml
vendored
@@ -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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@67018539274d69449ef7c02e8e71183d1719ab42 # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
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@67018539274d69449ef7c02e8e71183d1719ab42 # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@271a8d0340265f705b14b6d32b9829c1cb33d45e # 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@67018539274d69449ef7c02e8e71183d1719ab42 # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
branch: bump-${{ inputs.version }}
|
||||
|
||||
22
.github/workflows/repo-mirror-cleanup.yml
vendored
Normal file
22
.github/workflows/repo-mirror-cleanup.yml
vendored
Normal 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
21
.github/workflows/repo-mirror.yml
vendored
Normal 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 }}
|
||||
9
.github/workflows/repo-stale.yml
vendored
9
.github/workflows/repo-stale.yml
vendored
@@ -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@67018539274d69449ef7c02e8e71183d1719ab42 # 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@v10
|
||||
with:
|
||||
repo-token: ${{ steps.generate_token.outputs.token }}
|
||||
days-before-stale: 60
|
||||
|
||||
4
.github/workflows/translation-advice.yml
vendored
4
.github/workflows/translation-advice.yml
vendored
@@ -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@v4
|
||||
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@v5
|
||||
with:
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
||||
@@ -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@67018539274d69449ef7c02e8e71183d1719ab42 # v2
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@93cb6efe18208431cddfb8368fd83d5badbf9bfd # 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@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: extract-compile-backend-translation
|
||||
|
||||
10
.github/workflows/translation-rename.yml
vendored
10
.github/workflows/translation-rename.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.pull_request.user.login == 'transifex-integration[bot]'}}
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
- uses: actions/checkout@v5
|
||||
- id: generate_token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # 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: Get current title
|
||||
id: title
|
||||
env:
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
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@a660677d5469627102a1c1e11409dd063606628d # v3
|
||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
pull-request-number: ${{ github.event.pull_request.number }}
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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:
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -49,9 +49,6 @@
|
||||
"go.testFlags": [
|
||||
"-count=1"
|
||||
],
|
||||
"go.testEnvVars": {
|
||||
"WORKSPACE_DIR": "${workspaceFolder}"
|
||||
},
|
||||
"github-actions.workflows.pinned.workflows": [
|
||||
".github/workflows/ci-main.yml"
|
||||
]
|
||||
|
||||
@@ -24,7 +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
|
||||
|
||||
11
Dockerfile
11
Dockerfile
@@ -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:27e1c927a07ed2c7295d39941d6d881424739dbde9ae3055d0d3013699ed35e8 AS go-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.1-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.10@sha256:29bd45092ea8902c0bbb7f0a338f0494a382b1f4b18355df5be270ade679ff1d AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.8.22 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.7-slim-trixie-fips AS python-base
|
||||
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
@@ -139,7 +139,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" \
|
||||
|
||||
55
Makefile
55
Makefile
@@ -16,6 +16,7 @@ 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)
|
||||
|
||||
UNAME := $(shell uname)
|
||||
|
||||
@@ -106,6 +107,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}
|
||||
@@ -149,13 +151,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/{/{/g' diff.md
|
||||
sed -i 's/}/}/g' diff.md
|
||||
npx prettier --write diff.md
|
||||
@@ -164,21 +167,28 @@ 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.15.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
|
||||
@@ -188,14 +198,17 @@ gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescri
|
||||
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.15.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: gen-clean-go ## Build and install the authentik API for Golang
|
||||
mkdir -p ${PWD}/${GEN_API_GO}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
|
||||
[](https://codecov.io/gh/goauthentik/authentik)
|
||||

|
||||
[](https://explore.transifex.com/authentik/authentik/)
|
||||
[](https://www.transifex.com/authentik/authentik/)
|
||||
|
||||
## What is authentik?
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ 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.6.x | ✅ |
|
||||
| 2025.8.x | ✅ |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -3,23 +3,53 @@
|
||||
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.plumbing import (
|
||||
ResolvedComponent,
|
||||
build_array_type,
|
||||
build_basic_type,
|
||||
build_object_type,
|
||||
)
|
||||
from drf_spectacular.renderers import OpenApiJsonRenderer
|
||||
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
|
||||
|
||||
GENERIC_ERROR = build_object_type(
|
||||
description=_("Generic API Error"),
|
||||
properties={
|
||||
"detail": build_basic_type(OpenApiTypes.STR),
|
||||
"code": build_basic_type(OpenApiTypes.STR),
|
||||
},
|
||||
required=["detail"],
|
||||
)
|
||||
VALIDATION_ERROR = 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={},
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
def create_component(
|
||||
generator: SchemaGenerator, name: str, schema: Any, type_=ResolvedComponent.SCHEMA
|
||||
) -> ResolvedComponent:
|
||||
"""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):
|
||||
@@ -31,30 +61,45 @@ def preprocess_schema_exclude_non_api(endpoints: list[tuple[str, Any, Any, Calla
|
||||
]
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def postprocess_schema_responses(
|
||||
result: dict[str, Any], generator: SchemaGenerator, **kwargs
|
||||
) -> dict[str, Any]:
|
||||
"""Default error responses"""
|
||||
LOGGER.debug("Adding default error responses")
|
||||
"""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)
|
||||
|
||||
generic_error = create_component(generator, "GenericError", GENERIC_ERROR)
|
||||
validation_error = create_component(generator, "ValidationError", VALIDATION_ERROR)
|
||||
|
||||
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,18 +113,67 @@ def postprocess_schema_responses(
|
||||
return result
|
||||
|
||||
|
||||
def postprocess_schema_query_params(
|
||||
def postprocess_schema_pagination(
|
||||
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")
|
||||
to_replace = {
|
||||
"ordering": create_component(
|
||||
generator,
|
||||
"QueryPaginationOrdering",
|
||||
{
|
||||
"name": "ordering",
|
||||
"required": False,
|
||||
"in": "query",
|
||||
"description": "Which field to use when ordering the results.",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
ResolvedComponent.PARAMETER,
|
||||
),
|
||||
"page": create_component(
|
||||
generator,
|
||||
"QueryPaginationPage",
|
||||
{
|
||||
"name": "page",
|
||||
"required": False,
|
||||
"in": "query",
|
||||
"description": "A page number within the paginated result set.",
|
||||
"schema": {"type": "integer"},
|
||||
},
|
||||
ResolvedComponent.PARAMETER,
|
||||
),
|
||||
"page_size": create_component(
|
||||
generator,
|
||||
"QueryPaginationPageSize",
|
||||
{
|
||||
"name": "page_size",
|
||||
"required": False,
|
||||
"in": "query",
|
||||
"description": "Number of results to return per page.",
|
||||
"schema": {"type": "integer"},
|
||||
},
|
||||
ResolvedComponent.PARAMETER,
|
||||
),
|
||||
"search": create_component(
|
||||
generator,
|
||||
"QuerySearch",
|
||||
{
|
||||
"name": "search",
|
||||
"required": False,
|
||||
"in": "query",
|
||||
"description": "A search term.",
|
||||
"schema": {"type": "string"},
|
||||
},
|
||||
ResolvedComponent.PARAMETER,
|
||||
),
|
||||
}
|
||||
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
|
||||
for replace_name, replace_ref in to_replace.items():
|
||||
if param["name"] == replace_name:
|
||||
method["parameters"][idx] = replace_ref.ref
|
||||
return result
|
||||
|
||||
|
||||
@@ -91,13 +185,9 @@ def postprocess_schema_remove_unused(
|
||||
# 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:
|
||||
if raw.count(key) > 1:
|
||||
continue
|
||||
del generator.registry[(key, ResolvedComponent.SCHEMA)]
|
||||
count += 1
|
||||
LOGGER.debug("Removing unused components", count=count)
|
||||
del generator.registry._components[(key, ResolvedComponent.SCHEMA)]
|
||||
result["components"] = generator.registry.build(spectacular_settings.APPEND_COMPONENTS)
|
||||
return result
|
||||
|
||||
@@ -56,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):
|
||||
@@ -102,6 +103,7 @@ class ConfigView(APIView):
|
||||
"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"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
}
|
||||
@@ -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",
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -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
|
||||
@@ -72,15 +71,13 @@ 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
|
||||
@@ -123,12 +120,10 @@ def excluded_models() -> list[type[Model]]:
|
||||
SCIMProviderUser,
|
||||
Tenant,
|
||||
Task,
|
||||
TaskLog,
|
||||
ConnectionToken,
|
||||
AuthorizationCode,
|
||||
AccessToken,
|
||||
RefreshToken,
|
||||
ProxySession,
|
||||
Reputation,
|
||||
WebAuthnDeviceType,
|
||||
SCIMSourceUser,
|
||||
@@ -142,9 +137,6 @@ def excluded_models() -> list[type[Model]]:
|
||||
DeviceToken,
|
||||
StreamEvent,
|
||||
UserConsent,
|
||||
SAMLSession,
|
||||
Message,
|
||||
GroupChannel,
|
||||
)
|
||||
|
||||
|
||||
@@ -313,7 +305,6 @@ class Importer:
|
||||
|
||||
serializer_kwargs = {}
|
||||
model_instance = existing_models.first()
|
||||
override_serializer_instance = False
|
||||
if (
|
||||
not isinstance(model(), BaseMetaModel)
|
||||
and model_instance
|
||||
@@ -342,7 +333,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:
|
||||
@@ -365,12 +360,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):
|
||||
@@ -449,7 +438,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
|
||||
|
||||
@@ -112,6 +112,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
|
||||
|
||||
@actor(
|
||||
description=_("Find blueprints as `blueprints_find` does, but return a safe dict."),
|
||||
throws=(DatabaseError, ProgrammingError, InternalError),
|
||||
priority=PRIORITY_HIGH,
|
||||
)
|
||||
def blueprints_find_dict():
|
||||
@@ -150,7 +151,10 @@ 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()
|
||||
count = 0
|
||||
|
||||
@@ -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(
|
||||
@@ -68,6 +65,7 @@ class TestBrands(APITestCase):
|
||||
|
||||
def test_fallback(self):
|
||||
"""Test fallback brand"""
|
||||
Brand.objects.all().delete()
|
||||
self.assertJSONEqual(
|
||||
self.client.get(reverse("authentik_api:brand-current")).content.decode(),
|
||||
{
|
||||
@@ -83,109 +81,6 @@ class TestBrands(APITestCase):
|
||||
},
|
||||
)
|
||||
|
||||
@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")
|
||||
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-strong",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "foo.bar.baz",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
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")
|
||||
self.assertJSONEqual(
|
||||
self.client.get(
|
||||
reverse("authentik_api:brand-current"), HTTP_HOST="other.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-weak",
|
||||
"branding_custom_css": "",
|
||||
"matched_domain": "bar.baz",
|
||||
"ui_footer_links": [],
|
||||
"ui_theme": Themes.AUTOMATIC,
|
||||
"default_locale": "",
|
||||
"flags": self.default_flags,
|
||||
},
|
||||
)
|
||||
|
||||
def test_create_default_multiple(self):
|
||||
"""Test attempted creation of multiple default brands"""
|
||||
Brand.objects.create(
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.db.models import Case, F, IntegerField, Q, Value, When
|
||||
from django.db.models.functions import Length
|
||||
from django.db.models import F, Q
|
||||
from django.db.models import Value as V
|
||||
from django.http.request import HttpRequest
|
||||
from django.utils.html import _json_script_escapes
|
||||
from django.utils.safestring import mark_safe
|
||||
@@ -19,36 +19,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()))
|
||||
.filter(Q(host_domain__iendswith=F("domain")) | _q_default)
|
||||
.order_by("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]:
|
||||
|
||||
@@ -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
|
||||
@@ -1,8 +0,0 @@
|
||||
from django.db.migrations.autodetector import MigrationAutodetector as BaseMigrationAutodetector
|
||||
from pgtrigger.migrations import MigrationAutodetectorMixin
|
||||
|
||||
MigrationAutodetector = type(
|
||||
"MigrationAutodetector",
|
||||
(MigrationAutodetectorMixin, BaseMigrationAutodetector),
|
||||
{},
|
||||
)
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -228,19 +228,6 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
filterset_class = GroupFilter
|
||||
ordering = ["name"]
|
||||
|
||||
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")
|
||||
|
||||
|
||||
@@ -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: # noqa
|
||||
# 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)
|
||||
|
||||
@@ -15,7 +15,7 @@ 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
|
||||
@@ -29,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
|
||||
@@ -44,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
|
||||
@@ -576,26 +574,24 @@ 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
|
||||
return url % user.__dict__
|
||||
|
||||
except Exception as exc: # noqa
|
||||
LOGGER.warning("Failed to format launch url", exc=exc)
|
||||
return url
|
||||
@@ -781,12 +777,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:
|
||||
|
||||
@@ -4,7 +4,6 @@ from datetime import datetime, timedelta
|
||||
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_channels_postgres.models import GroupChannel, Message
|
||||
from django_postgres_cache.tasks import clear_expired_cache
|
||||
from dramatiq.actor import actor
|
||||
from structlog.stdlib import get_logger
|
||||
@@ -35,8 +34,6 @@ def clean_expired_models():
|
||||
LOGGER.debug("Expired models", model=cls, amount=amount)
|
||||
self.info(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
||||
clear_expired_cache()
|
||||
Message.delete_expired()
|
||||
GroupChannel.delete_expired()
|
||||
|
||||
|
||||
@actor(description=_("Remove temporary users created by SAML Sources."))
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<script data-id="authentik-config">
|
||||
"use strict";
|
||||
|
||||
<script>
|
||||
window.authentik = {
|
||||
locale: "{{ LANGUAGE_CODE }}",
|
||||
config: JSON.parse('{{ config_json|escapejs }}' || "{}"),
|
||||
brand: JSON.parse('{{ brand_json|escapejs }}' || "{}"),
|
||||
config: JSON.parse('{{ config_json|escapejs }}'),
|
||||
brand: JSON.parse('{{ brand_json|escapejs }}'),
|
||||
versionFamily: "{{ version_family }}",
|
||||
versionSubdomain: "{{ version_subdomain }}",
|
||||
build: "{{ build }}",
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="ak-c-placeholder" id="ak-placeholder" slot="placeholder">
|
||||
<span
|
||||
class="pf-c-spinner"
|
||||
role="progressbar"
|
||||
aria-valuetext="Loading..."
|
||||
>
|
||||
<span class="pf-c-spinner__clipper"></span>
|
||||
<span class="pf-c-spinner__lead-ball"></span>
|
||||
<span class="pf-c-spinner__tail-ball"></span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -3,10 +3,8 @@
|
||||
{% load authentik_core %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html
|
||||
data-theme="{% if ui_theme == "dark" %}dark{% else %}light{% endif %}"
|
||||
data-theme-choice="{% if ui_theme == "dark" %}dark{% elif ui_theme == "light" %}light{% else %}auto{% endif %}"
|
||||
>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
@@ -20,8 +18,11 @@
|
||||
|
||||
{% include "base/theme.html" %}
|
||||
|
||||
<style data-id="brand-css">{{ brand_css }}</style>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
|
||||
<style>{{ brand_css }}</style>
|
||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
{% for key, value in html_meta.items %}
|
||||
|
||||
@@ -1,45 +1,10 @@
|
||||
{% load static %}
|
||||
{% load authentik_core %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/interface-%v.css' %}" />
|
||||
|
||||
{% if ui_theme == "dark" %}
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="theme-color" content="#18191a">
|
||||
|
||||
{% elif ui_theme == "light" %}
|
||||
{% elif ui_theme == "light" %}
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
{% else %}
|
||||
<script data-id="theme-script">
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
try {
|
||||
const initialThemeChoice =
|
||||
new URLSearchParams(window.location.search).get("theme") ||
|
||||
window.localStorage?.getItem("theme");
|
||||
|
||||
const themeChoice =
|
||||
initialThemeChoice || document.documentElement.dataset.themeChoice || "auto";
|
||||
|
||||
document.documentElement.dataset.themeChoice = themeChoice;
|
||||
|
||||
if (themeChoice === "auto") {
|
||||
document.documentElement.dataset.theme = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches
|
||||
? "dark"
|
||||
: "light";
|
||||
} else {
|
||||
document.documentElement.dataset.theme = themeChoice;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to apply theme", e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
<ak-skip-to-content></ak-skip-to-content>
|
||||
<ak-message-container alignment="bottom"></ak-message-container>
|
||||
<ak-interface-admin>
|
||||
{% include "base/placeholder.html" %}
|
||||
<ak-loading></ak-loading>
|
||||
</ak-interface-admin>
|
||||
{% endblock %}
|
||||
|
||||
@@ -12,13 +12,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<div class="pf-c-form">
|
||||
<form method="POST" class="pf-c-form">
|
||||
<p>{% trans message %}</p>
|
||||
|
||||
<div class="pf-c-form__group">
|
||||
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary pf-m-block">
|
||||
{% trans 'Go home' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Go home' %}
|
||||
</a>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
<ak-skip-to-content></ak-skip-to-content>
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-user>
|
||||
{% include "base/placeholder.html" %}
|
||||
<ak-loading></ak-loading>
|
||||
</ak-interface-user>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,66 +2,82 @@
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load authentik_core %}
|
||||
|
||||
{% block head_before %}
|
||||
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||
{% include "base/header_js.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style data-id="static-styles">
|
||||
:root {
|
||||
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url }}");
|
||||
}
|
||||
<style>
|
||||
:root {
|
||||
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
|
||||
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--sm-2x: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--lg: var(--ak-flow-background);
|
||||
}
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
margin-top: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-control-static .avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: var(--pf-global--spacer--xs);
|
||||
padding-bottom: var(--pf-global--spacer--xs);
|
||||
line-height: var(--pf-global--spacer--xl);
|
||||
}
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/static-%v.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="pf-c-background-image">
|
||||
</div>
|
||||
<ak-skip-to-content></ak-skip-to-content>
|
||||
<ak-message-container></ak-message-container>
|
||||
|
||||
<div class="pf-c-page__drawer">
|
||||
<div class="pf-c-drawer pf-m-collapsed">
|
||||
<div class="pf-c-drawer__main">
|
||||
<div class="pf-c-drawer__content">
|
||||
<div class="pf-c-drawer__body">
|
||||
<div class="pf-c-login" data-layout="stacked">
|
||||
<main class="pf-c-login__main" aria-label="Authentication form">
|
||||
<div class="pf-c-login__main-header pf-c-brand">
|
||||
<img class="branding-logo" src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
|
||||
</div>
|
||||
<div class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl" data-test-id="card-title">
|
||||
{% block card_title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
<footer aria-label="Site footer" class="pf-c-login__footer pf-m-dark">
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
{% for link in footer_links %}
|
||||
<li>
|
||||
<a href="{{ link.href }}">{{ link.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
<span>
|
||||
{% trans 'Powered by authentik' %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-login stacked">
|
||||
<div class="ak-login-container">
|
||||
<main class="pf-c-login__main">
|
||||
<div class="pf-c-login__main-header pf-c-brand ak-brand">
|
||||
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
|
||||
</div>
|
||||
</div>
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block card_title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
<footer class="pf-c-login__footer">
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
{% for link in footer_links %}
|
||||
<li>
|
||||
<a href="{{ link.href }}">{{ link.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
<span>
|
||||
{% trans 'Powered by authentik' %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -82,66 +82,6 @@ class TestApplicationsAPI(APITestCase):
|
||||
self.assertEqual(self.allowed.get_meta_icon, app["meta_icon"])
|
||||
self.assertEqual(self.allowed.meta_icon.read(), b"text")
|
||||
|
||||
def test_set_icon_relative(self):
|
||||
"""Test set_icon (relative path)"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:application-set-icon-url",
|
||||
kwargs={"slug": self.allowed.slug},
|
||||
),
|
||||
data={"url": "relative/path"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.allowed.refresh_from_db()
|
||||
self.assertEqual(self.allowed.get_meta_icon, "/media/public/relative/path")
|
||||
|
||||
def test_set_icon_absolute(self):
|
||||
"""Test set_icon (absolute path)"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:application-set-icon-url",
|
||||
kwargs={"slug": self.allowed.slug},
|
||||
),
|
||||
data={"url": "/relative/path"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.allowed.refresh_from_db()
|
||||
self.assertEqual(self.allowed.get_meta_icon, "/relative/path")
|
||||
|
||||
def test_set_icon_url(self):
|
||||
"""Test set_icon (url)"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:application-set-icon-url",
|
||||
kwargs={"slug": self.allowed.slug},
|
||||
),
|
||||
data={"url": "https://authentik.company/img.png"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.allowed.refresh_from_db()
|
||||
self.assertEqual(self.allowed.get_meta_icon, "https://authentik.company/img.png")
|
||||
|
||||
def test_set_icon_fa(self):
|
||||
"""Test set_icon (url)"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse(
|
||||
"authentik_api:application-set-icon-url",
|
||||
kwargs={"slug": self.allowed.slug},
|
||||
),
|
||||
data={"url": "fa://fa-check-circle"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.allowed.refresh_from_db()
|
||||
self.assertEqual(self.allowed.get_meta_icon, "fa://fa-check-circle")
|
||||
|
||||
def test_check_access(self):
|
||||
"""Test check_access operation"""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
@@ -9,14 +9,9 @@ from django.http.response import HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_filters import FilterSet
|
||||
from django_filters.filters import BooleanFilter, MultipleChoiceFilter
|
||||
from django_filters.filters import BooleanFilter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
extend_schema,
|
||||
extend_schema_field,
|
||||
)
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import (
|
||||
@@ -38,7 +33,7 @@ from authentik.core.api.utils import ModelSerializer, PassiveSerializer
|
||||
from authentik.core.models import UserTypes
|
||||
from authentik.crypto.apps import MANAGED_KEY
|
||||
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
|
||||
from authentik.crypto.models import CertificateKeyPair, KeyType
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.rbac.decorators import permission_required
|
||||
from authentik.rbac.filters import ObjectFilter, SecretKeyFilter
|
||||
@@ -55,7 +50,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
cert_expiry = SerializerMethodField()
|
||||
cert_subject = SerializerMethodField()
|
||||
private_key_available = SerializerMethodField()
|
||||
key_type = SerializerMethodField()
|
||||
private_key_type = SerializerMethodField()
|
||||
|
||||
certificate_download_url = SerializerMethodField()
|
||||
private_key_download_url = SerializerMethodField()
|
||||
@@ -95,12 +90,14 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"""Show if this keypair has a private key configured or not"""
|
||||
return instance.key_data != "" and instance.key_data is not None
|
||||
|
||||
@extend_schema_field(ChoiceField(choices=KeyType.choices, allow_null=True))
|
||||
def get_key_type(self, instance: CertificateKeyPair) -> str | None:
|
||||
"""Get the key algorithm type from the certificate's public key"""
|
||||
def get_private_key_type(self, instance: CertificateKeyPair) -> str | None:
|
||||
"""Get the private key's type, if set"""
|
||||
if not self._should_include_details:
|
||||
return None
|
||||
return instance.key_type
|
||||
key = instance.private_key
|
||||
if key:
|
||||
return key.__class__.__name__.replace("_", "").lower().replace("privatekey", "")
|
||||
return None
|
||||
|
||||
def get_certificate_download_url(self, instance: CertificateKeyPair) -> str:
|
||||
"""Get URL to download certificate"""
|
||||
@@ -164,7 +161,7 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"cert_expiry",
|
||||
"cert_subject",
|
||||
"private_key_available",
|
||||
"key_type",
|
||||
"private_key_type",
|
||||
"certificate_download_url",
|
||||
"private_key_download_url",
|
||||
"managed",
|
||||
@@ -201,31 +198,12 @@ class CertificateKeyPairFilter(FilterSet):
|
||||
label="Only return certificate-key pairs with keys", method="filter_has_key"
|
||||
)
|
||||
|
||||
key_type = MultipleChoiceFilter(
|
||||
choices=KeyType.choices,
|
||||
label="Filter by key algorithm type",
|
||||
method="filter_key_type",
|
||||
)
|
||||
|
||||
def filter_has_key(self, queryset, name, value): # pragma: no cover
|
||||
"""Only return certificate-key pairs with keys"""
|
||||
if not value:
|
||||
return queryset
|
||||
return queryset.exclude(key_data__exact="")
|
||||
|
||||
def filter_key_type(self, queryset, name, value): # pragma: no cover
|
||||
"""Filter certificates by key type using the public key from the certificate"""
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
# value is a list of KeyType enum values from MultipleChoiceFilter
|
||||
filtered_pks = []
|
||||
for cert in queryset:
|
||||
if cert.key_type in value:
|
||||
filtered_pks.append(cert.pk)
|
||||
|
||||
return queryset.filter(pk__in=filtered_pks)
|
||||
|
||||
class Meta:
|
||||
model = CertificateKeyPair
|
||||
fields = ["name", "managed"]
|
||||
@@ -250,17 +228,6 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||
required=False,
|
||||
description="Only return certificate-key pairs with keys",
|
||||
),
|
||||
OpenApiParameter(
|
||||
"key_type",
|
||||
OpenApiTypes.STR,
|
||||
required=False,
|
||||
many=True,
|
||||
enum=[choice[0] for choice in KeyType.choices],
|
||||
description=(
|
||||
"Filter by key algorithm type (RSA, EC, DSA, etc). "
|
||||
"Can be specified multiple times (e.g. '?key_type=rsa&key_type=ec')"
|
||||
),
|
||||
),
|
||||
OpenApiParameter("include_details", bool, default=True),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from dramatiq.broker import get_broker
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.time import fqdn_rand
|
||||
@@ -72,12 +70,6 @@ class AuthentikCryptoConfig(ManagedAppConfig):
|
||||
},
|
||||
)
|
||||
|
||||
@ManagedAppConfig.reconcile_global
|
||||
def tasks_middlewares(self):
|
||||
from authentik.crypto.tasks import CertificateWatcherMiddleware
|
||||
|
||||
get_broker().add_middleware(CertificateWatcherMiddleware())
|
||||
|
||||
@property
|
||||
def tenant_schedule_specs(self) -> list[ScheduleSpec]:
|
||||
from authentik.crypto.tasks import certificate_discovery
|
||||
|
||||
@@ -6,11 +6,6 @@ from uuid import uuid4
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes, PublicKeyTypes
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.x509 import Certificate, load_pem_x509_certificate
|
||||
@@ -25,16 +20,6 @@ from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class KeyType(models.TextChoices):
|
||||
"""Cryptographic key algorithm types"""
|
||||
|
||||
RSA = "rsa", _("RSA")
|
||||
EC = "ec", _("Elliptic Curve")
|
||||
DSA = "dsa", _("DSA")
|
||||
ED25519 = "ed25519", _("Ed25519")
|
||||
ED448 = "ed448", _("Ed448")
|
||||
|
||||
|
||||
def fingerprint_sha256(cert: Certificate) -> str:
|
||||
"""Get SHA256 Fingerprint of certificate"""
|
||||
return hexlify(cert.fingerprint(hashes.SHA256()), ":").decode("utf-8")
|
||||
@@ -118,22 +103,6 @@ class CertificateKeyPair(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
||||
else ""
|
||||
) # nosec
|
||||
|
||||
@property
|
||||
def key_type(self) -> str | None:
|
||||
"""Get the key algorithm type from the certificate's public key"""
|
||||
public_key = self.certificate.public_key()
|
||||
if isinstance(public_key, RSAPublicKey):
|
||||
return KeyType.RSA
|
||||
if isinstance(public_key, EllipticCurvePublicKey):
|
||||
return KeyType.EC
|
||||
if isinstance(public_key, DSAPublicKey):
|
||||
return KeyType.DSA
|
||||
if isinstance(public_key, Ed25519PublicKey):
|
||||
return KeyType.ED25519
|
||||
if isinstance(public_key, Ed448PublicKey):
|
||||
return KeyType.ED448
|
||||
return None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Certificate-Key Pair {self.name}"
|
||||
|
||||
|
||||
@@ -2,29 +2,17 @@
|
||||
|
||||
from glob import glob
|
||||
from pathlib import Path
|
||||
from sys import platform
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.x509.base import load_pem_x509_certificate
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dramatiq.actor import actor
|
||||
from dramatiq.middleware import Middleware
|
||||
from structlog.stdlib import get_logger
|
||||
from watchdog.events import (
|
||||
FileCreatedEvent,
|
||||
FileModifiedEvent,
|
||||
FileSystemEvent,
|
||||
FileSystemEventHandler,
|
||||
)
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.tasks.middleware import CurrentTask
|
||||
from authentik.tasks.schedules.models import Schedule
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -47,65 +35,6 @@ def ensure_certificate_valid(body: str):
|
||||
return body
|
||||
|
||||
|
||||
class CertificateWatcherMiddleware(Middleware):
|
||||
"""Middleware to start certificate file watcher"""
|
||||
|
||||
def start_certificate_watcher(self):
|
||||
"""Start certificate file watcher"""
|
||||
observer = Observer()
|
||||
kwargs = {}
|
||||
if platform.startswith("linux"):
|
||||
kwargs["event_filter"] = (FileCreatedEvent, FileModifiedEvent)
|
||||
observer.schedule(
|
||||
CertificateEventHandler(),
|
||||
CONFIG.get("cert_discovery_dir"),
|
||||
recursive=True,
|
||||
**kwargs,
|
||||
)
|
||||
observer.start()
|
||||
|
||||
def after_worker_boot(self, broker, worker):
|
||||
if not settings.TEST:
|
||||
self.start_certificate_watcher()
|
||||
|
||||
|
||||
class CertificateEventHandler(FileSystemEventHandler):
|
||||
"""Event handler for certificate file events"""
|
||||
|
||||
# We only ever get creation and modification events.
|
||||
# See the creation of the Observer instance above for the event filtering.
|
||||
|
||||
# Even though we filter to only get file events, we might still get
|
||||
# directory events as some implementations such as inotify do not support
|
||||
# filtering on file/directory.
|
||||
|
||||
def dispatch(self, event: FileSystemEvent) -> None:
|
||||
"""Call specific event handler method. Ignores directory changes."""
|
||||
if event.is_directory:
|
||||
return None
|
||||
return super().dispatch(event)
|
||||
|
||||
def on_created(self, event: FileSystemEvent):
|
||||
"""Process certificate file creation"""
|
||||
LOGGER.debug(
|
||||
"Certificate file created, triggering discovery",
|
||||
file=event.src_path,
|
||||
)
|
||||
for tenant in Tenant.objects.filter(ready=True):
|
||||
with tenant:
|
||||
Schedule.dispatch_by_actor(certificate_discovery)
|
||||
|
||||
def on_modified(self, event: FileSystemEvent):
|
||||
"""Process certificate file modification"""
|
||||
LOGGER.debug(
|
||||
"Certificate file modified, triggering discovery",
|
||||
file=event.src_path,
|
||||
)
|
||||
for tenant in Tenant.objects.filter(ready=True):
|
||||
with tenant:
|
||||
Schedule.dispatch_by_actor(certificate_discovery)
|
||||
|
||||
|
||||
@actor(description=_("Discover, import and update certificates from the filesystem."))
|
||||
def certificate_discovery():
|
||||
self = CurrentTask.get_task()
|
||||
@@ -137,35 +66,12 @@ def certificate_discovery():
|
||||
except (OSError, ValueError) as exc:
|
||||
LOGGER.warning("Failed to open file or invalid format", exc=exc, file=path)
|
||||
for name, cert_data in certs.items():
|
||||
# First, try to find by filename-based managed field
|
||||
cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first()
|
||||
|
||||
# If not found by filename and we have a private key, check for existing key match
|
||||
if not cert and name in private_keys:
|
||||
existing_with_key = (
|
||||
CertificateKeyPair.objects.filter(
|
||||
managed__startswith="goauthentik.io/crypto/discovered/",
|
||||
key_data=private_keys[name],
|
||||
)
|
||||
.exclude(key_data="")
|
||||
.first()
|
||||
)
|
||||
if existing_with_key:
|
||||
cert = existing_with_key
|
||||
# Update name and managed field to reflect the new filename
|
||||
if cert.name != name:
|
||||
cert.name = name
|
||||
cert.managed = MANAGED_DISCOVERED % name
|
||||
cert.save()
|
||||
|
||||
# Create new certificate if not found
|
||||
if not cert:
|
||||
cert = CertificateKeyPair(
|
||||
name=name,
|
||||
managed=MANAGED_DISCOVERED % name,
|
||||
)
|
||||
|
||||
# Update certificate data if changed
|
||||
dirty = False
|
||||
if cert.certificate_data != cert_data:
|
||||
cert.certificate_data = cert_data
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from json import loads
|
||||
from os import makedirs
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from cryptography.x509.extensions import SubjectAlternativeName
|
||||
@@ -320,8 +319,6 @@ class TestCrypto(APITestCase):
|
||||
|
||||
def test_discovery(self):
|
||||
"""Test certificate discovery"""
|
||||
# This test generates 2 separate cert/key combinations
|
||||
# and verifies they both import properly
|
||||
name = generate_id()
|
||||
builder = CertificateBuilder(name)
|
||||
with self.assertRaises(ValueError):
|
||||
@@ -330,15 +327,6 @@ class TestCrypto(APITestCase):
|
||||
subject_alt_names=[],
|
||||
validity_days=3,
|
||||
)
|
||||
|
||||
name2 = generate_id()
|
||||
builder2 = CertificateBuilder(name2)
|
||||
with self.assertRaises(ValueError):
|
||||
builder2.save()
|
||||
builder2.build(
|
||||
subject_alt_names=[],
|
||||
validity_days=3,
|
||||
)
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
with open(f"{temp_dir}/foo.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
@@ -346,9 +334,9 @@ class TestCrypto(APITestCase):
|
||||
_key.write(builder.private_key)
|
||||
makedirs(f"{temp_dir}/foo.bar", exist_ok=True)
|
||||
with open(f"{temp_dir}/foo.bar/fullchain.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder2.certificate)
|
||||
_cert.write(builder.certificate)
|
||||
with open(f"{temp_dir}/foo.bar/privkey.pem", "w+", encoding="utf-8") as _key:
|
||||
_key.write(builder2.private_key)
|
||||
_key.write(builder.private_key)
|
||||
with CONFIG.patch("cert_discovery_dir", temp_dir):
|
||||
certificate_discovery.send()
|
||||
keypair: CertificateKeyPair = CertificateKeyPair.objects.filter(
|
||||
@@ -360,58 +348,3 @@ class TestCrypto(APITestCase):
|
||||
self.assertTrue(
|
||||
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists()
|
||||
)
|
||||
|
||||
def test_discovery_updating_same_private_key(self):
|
||||
"""Test certificate discovery updating certs with matching private keys"""
|
||||
name = generate_id()
|
||||
builder = CertificateBuilder(name)
|
||||
builder.build(
|
||||
subject_alt_names=[],
|
||||
validity_days=3,
|
||||
)
|
||||
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
# First discovery: write cert as "original"
|
||||
with open(f"{temp_dir}/original.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
with open(f"{temp_dir}/original.key", "w+", encoding="utf-8") as _key:
|
||||
_key.write(builder.private_key)
|
||||
|
||||
with CONFIG.patch("cert_discovery_dir", temp_dir):
|
||||
certificate_discovery.send()
|
||||
|
||||
# Verify "original" cert was created
|
||||
original = CertificateKeyPair.objects.filter(
|
||||
managed=MANAGED_DISCOVERED % "original"
|
||||
).first()
|
||||
self.assertIsNotNone(original)
|
||||
self.assertEqual(original.name, "original")
|
||||
self.assertIsNotNone(original.private_key)
|
||||
|
||||
# Second discovery: write same cert/key as "renamed"
|
||||
Path(f"{temp_dir}/original.pem").unlink()
|
||||
Path(f"{temp_dir}/original.key").unlink()
|
||||
|
||||
with open(f"{temp_dir}/renamed.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
with open(f"{temp_dir}/renamed.key", "w+", encoding="utf-8") as _key:
|
||||
_key.write(builder.private_key)
|
||||
|
||||
with CONFIG.patch("cert_discovery_dir", temp_dir):
|
||||
certificate_discovery.send()
|
||||
|
||||
# Verify the cert was updated
|
||||
renamed = CertificateKeyPair.objects.filter(
|
||||
managed=MANAGED_DISCOVERED % "renamed"
|
||||
).first()
|
||||
self.assertIsNotNone(renamed, "Renamed certificate should exist")
|
||||
self.assertEqual(renamed.name, "renamed")
|
||||
self.assertEqual(renamed.pk, original.pk, "Should be same database object")
|
||||
|
||||
# Verify no new cert was created
|
||||
final_count = CertificateKeyPair.objects.filter(
|
||||
managed__startswith="goauthentik.io/crypto/discovered/"
|
||||
).count()
|
||||
self.assertEqual(
|
||||
1, final_count, "Should not create duplicate cert for same private key"
|
||||
)
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
"""Enterprise app config"""
|
||||
|
||||
from django.conf import settings
|
||||
from prometheus_client import Gauge
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.utils.time import fqdn_rand
|
||||
from authentik.tasks.schedules.common import ScheduleSpec
|
||||
|
||||
GAUGE_LICENSE_USAGE = Gauge(
|
||||
"authentik_enterprise_license_usage",
|
||||
"Enterprise license usage (percentage per user type).",
|
||||
["user_type"],
|
||||
)
|
||||
GAUGE_LICENSE_EXPIRY = Gauge(
|
||||
"authentik_enterprise_license_expiry_seconds", "Duration until license expires, in seconds."
|
||||
)
|
||||
|
||||
|
||||
class EnterpriseConfig(ManagedAppConfig):
|
||||
"""Base app config for all enterprise apps"""
|
||||
|
||||
@@ -217,7 +217,7 @@ class LicenseKey:
|
||||
def summary(self) -> LicenseSummary:
|
||||
"""Summary of license status"""
|
||||
status = self.status()
|
||||
latest_valid = datetime.fromtimestamp(self.exp).replace(tzinfo=UTC)
|
||||
latest_valid = datetime.fromtimestamp(self.exp)
|
||||
return LicenseSummary(
|
||||
latest_valid=latest_valid,
|
||||
internal_users=self.internal_users,
|
||||
|
||||
@@ -37,8 +37,6 @@ class GoogleWorkspaceProviderSerializer(EnterpriseRequiredMixin, ProviderSeriali
|
||||
"user_delete_action",
|
||||
"group_delete_action",
|
||||
"default_group_email_domain",
|
||||
"sync_page_size",
|
||||
"sync_page_timeout",
|
||||
"dry_run",
|
||||
]
|
||||
extra_kwargs = {}
|
||||
|
||||
@@ -25,7 +25,7 @@ class GoogleWorkspaceGroupClient(
|
||||
"""Google client for groups"""
|
||||
|
||||
connection_type = GoogleWorkspaceProviderGroup
|
||||
connection_type_query = "group"
|
||||
connection_attr = "googleworkspaceprovidergroup_set"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: GoogleWorkspaceProvider) -> None:
|
||||
@@ -208,11 +208,11 @@ class GoogleWorkspaceGroupClient(
|
||||
)
|
||||
if not matching_authentik_group:
|
||||
return
|
||||
GoogleWorkspaceProviderGroup.objects.update_or_create(
|
||||
GoogleWorkspaceProviderGroup.objects.get_or_create(
|
||||
provider=self.provider,
|
||||
group=matching_authentik_group,
|
||||
google_id=google_id,
|
||||
defaults={"attributes": group},
|
||||
attributes=group,
|
||||
)
|
||||
|
||||
def update_single_attribute(self, connection: GoogleWorkspaceProviderUser):
|
||||
|
||||
@@ -20,7 +20,7 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
|
||||
"""Sync authentik users into google workspace"""
|
||||
|
||||
connection_type = GoogleWorkspaceProviderUser
|
||||
connection_type_query = "user"
|
||||
connection_attr = "googleworkspaceprovideruser_set"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: GoogleWorkspaceProvider) -> None:
|
||||
@@ -113,11 +113,11 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
|
||||
matching_authentik_user = self.provider.get_object_qs(User).filter(email=email).first()
|
||||
if not matching_authentik_user:
|
||||
return
|
||||
GoogleWorkspaceProviderUser.objects.update_or_create(
|
||||
GoogleWorkspaceProviderUser.objects.get_or_create(
|
||||
provider=self.provider,
|
||||
user=matching_authentik_user,
|
||||
google_id=email,
|
||||
defaults={"attributes": user},
|
||||
attributes=user,
|
||||
)
|
||||
|
||||
def update_single_attribute(self, connection: GoogleWorkspaceProviderUser):
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-21 12:35
|
||||
|
||||
import authentik.lib.utils.time
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_google_workspace", "0004_googleworkspaceprovider_dry_run"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="googleworkspaceprovider",
|
||||
name="sync_page_size",
|
||||
field=models.PositiveIntegerField(
|
||||
default=100,
|
||||
help_text="Controls the number of objects synced in a single task",
|
||||
validators=[django.core.validators.MinValueValidator(1)],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="googleworkspaceprovider",
|
||||
name="sync_page_timeout",
|
||||
field=models.TextField(
|
||||
default="minutes=30",
|
||||
help_text="Timeout for synchronization of a single page",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -139,7 +139,11 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
if type == User:
|
||||
# Get queryset of all users with consistent ordering
|
||||
# according to the provider's settings
|
||||
base = User.objects.all().exclude_anonymous()
|
||||
base = (
|
||||
User.objects.prefetch_related("googleworkspaceprovideruser_set")
|
||||
.all()
|
||||
.exclude_anonymous()
|
||||
)
|
||||
if self.exclude_users_service_account:
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
@@ -149,7 +153,11 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
return base.order_by("pk")
|
||||
if type == Group:
|
||||
# Get queryset of all groups with consistent ordering
|
||||
return Group.objects.all().order_by("pk")
|
||||
return (
|
||||
Group.objects.prefetch_related("googleworkspaceprovidergroup_set")
|
||||
.all()
|
||||
.order_by("pk")
|
||||
)
|
||||
raise ValueError(f"Invalid type {type}")
|
||||
|
||||
def google_credentials(self):
|
||||
|
||||
@@ -292,7 +292,7 @@ class GoogleWorkspaceGroupTests(TestCase):
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_sync_discover(self):
|
||||
def test_sync_task(self):
|
||||
"""Test group discovery"""
|
||||
uid = generate_id()
|
||||
http = MockHTTP()
|
||||
@@ -332,57 +332,3 @@ class GoogleWorkspaceGroupTests(TestCase):
|
||||
)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
self.assertEqual(len(http.requests()), 5)
|
||||
|
||||
def test_sync_discover_multiple(self):
|
||||
"""Test group discovery"""
|
||||
uid = generate_id()
|
||||
http = MockHTTP()
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
|
||||
domains_list_v1_mock,
|
||||
)
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
|
||||
method="GET",
|
||||
body={"users": []},
|
||||
)
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
|
||||
method="GET",
|
||||
body={"groups": [{"id": uid, "name": uid}]},
|
||||
)
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/groups/{uid}?key={self.api_key}&alt=json",
|
||||
method="PUT",
|
||||
body={"id": uid},
|
||||
)
|
||||
self.app.backchannel_providers.remove(self.provider)
|
||||
different_group = Group.objects.create(
|
||||
name=uid,
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
with patch(
|
||||
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
|
||||
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
|
||||
):
|
||||
google_workspace_sync.send(self.provider.pk).get_result()
|
||||
self.assertTrue(
|
||||
GoogleWorkspaceProviderGroup.objects.filter(
|
||||
group=different_group, provider=self.provider
|
||||
).exists()
|
||||
)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
self.assertEqual(len(http.requests()), 5)
|
||||
# Change response to trigger update
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
|
||||
method="GET",
|
||||
body={"groups": [{"id": uid, "name": uid, "bar": "baz"}]},
|
||||
)
|
||||
google_workspace_sync.send(self.provider.pk).get_result()
|
||||
self.assertTrue(
|
||||
GoogleWorkspaceProviderGroup.objects.filter(
|
||||
group=different_group, provider=self.provider
|
||||
).exists()
|
||||
)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
|
||||
@@ -269,7 +269,7 @@ class GoogleWorkspaceUserTests(TestCase):
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_sync_discover(self):
|
||||
def test_sync_task(self):
|
||||
"""Test user discovery"""
|
||||
uid = generate_id()
|
||||
http = MockHTTP()
|
||||
@@ -310,63 +310,3 @@ class GoogleWorkspaceUserTests(TestCase):
|
||||
)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
self.assertEqual(len(http.requests()), 5)
|
||||
|
||||
def test_sync_discover_multiple(self):
|
||||
"""Test user discovery, running multiple times"""
|
||||
uid = generate_id()
|
||||
http = MockHTTP()
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
|
||||
domains_list_v1_mock,
|
||||
)
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
|
||||
method="GET",
|
||||
body={"users": [{"primaryEmail": f"{uid}@goauthentik.io"}]},
|
||||
)
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
|
||||
method="GET",
|
||||
body={"groups": []},
|
||||
)
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/users/{uid}%40goauthentik.io?key={self.api_key}&alt=json",
|
||||
method="PUT",
|
||||
body={"primaryEmail": f"{uid}@goauthentik.io"},
|
||||
)
|
||||
self.app.backchannel_providers.remove(self.provider)
|
||||
different_user = User.objects.create(
|
||||
username=uid,
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
self.app.backchannel_providers.add(self.provider)
|
||||
# Sync once
|
||||
with patch(
|
||||
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
|
||||
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
|
||||
):
|
||||
google_workspace_sync.send(self.provider.pk).get_result()
|
||||
self.assertTrue(
|
||||
GoogleWorkspaceProviderUser.objects.filter(
|
||||
user=different_user, provider=self.provider
|
||||
).exists()
|
||||
)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
self.assertEqual(len(http.requests()), 5)
|
||||
# Change response, which will trigger a discovery update
|
||||
http.add_response(
|
||||
f"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
|
||||
method="GET",
|
||||
body={
|
||||
"users": [
|
||||
{"primaryEmail": f"{uid}@goauthentik.io", "foo": "bar"},
|
||||
]
|
||||
},
|
||||
)
|
||||
google_workspace_sync.send(self.provider.pk).get_result()
|
||||
self.assertTrue(
|
||||
GoogleWorkspaceProviderUser.objects.filter(
|
||||
user=different_user, provider=self.provider
|
||||
).exists()
|
||||
)
|
||||
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
|
||||
|
||||
@@ -36,8 +36,6 @@ class MicrosoftEntraProviderSerializer(EnterpriseRequiredMixin, ProviderSerializ
|
||||
"filter_group",
|
||||
"user_delete_action",
|
||||
"group_delete_action",
|
||||
"sync_page_size",
|
||||
"sync_page_timeout",
|
||||
"dry_run",
|
||||
]
|
||||
extra_kwargs = {}
|
||||
|
||||
@@ -29,7 +29,7 @@ class MicrosoftEntraGroupClient(
|
||||
"""Microsoft client for groups"""
|
||||
|
||||
connection_type = MicrosoftEntraProviderGroup
|
||||
connection_type_query = "group"
|
||||
connection_attr = "microsoftentraprovidergroup_set"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: MicrosoftEntraProvider) -> None:
|
||||
@@ -220,11 +220,11 @@ class MicrosoftEntraGroupClient(
|
||||
)
|
||||
if not matching_authentik_group:
|
||||
return
|
||||
MicrosoftEntraProviderGroup.objects.update_or_create(
|
||||
MicrosoftEntraProviderGroup.objects.get_or_create(
|
||||
provider=self.provider,
|
||||
group=matching_authentik_group,
|
||||
microsoft_id=group.id,
|
||||
defaults={"attributes": self.entity_as_dict(group)},
|
||||
attributes=self.entity_as_dict(group),
|
||||
)
|
||||
|
||||
def update_single_attribute(self, connection: MicrosoftEntraProviderGroup):
|
||||
|
||||
@@ -24,7 +24,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
|
||||
"""Sync authentik users into microsoft entra"""
|
||||
|
||||
connection_type = MicrosoftEntraProviderUser
|
||||
connection_type_query = "user"
|
||||
connection_attr = "microsoftentraprovideruser_set"
|
||||
can_discover = True
|
||||
|
||||
def __init__(self, provider: MicrosoftEntraProvider) -> None:
|
||||
@@ -159,11 +159,11 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
|
||||
matching_authentik_user = self.provider.get_object_qs(User).filter(email=user.mail).first()
|
||||
if not matching_authentik_user:
|
||||
return
|
||||
MicrosoftEntraProviderUser.objects.update_or_create(
|
||||
MicrosoftEntraProviderUser.objects.get_or_create(
|
||||
provider=self.provider,
|
||||
user=matching_authentik_user,
|
||||
microsoft_id=user.id,
|
||||
defaults={"attributes": self.entity_as_dict(user)},
|
||||
attributes=self.entity_as_dict(user),
|
||||
)
|
||||
|
||||
def update_single_attribute(self, connection: MicrosoftEntraProviderUser):
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-21 12:35
|
||||
|
||||
import authentik.lib.utils.time
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_microsoft_entra", "0003_microsoftentraprovider_dry_run"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="microsoftentraprovider",
|
||||
name="sync_page_size",
|
||||
field=models.PositiveIntegerField(
|
||||
default=100,
|
||||
help_text="Controls the number of objects synced in a single task",
|
||||
validators=[django.core.validators.MinValueValidator(1)],
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="microsoftentraprovider",
|
||||
name="sync_page_timeout",
|
||||
field=models.TextField(
|
||||
default="minutes=30",
|
||||
help_text="Timeout for synchronization of a single page",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -128,7 +128,11 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
if type == User:
|
||||
# Get queryset of all users with consistent ordering
|
||||
# according to the provider's settings
|
||||
base = User.objects.all().exclude_anonymous()
|
||||
base = (
|
||||
User.objects.prefetch_related("microsoftentraprovideruser_set")
|
||||
.all()
|
||||
.exclude_anonymous()
|
||||
)
|
||||
if self.exclude_users_service_account:
|
||||
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
|
||||
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
|
||||
@@ -138,7 +142,11 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
|
||||
return base.order_by("pk")
|
||||
if type == Group:
|
||||
# Get queryset of all groups with consistent ordering
|
||||
return Group.objects.all().order_by("pk")
|
||||
return (
|
||||
Group.objects.prefetch_related("microsoftentraprovidergroup_set")
|
||||
.all()
|
||||
.order_by("pk")
|
||||
)
|
||||
raise ValueError(f"Invalid type {type}")
|
||||
|
||||
def microsoft_credentials(self):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user