Compare commits

..

18 Commits

Author SHA1 Message Date
Jens Langhammer
918355e1c9 release: 2023.4.3 2023-07-06 18:15:11 +02:00
Jens Langhammer
c07a48a3ec security: fix CVE-2023-36456
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	website/sidebars.js
2023-07-06 18:13:19 +02:00
Jens Langhammer
e1bae1240f release: 2023.4.2 2023-06-22 22:21:53 +02:00
risson
37bd62d291 ci: replace github bot account with github app (#5819)
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2023-06-22 22:21:48 +02:00
Jens Langhammer
ac63db0136 bump web api client
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-22 21:27:30 +02:00
Jens Langhammer
5cdf3a09a9 ATH-01-012: escape quotation marks
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:45:47 +02:00
Jens Langhammer
3e17adf33f ATH-01-014: save authenticator validation state in flow context
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

bugfixes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:45:47 +02:00
Jens Langhammer
8392916c84 ATH-01-010: rework
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:45:18 +02:00
Jens Langhammer
7e75a48fd0 ATH-01-009: migrate impersonation to use API
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	authentik/core/urls.py
#	web/src/admin/AdminInterface.ts
#	web/src/admin/users/RelatedUserList.ts
#	web/src/admin/users/UserListPage.ts
#	web/src/admin/users/UserViewPage.ts
#	web/src/user/UserInterface.ts
2023-06-19 13:45:07 +02:00
Jens Langhammer
d69d84e48c ATH-01-005: use hmac.compare_digest for secret_key authentication
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:43:09 +02:00
Jens Langhammer
78cc8fa498 ATH-01-003 / ATH-01-012: disable htmlLabels in mermaid
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:43:07 +02:00
Jens Langhammer
0fcdf5e968 ATH-01-004: remove env from admin system endpoint
this endpoint already required admin access, but for debugging the env variables are used very little

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:43:03 +02:00
Jens Langhammer
f05997740f ATH-01-008: fix web forms not submitting correctly when pressing enter
When submitting some forms with the Enter key instead of clicking "Confirm"/etc, the form would not get submitted correctly

This would in the worst case is when setting a user's password, where the new password can end up in the URL, but the password was not actually saved to the user.

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

# Conflicts:
#	web/src/admin/applications/ApplicationCheckAccessForm.ts
#	web/src/admin/crypto/CertificateGenerateForm.ts
#	web/src/admin/flows/FlowImportForm.ts
#	web/src/admin/groups/RelatedGroupList.ts
#	web/src/admin/policies/PolicyTestForm.ts
#	web/src/admin/property-mappings/PropertyMappingTestForm.ts
#	web/src/admin/providers/saml/SAMLProviderImportForm.ts
#	web/src/admin/users/RelatedUserList.ts
#	web/src/admin/users/ServiceAccountForm.ts
#	web/src/admin/users/UserPasswordForm.ts
#	web/src/admin/users/UserResetEmailForm.ts
2023-06-19 13:42:51 +02:00
Jens Langhammer
1aff300171 ATH-01-010: fix missing user filter for webauthn device
This prevents an attack that is only possible when an attacker can intercept HTTP traffic and in the case of HTTPS decrypt it.
2023-06-19 13:38:31 +02:00
Jens Langhammer
ffb98eaa75 ATH-01-001: resolve path and check start before loading blueprints
This is even less of an issue since 411ef239f6, since with that commit we only allow files that the listing returns

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-06-19 13:38:19 +02:00
Jens Langhammer
5c1db432f0 release: 2023.4.1 2023-04-18 10:50:44 +03:00
Jens Langhammer
07fd4daa3e Merge branch 'main' into version-2023.4 2023-04-17 22:46:09 +03:00
Jens Langhammer
aa80babfff release: 2023.4.0 2023-04-14 13:28:57 +03:00
179 changed files with 1811 additions and 2504 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2023.4.1
current_version = 2023.4.3
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@@ -6,4 +6,3 @@ dist/**
build/**
build_docs/**
Dockerfile
authentik/enterprise

View File

@@ -1,9 +1,10 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
title: ''
labels: bug
assignees: ""
assignees: ''
---
**Describe the bug**
@@ -11,7 +12,6 @@ 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 '....'
@@ -27,9 +27,8 @@ If applicable, add screenshots to help explain your problem.
Output of docker-compose logs or kubectl logs respectively
**Version and Deployment (please complete the following information):**
- authentik version: [e.g. 2021.8.5]
- Deployment: [e.g. docker-compose, helm]
- authentik version: [e.g. 2021.8.5]
- Deployment: [e.g. docker-compose, helm]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,9 +1,10 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
title: ''
labels: enhancement
assignees: ""
assignees: ''
---
**Is your feature request related to a problem? Please describe.**

View File

@@ -1,9 +1,10 @@
---
name: Question
about: Ask a question about a feature or specific configuration
title: ""
title: ''
labels: question
assignees: ""
assignees: ''
---
**Describe your question/**
@@ -19,9 +20,8 @@ If applicable, add screenshots to help explain your problem.
Output of docker-compose logs or kubectl logs respectively
**Version and Deployment (please complete the following information):**
- authentik version: [e.g. 2021.8.5]
- Deployment: [e.g. docker-compose, helm]
- authentik version: [e.g. 2021.8.5]
- Deployment: [e.g. docker-compose, helm]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,5 +1,5 @@
name: "Comment usage instructions on PRs"
description: "Comment usage instructions on PRs"
name: 'Comment usage instructions on PRs'
description: 'Comment usage instructions on PRs'
inputs:
tag:
@@ -17,7 +17,7 @@ runs:
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
comment-author: 'github-actions[bot]'
body-includes: authentik PR Installation instructions
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v2

View File

@@ -1,5 +1,5 @@
name: "Prepare docker environment variables"
description: "Prepare docker environment variables"
name: 'Prepare docker environment variables'
description: 'Prepare docker environment variables'
outputs:
shouldBuild:
@@ -51,14 +51,12 @@ runs:
version_family = ".".join(version.split(".")[:-1])
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
sha = os.environ["GITHUB_SHA"] if not "${{ github.event.pull_request.head.sha }}" else "${{ github.event.pull_request.head.sha }}"
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print("branchName=%s" % branch_name, file=_output)
print("branchNameContainer=%s" % safe_branch_name, file=_output)
print("timestamp=%s" % int(time()), file=_output)
print("sha=%s" % sha, file=_output)
print("shortHash=%s" % sha[:7], file=_output)
print("sha=%s" % os.environ["GITHUB_SHA"], file=_output)
print("shortHash=%s" % os.environ["GITHUB_SHA"][:7], file=_output)
print("shouldBuild=%s" % should_build, file=_output)
print("version=%s" % version, file=_output)
print("versionFamily=%s" % version_family, file=_output)

View File

@@ -1,5 +1,5 @@
name: "Setup authentik testing environment"
description: "Setup authentik testing environment"
name: 'Setup authentik testing environment'
description: 'Setup authentik testing environment'
inputs:
postgresql_tag:
@@ -18,13 +18,13 @@ runs:
- name: Setup python and restore poetry
uses: actions/setup-python@v3
with:
python-version: "3.11"
cache: "poetry"
python-version: '3.11'
cache: 'poetry'
- name: Setup node
uses: actions/setup-node@v3
uses: actions/setup-node@v3.1.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Setup dependencies
shell: bash

View File

@@ -1,21 +1,23 @@
version: "3.7"
version: '3.7'
services:
postgresql:
image: docker.io/library/postgres:${PSQL_TAG:-12}
container_name: postgres
image: library/postgres:${PSQL_TAG:-12}
volumes:
- db-data:/var/lib/postgresql/data
- db-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: authentik
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
POSTGRES_DB: authentik
ports:
- 5432:5432
- 5432:5432
restart: always
redis:
image: docker.io/library/redis
container_name: redis
image: library/redis
ports:
- 6379:6379
- 6379:6379
restart: always
volumes:

120
.github/dependabot.yml vendored
View File

@@ -1,62 +1,62 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "ci:"
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "core:"
- package-ecosystem: npm
directory: "/web"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "web:"
- package-ecosystem: npm
directory: "/website"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "website:"
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "core:"
- package-ecosystem: docker
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "core:"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "ci:"
- package-ecosystem: gomod
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "core:"
- package-ecosystem: npm
directory: "/web"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "web:"
- package-ecosystem: npm
directory: "/website"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "website:"
- package-ecosystem: pip
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "core:"
- package-ecosystem: docker
directory: "/"
schedule:
interval: daily
time: "04:00"
open-pull-requests-limit: 10
reviewers:
- "@goauthentik/core"
commit-message:
prefix: "core:"

View File

@@ -5,20 +5,15 @@ Please check the [Contributing guidelines](https://github.com/goauthentik/authen
-->
# Details
- **Does this resolve an issue?**
Resolves #
* **Does this resolve an issue?**
Resolves #
## Changes
### New Features
- Adds feature which does x, y, and z.
* Adds feature which does x, y, and z.
### Breaking Changes
- Adds breaking change which causes \<issue\>.
* Adds breaking change which causes \<issue\>.
## Additional
Any further notes or comments you want to make.

View File

@@ -6,11 +6,11 @@ git:
source_language: en
source_file: web/src/locales/en.po
# path expression to translation files, must contain <lang> placeholder
translation_files_expression: "web/src/locales/<lang>.po"
translation_files_expression: 'web/src/locales/<lang>.po'
- filter_type: file
# all supported i18n types: https://docs.transifex.com/formats
file_format: PO
source_language: en
source_file: locale/en/LC_MESSAGES/django.po
# path expression to translation files, must contain <lang> placeholder
translation_files_expression: "locale/<lang>/LC_MESSAGES/django.po"
translation_files_expression: 'locale/<lang>/LC_MESSAGES/django.po'

View File

@@ -23,14 +23,13 @@ jobs:
fail-fast: false
matrix:
job:
- bandit
- black
- codespell
- isort
- pending-migrations
- pylint
- black
- isort
- bandit
- pyright
- ruff
- pending-migrations
- codespell
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -187,8 +186,6 @@ jobs:
timeout-minutes: 120
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
@@ -214,7 +211,6 @@ jobs:
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
tags: |
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.sha }}
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }}
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
@@ -231,8 +227,6 @@ jobs:
timeout-minutes: 120
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
@@ -258,7 +252,6 @@ jobs:
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
tags: |
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-arm64
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.sha }}-arm64
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }}-arm64
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}

View File

@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
go-version: "^1.17"
- name: Prepare and generate API
run: |
# Create folder structure for go embeds
@@ -30,14 +30,13 @@ jobs:
uses: golangci/golangci-lint-action@v3
with:
args: --timeout 5000s
skip-pkg-cache: true
test-unittest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
go-version: "^1.17"
- name: Generate API
run: make gen-client-go
- name: Go unittests
@@ -61,11 +60,11 @@ jobs:
- proxy
- ldap
- radius
arch:
- "linux/amd64"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
@@ -95,7 +94,7 @@ jobs:
build-args: |
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
platforms: linux/amd64,linux/arm64
platforms: ${{ matrix.arch }}
context: .
build-binary:
timeout-minutes: 120
@@ -113,14 +112,12 @@ jobs:
goarch: [amd64, arm64]
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
go-version: "^1.17"
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
node-version: "18"
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Generate API
@@ -136,3 +133,7 @@ jobs:
export GOOS=${{ matrix.goos }}
export GOARCH=${{ matrix.goarch }}
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
- uses: actions/upload-artifact@v3
with:
name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
path: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}

View File

@@ -17,8 +17,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci
@@ -33,8 +33,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci
@@ -49,8 +49,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci
@@ -65,8 +65,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: |
@@ -97,8 +97,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci

View File

@@ -17,8 +17,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
@@ -31,8 +31,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
@@ -52,8 +52,8 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci

View File

@@ -2,11 +2,12 @@ name: "CodeQL"
on:
push:
branches: [main, "*", next, version*]
branches: [ main, '*', next, version* ]
pull_request:
branches: [main]
# The branches below must be a subset of the branches above
branches: [ main ]
schedule:
- cron: "30 6 * * 5"
- cron: '30 6 * * 5'
jobs:
analyze:
@@ -20,17 +21,40 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ["go", "javascript", "python"]
language: [ 'go', 'javascript', 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

View File

@@ -2,7 +2,7 @@ name: ghcr-retention
on:
schedule:
- cron: "0 0 * * *" # every day at midnight
- cron: '0 0 * * *' # every day at midnight
workflow_dispatch:
jobs:
@@ -10,6 +10,11 @@ jobs:
name: Delete old unused container images
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
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@v2
with:
@@ -18,5 +23,5 @@ jobs:
account-type: org
org-name: goauthentik
untagged-only: false
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
skip-tags: gh-next,gh-main

View File

@@ -57,7 +57,7 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
go-version: "^1.17"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.1.0
- name: Set up Docker Buildx
@@ -107,11 +107,11 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
go-version: "^1.17"
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
cache: "npm"
node-version: '18'
cache: 'npm'
cache-dependency-path: web/package-lock.json
- name: Build web
working-directory: web/
@@ -173,5 +173,5 @@ jobs:
SENTRY_PROJECT: authentik
with:
version: authentik@${{ steps.ev.outputs.version }}
sourcemaps: "./web/dist"
url_prefix: "~/static/dist"
sourcemaps: './web/dist'
url_prefix: '~/static/dist'

View File

@@ -3,7 +3,7 @@ name: authentik-on-tag
on:
push:
tags:
- "version/*"
- 'version/*'
jobs:
build:
@@ -22,18 +22,23 @@ jobs:
docker-compose up --no-start
docker-compose start postgresql redis
docker-compose run -u root server test-all
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Extract version number
id: get_version
uses: actions/github-script@v6
with:
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
github-token: ${{ steps.generate_token.outputs.token }}
script: |
return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
- name: Create Release
id: create_release
uses: actions/create-release@v1.1.4
env:
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ steps.get_version.outputs.result }}

View File

@@ -1,12 +1,12 @@
name: authentik-backend-translate-compile
on:
push:
branches: [main]
branches: [ main ]
paths:
- "/locale/"
- '/locale/'
pull_request:
paths:
- "/locale/"
- '/locale/'
workflow_dispatch:
env:
@@ -18,9 +18,14 @@ jobs:
compile:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v3
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
- name: Setup authentik env
uses: ./.github/actions/setup
- name: run compile
@@ -29,7 +34,7 @@ jobs:
uses: peter-evans/create-pull-request@v5
id: cpr
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
branch: compile-backend-translation
commit-message: "core: compile backend translations"
title: "core: compile backend translations"

View File

@@ -1,21 +1,26 @@
name: authentik-web-api-publish
on:
push:
branches: [main]
branches: [ main ]
paths:
- "schema.yml"
- 'schema.yml'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- uses: actions/checkout@v3
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
- uses: actions/setup-node@v3.6.0
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- name: Generate API Client
run: make gen-client-ts
- name: Publish package
@@ -33,7 +38,7 @@ jobs:
- uses: peter-evans/create-pull-request@v5
id: cpr
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
branch: update-web-api-client
commit-message: "web: bump API Client version"
title: "web: bump API Client version"
@@ -44,6 +49,6 @@ jobs:
author: authentik bot <github-bot@goauthentik.io>
- uses: peter-evans/enable-pull-request-automerge@v3
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
token: ${{ steps.generate_token.outputs.token }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
merge-method: squash

View File

@@ -1,5 +1,5 @@
# Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/node:20 as website-builder
FROM --platform=${BUILDPLATFORM} docker.io/node:18 as website-builder
COPY ./website /work/website/
COPY ./blueprints /work/blueprints/
@@ -10,7 +10,7 @@ WORKDIR /work/website
RUN npm ci && npm run build-docs-only
# Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/node:20 as web-builder
FROM --platform=${BUILDPLATFORM} docker.io/node:18 as web-builder
COPY ./web /work/web/
COPY ./website /work/website/
@@ -83,9 +83,7 @@ RUN apt-get update && \
# Required for runtime
apt-get install -y --no-install-recommends libxmlsec1-openssl libmaxminddb0 && \
# Required for bootstrap & healtcheck
apt-get install -y --no-install-recommends runit && \
# Required for outposts
apt-get install -y --no-install-recommends openssh-client && \
apt-get install -y --no-install-recommends curl runit && \
pip install --no-cache-dir -r /requirements.txt && \
apt-get remove --purge -y build-essential pkg-config libxmlsec1-dev && \
apt-get autoremove --purge -y && \
@@ -93,9 +91,8 @@ RUN apt-get update && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
mkdir -p /certs /media /blueprints && \
chown authentik:authentik /certs /media && \
chmod g+w /etc/ssh/ssh_config.d/ && \
chgrp authentik /etc/ssh/ssh_config.d/
mkdir -p /authentik/.ssh && \
chown authentik:authentik /certs /media /authentik/.ssh
COPY ./authentik/ /authentik
COPY ./pyproject.toml /

View File

@@ -1,11 +1,6 @@
Copyright (c) 2023 Jens Langhammer
MIT License
Portions of this software are licensed as follows:
* All content residing under the "website/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license".
* All content that resides under the "authentik/enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "authentik/enterprise/LICENSE".
* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license.
* All third party components incorporated into the authentik are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "MIT" license as defined below.
Copyright (c) 2022 Jens Langhammer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -3,7 +3,6 @@ PWD = $(shell pwd)
UID = $(shell id -u)
GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.npm_version)
PY_SOURCES = authentik tests scripts lifecycle
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
-I .github/codespell-words.txt \
@@ -39,14 +38,13 @@ test:
coverage report
lint-fix:
isort authentik $(PY_SOURCES)
black authentik $(PY_SOURCES)
ruff authentik $(PY_SOURCES)
isort authentik tests scripts lifecycle
black authentik tests scripts lifecycle
codespell -w $(CODESPELL_ARGS)
lint:
pylint $(PY_SOURCES)
bandit -r $(PY_SOURCES) -x node_modules
pylint authentik tests lifecycle
bandit -r authentik tests lifecycle -x node_modules
golangci-lint run -v
migrate:
@@ -173,6 +171,7 @@ website-watch:
# These targets are use by GitHub actions to allow usage of matrix
# which makes the YAML File a lot smaller
PY_SOURCES=authentik tests lifecycle
ci--meta-debug:
python -V
node --version
@@ -183,9 +182,6 @@ ci-pylint: ci--meta-debug
ci-black: ci--meta-debug
black --check $(PY_SOURCES)
ci-ruff: ci--meta-debug
ruff check $(PY_SOURCES)
ci-codespell: ci--meta-debug
codespell $(CODESPELL_ARGS) -s

View File

@@ -2,7 +2,7 @@
from os import environ
from typing import Optional
__version__ = "2023.4.1"
__version__ = "2023.4.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@@ -1,5 +1,4 @@
"""authentik administration overview"""
import os
import platform
from datetime import datetime
from sys import version as python_version
@@ -34,7 +33,6 @@ class RuntimeDict(TypedDict):
class SystemSerializer(PassiveSerializer):
"""Get system information."""
env = SerializerMethodField()
http_headers = SerializerMethodField()
http_host = SerializerMethodField()
http_is_secure = SerializerMethodField()
@@ -43,10 +41,6 @@ class SystemSerializer(PassiveSerializer):
server_time = SerializerMethodField()
embedded_outpost_host = SerializerMethodField()
def get_env(self, request: Request) -> dict[str, str]:
"""Get Environment"""
return os.environ.copy()
def get_http_headers(self, request: Request) -> dict[str, str]:
"""Get HTTP Request headers"""
headers = {}

View File

@@ -1,4 +1,5 @@
"""API Authentication"""
from hmac import compare_digest
from typing import Any, Optional
from django.conf import settings
@@ -78,7 +79,7 @@ def token_secret_key(value: str) -> Optional[User]:
and return the service account for the managed outpost"""
from authentik.outposts.apps import MANAGED_OUTPOST
if value != settings.SECRET_KEY:
if not compare_digest(value, settings.SECRET_KEY):
return None
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
if not outposts:

View File

@@ -29,7 +29,6 @@ class Capabilities(models.TextChoices):
CAN_GEO_IP = "can_geo_ip"
CAN_IMPERSONATE = "can_impersonate"
CAN_DEBUG = "can_debug"
IS_ENTERPRISE = "is_enterprise"
class ErrorReportingConfigSerializer(PassiveSerializer):
@@ -71,8 +70,6 @@ class ConfigView(APIView):
caps.append(Capabilities.CAN_IMPERSONATE)
if settings.DEBUG: # pragma: no cover
caps.append(Capabilities.CAN_DEBUG)
if "authentik.enterprise" in settings.INSTALLED_APPS:
caps.append(Capabilities.IS_ENTERPRISE)
return caps
def get_config(self) -> ConfigSerializer:

View File

@@ -6,6 +6,7 @@ from pathlib import Path
import django.contrib.postgres.fields
from dacite.core import from_dict
from django.apps.registry import Apps
from django.conf import settings
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from yaml import load
@@ -14,7 +15,7 @@ from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_SYSTEM
from authentik.lib.config import CONFIG
def check_blueprint_v1_file(BlueprintInstance: type, path: Path):
def check_blueprint_v1_file(BlueprintInstance: type["BlueprintInstance"], path: Path):
"""Check if blueprint should be imported"""
from authentik.blueprints.models import BlueprintInstanceStatus
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata

View File

@@ -82,7 +82,10 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
def retrieve_file(self) -> str:
"""Get blueprint from path"""
try:
full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path))
base = Path(CONFIG.y("blueprints_dir"))
full_path = base.joinpath(Path(self.path)).resolve()
if not str(full_path).startswith(str(base.resolve())):
raise BlueprintRetrievalFailed("Invalid blueprint path")
with full_path.open("r", encoding="utf-8") as _file:
return _file.read()
except (IOError, OSError) as exc:

View File

@@ -1,34 +1,15 @@
"""authentik managed models tests"""
from typing import Callable, Type
from django.apps import apps
from django.test import TestCase
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.lib.models import SerializerModel
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed
from authentik.lib.generators import generate_id
class TestModels(TestCase):
"""Test Models"""
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
"""Test serializer"""
def tester(self: TestModels):
if test_model._meta.abstract: # pragma: no cover
return
model_class = test_model()
self.assertTrue(isinstance(model_class, SerializerModel))
self.assertIsNotNone(model_class.serializer)
return tester
for app in apps.get_app_configs():
if not app.label.startswith("authentik"):
continue
for model in app.get_models():
if not is_model_allowed(model):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
def test_retrieve_file(self):
"""Test retrieve_file"""
instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts")
with self.assertRaises(BlueprintRetrievalFailed):
instance.retrieve()

View File

@@ -0,0 +1,34 @@
"""authentik managed models tests"""
from typing import Callable, Type
from django.apps import apps
from django.test import TestCase
from authentik.blueprints.v1.importer import is_model_allowed
from authentik.lib.models import SerializerModel
class TestModels(TestCase):
"""Test Models"""
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
"""Test serializer"""
def tester(self: TestModels):
if test_model._meta.abstract: # pragma: no cover
return
model_class = test_model()
self.assertTrue(isinstance(model_class, SerializerModel))
self.assertIsNotNone(model_class.serializer)
return tester
for app in apps.get_app_configs():
if not app.label.startswith("authentik"):
continue
for model in app.get_models():
if not is_model_allowed(model):
continue
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))

View File

@@ -67,11 +67,12 @@ from authentik.core.models import (
TokenIntents,
User,
)
from authentik.events.models import EventAction
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.lib.config import CONFIG
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@@ -543,6 +544,58 @@ class UserViewSet(UsedByMixin, ModelViewSet):
send_mails(email_stage, message)
return Response(status=204)
@permission_required("authentik_core.impersonate")
@extend_schema(
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully started impersonation"),
"401": OpenApiResponse(description="Access denied"),
},
)
@action(detail=True, methods=["POST"])
def impersonate(self, request: Request, pk: int) -> Response:
"""Impersonate a user"""
if not CONFIG.y_bool("impersonation"):
LOGGER.debug("User attempted to impersonate", user=request.user)
return Response(status=401)
if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return Response(status=401)
user_to_be = self.get_object()
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return Response(status=201)
@extend_schema(
request=OpenApiTypes.NONE,
responses={
"204": OpenApiResponse(description="Successfully started impersonation"),
},
)
@action(detail=False, methods=["GET"])
def impersonate_end(self, request: Request) -> Response:
"""End Impersonation a user"""
if (
SESSION_KEY_IMPERSONATE_USER not in request.session
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
return Response(status=204)
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
del request.session[SESSION_KEY_IMPERSONATE_USER]
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return Response(status=204)
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends):

View File

@@ -1,14 +1,14 @@
"""impersonation tests"""
from json import loads
from django.test.testcases import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
class TestImpersonation(TestCase):
class TestImpersonation(APITestCase):
"""impersonation tests"""
def setUp(self) -> None:
@@ -23,10 +23,10 @@ class TestImpersonation(TestCase):
self.other_user.save()
self.client.force_login(self.user)
self.client.get(
self.client.post(
reverse(
"authentik_core:impersonate-init",
kwargs={"user_id": self.other_user.pk},
"authentik_api:user-impersonate",
kwargs={"pk": self.other_user.pk},
)
)
@@ -35,7 +35,7 @@ class TestImpersonation(TestCase):
self.assertEqual(response_body["user"]["username"], self.other_user.username)
self.assertEqual(response_body["original"]["username"], self.user.username)
self.client.get(reverse("authentik_core:impersonate-end"))
self.client.get(reverse("authentik_api:user-impersonate-end"))
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
@@ -46,9 +46,7 @@ class TestImpersonation(TestCase):
"""test impersonation without permissions"""
self.client.force_login(self.other_user)
self.client.get(
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
)
self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}))
response = self.client.get(reverse("authentik_api:user-me"))
response_body = loads(response.content.decode())
@@ -58,5 +56,5 @@ class TestImpersonation(TestCase):
"""test un-impersonation without impersonating first"""
self.client.force_login(self.other_user)
response = self.client.get(reverse("authentik_core:impersonate-end"))
self.assertRedirects(response, reverse("authentik_core:if-user"))
response = self.client.get(reverse("authentik_api:user-impersonate-end"))
self.assertEqual(response.status_code, 204)

View File

@@ -7,7 +7,7 @@ from django.urls import path
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.generic import RedirectView
from authentik.core.views import apps, impersonate
from authentik.core.views import apps
from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView
@@ -28,17 +28,6 @@ urlpatterns = [
apps.RedirectToAppLaunch.as_view(),
name="application-launch",
),
# Impersonation
path(
"-/impersonation/<int:user_id>/",
impersonate.ImpersonateInitView.as_view(),
name="impersonate-init",
),
path(
"-/impersonation/end/",
impersonate.ImpersonateEndView.as_view(),
name="impersonate-end",
),
# Interfaces
path(
"if/admin/",

View File

@@ -1,60 +0,0 @@
"""authentik impersonation views"""
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.views import View
from structlog.stdlib import get_logger
from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
SESSION_KEY_IMPERSONATE_USER,
)
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG
LOGGER = get_logger()
class ImpersonateInitView(View):
"""Initiate Impersonation"""
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
"""Impersonation handler, checks permissions"""
if not CONFIG.y_bool("impersonation"):
LOGGER.debug("User attempted to impersonate", user=request.user)
return HttpResponse("Unauthorized", status=401)
if not request.user.has_perm("impersonate"):
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
return HttpResponse("Unauthorized", status=401)
user_to_be = get_object_or_404(User, pk=user_id)
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
return redirect("authentik_core:if-user")
class ImpersonateEndView(View):
"""End User impersonation"""
def get(self, request: HttpRequest) -> HttpResponse:
"""End Impersonation handler"""
if (
SESSION_KEY_IMPERSONATE_USER not in request.session
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
):
LOGGER.debug("Can't end impersonation", user=request.user)
return redirect("authentik_core:if-user")
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
del request.session[SESSION_KEY_IMPERSONATE_USER]
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
return redirect("authentik_core:root-redirect")

View File

@@ -2,6 +2,8 @@
from django.db import migrations
from authentik.lib.generators import generate_id
class Migration(migrations.Migration):
dependencies = [

View File

@@ -1,45 +0,0 @@
The authentik Enterprise Edition (EE) license (the “EE License”)
Copyright (c) 2022-present Authentik Security Inc.
With regard to the authentik Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Authentik Subscription Terms of Service, available
at https://goauthentik.io/legal/terms (the "EE Terms"), or other
agreement governing the use of the Software, as agreed by you and authentik Security Inc,
and otherwise have a valid authentik Enterprise Edition subscription for the
correct number of user seats. Subject to the foregoing sentence, you are free to
modify this Software and publish patches to the Software. You agree that Authentik
Security Inc. and/or its licensors (as applicable) retain all right, title and interest
in and to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid authentik Enterprise Edition subscription for the correct
number of user seats. Notwithstanding the foregoing, you may copy and modify
the Software for development and testing purposes, without requiring a
subscription. You agree that Authentik Security Inc. and/or its
licensors (as applicable) retain all right, title and interest in
and to all such modifications. You are not granted any other rights
beyond what is expressly stated herein. Subject to the
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
This EE License applies only to the part of this Software that is not
distributed as part of authentik Open Source (OSS). Any part of this Software
distributed as part of authentik OSS or is served client-side as an image, font,
cascading stylesheet (CSS), file which produces or is compiled, arranged,
augmented, or combined into client-side JavaScript, in whole or in part, is
copyrighted under the MIT license. The full text of this EE License shall
be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
For all third party components incorporated into the authentik Software, those
components are licensed under the original license provided by the owner of the
applicable component.

View File

@@ -1,11 +0,0 @@
"""Enterprise app config"""
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikEnterpriseConfig(ManagedAppConfig):
"""Enterprise app config"""
name = "authentik.enterprise"
label = "authentik_enterprise"
verbose_name = "authentik Enterprise"
default = True

View File

@@ -1 +0,0 @@
"""Enterprise additional settings"""

View File

@@ -11,6 +11,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.events.models
import authentik.lib.models
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
from authentik.lib.migrations import progress_bar

View File

@@ -57,6 +57,10 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
LOGGER.debug("e(trigger): attempting to prevent infinite loop", trigger=trigger)
return
if not trigger.group:
LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
return
LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger)
try:
user = User.objects.filter(pk=event.user.get("pk")).first() or get_anonymous_user()
@@ -73,10 +77,6 @@ def event_trigger_handler(event_uuid: str, trigger_name: str):
if not result.passing:
return
if not trigger.group:
LOGGER.debug("e(trigger): trigger has no group", trigger=trigger)
return
LOGGER.debug("e(trigger): event trigger matched", trigger=trigger)
# Create the notification objects
for transport in trigger.transports.all():

View File

@@ -23,7 +23,8 @@ class DiagramElement:
style: list[str] = field(default_factory=lambda: ["[", "]"])
def __str__(self) -> str:
element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}'
description = self.description.replace('"', "#quot;")
element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}'
if self.action is not None:
if self.action != "":
element = f"--{self.action}--> {element}"

View File

@@ -204,12 +204,12 @@ class ChallengeStageView(StageView):
for field, errors in response.errors.items():
for error in errors:
full_errors.setdefault(field, [])
full_errors[field].append(
{
"string": str(error),
"code": error.code,
}
)
field_error = {
"string": str(error),
}
if hasattr(error, "code"):
field_error["code"] = error.code
full_errors[field].append(field_error)
challenge_response.initial_data["response_errors"] = full_errors
if not challenge_response.is_valid():
self.logger.error(

View File

@@ -5,18 +5,25 @@ postgresql:
name: authentik
user: authentik
port: 5432
password: 'env://POSTGRES_PASSWORD'
password: "env://POSTGRES_PASSWORD"
use_pgbouncer: false
listen:
listen_http: 0.0.0.0:9000
listen_https: 0.0.0.0:9443
listen_metrics: 0.0.0.0:9300
trusted_proxy_cidrs:
- 127.0.0.0/8
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- fe80::/10
- ::1/128
redis:
host: localhost
port: 6379
password: ''
password: ""
tls: false
tls_reqs: "none"
db: 0

View File

@@ -67,7 +67,7 @@ def sentry_init(**sentry_init_kwargs):
ArgvIntegration(),
StdlibIntegration(),
DjangoIntegration(transaction_style="function_name"),
CeleryIntegration(),
CeleryIntegration(monitor_beat_tasks=True),
RedisIntegration(),
ThreadingIntegration(propagate_hub=True),
SocketIntegration(),

View File

@@ -16,10 +16,12 @@ LOGGER = get_logger()
def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
"""Attempt to get the client's IP by checking common HTTP Headers.
Returns none if no IP Could be found"""
Returns none if no IP Could be found
No additional validation is done here as requests are expected to only arrive here
via the go proxy, which deals with validating these headers for us"""
headers = (
"HTTP_X_FORWARDED_FOR",
"HTTP_X_REAL_IP",
"REMOTE_ADDR",
)
for _header in headers:

View File

@@ -1,5 +1,4 @@
"""Docker controller"""
from subprocess import SubprocessError # nosec
from time import sleep
from typing import Optional
from urllib.parse import urlparse
@@ -10,9 +9,11 @@ from docker import DockerClient as UpstreamDockerClient
from docker.errors import DockerException, NotFound
from docker.models.containers import Container
from docker.utils.utils import kwargs_from_env
from paramiko.ssh_exception import SSHException
from structlog.stdlib import get_logger
from yaml import safe_dump
from authentik import __version__
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
@@ -58,9 +59,8 @@ class DockerClient(UpstreamDockerClient, BaseClient):
super().__init__(
base_url=connection.url,
tls=tls_config,
use_ssh_client=True,
)
except SubprocessError as exc:
except SSHException as exc:
if self.ssh:
self.ssh.cleanup()
raise ServiceConnectionInvalid(exc) from exc

View File

@@ -22,7 +22,7 @@ from kubernetes.client import (
V1SecurityContext,
)
from authentik import get_full_version
from authentik import __version__, get_full_version
from authentik.outposts.controllers.base import FIELD_MANAGER
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate

View File

@@ -7,7 +7,8 @@ from docker.errors import DockerException
from authentik.crypto.models import CertificateKeyPair
SSH_CONFIG_DIR = Path("/etc/ssh/ssh_config.d/")
HEADER = "### Managed by authentik"
FOOTER = "### End Managed by authentik"
def opener(path, flags):
@@ -27,54 +28,70 @@ class DockerInlineSSH:
key_path: str
config_path: Path
header: str
def __init__(self, host: str, keypair: CertificateKeyPair) -> None:
self.host = host
self.keypair = keypair
self.config_path = SSH_CONFIG_DIR / Path(self.host + ".conf")
with open(self.config_path, "w", encoding="utf-8") as _config:
if not _config.writable():
# SSH Config file already exists and there's no header from us, meaning that it's
# been externally mapped into the container for more complex configs
raise SSHManagedExternallyException(
"SSH Config exists and does not contain authentik header"
)
self.config_path = Path("~/.ssh/config").expanduser()
if self.config_path.exists() and HEADER not in self.config_path.read_text(encoding="utf-8"):
# SSH Config file already exists and there's no header from us, meaning that it's
# been externally mapped into the container for more complex configs
raise SSHManagedExternallyException(
"SSH Config exists and does not contain authentik header"
)
if not self.keypair:
raise DockerException("keypair must be set for SSH connections")
self.header = f"{HEADER} - {self.host}\n"
def write_config(self, key_path: str):
def write_config(self, key_path: str) -> bool:
"""Update the local user's ssh config file"""
with open(self.config_path, "w", encoding="utf-8") as ssh_config:
with open(self.config_path, "a+", encoding="utf-8") as ssh_config:
if self.header in ssh_config.readlines():
return False
ssh_config.writelines(
[
self.header,
f"Host {self.host}\n",
f" IdentityFile {str(key_path)}\n",
f" IdentityFile {key_path}\n",
" StrictHostKeyChecking No\n",
" UserKnownHostsFile /dev/null\n",
f"{FOOTER}\n",
"\n",
]
)
return True
def write_key(self) -> Path:
def write_key(self):
"""Write keypair's private key to a temporary file"""
path = Path(gettempdir(), f"{self.keypair.pk}_private.pem")
with open(path, "w", encoding="utf8", opener=opener) as _file:
_file.write(self.keypair.key_data)
return path
return str(path)
def write(self):
"""Write keyfile and update ssh config"""
self.key_path = self.write_key()
try:
self.write_config(self.key_path)
except OSError:
was_written = self.write_config(self.key_path)
if not was_written:
self.cleanup()
def cleanup(self):
"""Cleanup when we're done"""
try:
os.unlink(self.key_path)
os.unlink(self.config_path)
with open(self.config_path, "r", encoding="utf-8") as ssh_config:
start = 0
end = 0
lines = ssh_config.readlines()
for idx, line in enumerate(lines):
if line == self.header:
start = idx
if start != 0 and line == f"{FOOTER}\n":
end = idx
with open(self.config_path, "w+", encoding="utf-8") as ssh_config:
lines = lines[:start] + lines[end + 2 :]
ssh_config.writelines(lines)
except OSError:
# If we fail deleting a file it doesn't matter that much
# since we're just in a container

View File

@@ -59,7 +59,6 @@ class TestPasswordPolicyFlow(FlowTestCase):
"label": "PASSWORD_LABEL",
"order": 0,
"placeholder": "PASSWORD_PLACEHOLDER",
"initial_value": "",
"required": True,
"type": "password",
"sub_text": "",

View File

@@ -132,9 +132,9 @@ class TestPolicyProcess(TestCase):
)
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
http_request = self.factory.get(reverse("authentik_core:impersonate-end"))
http_request = self.factory.get(reverse("authentik_api:user-impersonate-end"))
http_request.user = self.user
http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end"))
http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end"))
request = PolicyRequest(self.user)
request.set_http_request(http_request)

View File

@@ -1,8 +1,10 @@
# Generated by Django 3.1 on 2020-08-18 15:59
import django.db.models.deletion
from django.apps.registry import Apps
from django.conf import settings
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.core.models
import authentik.lib.generators

View File

@@ -68,7 +68,7 @@ from authentik.stages.consent.stage import (
LOGGER = get_logger()
PLAN_CONTEXT_PARAMS = "goauthentik.io/providers/oauth2/params"
PLAN_CONTEXT_PARAMS = "params"
SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}

View File

@@ -8,7 +8,7 @@ from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.providers.oauth2.models import DeviceToken
PLAN_CONTEXT_DEVICE = "goauthentik.io/providers/oauth2/device"
PLAN_CONTEXT_DEVICE = "device"
class OAuthDeviceCodeFinishChallenge(Challenge):

View File

@@ -29,6 +29,9 @@ from authentik.providers.oauth2.utils import cors_allow
LOGGER = get_logger()
PLAN_CONTEXT_PARAMS = "params"
PLAN_CONTEXT_SCOPES = "scopes"
class ProviderInfoView(View):
"""OpenID-compliant Provider Info"""

View File

@@ -492,12 +492,3 @@ if DEBUG:
INSTALLED_APPS.append("authentik.core")
CONFIG.log("info", "Booting authentik", version=__version__)
# Attempt to load enterprise app, if available
try:
importlib.import_module("authentik.enterprise.apps")
CONFIG.log("info", "Enabled authentik enterprise")
INSTALLED_APPS.append("authentik.enterprise")
_update_settings("authentik.enterprise.settings")
except ImportError:
pass

View File

@@ -63,8 +63,8 @@ def ldap_sync_password(sender, user: User, password: str, **_):
if not sources.exists():
return
source = sources.first()
changer = LDAPPasswordChanger(source)
try:
changer = LDAPPasswordChanger(source)
changer.change_password(user, password)
except LDAPOperationResult as exc:
Event.new(

View File

@@ -3,6 +3,8 @@
import django.contrib.postgres.fields
from django.db import migrations, models
import authentik.lib.generators
class Migration(migrations.Migration):
dependencies = [

View File

@@ -40,7 +40,11 @@ from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
from authentik.sources.saml.processors.metadata import MetadataProcessor
from authentik.sources.saml.processors.request import RequestProcessor
from authentik.sources.saml.processors.response import ResponseProcessor
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_HEADER, ConsentStageView
from authentik.stages.consent.stage import (
PLAN_CONTEXT_CONSENT_HEADER,
PLAN_CONTEXT_CONSENT_TITLE,
ConsentStageView,
)
LOGGER = get_logger()
@@ -124,6 +128,7 @@ class InitiateView(View):
injected_stages = []
plan_kwargs = {
PLAN_CONTEXT_TITLE: f"Redirecting to {source.name}...",
PLAN_CONTEXT_CONSENT_TITLE: f"Redirecting to {source.name}...",
PLAN_CONTEXT_ATTRS: {
"SAMLRequest": saml_request,
"RelayState": relay_state,

View File

@@ -10,6 +10,7 @@ from duo_client.admin import Admin
from duo_client.auth import Auth
from rest_framework.serializers import BaseSerializer, Serializer
from authentik import __version__
from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
from authentik.lib.models import SerializerModel

View File

@@ -33,6 +33,7 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TITLE
LOGGER = get_logger()
@@ -133,6 +134,12 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
if not device:
raise ValidationError("Invalid device")
# We can only check the device's user if the user we're given isn't anonymous
# as this validation is also used for password-less login where webauthn is the very first
# step done by a user. Only if this validation happens at a later stage we can check
# that the device belongs to the user
if not user.is_anonymous and device.user != user:
raise ValidationError("Invalid device")
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
@@ -174,6 +181,8 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
pushinfo = {
__("Domain"): stage_view.request.get_host(),
}
if PLAN_CONTEXT_CONSENT_TITLE in stage_view.executor.plan.context:
pushinfo[__("Title")] = stage_view.executor.plan.context[PLAN_CONTEXT_CONSENT_TITLE]
if SESSION_KEY_APPLICATION_PRE in stage_view.request.session:
pushinfo[__("Application")] = stage_view.request.session.get(
SESSION_KEY_APPLICATION_PRE, Application()

View File

@@ -14,6 +14,7 @@ from rest_framework.serializers import ValidationError
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.events.utils import cleanse_dict, sanitize_dict
from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
from authentik.flows.exceptions import FlowSkipStageException
from authentik.flows.models import FlowDesignation, NotConfiguredAction, Stage
@@ -36,9 +37,9 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME
COOKIE_NAME_MFA = "authentik_mfa"
SESSION_KEY_STAGES = "authentik/stages/authenticator_validate/stages"
SESSION_KEY_SELECTED_STAGE = "authentik/stages/authenticator_validate/selected_stage"
SESSION_KEY_DEVICE_CHALLENGES = "authentik/stages/authenticator_validate/device_challenges"
PLAN_CONTEXT_STAGES = "goauthentik.io/stages/authenticator_validate/stages"
PLAN_CONTEXT_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage"
PLAN_CONTEXT_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges"
class SelectableStageSerializer(PassiveSerializer):
@@ -72,8 +73,8 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
component = CharField(default="ak-stage-authenticator-validate")
def _challenge_allowed(self, classes: list):
device_challenges: list[dict] = self.stage.request.session.get(
SESSION_KEY_DEVICE_CHALLENGES, []
device_challenges: list[dict] = self.stage.executor.plan.context.get(
PLAN_CONTEXT_DEVICE_CHALLENGES, []
)
if not any(x["device_class"] in classes for x in device_challenges):
raise ValidationError("No compatible device class allowed")
@@ -103,7 +104,9 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
# First check if the challenge is valid
allowed = False
for device_challenge in self.stage.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []):
for device_challenge in self.stage.executor.plan.context.get(
PLAN_CONTEXT_DEVICE_CHALLENGES, []
):
if device_challenge.get("device_class", "") == challenge.get(
"device_class", ""
) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""):
@@ -121,11 +124,11 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
def validate_selected_stage(self, stage_pk: str) -> str:
"""Check that the selected stage is valid"""
stages = self.stage.request.session.get(SESSION_KEY_STAGES, [])
stages = self.stage.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
if not any(str(stage.pk) == stage_pk for stage in stages):
raise ValidationError("Selected stage is invalid")
self.stage.logger.debug("Setting selected stage to ", stage=stage_pk)
self.stage.request.session[SESSION_KEY_SELECTED_STAGE] = stage_pk
self.stage.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = stage_pk
return stage_pk
def validate(self, attrs: dict):
@@ -230,7 +233,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
else:
self.logger.debug("No pending user, continuing")
return self.executor.stage_ok()
self.request.session[SESSION_KEY_DEVICE_CHALLENGES] = challenges
self.executor.plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = challenges
# No allowed devices
if len(challenges) < 1:
@@ -263,23 +266,23 @@ class AuthenticatorValidateStageView(ChallengeStageView):
if stage.configuration_stages.count() == 1:
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
self.request.session[SESSION_KEY_SELECTED_STAGE] = next_stage
self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage
# Because that normal execution only happens on post, we directly inject it here and
# return it
self.executor.plan.insert_stage(next_stage)
return self.executor.stage_ok()
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
self.request.session[SESSION_KEY_STAGES] = stages
self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages
return super().get(self.request, *args, **kwargs)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
res = super().post(request, *args, **kwargs)
if (
SESSION_KEY_SELECTED_STAGE in self.request.session
PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
):
self.logger.debug("Got selected stage in session, running that")
stage_pk = self.request.session.get(SESSION_KEY_SELECTED_STAGE)
self.logger.debug("Got selected stage in context, running that")
stage_pk = self.executor.plan.context(PLAN_CONTEXT_SELECTED_STAGE)
# Because the foreign key to stage.configuration_stage points to
# a base stage class, we need to do another lookup
stage = Stage.objects.get_subclass(pk=stage_pk)
@@ -290,8 +293,8 @@ class AuthenticatorValidateStageView(ChallengeStageView):
return res
def get_challenge(self) -> AuthenticatorValidationChallenge:
challenges = self.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, [])
stages = self.request.session.get(SESSION_KEY_STAGES, [])
challenges = self.executor.plan.context.get(PLAN_CONTEXT_DEVICE_CHALLENGES, [])
stages = self.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
stage_challenges = []
for stage in stages:
serializer = SelectableStageSerializer(
@@ -306,6 +309,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
stage_challenges.append(serializer.data)
return AuthenticatorValidationChallenge(
data={
"component": "ak-stage-authenticator-validate",
"type": ChallengeTypes.NATIVE.value,
"device_challenges": challenges,
"configuration_stages": stage_challenges,
@@ -381,12 +385,11 @@ class AuthenticatorValidateStageView(ChallengeStageView):
self.logger.debug("Set user from user-less flow", user=webauthn_device.user)
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = webauthn_device.user
self.executor.plan.context[PLAN_CONTEXT_METHOD] = "auth_webauthn_pwl"
self.executor.plan.context[PLAN_CONTEXT_METHOD_ARGS] = {
"device": webauthn_device,
}
self.executor.plan.context[PLAN_CONTEXT_METHOD_ARGS] = cleanse_dict(
sanitize_dict(
{
"device": webauthn_device,
}
)
)
return self.set_valid_mfa_cookie(response.device)
def cleanup(self):
self.request.session.pop(SESSION_KEY_STAGES, None)
self.request.session.pop(SESSION_KEY_SELECTED_STAGE, None)
self.request.session.pop(SESSION_KEY_DEVICE_CHALLENGES, None)

View File

@@ -1,26 +1,19 @@
"""Test validator stage"""
from unittest.mock import MagicMock, patch
from django.contrib.sessions.middleware import SessionMiddleware
from django.test.client import RequestFactory
from django.urls.base import reverse
from rest_framework.exceptions import ValidationError
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
from authentik.flows.planner import FlowPlan
from authentik.flows.stage import StageView
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.generators import generate_id, generate_key
from authentik.lib.tests.utils import dummy_get_response
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_validate.stage import (
SESSION_KEY_DEVICE_CHALLENGES,
AuthenticatorValidationChallengeResponse,
)
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
from authentik.stages.identification.models import IdentificationStage, UserFields
@@ -86,12 +79,17 @@ class AuthenticatorValidateStageTests(FlowTestCase):
def test_validate_selected_challenge(self):
"""Test validate_selected_challenge"""
# Prepare request with session
request = self.request_factory.get("/")
flow = create_test_flow()
stage = AuthenticatorValidateStage.objects.create(
name=generate_id(),
not_configured_action=NotConfiguredAction.CONFIGURE,
device_classes=[DeviceClasses.STATIC, DeviceClasses.TOTP],
)
middleware = SessionMiddleware(dummy_get_response)
middleware.process_request(request)
request.session[SESSION_KEY_DEVICE_CHALLENGES] = [
session = self.client.session
plan = FlowPlan(flow_pk=flow.pk.hex)
plan.append_stage(stage)
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
{
"device_class": "static",
"device_uid": "1",
@@ -101,23 +99,43 @@ class AuthenticatorValidateStageTests(FlowTestCase):
"device_uid": "2",
},
]
request.session.save()
session[SESSION_KEY_PLAN] = plan
session.save()
res = AuthenticatorValidationChallengeResponse()
res.stage = StageView(FlowExecutorView())
res.stage.request = request
with self.assertRaises(ValidationError):
res.validate_selected_challenge(
{
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
data={
"selected_challenge": {
"device_class": "baz",
"device_uid": "quox",
"challenge": {},
}
)
res.validate_selected_challenge(
{
"device_class": "static",
"device_uid": "1",
}
},
)
self.assertStageResponse(
response,
flow,
response_errors={
"selected_challenge": [{"string": "invalid challenge selected", "code": "invalid"}]
},
component="ak-stage-authenticator-validate",
)
response = self.client.post(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
data={
"selected_challenge": {
"device_class": "static",
"device_uid": "1",
"challenge": {},
},
},
)
self.assertStageResponse(
response,
flow,
response_errors={"non_field_errors": [{"string": "Empty response", "code": "invalid"}]},
component="ak-stage-authenticator-validate",
)
@patch(

View File

@@ -22,7 +22,7 @@ from authentik.stages.authenticator_validate.challenge import (
)
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
from authentik.stages.authenticator_validate.stage import (
SESSION_KEY_DEVICE_CHALLENGES,
PLAN_CONTEXT_DEVICE_CHALLENGES,
AuthenticatorValidateStageView,
)
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
@@ -211,14 +211,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
plan.append_stage(stage)
plan.append_stage(UserLoginStage(name=generate_id()))
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_DEVICE_CHALLENGES] = [
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
{
"device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk,
"challenge": {},
}
]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
)
@@ -283,14 +283,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
plan = FlowPlan(flow_pk=flow.pk.hex)
plan.append_stage(stage)
plan.append_stage(UserLoginStage(name=generate_id()))
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_DEVICE_CHALLENGES] = [
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
{
"device_class": device.__class__.__name__.lower().replace("device", ""),
"device_uid": device.pk,
"challenge": {},
}
]
session[SESSION_KEY_PLAN] = plan
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
)

View File

@@ -19,6 +19,7 @@ from authentik.lib.utils.time import timedelta_from_string
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
PLAN_CONTEXT_CONSENT = "consent"
PLAN_CONTEXT_CONSENT_TITLE = "consent_title"
PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS = "consent_additional_permissions"
@@ -58,6 +59,8 @@ class ConsentStageView(ChallengeStageView):
),
"token": token,
}
if PLAN_CONTEXT_CONSENT_TITLE in self.executor.plan.context:
data["title"] = self.executor.plan.context[PLAN_CONTEXT_CONSENT_TITLE]
if PLAN_CONTEXT_CONSENT_HEADER in self.executor.plan.context:
data["header_text"] = self.executor.plan.context[PLAN_CONTEXT_CONSENT_HEADER]
challenge = ConsentChallenge(data=data)

View File

@@ -4,7 +4,7 @@ from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.stages.password import BACKEND_APP_PASSWORD
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT
def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):

View File

@@ -1,6 +1,7 @@
# Generated by Django 3.2.6 on 2021-08-23 14:34
import django.contrib.postgres.fields
from django.apps.registry import Apps
from django.db import migrations
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.stages.password import BACKEND_INBUILT

View File

@@ -57,12 +57,10 @@ class PromptSerializer(ModelSerializer):
"type",
"required",
"placeholder",
"initial_value",
"order",
"promptstage_set",
"sub_text",
"placeholder_expression",
"initial_value_expression",
]

View File

@@ -1,53 +0,0 @@
# Generated by Django 4.1.7 on 2023-03-24 17:32
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def migrate_placeholder_expressions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.stages.prompt.models import CHOICE_FIELDS
db_alias = schema_editor.connection.alias
Prompt = apps.get_model("authentik_stages_prompt", "prompt")
for prompt in Prompt.objects.using(db_alias).all():
if not prompt.placeholder_expression or prompt.type in CHOICE_FIELDS:
continue
prompt.initial_value = prompt.placeholder
prompt.initial_value_expression = True
prompt.placeholder = ""
prompt.placeholder_expression = False
prompt.save()
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_prompt", "0010_alter_prompt_placeholder_alter_prompt_type"),
]
operations = [
migrations.AddField(
model_name="prompt",
name="initial_value",
field=models.TextField(
blank=True,
help_text="Optionally pre-fill the input with an initial value. When creating a fixed choice field, enable interpreting as expression and return a list to return multiple default choices.",
),
),
migrations.AddField(
model_name="prompt",
name="initial_value_expression",
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name="prompt",
name="placeholder",
field=models.TextField(
blank=True,
help_text="Optionally provide a short hint that describes the expected input value. When creating a fixed choice field, enable interpreting as expression and return a list to return multiple choices.",
),
),
migrations.RunPython(code=migrate_placeholder_expressions),
]

View File

@@ -29,8 +29,6 @@ from authentik.flows.models import Stage
from authentik.lib.models import SerializerModel
from authentik.policies.models import Policy
CHOICES_CONTEXT_SUFFIX = "__choices"
LOGGER = get_logger()
@@ -121,25 +119,15 @@ class Prompt(SerializerModel):
placeholder = models.TextField(
blank=True,
help_text=_(
"Optionally provide a short hint that describes the expected input value. "
"When creating a fixed choice field, enable interpreting as "
"When creating a Radio Button Group or Dropdown, enable interpreting as "
"expression and return a list to return multiple choices."
),
)
initial_value = models.TextField(
blank=True,
help_text=_(
"Optionally pre-fill the input with an initial value. "
"When creating a fixed choice field, enable interpreting as "
"expression and return a list to return multiple default choices."
),
)
sub_text = models.TextField(blank=True, default="")
order = models.IntegerField(default=0)
placeholder_expression = models.BooleanField(default=False)
initial_value_expression = models.BooleanField(default=False)
@property
def serializer(self) -> Type[BaseSerializer]:
@@ -160,8 +148,8 @@ class Prompt(SerializerModel):
raw_choices = self.placeholder
if self.field_key + CHOICES_CONTEXT_SUFFIX in prompt_context:
raw_choices = prompt_context[self.field_key + CHOICES_CONTEXT_SUFFIX]
if self.field_key in prompt_context:
raw_choices = prompt_context[self.field_key]
elif self.placeholder_expression:
evaluator = PropertyMappingEvaluator(
self, user, request, prompt_context=prompt_context, dry_run=dry_run
@@ -196,9 +184,16 @@ class Prompt(SerializerModel):
) -> str:
"""Get fully interpolated placeholder"""
if self.type in CHOICE_FIELDS:
# Choice fields use the placeholder to define all valid choices.
# Therefore their actual placeholder is always blank
return ""
# Make sure to return a valid choice as placeholder
choices = self.get_choices(prompt_context, user, request, dry_run=dry_run)
if not choices:
return ""
return choices[0]
if self.field_key in prompt_context:
# We don't want to parse this as an expression since a user will
# be able to control the input
return prompt_context[self.field_key]
if self.placeholder_expression:
evaluator = PropertyMappingEvaluator(
@@ -216,47 +211,6 @@ class Prompt(SerializerModel):
raise wrapped from exc
return self.placeholder
def get_initial_value(
self,
prompt_context: dict,
user: User,
request: HttpRequest,
dry_run: Optional[bool] = False,
) -> str:
"""Get fully interpolated initial value"""
if self.field_key in prompt_context:
# We don't want to parse this as an expression since a user will
# be able to control the input
value = prompt_context[self.field_key]
elif self.initial_value_expression:
evaluator = PropertyMappingEvaluator(
self, user, request, prompt_context=prompt_context, dry_run=dry_run
)
try:
value = evaluator.evaluate(self.initial_value)
except Exception as exc: # pylint:disable=broad-except
wrapped = PropertyMappingExpressionException(str(exc))
LOGGER.warning(
"failed to evaluate prompt initial value",
exc=wrapped,
)
if dry_run:
raise wrapped from exc
value = self.initial_value
else:
value = self.initial_value
if self.type in CHOICE_FIELDS:
# Ensure returned value is a valid choice
choices = self.get_choices(prompt_context, user, request)
if not choices:
return ""
if value not in choices:
return choices[0]
return value
def field(self, default: Optional[Any], choices: Optional[list[Any]] = None) -> CharField:
"""Get field type for Challenge and response. Choices are only valid for CHOICE_FIELDS."""
field_class = CharField

View File

@@ -38,7 +38,6 @@ class StagePromptSerializer(PassiveSerializer):
type = ChoiceField(choices=FieldTypes.choices)
required = BooleanField()
placeholder = CharField(allow_blank=True)
initial_value = CharField(allow_blank=True)
order = IntegerField()
sub_text = CharField(allow_blank=True)
choices = ListField(child=CharField(allow_blank=True), allow_empty=True, allow_null=True)
@@ -77,7 +76,7 @@ class PromptChallengeResponse(ChallengeResponse):
choices = field.get_choices(
plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request
)
current = field.get_initial_value(
current = field.get_placeholder(
plan.context.get(PLAN_CONTEXT_PROMPT, {}), user, self.request
)
self.fields[field.field_key] = field.field(current, choices)
@@ -198,9 +197,8 @@ class PromptStageView(ChallengeStageView):
serializers = []
for field in fields:
data = StagePromptSerializer(field).data
# Ensure all choices, placeholders and initial values are str, as
# otherwise further in we can fail serializer validation if we return
# some types such as bool
# Ensure all choices and placeholders are str, as otherwise further in
# we can fail serializer validation if we return some types such as bool
choices = field.get_choices(context, self.get_pending_user(), self.request, dry_run)
if choices:
data["choices"] = [str(choice) for choice in choices]
@@ -209,9 +207,6 @@ class PromptStageView(ChallengeStageView):
data["placeholder"] = str(
field.get_placeholder(context, self.get_pending_user(), self.request, dry_run)
)
data["initial_value"] = str(
field.get_initial_value(context, self.get_pending_user(), self.request, dry_run)
)
serializers.append(data)
return serializers

View File

@@ -22,7 +22,6 @@ from authentik.stages.prompt.stage import (
)
# pylint: disable=too-many-public-methods
class TestPromptStage(FlowTestCase):
"""Prompt tests"""
@@ -38,7 +37,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.USERNAME,
required=True,
placeholder="USERNAME_PLACEHOLDER",
initial_value="akuser",
)
text_prompt = Prompt.objects.create(
name=generate_id(),
@@ -47,7 +45,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.TEXT,
required=True,
placeholder="TEXT_PLACEHOLDER",
initial_value="some text",
)
text_area_prompt = Prompt.objects.create(
name=generate_id(),
@@ -56,7 +53,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.TEXT_AREA,
required=True,
placeholder="TEXT_AREA_PLACEHOLDER",
initial_value="some text",
)
email_prompt = Prompt.objects.create(
name=generate_id(),
@@ -65,7 +61,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.EMAIL,
required=True,
placeholder="EMAIL_PLACEHOLDER",
initial_value="email@example.com",
)
password_prompt = Prompt.objects.create(
name=generate_id(),
@@ -74,7 +69,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.PASSWORD,
required=True,
placeholder="PASSWORD_PLACEHOLDER",
initial_value="supersecurepassword",
)
password2_prompt = Prompt.objects.create(
name=generate_id(),
@@ -83,7 +77,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.PASSWORD,
required=True,
placeholder="PASSWORD_PLACEHOLDER",
initial_value="supersecurepassword",
)
number_prompt = Prompt.objects.create(
name=generate_id(),
@@ -92,7 +85,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.NUMBER,
required=True,
placeholder="NUMBER_PLACEHOLDER",
initial_value="42",
)
hidden_prompt = Prompt.objects.create(
name=generate_id(),
@@ -100,7 +92,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.HIDDEN,
required=True,
placeholder="HIDDEN_PLACEHOLDER",
initial_value="something idk",
)
static_prompt = Prompt.objects.create(
name=generate_id(),
@@ -108,7 +99,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.STATIC,
required=True,
placeholder="static",
initial_value="something idk",
)
radio_button_group = Prompt.objects.create(
name=generate_id(),
@@ -116,7 +106,6 @@ class TestPromptStage(FlowTestCase):
type=FieldTypes.RADIO_BUTTON_GROUP,
required=True,
placeholder="test",
initial_value="test",
)
dropdown = Prompt.objects.create(
name=generate_id(),
@@ -148,9 +137,9 @@ class TestPromptStage(FlowTestCase):
password_prompt.field_key: "test",
password2_prompt.field_key: "test",
number_prompt.field_key: 3,
hidden_prompt.field_key: hidden_prompt.initial_value,
static_prompt.field_key: static_prompt.initial_value,
radio_button_group.field_key: radio_button_group.initial_value,
hidden_prompt.field_key: hidden_prompt.placeholder,
static_prompt.field_key: static_prompt.placeholder,
radio_button_group.field_key: radio_button_group.placeholder,
dropdown.field_key: "",
}
@@ -346,176 +335,106 @@ class TestPromptStage(FlowTestCase):
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
def test_prompt_placeholder_does_not_take_value_from_context(self):
"""Test placeholder does not automatically take value from context"""
context = {
"foo": generate_id(),
}
prompt: Prompt = Prompt(
field_key="text_prompt_expression",
label="TEXT_LABEL",
type=FieldTypes.TEXT,
placeholder="return prompt_context['foo']",
placeholder_expression=True,
)
context["text_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
def test_prompt_initial_value(self):
"""Test initial_value and expression"""
context = {
"foo": generate_id(),
}
prompt: Prompt = Prompt(
field_key="text_prompt_expression",
label="TEXT_LABEL",
type=FieldTypes.TEXT,
initial_value="return prompt_context['foo']",
initial_value_expression=True,
)
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"]
)
context["text_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")),
prompt.get_placeholder(context, self.user, self.factory.get("/")),
context["text_prompt_expression"],
)
self.assertNotEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"]
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
def test_choice_prompts_placeholder_and_initial_value_no_choices(self):
"""Test placeholder and initial value of choice fields with 0 choices"""
context = {}
def test_choice_prompts_placeholders(self):
"""Test placeholders and expression of choice fields"""
context = {"foo": generate_id()}
# No choices - unusable (in the sense it creates an unsubmittable form)
# but valid behaviour
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return []",
placeholder_expression=True,
initial_value="Invalid choice",
initial_value_expression=False,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(prompt.get_initial_value(context, self.user, self.factory.get("/")), "")
self.assertEqual(prompt.get_choices(context, self.user, self.factory.get("/")), tuple())
def test_choice_prompts_placeholder_and_initial_value_single_choice(self):
"""Test placeholder and initial value of choice fields with 1 choice"""
context = {"foo": generate_id()}
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.DROPDOWN,
placeholder=context["foo"],
placeholder_expression=False,
initial_value=context["foo"],
initial_value_expression=False,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
context["fixed_choice_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.DROPDOWN,
placeholder="return [prompt_context['foo']]",
placeholder_expression=True,
initial_value="return prompt_context['foo']",
initial_value_expression=True,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
def test_choice_prompts_placeholder_and_initial_value_multiple_choices(self):
"""Test placeholder and initial value of choice fields with multiple choices"""
context = {}
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return ['test', True, 42]",
placeholder_expression=True,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), "test"
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), ("test", True, 42)
)
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return ['test', True, 42]",
placeholder_expression=True,
initial_value="return True",
initial_value_expression=True,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(prompt.get_initial_value(context, self.user, self.factory.get("/")), True)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), ("test", True, 42)
)
def test_choice_prompts_placeholder_and_initial_value_from_context(self):
"""Test placeholder and initial value of choice fields with values from context"""
rand_value = generate_id()
context = {
"fixed_choice_prompt_expression": rand_value,
"fixed_choice_prompt_expression__choices": ["test", 42, rand_value],
}
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
)
self.assertEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")), rand_value
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), ("test", 42, rand_value)
)
def test_initial_value_not_valid_choice(self):
"""Test initial_value not a valid choice"""
context = {}
prompt: Prompt = Prompt(
field_key="choice_prompt",
label="TEXT_LABEL",
type=FieldTypes.DROPDOWN,
placeholder="choice",
initial_value="another_choice",
prompt.get_placeholder(context, self.user, self.factory.get("/")),
context["fixed_choice_prompt_expression"],
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
("choice",),
(context["fixed_choice_prompt_expression"],),
)
self.assertNotEqual(prompt.get_placeholder(context, self.user, self.factory.get("/")), "")
self.assertNotEqual(prompt.get_choices(context, self.user, self.factory.get("/")), tuple())
del context["fixed_choice_prompt_expression"]
# Single choice
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.RADIO_BUTTON_GROUP,
placeholder="return prompt_context['foo']",
placeholder_expression=True,
)
self.assertEqual(
prompt.get_initial_value(context, self.user, self.factory.get("/")),
"choice",
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
context["fixed_choice_prompt_expression"] = generate_id()
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")),
context["fixed_choice_prompt_expression"],
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["fixed_choice_prompt_expression"],),
)
self.assertNotEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertNotEqual(
prompt.get_choices(context, self.user, self.factory.get("/")), (context["foo"],)
)
del context["fixed_choice_prompt_expression"]
# Multi choice
prompt: Prompt = Prompt(
field_key="fixed_choice_prompt_expression",
label="LABEL",
type=FieldTypes.DROPDOWN,
placeholder="return [prompt_context['foo'], True, 'text']",
placeholder_expression=True,
)
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["foo"], True, "text"),
)
context["fixed_choice_prompt_expression"] = tuple(["text", generate_id(), 2])
self.assertEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")),
"text",
)
self.assertEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
context["fixed_choice_prompt_expression"],
)
self.assertNotEqual(
prompt.get_placeholder(context, self.user, self.factory.get("/")), context["foo"]
)
self.assertNotEqual(
prompt.get_choices(context, self.user, self.factory.get("/")),
(context["foo"], True, "text"),
)
def test_choices_are_none_for_non_choice_fields(self):
@@ -586,8 +505,6 @@ class TestPromptStage(FlowTestCase):
"type": FieldTypes.TEXT,
"placeholder": 'return "Hello world"',
"placeholder_expression": True,
"initial_value": 'return "Hello Hello world"',
"initial_value_expression": True,
"sub_text": "test",
"order": 123,
},
@@ -605,7 +522,6 @@ class TestPromptStage(FlowTestCase):
"type": "text",
"required": True,
"placeholder": "Hello world",
"initial_value": "Hello Hello world",
"order": 123,
"sub_text": "test",
"choices": None,

View File

@@ -3,7 +3,9 @@
import uuid
import django.db.models.deletion
from django.apps.registry import Apps
from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.lib.utils.time

View File

@@ -13,14 +13,12 @@ entries:
id: flow
- attrs:
order: 200
placeholder: Username
placeholder_expression: false
initial_value: |
placeholder: |
try:
return user.username
except:
return ''
initial_value_expression: true
placeholder_expression: true
required: true
type: text
field_key: username
@@ -31,14 +29,12 @@ entries:
model: authentik_stages_prompt.prompt
- attrs:
order: 201
placeholder: Name
placeholder_expression: false
initial_value: |
placeholder: |
try:
return user.name
except:
return ''
initial_value_expression: true
placeholder_expression: true
required: true
type: text
field_key: name
@@ -49,14 +45,12 @@ entries:
model: authentik_stages_prompt.prompt
- attrs:
order: 202
placeholder: Email
placeholder_expression: false
initial_value: |
placeholder: |
try:
return user.email
except:
return ''
initial_value_expression: true
placeholder_expression: true
required: true
type: email
field_key: email
@@ -67,14 +61,12 @@ entries:
model: authentik_stages_prompt.prompt
- attrs:
order: 203
placeholder: Locale
placeholder_expression: false
initial_value: |
placeholder: |
try:
return user.attributes.get("settings", {}).get("locale", "")
except:
return ''
initial_value_expression: true
placeholder_expression: true
required: true
type: ak-locale
field_key: attributes.settings.locale

View File

@@ -6,13 +6,11 @@ import (
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/ldap"
)
@@ -23,72 +21,59 @@ Required environment variables:
- AUTHENTIK_TOKEN: Token to authenticate with
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL := config.Get().AuthentikHost
if akURL == "" {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken := config.Get().AuthentikToken
if akToken == "" {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = ldap.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
},
}
func main() {
rootCmd.AddCommand(healthcheck.Command)
err := rootCmd.Execute()
if err != nil {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
debug.EnableDebugServer()
akURL := config.Get().AuthentikHost
if akURL == "" {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken := config.Get().AuthentikToken
if akToken == "" {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = ldap.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
}

View File

@@ -6,13 +6,11 @@ import (
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/proxyv2"
)
@@ -26,72 +24,59 @@ Required environment variables:
Optionally, you can set these:
- AUTHENTIK_HOST_BROWSER: URL to use in the browser, when it differs from AUTHENTIK_HOST`
var rootCmd = &cobra.Command{
Long: helpMessage,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL := config.Get().AuthentikHost
if akURL == "" {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken := config.Get().AuthentikToken
if akToken == "" {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = proxyv2.NewProxyServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
},
}
func main() {
rootCmd.AddCommand(healthcheck.Command)
err := rootCmd.Execute()
if err != nil {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
debug.EnableDebugServer()
akURL := config.Get().AuthentikHost
if akURL == "" {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken := config.Get().AuthentikToken
if akToken == "" {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = proxyv2.NewProxyServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
}

View File

@@ -1,94 +0,0 @@
package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ak/healthcheck"
"goauthentik.io/internal/outpost/radius"
)
const helpMessage = `authentik radius
Required environment variables:
- AUTHENTIK_HOST: URL to connect to (format "http://authentik.company")
- AUTHENTIK_TOKEN: Token to authenticate with
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
var rootCmd = &cobra.Command{
Long: helpMessage,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
if !found {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
if !found {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = radius.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
},
}
func main() {
rootCmd.AddCommand(healthcheck.Command)
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}

78
cmd/radius/server.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"fmt"
"net/url"
"os"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/common"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/radius"
)
const helpMessage = `authentik radius
Required environment variables:
- AUTHENTIK_HOST: URL to connect to (format "http://authentik.company")
- AUTHENTIK_TOKEN: Token to authenticate with
- AUTHENTIK_INSECURE: Skip SSL Certificate verification`
func main() {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
go debug.EnableDebugServer()
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
if !found {
fmt.Println("env AUTHENTIK_HOST not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
if !found {
fmt.Println("env AUTHENTIK_TOKEN not set!")
fmt.Println(helpMessage)
os.Exit(1)
}
akURLActual, err := url.Parse(akURL)
if err != nil {
fmt.Println(err)
fmt.Println(helpMessage)
os.Exit(1)
}
ex := common.Init()
defer common.Defer()
go func() {
for {
<-ex
os.Exit(0)
}
}()
ac := ak.NewAPIController(*akURLActual, akToken)
if ac == nil {
os.Exit(1)
}
defer ac.Shutdown()
ac.Server = radius.NewServer(ac)
err = ac.Start()
if err != nil {
log.WithError(err).Panic("Failed to run server")
}
for {
<-ex
}
}

View File

@@ -1,76 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"path"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/config"
"goauthentik.io/internal/utils/web"
)
var workerHeartbeat = path.Join(os.TempDir(), "authentik-worker")
const workerThreshold = 30
var healthcheckCmd = &cobra.Command{
Use: "healthcheck",
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
os.Exit(1)
}
mode := args[0]
exitCode := 1
log.WithField("mode", mode).Debug("checking health")
switch strings.ToLower(mode) {
case "server":
exitCode = checkServer()
case "worker":
exitCode = checkWorker()
default:
log.Warn("Invalid mode")
}
os.Exit(exitCode)
},
}
func init() {
rootCmd.AddCommand(healthcheckCmd)
}
func checkServer() int {
h := &http.Client{
Transport: web.NewUserAgentTransport("goauthentik.io/healthcheck", http.DefaultTransport),
}
url := fmt.Sprintf("http://%s/-/health/ready/", config.Get().Listen.HTTP)
res, err := h.Head(url)
if err != nil {
log.WithError(err).Warning("failed to send healthcheck request")
return 1
}
if res.StatusCode >= 400 {
log.WithField("status", res.StatusCode).Warning("unhealthy status code")
return 1
}
log.Debug("successfully checked health")
return 0
}
func checkWorker() int {
stat, err := os.Stat(workerHeartbeat)
if err != nil {
log.WithError(err).Warning("failed to check worker heartbeat file")
return 1
}
delta := time.Since(stat.ModTime()).Seconds()
if delta > workerThreshold {
log.WithField("threshold", workerThreshold).WithField("delta", delta).Warning("Worker hasn't updated heartbeat in threshold")
return 1
}
return 0
}

View File

@@ -1,10 +1,132 @@
package main
import "os"
import (
"fmt"
"net/http"
"net/url"
"time"
"github.com/getsentry/sentry-go"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/gounicorn"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/proxyv2"
sentryutils "goauthentik.io/internal/utils/sentry"
webutils "goauthentik.io/internal/utils/web"
"goauthentik.io/internal/web"
"goauthentik.io/internal/web/tenant_tls"
)
var running = true
func main() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
debug.EnableDebugServer()
l := log.WithField("logger", "authentik.root")
if config.Get().ErrorReporting.Enabled {
err := sentry.Init(sentry.ClientOptions{
Dsn: config.Get().ErrorReporting.SentryDSN,
AttachStacktrace: true,
EnableTracing: true,
TracesSampler: sentryutils.SamplerFunc(config.Get().ErrorReporting.SampleRate),
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
Environment: config.Get().ErrorReporting.Environment,
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgent(), http.DefaultTransport),
IgnoreErrors: []string{
http.ErrAbortHandler.Error(),
},
})
if err != nil {
l.WithError(err).Warning("failed to init sentry")
}
}
ex := common.Init()
defer common.Defer()
u, _ := url.Parse("http://localhost:8000")
g := gounicorn.New()
defer func() {
l.Info("shutting down gunicorn")
g.Kill()
}()
ws := web.NewWebServer(g)
g.HealthyCallback = func() {
if !config.Get().Outposts.DisableEmbeddedOutpost {
go attemptProxyStart(ws, u)
}
}
go web.RunMetricsServer()
go attemptStartBackend(g)
ws.Start()
<-ex
running = false
l.Info("shutting down webserver")
go ws.Shutdown()
}
func attemptStartBackend(g *gounicorn.GoUnicorn) {
for {
if !running {
return
}
err := g.Start()
log.WithField("logger", "authentik.router").WithError(err).Warning("gunicorn process died, restarting")
}
}
func attemptProxyStart(ws *web.WebServer, u *url.URL) {
maxTries := 100
attempt := 0
l := log.WithField("logger", "authentik.server")
for {
l.Debug("attempting to init outpost")
ac := ak.NewAPIController(*u, config.Get().SecretKey)
if ac == nil {
attempt += 1
time.Sleep(1 * time.Second)
if attempt > maxTries {
break
}
continue
}
// Init tenant_tls here too since it requires an API Client,
// so we just re-use the same one as the outpost uses
tw := tenant_tls.NewWatcher(ac.Client)
go tw.Start()
ws.TenantTLS = tw
ac.AddRefreshHandler(func() {
tw.Check()
})
srv := proxyv2.NewProxyServer(ac)
ws.ProxyServer = srv
ac.Server = srv
l.Debug("attempting to start outpost")
err := ac.StartBackgroundTasks()
if err != nil {
l.WithError(err).Warning("outpost failed to start")
attempt += 1
time.Sleep(15 * time.Second)
if attempt > maxTries {
break
}
continue
} else {
select {}
}
}
}

View File

@@ -1,141 +0,0 @@
package main
import (
"fmt"
"net/http"
"net/url"
"time"
"github.com/getsentry/sentry-go"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/common"
"goauthentik.io/internal/config"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/debug"
"goauthentik.io/internal/gounicorn"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/proxyv2"
sentryutils "goauthentik.io/internal/utils/sentry"
webutils "goauthentik.io/internal/utils/web"
"goauthentik.io/internal/web"
"goauthentik.io/internal/web/tenant_tls"
)
var running = true
var rootCmd = &cobra.Command{
Use: "authentik",
Short: "Start authentik instance",
Version: constants.FullVersion(),
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{
FieldMap: log.FieldMap{
log.FieldKeyMsg: "event",
log.FieldKeyTime: "timestamp",
},
DisableHTMLEscape: true,
})
},
Run: func(cmd *cobra.Command, args []string) {
debug.EnableDebugServer()
l := log.WithField("logger", "authentik.root")
if config.Get().ErrorReporting.Enabled {
err := sentry.Init(sentry.ClientOptions{
Dsn: config.Get().ErrorReporting.SentryDSN,
AttachStacktrace: true,
EnableTracing: true,
TracesSampler: sentryutils.SamplerFunc(config.Get().ErrorReporting.SampleRate),
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
Environment: config.Get().ErrorReporting.Environment,
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgent(), http.DefaultTransport),
IgnoreErrors: []string{
http.ErrAbortHandler.Error(),
},
})
if err != nil {
l.WithError(err).Warning("failed to init sentry")
}
}
ex := common.Init()
defer common.Defer()
u, _ := url.Parse("http://localhost:8000")
g := gounicorn.New()
defer func() {
l.Info("shutting down gunicorn")
g.Kill()
}()
ws := web.NewWebServer(g)
g.HealthyCallback = func() {
if !config.Get().Outposts.DisableEmbeddedOutpost {
go attemptProxyStart(ws, u)
}
}
go web.RunMetricsServer()
go attemptStartBackend(g)
ws.Start()
<-ex
running = false
l.Info("shutting down webserver")
go ws.Shutdown()
},
}
func attemptStartBackend(g *gounicorn.GoUnicorn) {
for {
if !running {
return
}
err := g.Start()
log.WithField("logger", "authentik.router").WithError(err).Warning("gunicorn process died, restarting")
}
}
func attemptProxyStart(ws *web.WebServer, u *url.URL) {
maxTries := 100
attempt := 0
l := log.WithField("logger", "authentik.server")
for {
l.Debug("attempting to init outpost")
ac := ak.NewAPIController(*u, config.Get().SecretKey)
if ac == nil {
attempt += 1
time.Sleep(1 * time.Second)
if attempt > maxTries {
break
}
continue
}
// Init tenant_tls here too since it requires an API Client,
// so we just re-use the same one as the outpost uses
tw := tenant_tls.NewWatcher(ac.Client)
go tw.Start()
ws.TenantTLS = tw
ac.AddRefreshHandler(func() {
tw.Check()
})
srv := proxyv2.NewProxyServer(ac)
ws.ProxyServer = srv
ac.Server = srv
l.Debug("attempting to start outpost")
err := ac.StartBackgroundTasks()
if err != nil {
l.WithError(err).Warning("outpost failed to start")
attempt += 1
time.Sleep(15 * time.Second)
if attempt > maxTries {
break
}
continue
} else {
select {}
}
}
}

View File

@@ -1,5 +1,5 @@
---
version: "3.4"
version: '3.4'
services:
postgresql:
@@ -14,9 +14,9 @@ services:
volumes:
- database:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
POSTGRES_DB: ${PG_DB:-authentik}
- POSTGRES_PASSWORD=${PG_PASS:?database password required}
- POSTGRES_USER=${PG_USER:-authentik}
- POSTGRES_DB=${PG_DB:-authentik}
env_file:
- .env
redis:
@@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.3}
restart: unless-stopped
command: server
environment:
@@ -47,10 +47,10 @@ services:
env_file:
- .env
ports:
- "${COMPOSE_PORT_HTTP:-9000}:9000"
- "${COMPOSE_PORT_HTTPS:-9443}:9443"
- "${AUTHENTIK_PORT_HTTP:-9000}:9000"
- "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.3}
restart: unless-stopped
command: worker
environment:

25
go.mod
View File

@@ -9,7 +9,7 @@ require (
github.com/getsentry/sentry-go v0.20.0
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
github.com/go-ldap/ldap/v3 v3.4.4
github.com/go-openapi/runtime v0.26.0
github.com/go-openapi/runtime v0.25.0
github.com/go-openapi/strfmt v0.21.7
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.3.0
@@ -24,9 +24,8 @@ require (
github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.15.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.2
goauthentik.io/api/v3 v3.2023041.3
goauthentik.io/api/v3 v3.2023040.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.7.0
golang.org/x/sync v0.1.0
@@ -37,6 +36,8 @@ require (
require (
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
@@ -47,16 +48,15 @@ require (
github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect
github.com/go-openapi/analysis v0.21.2 // indirect
github.com/go-openapi/errors v0.20.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/spec v0.20.8 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/validate v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/loads v0.21.1 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.21.1 // indirect
github.com/go-openapi/validate v0.21.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
@@ -69,10 +69,9 @@ require (
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
go.mongodb.org/mongo-driver v1.11.3 // indirect
go.opentelemetry.io/otel v1.14.0 // indirect
go.opentelemetry.io/otel/trace v1.14.0 // indirect
go.opentelemetry.io/otel v1.11.1 // indirect
go.opentelemetry.io/otel/trace v1.11.1 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect

51
go.sum
View File

@@ -38,7 +38,9 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb h1:w9IDEB7P1VzNcBpOG7kMpFkZp2DkyJIUt0gDx5MBhRU=
github.com/Netflix/go-env v0.0.0-20210215222557-e437a7e7f9fb/go.mod h1:9XMFaCeRyW7fC9XJOWQ+NdAv8VLG7ys7l3x4ozEGLUQ=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
@@ -55,7 +57,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -89,41 +90,33 @@ github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.21.2 h1:hXFrOYFHUAMQdu6zwAiKKJHJQ8kqZs1ux/ru1P1wLJU=
github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY=
github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc=
github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo=
github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc=
github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/loads v0.21.1 h1:Wb3nVZpdEzDTcly8S4HMkey6fjARRzb7iEaySimlDW0=
github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g=
github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro=
github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw=
github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc=
github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ=
github.com/go-openapi/runtime v0.25.0 h1:7yQTCdRbWhX8vnIjdzU8S00tBYf7Sg71EBeorlPHvhc=
github.com/go-openapi/runtime v0.25.0/go.mod h1:Ux6fikcHXyyob6LNWxtE96hWwjBPYF0DXgVFuMTneOs=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU=
github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg=
github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k=
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k=
github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU=
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU=
github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg=
github.com/go-openapi/validate v0.21.0 h1:+Wqk39yKOhfpLqNLEC0/eViCkzM5FVXVqrvt526+wcI=
github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
@@ -222,8 +215,6 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jellydator/ttlcache/v3 v3.0.1 h1:cHgCSMS7TdQcoprXnWUptJZzyFsqs18Lt8VVhRuZYVU=
github.com/jellydator/ttlcache/v3 v3.0.1/go.mod h1:WwTaEmcXQ3MTjOm4bsZoDFiCu/hMvNWLO1w67RXz6h4=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
@@ -292,18 +283,13 @@ github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -332,7 +318,6 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y=
go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -340,14 +325,14 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY=
go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
go.opentelemetry.io/otel v1.11.1 h1:4WLLAmcfkmDk2ukNXJyq3/kiz/3UzCaYq6PskJsaou4=
go.opentelemetry.io/otel v1.11.1/go.mod h1:1nNhXBbWSD0nsL38H6btgnFN2k4i0sNLHNNMZMSbUGE=
go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZpKxs=
go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
goauthentik.io/api/v3 v3.2023041.3 h1:2NkeBK1LywjcWYGLZjw9gmscTyOfAeRjUWfBZfq9aeU=
goauthentik.io/api/v3 v3.2023041.3/go.mod h1:A2I2iDSEu0pW13mAT6J6wMWVSeV5+F52MWlcIHmERvk=
goauthentik.io/api/v3 v3.2023040.1 h1:WuMkilnvamibI3wMrOdNbMz4q3jbTheq0u3K97ZwYGM=
goauthentik.io/api/v3 v3.2023040.1/go.mod h1:A2I2iDSEu0pW13mAT6J6wMWVSeV5+F52MWlcIHmERvk=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=

View File

@@ -30,7 +30,7 @@ type RedisConfig struct {
Password string `yaml:"password" env:"AUTHENTIK_REDIS__PASSWORD"`
TLS bool `yaml:"tls" env:"AUTHENTIK_REDIS__TLS"`
TLSReqs string `yaml:"tls_reqs" env:"AUTHENTIK_REDIS__TLS_REQS"`
DB int `yaml:"cache_db" env:"AUTHENTIK_REDIS__DB"`
DB int `yaml:"cache_db" env:"AUTHENTIK_REDIS__CACHE_DB"`
CacheTimeout int `yaml:"cache_timeout" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT"`
CacheTimeoutFlows int `yaml:"cache_timeout_flows" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_FLOWS"`
CacheTimeoutPolicies int `yaml:"cache_timeout_policies" env:"AUTHENTIK_REDIS__CACHE_TIMEOUT_POLICIES"`
@@ -38,13 +38,14 @@ type RedisConfig struct {
}
type ListenConfig struct {
HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS"`
}
type PathsConfig struct {

View File

@@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2023.4.1"
const VERSION = "2023.4.3"

View File

@@ -1,38 +0,0 @@
package healthcheck
import (
"fmt"
"net/http"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"goauthentik.io/internal/config"
"goauthentik.io/internal/utils/web"
)
var Command = &cobra.Command{
Use: "healthcheck",
Run: func(cmd *cobra.Command, args []string) {
config.Get()
os.Exit(check())
},
}
func check() int {
h := &http.Client{
Transport: web.NewUserAgentTransport("goauthentik.io/healthcheck", http.DefaultTransport),
}
url := fmt.Sprintf("http://%s/outpost.goauthentik.io/ping", config.Get().Listen.Metrics)
res, err := h.Head(url)
if err != nil {
log.WithError(err).Warning("failed to send healthcheck request")
return 1
}
if res.StatusCode >= 400 {
log.WithField("status", res.StatusCode).Warning("unhealthy status code")
return 1
}
log.Debug("successfully checked health")
return 0
}

View File

@@ -13,16 +13,33 @@ import (
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
dn := pi.GetUserDN(u.Username)
attrs := utils.AttributesToLDAP(u.Attributes, func(key string) string {
return utils.AttributeKeySanitize(key)
}, func(value []string) []string {
userValueMap := func(value []string) []string {
for i, v := range value {
if strings.Contains(v, "%s") {
value[i] = fmt.Sprintf(v, u.Username)
}
}
return value
})
}
attrs := utils.AttributesToLDAP(u.Attributes, func(key string) string {
return utils.AttributeKeySanitize(key)
}, userValueMap)
rawAttrs := utils.AttributesToLDAP(u.Attributes, func(key string) string {
return key
}, userValueMap)
// Only append attributes that don't already exist
// TODO: Remove in 2023.3
for _, rawAttr := range rawAttrs {
exists := false
for _, attr := range attrs {
if strings.EqualFold(attr.Name, rawAttr.Name) {
exists = true
}
}
if !exists {
attrs = append(attrs, rawAttr)
}
}
if u.IsActive == nil {
u.IsActive = api.PtrBool(false)
@@ -31,6 +48,10 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
u.Email = api.PtrString("")
}
attrs = utils.EnsureAttributes(attrs, map[string][]string{
// Old fields for backwards compatibility
"goauthentik.io/ldap/active": {strconv.FormatBool(*u.IsActive)},
"goauthentik.io/ldap/superuser": {strconv.FormatBool(u.IsSuperuser)},
// End old fields
"ak-active": {strconv.FormatBool(*u.IsActive)},
"ak-superuser": {strconv.FormatBool(u.IsSuperuser)},
"memberOf": pi.GroupsForUser(u),

View File

@@ -2,6 +2,7 @@ package group
import (
"strconv"
"strings"
"github.com/nmcclain/ldap"
"goauthentik.io/api/v3"
@@ -27,6 +28,24 @@ func (lg *LDAPGroup) Entry() *ldap.Entry {
}, func(value []string) []string {
return value
})
rawAttrs := utils.AttributesToLDAP(lg.Attributes, func(key string) string {
return key
}, func(value []string) []string {
return value
})
// Only append attributes that don't already exist
// TODO: Remove in 2023.3
for _, rawAttr := range rawAttrs {
exists := false
for _, attr := range attrs {
if strings.EqualFold(attr.Name, rawAttr.Name) {
exists = true
}
}
if !exists {
attrs = append(attrs, rawAttr)
}
}
objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCGroupOfNames, constants.OCAKGroup, constants.OCPosixGroup}
if lg.IsVirtualGroup {
@@ -34,6 +53,9 @@ func (lg *LDAPGroup) Entry() *ldap.Entry {
}
attrs = utils.EnsureAttributes(attrs, map[string][]string{
// Old fields for backwards compatibility
"goauthentik.io/ldap/superuser": {strconv.FormatBool(lg.IsSuperuser)},
// End old fields
"ak-superuser": {strconv.FormatBool(lg.IsSuperuser)},
"objectClass": objectClass,
"member": lg.Member,

View File

@@ -0,0 +1,44 @@
package web
import (
"net"
"net/http"
"github.com/gorilla/handlers"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/config"
)
// ProxyHeaders Set proxy headers like X-Forwarded-For and such, but only if the direct connection
// comes from a client that's in a list of trusted CIDRs
func ProxyHeaders() func(http.Handler) http.Handler {
nets := []*net.IPNet{}
for _, rn := range config.Get().Listen.TrustedProxyCIDRs {
_, cidr, err := net.ParseCIDR(rn)
if err != nil {
continue
}
nets = append(nets, cidr)
}
ph := handlers.ProxyHeaders
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err == nil {
// remoteAddr will be nil if the IP cannot be parsed
remoteAddr := net.ParseIP(host)
for _, allowedCidr := range nets {
if remoteAddr != nil && allowedCidr.Contains(remoteAddr) {
log.WithField("remoteAddr", remoteAddr).WithField("cidr", allowedCidr.String()).Trace("Setting proxy headers")
ph(h).ServeHTTP(w, r)
return
}
}
}
// Request is not directly coming from a CIDR we "trust"
// so set XFF to the direct host IP
r.Header.Set("X-Forwarded-For", host)
h.ServeHTTP(w, r)
})
}
}

View File

@@ -35,7 +35,7 @@ type WebServer struct {
func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
l := log.WithField("logger", "authentik.router")
mainHandler := mux.NewRouter()
mainHandler.Use(handlers.ProxyHeaders)
mainHandler.Use(web.ProxyHeaders())
mainHandler.Use(handlers.CompressHandler)
loggingHandler := mainHandler.NewRoute().Subrouter()
loggingHandler.Use(web.NewLoggingHandler(l, nil))

View File

@@ -19,7 +19,7 @@ ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
COPY --from=builder /go/ldap /
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/ldap", "healthcheck" ]
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "wget", "--spider", "http://localhost:9300/outpost.goauthentik.io/ping" ]
EXPOSE 3389 6636 9300

View File

@@ -1,5 +1,6 @@
#!/bin/bash -e
MODE_FILE="${TMPDIR}/authentik-mode"
WORKER_HEARTBEAT="${TMPDIR}/authentik-worker"
function log {
printf '{"event": "%s", "level": "info", "logger": "bootstrap"}\n' "$@" > /dev/stderr
@@ -37,14 +38,6 @@ function check_if_root {
exec chpst -u authentik:$GROUP env HOME=/authentik $1
}
function run_authentik {
if [[ -x "$(command -v authentik)" ]]; then
exec authentik $@
else
exec go run -v ./cmd/server/ $@
fi
}
function set_mode {
echo $1 > $MODE_FILE
trap cleanup EXIT
@@ -63,7 +56,11 @@ if [[ "$1" == "server" ]]; then
if [[ ! -z "${AUTHENTIK_BOOTSTRAP_PASSWORD}" || ! -z "${AUTHENTIK_BOOTSTRAP_TOKEN}" ]]; then
python -m manage bootstrap_tasks
fi
run_authentik
if [[ -x "$(command -v authentik)" ]]; then
exec authentik
else
exec go run -v ./cmd/server/
fi
elif [[ "$1" == "worker" ]]; then
wait_for_db
set_mode "worker"
@@ -80,7 +77,18 @@ elif [[ "$1" == "test-all" ]]; then
chown authentik:authentik /unittest.xml
check_if_root "python -m manage test authentik"
elif [[ "$1" == "healthcheck" ]]; then
run_authentik healthcheck $(cat $MODE_FILE)
mode=$(cat $MODE_FILE)
if [[ $mode == "server" ]]; then
exec curl --user-agent "goauthentik.io lifecycle Healthcheck" -I http://localhost:9000/-/health/ready/
elif [[ $mode == "worker" ]]; then
mtime=$(date -r $WORKER_HEARTBEAT +"%s")
time=$(date +"%s")
if [ "$(( $time - $mtime ))" -gt "30" ]; then
log "Worker hasn't updated heartbeat in 30 seconds"
exit 1
fi
exit 0
fi
elif [[ "$1" == "dump_config" ]]; then
exec python -m authentik.lib.config
elif [[ "$1" == "debug" ]]; then

View File

@@ -155,6 +155,6 @@ if not CONFIG.y_bool("disable_startup_analytics", False):
},
timeout=5,
)
# pylint: disable=broad-exception-caught
except Exception: # nosec
# pylint: disable=bare-except
except: # nosec
pass

152
poetry.lock generated
View File

@@ -175,64 +175,6 @@ doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"]
trio = ["trio (>=0.16,<0.22)"]
[[package]]
name = "argon2-cffi"
version = "21.3.0"
description = "The secure Argon2 password hashing algorithm."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "argon2-cffi-21.3.0.tar.gz", hash = "sha256:d384164d944190a7dd7ef22c6aa3ff197da12962bd04b17f64d4e93d934dba5b"},
{file = "argon2_cffi-21.3.0-py3-none-any.whl", hash = "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80"},
]
[package.dependencies]
argon2-cffi-bindings = "*"
[package.extras]
dev = ["cogapp", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "pre-commit", "pytest", "sphinx", "sphinx-notfound-page", "tomli"]
docs = ["furo", "sphinx", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pytest"]
[[package]]
name = "argon2-cffi-bindings"
version = "21.2.0"
description = "Low-level CFFI bindings for Argon2"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"},
{file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"},
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"},
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"},
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"},
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"},
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"},
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"},
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"},
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"},
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"},
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"},
]
[package.dependencies]
cffi = ">=1.0.1"
[package.extras]
dev = ["cogapp", "pre-commit", "pytest", "wheel"]
tests = ["pytest"]
[[package]]
name = "asgiref"
version = "3.5.2"
@@ -300,6 +242,20 @@ files = [
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
]
[[package]]
name = "asyncio"
version = "3.4.3"
description = "reference implementation of PEP 3156"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "asyncio-3.4.3-cp33-none-win32.whl", hash = "sha256:b62c9157d36187eca799c378e572c969f0da87cd5fc42ca372d92cdb06e7e1de"},
{file = "asyncio-3.4.3-cp33-none-win_amd64.whl", hash = "sha256:c46a87b48213d7464f22d9a497b9eef8c1928b68320a2fa94240f969f6fec08c"},
{file = "asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d"},
{file = "asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41"},
]
[[package]]
name = "attrs"
version = "22.1.0"
@@ -1676,14 +1632,14 @@ files = [
[[package]]
name = "importlib-metadata"
version = "6.6.0"
version = "6.4.1"
description = "Read metadata from Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"},
{file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"},
{file = "importlib_metadata-6.4.1-py3-none-any.whl", hash = "sha256:63ace321e24167d12fbb176b6015f4dbe06868c54a2af4f15849586afb9027fd"},
{file = "importlib_metadata-6.4.1.tar.gz", hash = "sha256:eb1a7933041f0f85c94cd130258df3fb0dec060ad8c1c9318892ef4192c47ce1"},
]
[package.dependencies]
@@ -2824,21 +2780,6 @@ pytest = ">=5.4.0"
docs = ["sphinx", "sphinx-rtd-theme"]
testing = ["Django", "django-configurations (>=2.0)"]
[[package]]
name = "pytest-github-actions-annotate-failures"
version = "0.1.8"
description = "pytest plugin to annotate failed tests with a workflow command for GitHub Actions"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"
files = [
{file = "pytest-github-actions-annotate-failures-0.1.8.tar.gz", hash = "sha256:2d6e6cb5f8d0aae4a27a20cc4e20fabd3199a121c57f44bc48fe28e372e0be23"},
{file = "pytest_github_actions_annotate_failures-0.1.8-py2.py3-none-any.whl", hash = "sha256:6a882ff21672fa79deae8d917eb965a6bde2b25191e7632e1adfc23ffac008ab"},
]
[package.dependencies]
pytest = ">=4.0.0"
[[package]]
name = "pytest-randomly"
version = "3.12.0"
@@ -3084,43 +3025,16 @@ files = [
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.0.262"
description = "An extremely fast Python linter, written in Rust."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.0.262-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:c26c1abd420d041592d05d63aee8c6a18feb24aed4deb6e91129e9f2c7b4914a"},
{file = "ruff-0.0.262-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b379e9765afa679316e52288a942df085e590862f8945088936a7bce3116d8f3"},
{file = "ruff-0.0.262-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7e0ca6821aafbd2b059df3119fcd5881250721ca8e825789fd2c471f7c59be"},
{file = "ruff-0.0.262-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cca35e2aeddff72bb4379a1dabc134e0c0d25ebc754a2cb733a1f8d4dbbb5e0"},
{file = "ruff-0.0.262-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15bf5533ce169aebbafa00017987f673e879f60a625d932b464b8cdaf32a4fce"},
{file = "ruff-0.0.262-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3909e249d984c4517194005a1c30eaa0c3a6d906c789d9fc0c9c7e007fb3e759"},
{file = "ruff-0.0.262-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e2813013a19b3e147e840bdb2e42db5825b53b47364e58e7b467c5fa47ffda2"},
{file = "ruff-0.0.262-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d25a94996b2037e566c2a801c8b324c0a826194d5d4d90ad7c1ccb8cf06521fa"},
{file = "ruff-0.0.262-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ca04348372efc59f6ee808d903d35e0d352cf2c78e487757cd48b65104b83e"},
{file = "ruff-0.0.262-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:24f989363e9bb5d0283490298102a5218682e49ebf300e445d69e24bee03ac83"},
{file = "ruff-0.0.262-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3c24e678e43ca4b67e29cc9a7a54eea05f31a5898cbf17bfec47b68f08d32a60"},
{file = "ruff-0.0.262-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0baff3c9a22227358ea109c165efe62dbdd0f2b9fd5256567dda8682b444fe23"},
{file = "ruff-0.0.262-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:083bac6e238d8b7d5ac3618666ea63b7ac661cf94c5da160070a58e190082831"},
{file = "ruff-0.0.262-py3-none-win32.whl", hash = "sha256:15bbfa2d15c137717627e0d56b0e535ae297b734551e34e03fcc25d7642cf43a"},
{file = "ruff-0.0.262-py3-none-win_amd64.whl", hash = "sha256:973ac29193f718349cf5746b7d86dfeaf7d40e9651ed97790a9b9327305888b9"},
{file = "ruff-0.0.262-py3-none-win_arm64.whl", hash = "sha256:f102904ebe395acd2a181d295b98120acd7a63f732b691672977fc688674f4af"},
{file = "ruff-0.0.262.tar.gz", hash = "sha256:faea54231c265f5349975ba6f3d855b71881a01f391b2000c47740390c6d5f68"},
]
[[package]]
name = "selenium"
version = "4.9.0"
version = "4.8.3"
description = ""
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "selenium-4.9.0-py3-none-any.whl", hash = "sha256:4c19e6aac202719373108d53a5a8e9336ba8d2b25822ca32ae6ff37acbabbdbe"},
{file = "selenium-4.9.0.tar.gz", hash = "sha256:478fae77cdfaec32adb1e68d59632c8c191f920535282abcaa2d1a3d98655624"},
{file = "selenium-4.8.3-py3-none-any.whl", hash = "sha256:28430ac54a54fa59ad1f5392a1b89b169fe3ab2c2ccd1a9a10b6fe74f36cd6da"},
{file = "selenium-4.8.3.tar.gz", hash = "sha256:61cda3a304f82637162bc155cae7bf88fdb04c115fa2cb1c1c2e1358fcd19a9f"},
]
[package.dependencies]
@@ -3131,14 +3045,14 @@ urllib3 = {version = ">=1.26,<2.0", extras = ["socks"]}
[[package]]
name = "sentry-sdk"
version = "1.20.0"
version = "1.19.1"
description = "Python client for Sentry (https://sentry.io)"
category = "main"
optional = false
python-versions = "*"
files = [
{file = "sentry-sdk-1.20.0.tar.gz", hash = "sha256:a3410381ae769a436c0852cce140a5e5e49f566a07fb7c2ab445af1302f6ad89"},
{file = "sentry_sdk-1.20.0-py2.py3-none-any.whl", hash = "sha256:0ad6bbbe78057b8031a07de7aca6d2a83234e51adc4d436eaf8d8c697184db71"},
{file = "sentry-sdk-1.19.1.tar.gz", hash = "sha256:7ae78bd921981a5010ab540d6bdf3b793659a4db8cccf7f16180702d48a80d84"},
{file = "sentry_sdk-1.19.1-py2.py3-none-any.whl", hash = "sha256:885a11c69df23e53eb281d003b9ff15a5bdfa43d8a2a53589be52104a1b4582f"},
]
[package.dependencies]
@@ -3263,21 +3177,16 @@ files = [
[[package]]
name = "sqlparse"
version = "0.4.4"
version = "0.4.3"
description = "A non-validating SQL parser."
category = "main"
optional = false
python-versions = ">=3.5"
files = [
{file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
{file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
{file = "sqlparse-0.4.3-py3-none-any.whl", hash = "sha256:0323c0ec29cd52bceabc1b4d9d579e311f3e4961b98d174201d5622a23b85e34"},
{file = "sqlparse-0.4.3.tar.gz", hash = "sha256:69ca804846bb114d2ec380e4360a8a340db83f0ccf3afceeb1404df028f57268"},
]
[package.extras]
dev = ["build", "flake8"]
doc = ["sphinx"]
test = ["pytest", "pytest-cov"]
[[package]]
name = "stevedore"
version = "4.1.1"
@@ -3413,19 +3322,20 @@ wsproto = ">=0.14"
[[package]]
name = "twilio"
version = "8.1.0"
version = "8.0.0"
description = "Twilio API client and TwiML generator"
category = "main"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "twilio-8.1.0-py2.py3-none-any.whl", hash = "sha256:19be48f21e799b9dd10e2e0a5633962438e04842864e806409f4f2dbe446a868"},
{file = "twilio-8.1.0.tar.gz", hash = "sha256:a31863119655cd3643f788099f6ea3fe74eea59ce3f65600f9a4931301311c08"},
{file = "twilio-8.0.0-py2.py3-none-any.whl", hash = "sha256:40212f0e03947201e0a8f84e6c2a08acd6e61f31f0c94ec04e6b9abae109113a"},
{file = "twilio-8.0.0.tar.gz", hash = "sha256:ee2eaaf24df4361200f18d04e14b9937de2c31f3322dd739491590d8ff7196fc"},
]
[package.dependencies]
aiohttp = ">=3.8.4"
aiohttp-retry = ">=2.8.3"
asyncio = ">=3.4.3"
PyJWT = ">=2.0.0,<3.0.0"
pytz = "*"
requests = ">=2.0.0"
@@ -4152,4 +4062,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "82fc267d6041997d1410a951033cdb9f6c57d91df7d48acaecdbab320daab58e"
content-hash = "61d20a7f2b76b6554b4c2095db152395c41d6c650b2f7f16662d5ed2f8cc0983"

View File

@@ -1,5 +1,5 @@
# Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/node:20 as web-builder
FROM --platform=${BUILDPLATFORM} docker.io/node:18 as web-builder
COPY ./web /static/
@@ -32,7 +32,7 @@ COPY --from=web-builder /static/security.txt /web/security.txt
COPY --from=web-builder /static/dist/ /web/dist/
COPY --from=web-builder /static/authentik/ /web/authentik/
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/proxy", "healthcheck" ]
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "wget", "--spider", "http://localhost:9300/outpost.goauthentik.io/ping" ]
EXPOSE 9000 9300 9443

View File

@@ -17,11 +17,6 @@ line-length = 100
target-version = ['py311']
exclude = 'node_modules'
[tool.ruff]
line-length = 100
target-version = "py311"
exclude = ["**/migrations/**", "**/node_modules/**"]
[tool.isort]
multi_line_output = 3
include_trailing_comma = true
@@ -35,12 +30,12 @@ force_to_top = "*"
source = ["authentik"]
relative_files = true
omit = [
"*/asgi.py",
"manage.py",
"*/migrations/*",
"*/management/commands/*",
"*/apps.py",
"website/",
"*/asgi.py",
"manage.py",
"*/migrations/*",
"*/management/commands/*",
"*/apps.py",
"website/",
]
[tool.coverage.report]
@@ -48,19 +43,19 @@ sort = "Cover"
skip_covered = true
precision = 2
exclude_lines = [
"pragma: no cover",
# Don't complain about missing debug-only code:
"def __unicode__",
"def __str__",
"def __repr__",
"if self.debug",
"if TYPE_CHECKING",
# Don't complain if tests don't hit defensive assertion code:
"raise AssertionError",
"raise NotImplementedError",
# Don't complain if non-runnable code isn't run:
"if 0:",
"if __name__ == .__main__.:",
"pragma: no cover",
# Don't complain about missing debug-only code:
"def __unicode__",
"def __str__",
"def __repr__",
"if self.debug",
"if TYPE_CHECKING",
# Don't complain if tests don't hit defensive assertion code:
"raise AssertionError",
"raise NotImplementedError",
# Don't complain if non-runnable code isn't run:
"if 0:",
"if __name__ == .__main__.:",
]
show_missing = true
@@ -69,20 +64,20 @@ good-names = ["pk", "id", "i", "j", "k", "_", "bar"]
[tool.pylint.master]
disable = [
"arguments-differ",
"locally-disabled",
"too-many-ancestors",
"too-few-public-methods",
"import-outside-toplevel",
"signature-differs",
"similarities",
"cyclic-import",
"protected-access",
"unused-argument",
"raise-missing-from",
"fixme",
# To preserve django's translation function we need to use %-formatting
"consider-using-f-string",
"arguments-differ",
"locally-disabled",
"too-many-ancestors",
"too-few-public-methods",
"import-outside-toplevel",
"signature-differs",
"similarities",
"cyclic-import",
"protected-access",
"unused-argument",
"raise-missing-from",
"fixme",
# To preserve django's translation function we need to use %-formatting
"consider-using-f-string",
]
load-plugins = ["pylint_django", "pylint.extensions.bad_builtin"]
@@ -104,18 +99,17 @@ python_files = ["tests.py", "test_*.py", "*_tests.py"]
junit_family = "xunit2"
addopts = "-p no:celery --junitxml=unittest.xml"
filterwarnings = [
"ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning",
"ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning",
"ignore:defusedxml.lxml is no longer supported and will be removed in a future release.:DeprecationWarning",
"ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning",
]
[tool.poetry]
name = "authentik"
version = "2023.4.1"
version = "2023.4.3"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]
[tool.poetry.dependencies]
argon2-cffi = "*"
celery = "*"
channels = { version = "*", extras = ["daphne"] }
channels-redis = "*"
@@ -182,10 +176,8 @@ pylint-django = "*"
pyrad = "*"
pytest = "*"
pytest-django = "*"
pytest-github-actions-annotate-failures = "*"
pytest-randomly = "*"
requests-mock = "*"
ruff = "*"
selenium = "*"
[build-system]

View File

@@ -19,7 +19,7 @@ ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
COPY --from=builder /go/radius /
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "/radius", "healthcheck" ]
HEALTHCHECK --interval=5s --retries=20 --start-period=3s CMD [ "wget", "--spider", "http://localhost:9300/outpost.goauthentik.io/ping" ]
EXPOSE 1812/udp 9300

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