mirror of
https://github.com/goauthentik/authentik
synced 2026-05-13 02:16:30 +02:00
Compare commits
97 Commits
docker-reh
...
fix-locale
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb4163a428 | ||
|
|
1525bff851 | ||
|
|
e16136f086 | ||
|
|
0ed9e3b636 | ||
|
|
0c707a7d44 | ||
|
|
83699cdc69 | ||
|
|
8aa5814d91 | ||
|
|
c9c13665f6 | ||
|
|
25f1be496f | ||
|
|
37f64ca773 | ||
|
|
779d98a808 | ||
|
|
5389709ab7 | ||
|
|
650c1f93f2 | ||
|
|
2b794e616d | ||
|
|
fd7cea1344 | ||
|
|
8fdc6e44eb | ||
|
|
fb76f3e1ac | ||
|
|
f7e04b8bcf | ||
|
|
1026601085 | ||
|
|
8b7cceadec | ||
|
|
232bc52960 | ||
|
|
1c282c339b | ||
|
|
e99e5b7355 | ||
|
|
9e1244f764 | ||
|
|
1115e6f82f | ||
|
|
3d66864735 | ||
|
|
1cc5fa1412 | ||
|
|
60adbd9245 | ||
|
|
d4e0694cbe | ||
|
|
4821198f92 | ||
|
|
b791737c47 | ||
|
|
f419173029 | ||
|
|
75d4c2d2ee | ||
|
|
deb7765dee | ||
|
|
acf5b5f2ff | ||
|
|
9295d876a7 | ||
|
|
acbecff09c | ||
|
|
d7514c022a | ||
|
|
d53f6e1035 | ||
|
|
e4e9062096 | ||
|
|
d5bdea68c6 | ||
|
|
5415c7660f | ||
|
|
769e22c7ed | ||
|
|
21b085b586 | ||
|
|
44ac0d0bda | ||
|
|
c37ef6d728 | ||
|
|
7743774905 | ||
|
|
7bb593da22 | ||
|
|
bbacea5b9a | ||
|
|
02072bda93 | ||
|
|
a8327101c6 | ||
|
|
1905fbb58f | ||
|
|
5c05050de7 | ||
|
|
5bdd6815bf | ||
|
|
40645b8abe | ||
|
|
74742ff288 | ||
|
|
d2dfcf5883 | ||
|
|
ac5097dd14 | ||
|
|
b682299edd | ||
|
|
53426293aa | ||
|
|
e426f88401 | ||
|
|
8ff1fc10ca | ||
|
|
c11f3d9f72 | ||
|
|
364ca70724 | ||
|
|
89bcfef363 | ||
|
|
80b79ab901 | ||
|
|
17f229e575 | ||
|
|
efc8822469 | ||
|
|
c04b491125 | ||
|
|
0809927def | ||
|
|
ec80106ee5 | ||
|
|
57ea7a3454 | ||
|
|
39c83c5048 | ||
|
|
413ee2dbb8 | ||
|
|
566969c7bf | ||
|
|
91650ea180 | ||
|
|
70e70c9203 | ||
|
|
e3a374f9c0 | ||
|
|
090f73b1f9 | ||
|
|
6538e94961 | ||
|
|
0c9de742fc | ||
|
|
5b450d07f6 | ||
|
|
2ce0db0515 | ||
|
|
cbc1351ef2 | ||
|
|
69000ea849 | ||
|
|
2aeb1b2448 | ||
|
|
b513baf980 | ||
|
|
4506b9703d | ||
|
|
daf3cf9ce3 | ||
|
|
abf5575001 | ||
|
|
dded84769f | ||
|
|
fd44a206d9 | ||
|
|
9625270aed | ||
|
|
2888f35e54 | ||
|
|
8ed53627ff | ||
|
|
7ce46ac301 | ||
|
|
60dbaf1fa1 |
@@ -1,63 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Start from the same FIPS Python base as production (python-base stage)
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff
|
||||
|
||||
USER root
|
||||
|
||||
# Setup environment matching production python-base stage
|
||||
ENV VENV_PATH="/ak-root/.venv" \
|
||||
PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_NATIVE_TLS=1 \
|
||||
UV_PYTHON_DOWNLOADS=0
|
||||
|
||||
WORKDIR /ak-root
|
||||
|
||||
# Copy uv package manager
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.7@sha256:ba4857bf2a068e9bc0e64eed8563b065908a4cd6bfb66b531a9c424c8e25e142 /uv /uvx /bin/
|
||||
|
||||
# Install build dependencies
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean && \
|
||||
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# Build essentials
|
||||
build-essential pkg-config libffi-dev git binutils \
|
||||
# cryptography
|
||||
curl \
|
||||
# libxml
|
||||
libxslt-dev zlib1g-dev \
|
||||
# postgresql
|
||||
libpq-dev \
|
||||
# python-kadmin-rs and kerberos testing
|
||||
clang libkrb5-dev sccache krb5-kdc krb5-admin-server \
|
||||
# xmlsec
|
||||
libltdl-dev \
|
||||
# runit (for chpst command used by lifecycle/ak)
|
||||
runit \
|
||||
# sudo (required by devcontainer features)
|
||||
sudo && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Environment for building native Python packages
|
||||
ENV UV_NO_BINARY_PACKAGE="cryptography lxml python-kadmin-rs xmlsec" \
|
||||
RUSTUP_PERMIT_COPY_RENAME="true"
|
||||
|
||||
# Create authentik user with proper home directory (required for devcontainer features)
|
||||
RUN adduser --disabled-password --gecos "" --uid 1000 --home /home/authentik authentik && \
|
||||
mkdir -p /certs /media /ak-root && \
|
||||
chown -R authentik:authentik /certs /media /ak-root /home/authentik && \
|
||||
echo "authentik ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/authentik
|
||||
|
||||
# FIPS configuration for Go development
|
||||
# Don't set GOFIPS/GOFIPS140 globally to avoid breaking Go tools like docker-compose
|
||||
# These will be set when building/running authentik Go code (see lifecycle/ak and Makefile)
|
||||
ENV CGO_ENABLED=1
|
||||
|
||||
# Set TMPDIR for PID files and temp data
|
||||
# Use /tmp instead of /dev/shm for development because go run needs to execute binaries
|
||||
ENV TMPDIR=/tmp
|
||||
|
||||
USER authentik
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"name": "authentik",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/ak-root",
|
||||
"containerUser": "authentik",
|
||||
"remoteUser": "authentik",
|
||||
"shutdownAction": "stopCompose",
|
||||
"containerEnv": {
|
||||
"LOCAL_PROJECT_DIR": "/ak-root"
|
||||
},
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/go:1": {
|
||||
"version": "1.24"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "22"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/rust:1": {
|
||||
"version": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"version": "latest",
|
||||
"moby": false
|
||||
}
|
||||
},
|
||||
"mounts": [],
|
||||
"forwardPorts": [9000, 9443],
|
||||
"portsAttributes": {
|
||||
"8000": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"3963": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"35151": {
|
||||
"onAutoForward": "ignore"
|
||||
},
|
||||
"9901": {
|
||||
"onAutoForward": "ignore"
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "bash .devcontainer/setup.sh",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"EditorConfig.EditorConfig",
|
||||
"bashmish.es6-string-css",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"golang.go",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"ms-python.black-formatter",
|
||||
"ms-python.isort",
|
||||
"ms-python.pylint",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"redhat.vscode-yaml",
|
||||
"Tobermory.es6-string-html",
|
||||
"charliermarsh.ruff"
|
||||
],
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/ak-root/.venv/bin/python",
|
||||
"python.terminal.activateEnvironment": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: .devcontainer/Dockerfile
|
||||
user: authentik
|
||||
privileged: true
|
||||
volumes:
|
||||
- ../:/ak-root
|
||||
entrypoint: []
|
||||
command: sleep infinity
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
env_file: .env
|
||||
environment:
|
||||
PATH: "/ak-root/.venv/bin:${PATH}"
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9443:9443"
|
||||
|
||||
postgresql:
|
||||
image: docker.io/library/postgres:16
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d authentik -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
env_file: .env
|
||||
command: ["postgres", "-c", "log_statement=all", "-c", "log_destination=stderr"]
|
||||
|
||||
s3:
|
||||
image: docker.io/zenko/cloudserver
|
||||
env_file: .env
|
||||
environment:
|
||||
REMOTE_MANAGEMENT_DISABLE: "1"
|
||||
ports:
|
||||
- "8020:8000"
|
||||
volumes:
|
||||
- s3-data:/usr/src/app/localData
|
||||
- s3-metadata:/usr/src/app/localMetadata
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
s3-data:
|
||||
s3-metadata:
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
echo "======================================"
|
||||
echo "Running authentik devcontainer setup"
|
||||
echo "======================================"
|
||||
|
||||
echo ""
|
||||
echo "Step 1/5: Installing dependencies"
|
||||
make install
|
||||
|
||||
echo ""
|
||||
echo "Step 2/5: Generating development config"
|
||||
make gen-dev-config
|
||||
|
||||
echo ""
|
||||
echo "Step 3/5: Running database migrations"
|
||||
make migrate
|
||||
|
||||
echo ""
|
||||
echo "Step 4/5: Generating API clients"
|
||||
make gen
|
||||
|
||||
echo ""
|
||||
echo "Step 5/5: Building web assets"
|
||||
make web
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Setup complete!"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "You can now run:"
|
||||
echo " - 'make run-server' to start the backend server"
|
||||
echo " - 'make run-worker' to start the worker (must be ran once after initial setup)"
|
||||
echo " - 'make web-watch' for live web development"
|
||||
echo ""
|
||||
81
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
Normal file
81
.github/ISSUE_TEMPLATE/1-bug-report.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Bug report
|
||||
description: Create a report to help us improve
|
||||
labels: ["bug", "triage"]
|
||||
type: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill out this bug report!
|
||||
- type: textarea
|
||||
id: describe-the-bug
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: "A clear and concise description of what the bug is."
|
||||
placeholder: "Describe the issue"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: how-to-reproduce
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
description: "Steps to reproduce the behavior."
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: "A clear and concise description of what you expected to happen."
|
||||
placeholder: "The behavior that I expect to see is [...]"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: "If applicable, add screenshots to help explain your problem."
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: "Add any other context about the problem here."
|
||||
placeholder: "Also note that [...]"
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: deployment-method
|
||||
attributes:
|
||||
label: Deployment Method
|
||||
description: "What deployment method are you using for authentik? Only Docker, Kubernetes and AWS CloudFormation are supported."
|
||||
options:
|
||||
- Docker
|
||||
- Kubernetes
|
||||
- AWS CloudFormation
|
||||
- Other (please specify)
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: "What version of authentik are you using?"
|
||||
placeholder: "[e.g. 2025.10.1]"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: "Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks."
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
49
.github/ISSUE_TEMPLATE/2-docs-issue.yml
vendored
Normal file
49
.github/ISSUE_TEMPLATE/2-docs-issue.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Documentation suggestion/problem
|
||||
description: Suggest an improvement or report a problem in our docs
|
||||
labels: ["area: docs", "triage"]
|
||||
type: task
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill out this documentation issue!
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
|
||||
**Consider opening a PR!**
|
||||
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR.
|
||||
|
||||
For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).
|
||||
- type: textarea
|
||||
id: issue
|
||||
attributes:
|
||||
label: Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link?
|
||||
description: "A clear and concise description of what the problem is, or where the document can be improved."
|
||||
placeholder: "I believe we need more details about [...]"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: link
|
||||
attributes:
|
||||
label: Link
|
||||
description: "Provide the URL or link to the exact page in the documentation to which you are referring."
|
||||
placeholder: "If there are multiple pages, list them all"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Solution
|
||||
description: "A clear and concise description of what you suggest as a solution"
|
||||
placeholder: "This issue could be resolved by [...]"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: "Add any other context or screenshots about the documentation issue here."
|
||||
placeholder: "Also note that [...]"
|
||||
validations:
|
||||
required: false
|
||||
41
.github/ISSUE_TEMPLATE/3-feature-request.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/3-feature-request.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for a feature
|
||||
labels: ["enhancement", "triage"]
|
||||
type: feature
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to fill out this feature request!
|
||||
- type: textarea
|
||||
id: related-to-problem
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
description: "A clear and concise description of what the problem is."
|
||||
placeholder: "I'm always frustrated when [...]"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: "I'd like authentik to have [...]"
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives that you've considered
|
||||
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||
placeholder: "I've tried this but [...]"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: "Add any other context or screenshots about the feature request here."
|
||||
placeholder: "Also note that [...]"
|
||||
validations:
|
||||
required: false
|
||||
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,39 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ""
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Logs**
|
||||
Output of docker-compose logs or kubectl logs respectively
|
||||
|
||||
**Version and Deployment (please complete the following information):**
|
||||
|
||||
<!--
|
||||
Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/.
|
||||
-->
|
||||
|
||||
- authentik version: [e.g. 2025.2.0]
|
||||
- Deployment: [e.g. docker-compose, helm]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: https://github.com/goauthentik/authentik/discussions
|
||||
about: Please ask questions via GitHub Discussions rather than creating issues.
|
||||
- name: authentik Discord
|
||||
url: https://discord.com/invite/jg33eMhnj6
|
||||
about: For community support, visit our Discord server.
|
||||
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
22
.github/ISSUE_TEMPLATE/docs_issue.md
vendored
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: Documentation issue
|
||||
about: Suggest an improvement or report a problem
|
||||
title: ""
|
||||
labels: documentation
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.**
|
||||
A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...]
|
||||
|
||||
**Provide the URL or link to the exact page in the documentation to which you are referring.**
|
||||
If there are multiple pages, list them all, and be sure to state the header or section where the content is.
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the documentation issue here.
|
||||
|
||||
**Consider opening a PR!**
|
||||
If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation).
|
||||
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ""
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
17
.github/ISSUE_TEMPLATE/hackathon_idea.md
vendored
17
.github/ISSUE_TEMPLATE/hackathon_idea.md
vendored
@@ -1,17 +0,0 @@
|
||||
---
|
||||
name: Hackathon Idea
|
||||
about: Propose an idea for the hackathon
|
||||
title: ""
|
||||
labels: hackathon
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the idea**
|
||||
|
||||
A clear concise description of the idea you want to implement
|
||||
|
||||
You're also free to work on existing GitHub issues, whether they be feature requests or bugs, just link the existing GitHub issue here.
|
||||
|
||||
<!-- Don't modify below here -->
|
||||
|
||||
If you want to help working on this idea or want to contribute in any other way, react to this issue with a :rocket:
|
||||
7
.github/ISSUE_TEMPLATE/issue_template.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/issue_template.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Blank issue
|
||||
about: This issue type is only for internal use
|
||||
title:
|
||||
labels:
|
||||
assignees:
|
||||
---
|
||||
32
.github/ISSUE_TEMPLATE/question.md
vendored
32
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,32 +0,0 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask a question about a feature or specific configuration
|
||||
title: ""
|
||||
labels: question
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe your question/**
|
||||
A clear and concise description of what you're trying to do.
|
||||
|
||||
**Relevant info**
|
||||
i.e. Version of other software you're using, specifics of your setup
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Logs**
|
||||
Output of docker-compose logs or kubectl logs respectively
|
||||
|
||||
**Version and Deployment (please complete the following information):**
|
||||
|
||||
<!--
|
||||
Notice: authentik supports installation via Docker, Kubernetes, and AWS CloudFormation only. Support is not available for other methods. For detailed installation and configuration instructions, please refer to the official documentation at https://docs.goauthentik.io/docs/install-config/.
|
||||
-->
|
||||
|
||||
|
||||
- authentik version: [e.g. 2025.2.0]
|
||||
- Deployment: [e.g. docker-compose, helm]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@@ -21,7 +21,7 @@ runs:
|
||||
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
|
||||
- name: Install uv
|
||||
if: ${{ contains(inputs.dependencies, 'python') }}
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v5
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v5
|
||||
with:
|
||||
enable-cache: true
|
||||
- name: Setup python
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
uses: ./.github/actions/docker-push-variables
|
||||
@@ -87,7 +87,6 @@ jobs:
|
||||
id: push
|
||||
with:
|
||||
context: .
|
||||
file: lifecycle/container/Dockerfile
|
||||
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
secrets: |
|
||||
GEOIPUPDATE_ACCOUNT_ID=${{ secrets.GEOIPUPDATE_ACCOUNT_ID }}
|
||||
|
||||
2
.github/workflows/ci-docs.yml
vendored
2
.github/workflows/ci-docs.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
|
||||
3
.github/workflows/ci-main.yml
vendored
3
.github/workflows/ci-main.yml
vendored
@@ -76,6 +76,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: checkout stable
|
||||
run: |
|
||||
set -e -o pipefail
|
||||
# Copy current, latest config to local
|
||||
cp authentik/lib/default.yml local.env.yml
|
||||
cp -R .github ..
|
||||
@@ -83,7 +84,7 @@ jobs:
|
||||
# Previous stable tag
|
||||
prev_stable=$(git tag --sort=version:refname | grep '^version/' | grep -vE -- '-rc[0-9]+$' | tail -n1)
|
||||
# Current version family based on
|
||||
current_version_family=$(python -c "from authentik import VERSION; print(VERSION)" | grep -vE -- 'rc[0-9]+$')
|
||||
current_version_family=$(cat internal/constants/VERSION | grep -vE -- 'rc[0-9]+$' || true)
|
||||
if [[ -n $current_version_family ]]; then
|
||||
prev_stable=$current_version_family
|
||||
fi
|
||||
|
||||
6
.github/workflows/ci-outpost.yml
vendored
6
.github/workflows/ci-outpost.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Generate API
|
||||
run: make gen-client-go
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
|
||||
uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # v8
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout 5000s --verbose
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: lifecycle/container/${{ matrix.type }}.Dockerfile
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
push: ${{ steps.ev.outputs.shouldPush == 'true' }}
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
|
||||
2
.github/workflows/gen-image-compress.yml
vendored
2
.github/workflows/gen-image-compress.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Compress images
|
||||
id: compress
|
||||
uses: calibreapp/image-actions@05b1cf44e88c3b041b841452482df9497f046ef7 # main
|
||||
uses: calibreapp/image-actions@420075c115b26f8785e293c5bd5bef0911c506e5 # main
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
compressOnly: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
6
.github/workflows/release-publish.yml
vendored
6
.github/workflows/release-publish.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
with:
|
||||
go-version-file: "go.mod"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
|
||||
- name: prepare variables
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
build-args: |
|
||||
VERSION=${{ github.ref }}
|
||||
tags: ${{ steps.ev.outputs.imageTags }}
|
||||
file: lifecycle/container/${{ matrix.type }}.Dockerfile
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
||||
|
||||
2
.github/workflows/release-tag.yml
vendored
2
.github/workflows/release-tag.yml
vendored
@@ -89,7 +89,7 @@ jobs:
|
||||
git tag "version/${{ inputs.version }}" HEAD -m "version/${{ inputs.version }}"
|
||||
git push --follow-tags
|
||||
- name: Create Release
|
||||
uses: goauthentik/action-gh-release@84da137b91a625a58fe8a34f3bd6bdb034a49138
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
with:
|
||||
token: "${{ steps.app-token.outputs.token }}"
|
||||
tag_name: "version/${{ inputs.version }}"
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -211,4 +211,4 @@ source_docs/
|
||||
/vendor/
|
||||
|
||||
### Docker ###
|
||||
/lifecycle/container/docker-compose.override.yml
|
||||
docker-compose.override.yml
|
||||
|
||||
@@ -16,8 +16,10 @@ go.sum @goauthentik/backend
|
||||
# Infrastructure
|
||||
.github/ @goauthentik/infrastructure
|
||||
lifecycle/aws/ @goauthentik/infrastructure
|
||||
lifecycle/container/ @goauthentik/infrastructure
|
||||
Dockerfile @goauthentik/infrastructure
|
||||
*Dockerfile @goauthentik/infrastructure
|
||||
.dockerignore @goauthentik/infrastructure
|
||||
docker-compose.yml @goauthentik/infrastructure
|
||||
Makefile @goauthentik/infrastructure
|
||||
.editorconfig @goauthentik/infrastructure
|
||||
CODEOWNERS @goauthentik/infrastructure
|
||||
|
||||
@@ -26,7 +26,7 @@ RUN npm run build && \
|
||||
npm run build:sfe
|
||||
|
||||
# Stage 2: Build go proxy
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:a13297bfdd45d9702badd83b45c6db1f8fee638bba6ece0c359ce51260577bbe AS go-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:27e1c927a07ed2c7295d39941d6d881424739dbde9ae3055d0d3013699ed35e8 AS go-builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -76,7 +76,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.9.7@sha256:ba4857bf2a068e9bc0e64eed8563b065908a4cd6bfb66b531a9c424c8e25e142 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.9.9@sha256:f6e3549ed287fee0ddde2460a2a74a2d74366f84b04aaa34c1f19fec40da8652 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.9-slim-trixie-fips@sha256:700fc8c1e290bd14e5eaca50b1d8e8c748c820010559cbfb4c4f8dfbe2c4c9ff AS python-base
|
||||
|
||||
4
Makefile
4
Makefile
@@ -47,7 +47,7 @@ help: ## Show this help
|
||||
@echo ""
|
||||
|
||||
go-test:
|
||||
GOFIPS140=latest CGO_ENABLED=1 go test -timeout 0 -v -race -cover ./...
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
test: ## Run the server tests and produce a coverage report (locally)
|
||||
uv run coverage run manage.py test --keepdb authentik
|
||||
@@ -293,7 +293,7 @@ docs-api-clean: ## Clean generated API documentation
|
||||
|
||||
docker: ## Build a docker image of the current source tree
|
||||
mkdir -p ${GEN_API_TS}
|
||||
DOCKER_BUILDKIT=1 docker build . -f lifecycle/container/Dockerfile --progress plain --tag ${DOCKER_IMAGE}
|
||||
DOCKER_BUILDKIT=1 docker build . --progress plain --tag ${DOCKER_IMAGE}
|
||||
|
||||
test-docker:
|
||||
BUILD=true ${PWD}/scripts/test_docker.sh
|
||||
|
||||
@@ -15,7 +15,7 @@ from django.db import models
|
||||
from django.db.models import Q, QuerySet, options
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.http import HttpRequest
|
||||
from django.utils.functional import SimpleLazyObject, cached_property
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_cte import CTE, with_cte
|
||||
@@ -44,18 +44,19 @@ from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGT
|
||||
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
|
||||
|
||||
LOGGER = get_logger()
|
||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||
USER_ATTRIBUTE_GENERATED = "goauthentik.io/user/generated"
|
||||
USER_ATTRIBUTE_EXPIRES = "goauthentik.io/user/expires"
|
||||
USER_ATTRIBUTE_DELETE_ON_LOGOUT = "goauthentik.io/user/delete-on-logout"
|
||||
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
||||
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME = "goauthentik.io/user/token-maximum-lifetime" # nosec
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
|
||||
USER_ATTRIBUTE_CHANGE_NAME = "goauthentik.io/user/can-change-name"
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
||||
USER_PATH_SYSTEM_PREFIX = "goauthentik.io"
|
||||
USER_PATH_SERVICE_ACCOUNT = USER_PATH_SYSTEM_PREFIX + "/service-accounts"
|
||||
_USER_ATTR_PREFIX = f"{USER_PATH_SYSTEM_PREFIX}/user"
|
||||
USER_ATTRIBUTE_DEBUG = f"{_USER_ATTR_PREFIX}/debug"
|
||||
USER_ATTRIBUTE_GENERATED = f"{_USER_ATTR_PREFIX}/generated"
|
||||
USER_ATTRIBUTE_EXPIRES = f"{_USER_ATTR_PREFIX}/expires"
|
||||
USER_ATTRIBUTE_DELETE_ON_LOGOUT = f"{_USER_ATTR_PREFIX}/delete-on-logout"
|
||||
USER_ATTRIBUTE_SOURCES = f"{_USER_ATTR_PREFIX}/sources"
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING = f"{_USER_ATTR_PREFIX}/token-expires" # nosec
|
||||
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME = f"{_USER_ATTR_PREFIX}/token-maximum-lifetime" # nosec
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME = f"{_USER_ATTR_PREFIX}/can-change-username"
|
||||
USER_ATTRIBUTE_CHANGE_NAME = f"{_USER_ATTR_PREFIX}/can-change-name"
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL = f"{_USER_ATTR_PREFIX}/can-change-email"
|
||||
USER_PATH_SERVICE_ACCOUNT = f"{USER_PATH_SYSTEM_PREFIX}/service-accounts"
|
||||
|
||||
options.DEFAULT_NAMES = options.DEFAULT_NAMES + (
|
||||
# used_by API that allows models to specify if they shadow an object
|
||||
@@ -585,18 +586,16 @@ class Application(SerializerModel, PolicyBindingModel):
|
||||
|
||||
def get_launch_url(self, user: Optional["User"] = None) -> str | None:
|
||||
"""Get launch URL if set, otherwise attempt to get launch URL based on provider."""
|
||||
from authentik.core.api.users import UserSerializer
|
||||
|
||||
url = None
|
||||
if self.meta_launch_url:
|
||||
url = self.meta_launch_url
|
||||
elif provider := self.get_provider():
|
||||
url = provider.launch_url
|
||||
if user and url:
|
||||
if isinstance(user, SimpleLazyObject):
|
||||
user._setup()
|
||||
user = user._wrapped
|
||||
try:
|
||||
return url % user.__dict__
|
||||
|
||||
return url % UserSerializer(instance=user).data
|
||||
except Exception as exc: # noqa
|
||||
LOGGER.warning("Failed to format launch url", exc=exc)
|
||||
return url
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<script>
|
||||
<script data-id="authentik-config">
|
||||
"use strict";
|
||||
|
||||
window.authentik = {
|
||||
locale: "{{ LANGUAGE_CODE }}",
|
||||
config: JSON.parse('{{ config_json|escapejs }}'),
|
||||
brand: JSON.parse('{{ brand_json|escapejs }}'),
|
||||
config: JSON.parse('{{ config_json|escapejs }}' || "{}"),
|
||||
brand: JSON.parse('{{ brand_json|escapejs }}' || "{}"),
|
||||
versionFamily: "{{ version_family }}",
|
||||
versionSubdomain: "{{ version_subdomain }}",
|
||||
build: "{{ build }}",
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
{% load authentik_core %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<html
|
||||
data-theme="{% if ui_theme == "dark" %}dark{% else %}light{% endif %}"
|
||||
data-theme-choice="{% if ui_theme == "dark" %}dark{% elif ui_theme == "light" %}light{% else %}auto{% endif %}"
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
@@ -18,9 +20,7 @@
|
||||
|
||||
{% include "base/theme.html" %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
|
||||
<style>{{ brand_css }}</style>
|
||||
<style data-id="brand-css">{{ brand_css }}</style>
|
||||
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
|
||||
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
|
||||
{% block head %}
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
{% load static %}
|
||||
{% load authentik_core %}
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/interface-%v.css' %}" />
|
||||
|
||||
{% if ui_theme == "dark" %}
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="theme-color" content="#18191a">
|
||||
{% elif ui_theme == "light" %}
|
||||
|
||||
{% elif ui_theme == "light" %}
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
{% else %}
|
||||
<script data-id="theme-script">
|
||||
"use strict";
|
||||
|
||||
(function () {
|
||||
try {
|
||||
const initialThemeChoice =
|
||||
new URLSearchParams(window.location.search).get("theme") ||
|
||||
window.localStorage?.getItem("theme");
|
||||
|
||||
const themeChoice =
|
||||
initialThemeChoice || document.documentElement.dataset.themeChoice || "auto";
|
||||
|
||||
document.documentElement.dataset.themeChoice = themeChoice;
|
||||
|
||||
if (themeChoice === "auto") {
|
||||
document.documentElement.dataset.theme = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches
|
||||
? "dark"
|
||||
: "light";
|
||||
} else {
|
||||
document.documentElement.dataset.theme = themeChoice;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to apply theme", e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form method="POST" class="pf-c-form">
|
||||
<div class="pf-c-form">
|
||||
<p>{% trans message %}</p>
|
||||
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">
|
||||
{% trans 'Go home' %}
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<div class="pf-c-form__group">
|
||||
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary pf-m-block">
|
||||
{% trans 'Go home' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,82 +2,67 @@
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
{% load authentik_core %}
|
||||
|
||||
{% block head_before %}
|
||||
<link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||
{% include "base/header_js.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
:root {
|
||||
--ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}");
|
||||
--pf-c-background-image--BackgroundImage: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--sm-2x: var(--ak-flow-background);
|
||||
--pf-c-background-image--BackgroundImage--lg: var(--ak-flow-background);
|
||||
}
|
||||
/* Form with user */
|
||||
.form-control-static {
|
||||
margin-top: var(--pf-global--spacer--sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.form-control-static .avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-control-static img {
|
||||
margin-right: var(--pf-global--spacer--xs);
|
||||
}
|
||||
.form-control-static a {
|
||||
padding-top: var(--pf-global--spacer--xs);
|
||||
padding-bottom: var(--pf-global--spacer--xs);
|
||||
line-height: var(--pf-global--spacer--xl);
|
||||
}
|
||||
<style data-id="static-styles">
|
||||
:root {
|
||||
--ak-global--background-color: var(--pf-global--BackgroundColor--150);
|
||||
--ak-global--background-image: url("{{ request.brand.branding_default_flow_background_url }}");
|
||||
}
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/static-%v.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="pf-c-background-image">
|
||||
</div>
|
||||
<ak-skip-to-content></ak-skip-to-content>
|
||||
<ak-message-container></ak-message-container>
|
||||
<div class="pf-c-login stacked">
|
||||
<div class="ak-login-container">
|
||||
<main class="pf-c-login__main">
|
||||
<div class="pf-c-login__main-header pf-c-brand ak-brand">
|
||||
<img src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
|
||||
|
||||
<div class="pf-c-page__drawer">
|
||||
<div class="pf-c-drawer pf-m-collapsed">
|
||||
<div class="pf-c-drawer__main">
|
||||
<div class="pf-c-drawer__content">
|
||||
<div class="pf-c-drawer__body">
|
||||
<div class="pf-c-login" data-layout="stacked">
|
||||
<main class="pf-c-login__main" aria-label="Authentication form">
|
||||
<div class="pf-c-login__main-header pf-c-brand">
|
||||
<img class="branding-logo" src="{{ brand.branding_logo_url }}" alt="authentik Logo" />
|
||||
</div>
|
||||
<div class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl" data-test-id="card-title">
|
||||
{% block card_title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
<footer aria-label="Site footer" class="pf-c-login__footer pf-m-dark">
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
{% for link in footer_links %}
|
||||
<li>
|
||||
<a href="{{ link.href }}">{{ link.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
<span>
|
||||
{% trans 'Powered by authentik' %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<header class="pf-c-login__main-header">
|
||||
<h1 class="pf-c-title pf-m-3xl">
|
||||
{% block card_title %}
|
||||
{% endblock %}
|
||||
</h1>
|
||||
</header>
|
||||
<div class="pf-c-login__main-body">
|
||||
{% block card %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
<footer class="pf-c-login__footer">
|
||||
<ul class="pf-c-list pf-m-inline">
|
||||
{% for link in footer_links %}
|
||||
<li>
|
||||
<a href="{{ link.href }}">{{ link.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li>
|
||||
<span>
|
||||
{% trans 'Powered by authentik' %}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from dramatiq.broker import get_broker
|
||||
|
||||
from authentik.blueprints.apps import ManagedAppConfig
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.utils.time import fqdn_rand
|
||||
@@ -70,6 +72,12 @@ class AuthentikCryptoConfig(ManagedAppConfig):
|
||||
},
|
||||
)
|
||||
|
||||
@ManagedAppConfig.reconcile_global
|
||||
def tasks_middlewares(self):
|
||||
from authentik.crypto.tasks import CertificateWatcherMiddleware
|
||||
|
||||
get_broker().add_middleware(CertificateWatcherMiddleware())
|
||||
|
||||
@property
|
||||
def tenant_schedule_specs(self) -> list[ScheduleSpec]:
|
||||
from authentik.crypto.tasks import certificate_discovery
|
||||
|
||||
@@ -2,17 +2,29 @@
|
||||
|
||||
from glob import glob
|
||||
from pathlib import Path
|
||||
from sys import platform
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.x509.base import load_pem_x509_certificate
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from dramatiq.actor import actor
|
||||
from dramatiq.middleware import Middleware
|
||||
from structlog.stdlib import get_logger
|
||||
from watchdog.events import (
|
||||
FileCreatedEvent,
|
||||
FileModifiedEvent,
|
||||
FileSystemEvent,
|
||||
FileSystemEventHandler,
|
||||
)
|
||||
from watchdog.observers import Observer
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.tasks.middleware import CurrentTask
|
||||
from authentik.tasks.schedules.models import Schedule
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@@ -35,6 +47,65 @@ def ensure_certificate_valid(body: str):
|
||||
return body
|
||||
|
||||
|
||||
class CertificateWatcherMiddleware(Middleware):
|
||||
"""Middleware to start certificate file watcher"""
|
||||
|
||||
def start_certificate_watcher(self):
|
||||
"""Start certificate file watcher"""
|
||||
observer = Observer()
|
||||
kwargs = {}
|
||||
if platform.startswith("linux"):
|
||||
kwargs["event_filter"] = (FileCreatedEvent, FileModifiedEvent)
|
||||
observer.schedule(
|
||||
CertificateEventHandler(),
|
||||
CONFIG.get("cert_discovery_dir"),
|
||||
recursive=True,
|
||||
**kwargs,
|
||||
)
|
||||
observer.start()
|
||||
|
||||
def after_worker_boot(self, broker, worker):
|
||||
if not settings.TEST:
|
||||
self.start_certificate_watcher()
|
||||
|
||||
|
||||
class CertificateEventHandler(FileSystemEventHandler):
|
||||
"""Event handler for certificate file events"""
|
||||
|
||||
# We only ever get creation and modification events.
|
||||
# See the creation of the Observer instance above for the event filtering.
|
||||
|
||||
# Even though we filter to only get file events, we might still get
|
||||
# directory events as some implementations such as inotify do not support
|
||||
# filtering on file/directory.
|
||||
|
||||
def dispatch(self, event: FileSystemEvent) -> None:
|
||||
"""Call specific event handler method. Ignores directory changes."""
|
||||
if event.is_directory:
|
||||
return None
|
||||
return super().dispatch(event)
|
||||
|
||||
def on_created(self, event: FileSystemEvent):
|
||||
"""Process certificate file creation"""
|
||||
LOGGER.debug(
|
||||
"Certificate file created, triggering discovery",
|
||||
file=event.src_path,
|
||||
)
|
||||
for tenant in Tenant.objects.filter(ready=True):
|
||||
with tenant:
|
||||
Schedule.dispatch_by_actor(certificate_discovery)
|
||||
|
||||
def on_modified(self, event: FileSystemEvent):
|
||||
"""Process certificate file modification"""
|
||||
LOGGER.debug(
|
||||
"Certificate file modified, triggering discovery",
|
||||
file=event.src_path,
|
||||
)
|
||||
for tenant in Tenant.objects.filter(ready=True):
|
||||
with tenant:
|
||||
Schedule.dispatch_by_actor(certificate_discovery)
|
||||
|
||||
|
||||
@actor(description=_("Discover, import and update certificates from the filesystem."))
|
||||
def certificate_discovery():
|
||||
self = CurrentTask.get_task()
|
||||
@@ -66,12 +137,35 @@ def certificate_discovery():
|
||||
except (OSError, ValueError) as exc:
|
||||
LOGGER.warning("Failed to open file or invalid format", exc=exc, file=path)
|
||||
for name, cert_data in certs.items():
|
||||
# First, try to find by filename-based managed field
|
||||
cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first()
|
||||
|
||||
# If not found by filename and we have a private key, check for existing key match
|
||||
if not cert and name in private_keys:
|
||||
existing_with_key = (
|
||||
CertificateKeyPair.objects.filter(
|
||||
managed__startswith="goauthentik.io/crypto/discovered/",
|
||||
key_data=private_keys[name],
|
||||
)
|
||||
.exclude(key_data="")
|
||||
.first()
|
||||
)
|
||||
if existing_with_key:
|
||||
cert = existing_with_key
|
||||
# Update name and managed field to reflect the new filename
|
||||
if cert.name != name:
|
||||
cert.name = name
|
||||
cert.managed = MANAGED_DISCOVERED % name
|
||||
cert.save()
|
||||
|
||||
# Create new certificate if not found
|
||||
if not cert:
|
||||
cert = CertificateKeyPair(
|
||||
name=name,
|
||||
managed=MANAGED_DISCOVERED % name,
|
||||
)
|
||||
|
||||
# Update certificate data if changed
|
||||
dirty = False
|
||||
if cert.certificate_data != cert_data:
|
||||
cert.certificate_data = cert_data
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from json import loads
|
||||
from os import makedirs
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from cryptography.x509.extensions import SubjectAlternativeName
|
||||
@@ -319,6 +320,8 @@ class TestCrypto(APITestCase):
|
||||
|
||||
def test_discovery(self):
|
||||
"""Test certificate discovery"""
|
||||
# This test generates 2 separate cert/key combinations
|
||||
# and verifies they both import properly
|
||||
name = generate_id()
|
||||
builder = CertificateBuilder(name)
|
||||
with self.assertRaises(ValueError):
|
||||
@@ -327,6 +330,15 @@ class TestCrypto(APITestCase):
|
||||
subject_alt_names=[],
|
||||
validity_days=3,
|
||||
)
|
||||
|
||||
name2 = generate_id()
|
||||
builder2 = CertificateBuilder(name2)
|
||||
with self.assertRaises(ValueError):
|
||||
builder2.save()
|
||||
builder2.build(
|
||||
subject_alt_names=[],
|
||||
validity_days=3,
|
||||
)
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
with open(f"{temp_dir}/foo.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
@@ -334,9 +346,9 @@ class TestCrypto(APITestCase):
|
||||
_key.write(builder.private_key)
|
||||
makedirs(f"{temp_dir}/foo.bar", exist_ok=True)
|
||||
with open(f"{temp_dir}/foo.bar/fullchain.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
_cert.write(builder2.certificate)
|
||||
with open(f"{temp_dir}/foo.bar/privkey.pem", "w+", encoding="utf-8") as _key:
|
||||
_key.write(builder.private_key)
|
||||
_key.write(builder2.private_key)
|
||||
with CONFIG.patch("cert_discovery_dir", temp_dir):
|
||||
certificate_discovery.send()
|
||||
keypair: CertificateKeyPair = CertificateKeyPair.objects.filter(
|
||||
@@ -348,3 +360,58 @@ class TestCrypto(APITestCase):
|
||||
self.assertTrue(
|
||||
CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % "foo.bar").exists()
|
||||
)
|
||||
|
||||
def test_discovery_updating_same_private_key(self):
|
||||
"""Test certificate discovery updating certs with matching private keys"""
|
||||
name = generate_id()
|
||||
builder = CertificateBuilder(name)
|
||||
builder.build(
|
||||
subject_alt_names=[],
|
||||
validity_days=3,
|
||||
)
|
||||
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
# First discovery: write cert as "original"
|
||||
with open(f"{temp_dir}/original.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
with open(f"{temp_dir}/original.key", "w+", encoding="utf-8") as _key:
|
||||
_key.write(builder.private_key)
|
||||
|
||||
with CONFIG.patch("cert_discovery_dir", temp_dir):
|
||||
certificate_discovery.send()
|
||||
|
||||
# Verify "original" cert was created
|
||||
original = CertificateKeyPair.objects.filter(
|
||||
managed=MANAGED_DISCOVERED % "original"
|
||||
).first()
|
||||
self.assertIsNotNone(original)
|
||||
self.assertEqual(original.name, "original")
|
||||
self.assertIsNotNone(original.private_key)
|
||||
|
||||
# Second discovery: write same cert/key as "renamed"
|
||||
Path(f"{temp_dir}/original.pem").unlink()
|
||||
Path(f"{temp_dir}/original.key").unlink()
|
||||
|
||||
with open(f"{temp_dir}/renamed.pem", "w+", encoding="utf-8") as _cert:
|
||||
_cert.write(builder.certificate)
|
||||
with open(f"{temp_dir}/renamed.key", "w+", encoding="utf-8") as _key:
|
||||
_key.write(builder.private_key)
|
||||
|
||||
with CONFIG.patch("cert_discovery_dir", temp_dir):
|
||||
certificate_discovery.send()
|
||||
|
||||
# Verify the cert was updated
|
||||
renamed = CertificateKeyPair.objects.filter(
|
||||
managed=MANAGED_DISCOVERED % "renamed"
|
||||
).first()
|
||||
self.assertIsNotNone(renamed, "Renamed certificate should exist")
|
||||
self.assertEqual(renamed.name, "renamed")
|
||||
self.assertEqual(renamed.pk, original.pk, "Should be same database object")
|
||||
|
||||
# Verify no new cert was created
|
||||
final_count = CertificateKeyPair.objects.filter(
|
||||
managed__startswith="goauthentik.io/crypto/discovered/"
|
||||
).count()
|
||||
self.assertEqual(
|
||||
1, final_count, "Should not create duplicate cert for same private key"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ from authentik.stages.authenticator.models import Device
|
||||
|
||||
|
||||
class AuthenticatorEndpointGDTCStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Setup Google Chrome Device Trust connection"""
|
||||
"""Verify Google Chrome Device Trust connection for the user's browser."""
|
||||
|
||||
credentials = models.JSONField()
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from authentik.lib.utils.time import timedelta_string_validator
|
||||
|
||||
|
||||
class SourceStage(Stage):
|
||||
"""Suspend the current flow execution and send the user to a source,
|
||||
"""Suspend the current flow execution and send the user to a federated source,
|
||||
after which this flow execution is resumed."""
|
||||
|
||||
source = models.ForeignKey("authentik_core.Source", on_delete=models.CASCADE)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from collections.abc import Generator
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from django.utils.timezone import now
|
||||
@@ -28,7 +28,7 @@ class LogEvent:
|
||||
def from_event_dict(item: EventDict) -> "LogEvent":
|
||||
event = item.pop("event")
|
||||
log_level = item.pop("level").lower()
|
||||
timestamp = datetime.fromisoformat(item.pop("timestamp"))
|
||||
timestamp = datetime.fromisoformat(item.pop("timestamp")).replace(tzinfo=UTC)
|
||||
item.pop("pid", None)
|
||||
# Sometimes log entries have both `level` and `log_level` set, but `level` is always set
|
||||
item.pop("log_level", None)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||
<link rel="prefetch" href="{{ flow_background_url }}" />
|
||||
{% include "base/header_js.html" %}
|
||||
<style>
|
||||
<style data-id="flow-sfe">
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
|
||||
@@ -7,22 +7,26 @@
|
||||
{{ block.super }}
|
||||
<link rel="prefetch" href="{{ flow_background_url }}" />
|
||||
{% if flow.compatibility_mode and not inspector %}
|
||||
<script>ShadyDOM = { force: true };</script>
|
||||
<link rel="stylesheet" type="text/css" href="{% versioned_script 'dist/styles/static-%v.css' %}" />
|
||||
<script data-id="shady-dom">ShadyDOM = { force: true };</script>
|
||||
{% endif %}
|
||||
{% include "base/header_js.html" %}
|
||||
<script>
|
||||
window.authentik.flow = {
|
||||
"layout": "{{ flow.layout }}",
|
||||
};
|
||||
<script data-id="flow-config">
|
||||
"use strict";
|
||||
|
||||
window.authentik.flow = {
|
||||
"layout": "{{ flow.layout }}",
|
||||
};
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% versioned_script 'dist/flow/FlowInterface-%v.js' %}" type="module"></script>
|
||||
<style>
|
||||
:root {
|
||||
--ak-flow-background: url("{{ flow_background_url }}");
|
||||
}
|
||||
<style data-id="flow-css">
|
||||
:root {
|
||||
--ak-global--background-color: var(--pf-global--BackgroundColor--150);
|
||||
--ak-global--background-image: url("{{ flow_background_url }}");
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -145,7 +145,6 @@ worker:
|
||||
consumer_listen_timeout: "seconds=30"
|
||||
task_max_retries: 5
|
||||
task_default_time_limit: "minutes=10"
|
||||
lock_purge_interval: "minutes=1"
|
||||
task_purge_interval: "days=1"
|
||||
task_expiration: "days=30"
|
||||
scheduler_interval: "seconds=60"
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block card %}
|
||||
<form class="pf-c-form">
|
||||
{% csrf_token %}
|
||||
<div class="pf-c-form">
|
||||
{% if user.is_authenticated %}
|
||||
<div class="pf-c-form__group">
|
||||
<div class="form-control-static">
|
||||
@@ -29,7 +28,7 @@
|
||||
{% endif %}
|
||||
<div class="pf-c-form__group">
|
||||
<p>
|
||||
<i class="pf-icon pf-icon-error-circle-o"></i>
|
||||
<i class="pf-icon pf-icon-error-circle-o pf-u-font-size-2xl" role="img" aria-label="{% trans 'Error' %}"></i>
|
||||
{% trans 'Request has been denied.' %}
|
||||
</p>
|
||||
{% if error %}
|
||||
@@ -71,5 +70,11 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pf-c-form__group">
|
||||
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary pf-m-block">
|
||||
{% trans 'Go home' %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -83,7 +83,7 @@ class EnterpriseUser(BaseModel):
|
||||
class User(BaseUser):
|
||||
"""Modified User schema with added externalId field"""
|
||||
|
||||
model_config = ConfigDict(serialize_by_alias=True)
|
||||
model_config = ConfigDict(serialize_by_alias=True, extra="allow")
|
||||
|
||||
id: str | int | None = None
|
||||
schemas: list[str] = [SCIM_USER_SCHEMA]
|
||||
@@ -106,6 +106,8 @@ class User(BaseUser):
|
||||
class Group(BaseGroup):
|
||||
"""Modified Group schema with added externalId field"""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
id: str | int | None = None
|
||||
schemas: list[str] = [SCIM_GROUP_SCHEMA]
|
||||
externalId: str | None = None
|
||||
|
||||
@@ -95,7 +95,12 @@ class SCIMUserTests(TestCase):
|
||||
"""Test user creation with custom schema"""
|
||||
schema = SCIMMapping.objects.create(
|
||||
name="custom_schema",
|
||||
expression="""return {"schemas": ["foo"]}""",
|
||||
expression="""return {
|
||||
"schemas": ["urn:ietf:params:scim:schemas:extension:slack:profile:2.0:User"],
|
||||
"urn:ietf:params:scim:schemas:extension:slack:profile:2.0:User": {
|
||||
"startDate": "2024-04-10T00:00:00+0000",
|
||||
},
|
||||
}""",
|
||||
)
|
||||
self.provider.property_mappings.add(schema)
|
||||
scim_id = generate_id()
|
||||
@@ -121,7 +126,10 @@ class SCIMUserTests(TestCase):
|
||||
self.assertJSONEqual(
|
||||
mock.request_history[1].body,
|
||||
{
|
||||
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "foo"],
|
||||
"schemas": [
|
||||
"urn:ietf:params:scim:schemas:core:2.0:User",
|
||||
"urn:ietf:params:scim:schemas:extension:slack:profile:2.0:User",
|
||||
],
|
||||
"active": True,
|
||||
"emails": [
|
||||
{
|
||||
@@ -138,6 +146,9 @@ class SCIMUserTests(TestCase):
|
||||
},
|
||||
"displayName": f"{uid} {uid}",
|
||||
"userName": uid,
|
||||
"urn:ietf:params:scim:schemas:extension:slack:profile:2.0:User": {
|
||||
"startDate": "2024-04-10T00:00:00+0000",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -380,9 +380,6 @@ DRAMATIQ = {
|
||||
"broker_class": "authentik.tasks.broker.Broker",
|
||||
"channel_prefix": "authentik",
|
||||
"task_model": "authentik.tasks.models.Task",
|
||||
"lock_purge_interval": timedelta_from_string(
|
||||
CONFIG.get("worker.lock_purge_interval")
|
||||
).total_seconds(),
|
||||
"task_purge_interval": timedelta_from_string(
|
||||
CONFIG.get("worker.task_purge_interval")
|
||||
).total_seconds(),
|
||||
|
||||
@@ -16,7 +16,7 @@ from authentik.stages.authenticator.models import Device
|
||||
|
||||
|
||||
class AuthenticatorDuoStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Setup Duo authenticator devices"""
|
||||
"""Setup Duo authentication for the user."""
|
||||
|
||||
api_hostname = models.TextField()
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
||||
|
||||
class AuthenticatorEmailStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Use Email-based authentication instead of authenticator-based."""
|
||||
"""Setup Email-based authentication for the user."""
|
||||
|
||||
use_global_settings = models.BooleanField(
|
||||
default=False,
|
||||
|
||||
@@ -16,7 +16,7 @@ from authentik.stages.authenticator.models import Device, ThrottlingMixin
|
||||
|
||||
|
||||
class AuthenticatorStaticStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Generate static tokens for the user as a backup."""
|
||||
"""Setup static token based authentication for the user."""
|
||||
|
||||
token_count = models.PositiveIntegerField(default=6)
|
||||
token_length = models.PositiveIntegerField(default=12)
|
||||
|
||||
@@ -27,7 +27,7 @@ class TOTPDigits(models.TextChoices):
|
||||
|
||||
|
||||
class AuthenticatorTOTPStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Enroll a user's device into Time-based OTP."""
|
||||
"""Setup Time-based OTP authentication for the user."""
|
||||
|
||||
digits = models.IntegerField(choices=TOTPDigits.choices)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ def default_device_classes() -> list:
|
||||
|
||||
|
||||
class AuthenticatorValidateStage(Stage):
|
||||
"""Validate user's configured OTP Device."""
|
||||
"""Validate user's configured Multi Factor Authentication."""
|
||||
|
||||
not_configured_action = models.TextField(
|
||||
choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP
|
||||
|
||||
@@ -68,7 +68,7 @@ class AuthenticatorAttachment(models.TextChoices):
|
||||
|
||||
|
||||
class AuthenticatorWebAuthnStage(ConfigurableStage, FriendlyNamedStage, Stage):
|
||||
"""Stage to enroll WebAuthn-based authenticators."""
|
||||
"""Setup WebAuthn-based authentication for the user."""
|
||||
|
||||
user_verification = models.TextField(
|
||||
choices=UserVerification.choices,
|
||||
|
||||
@@ -62,7 +62,7 @@ def get_template_choices():
|
||||
|
||||
|
||||
class EmailStage(Stage):
|
||||
"""Sends an Email to the user with a token to confirm their Email address."""
|
||||
"""Send an Email to the user with a token to confirm their Email address."""
|
||||
|
||||
use_global_settings = models.BooleanField(
|
||||
default=False,
|
||||
|
||||
@@ -21,7 +21,7 @@ class UserFields(models.TextChoices):
|
||||
|
||||
|
||||
class IdentificationStage(Stage):
|
||||
"""Allows the user to identify themselves for authentication."""
|
||||
"""Identify the user for authentication."""
|
||||
|
||||
user_fields = ArrayField(
|
||||
models.CharField(max_length=100, choices=UserFields.choices),
|
||||
|
||||
@@ -39,7 +39,7 @@ def get_authentication_backends():
|
||||
|
||||
|
||||
class PasswordStage(ConfigurableStage, Stage):
|
||||
"""Prompts the user for their password, and validates it against the configured backends."""
|
||||
"""Prompt the user for their password, and validate it against the configured backends."""
|
||||
|
||||
backends = ArrayField(
|
||||
models.TextField(choices=get_authentication_backends()),
|
||||
|
||||
@@ -329,7 +329,7 @@ class Prompt(SerializerModel):
|
||||
|
||||
|
||||
class PromptStage(Stage):
|
||||
"""Define arbitrary prompts for the user."""
|
||||
"""Prompt the user to enter information."""
|
||||
|
||||
fields = models.ManyToManyField(Prompt)
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class RedirectMode(models.TextChoices):
|
||||
|
||||
|
||||
class RedirectStage(Stage):
|
||||
"""Redirect the user to another flow, potentially with all gathered context"""
|
||||
"""Redirect the user to another flow, potentially with all gathered context."""
|
||||
|
||||
keep_context = models.BooleanField(default=True)
|
||||
mode = models.TextField(choices=RedirectMode.choices)
|
||||
|
||||
@@ -8,7 +8,7 @@ from authentik.flows.models import Stage
|
||||
|
||||
|
||||
class UserDeleteStage(Stage):
|
||||
"""Deletes the currently pending user without confirmation.
|
||||
"""Delete the pending user without confirmation.
|
||||
Use with caution."""
|
||||
|
||||
@property
|
||||
|
||||
@@ -30,7 +30,7 @@ class GeoIPBinding(models.TextChoices):
|
||||
|
||||
|
||||
class UserLoginStage(Stage):
|
||||
"""Attaches the currently pending user to the current session."""
|
||||
"""Attach the pending user to the current session."""
|
||||
|
||||
session_duration = models.TextField(
|
||||
default="seconds=0",
|
||||
|
||||
@@ -8,7 +8,7 @@ from authentik.flows.models import Stage
|
||||
|
||||
|
||||
class UserLogoutStage(Stage):
|
||||
"""Resets the users current session."""
|
||||
"""Ends the user's session."""
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[BaseSerializer]:
|
||||
|
||||
@@ -18,8 +18,8 @@ class UserCreationMode(models.TextChoices):
|
||||
|
||||
|
||||
class UserWriteStage(Stage):
|
||||
"""Writes currently pending data into the pending user, or if no user exists,
|
||||
creates a new user with the data."""
|
||||
"""Write pending data into the pending user, or if no user exists,
|
||||
create a new user with the data."""
|
||||
|
||||
user_creation_mode = models.TextField(
|
||||
choices=UserCreationMode.choices,
|
||||
|
||||
@@ -60,22 +60,6 @@ func checkServer() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func splitHostPort(address string) (host, port string) {
|
||||
lastColon := strings.LastIndex(address, ":")
|
||||
if lastColon == -1 {
|
||||
return address, ""
|
||||
}
|
||||
|
||||
host = address[:lastColon]
|
||||
port = address[lastColon+1:]
|
||||
|
||||
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
||||
host = host[1 : len(host)-1]
|
||||
}
|
||||
|
||||
return host, port
|
||||
}
|
||||
|
||||
func checkWorker() int {
|
||||
pidB, err := os.ReadFile(workerPidFile)
|
||||
if err != nil {
|
||||
@@ -98,41 +82,6 @@ func checkWorker() int {
|
||||
log.WithError(err).Warning("failed to signal worker process")
|
||||
return 1
|
||||
}
|
||||
h := &http.Client{
|
||||
Transport: web.NewUserAgentTransport("goauthentik.io/healthcheck", http.DefaultTransport),
|
||||
}
|
||||
|
||||
host, port := splitHostPort(config.Get().Listen.HTTP)
|
||||
|
||||
if host == "0.0.0.0" || host == "::" {
|
||||
url := fmt.Sprintf("http://%s:%s/-/health/ready/", "::1", port)
|
||||
_, err := h.Head(url)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("url", url).Warning("failed to send healthcheck request")
|
||||
url := fmt.Sprintf("http://%s:%s/-/health/ready/", "127.0.0.1", port)
|
||||
res, err := h.Head(url)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("url", url).Warning("failed to send healthcheck request")
|
||||
return 1
|
||||
}
|
||||
if res.StatusCode >= 400 {
|
||||
log.WithField("status", res.StatusCode).Warning("unhealthy status code")
|
||||
return 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
url := fmt.Sprintf("http://%s:%s/-/health/ready/", host, port)
|
||||
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.Info("successfully checked health")
|
||||
return 0
|
||||
}
|
||||
|
||||
37
go.mod
37
go.mod
@@ -9,10 +9,10 @@ require (
|
||||
beryju.io/radius-eap v0.1.0
|
||||
github.com/avast/retry-go/v4 v4.7.0
|
||||
github.com/coreos/go-oidc/v3 v3.16.0
|
||||
github.com/getsentry/sentry-go v0.36.1
|
||||
github.com/getsentry/sentry-go v0.37.0
|
||||
github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-openapi/runtime v0.29.0
|
||||
github.com/go-openapi/runtime v0.29.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/handlers v1.5.2
|
||||
@@ -32,19 +32,18 @@ require (
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025120.2
|
||||
goauthentik.io/api/v3 v3.2025120.3
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.32.0
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
golang.org/x/sync v0.18.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
gorm.io/gorm v1.31.1
|
||||
layeh.com/radius v0.0.0-20231213012653-1006025d24f8
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // 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.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -55,13 +54,13 @@ require (
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/analysis v0.24.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.3 // indirect
|
||||
github.com/go-openapi/analysis v0.24.1 // indirect
|
||||
github.com/go-openapi/errors v0.22.4 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.2 // indirect
|
||||
github.com/go-openapi/loads v0.23.1 // indirect
|
||||
github.com/go-openapi/spec v0.22.0 // indirect
|
||||
github.com/go-openapi/strfmt v0.24.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
||||
github.com/go-openapi/loads v0.23.2 // indirect
|
||||
github.com/go-openapi/spec v0.22.1 // indirect
|
||||
github.com/go-openapi/strfmt v0.25.0 // indirect
|
||||
github.com/go-openapi/swag/conv v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
|
||||
@@ -71,7 +70,7 @@ require (
|
||||
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
|
||||
github.com/go-openapi/validate v0.25.0 // indirect
|
||||
github.com/go-openapi/validate v0.25.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
@@ -88,17 +87,17 @@ require (
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
78
go.sum
78
go.sum
@@ -6,8 +6,6 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio=
|
||||
github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -22,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
|
||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/getsentry/sentry-go v0.36.1 h1:kMJt0WWsxWATUxkvFgVBZdIeHSk/Oiv5P0jZ9e5m/Lw=
|
||||
github.com/getsentry/sentry-go v0.36.1/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c=
|
||||
github.com/getsentry/sentry-go v0.37.0 h1:5bavywHxVkU/9aOIF4fn3s5RTJX5Hdw6K2W6jLYtM98=
|
||||
github.com/getsentry/sentry-go v0.37.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
@@ -43,22 +41,22 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
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.24.0 h1:vE/VFFkICKyYuTWYnplQ+aVr45vlG6NcZKC7BdIXhsA=
|
||||
github.com/go-openapi/analysis v0.24.0/go.mod h1:GLyoJA+bvmGGaHgpfeDh8ldpGo69fAJg7eeMDMRCIrw=
|
||||
github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs=
|
||||
github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ=
|
||||
github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM=
|
||||
github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84=
|
||||
github.com/go-openapi/errors v0.22.4 h1:oi2K9mHTOb5DPW2Zjdzs/NIvwi2N3fARKaTJLdNabaM=
|
||||
github.com/go-openapi/errors v0.22.4/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk=
|
||||
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
|
||||
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
|
||||
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
|
||||
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
|
||||
github.com/go-openapi/loads v0.23.1 h1:H8A0dX2KDHxDzc797h0+uiCZ5kwE2+VojaQVaTlXvS0=
|
||||
github.com/go-openapi/loads v0.23.1/go.mod h1:hZSXkyACCWzWPQqizAv/Ye0yhi2zzHwMmoXQ6YQml44=
|
||||
github.com/go-openapi/runtime v0.29.0 h1:Y7iDTFarS9XaFQ+fA+lBLngMwH6nYfqig1G+pHxMRO0=
|
||||
github.com/go-openapi/runtime v0.29.0/go.mod h1:52HOkEmLL/fE4Pg3Kf9nxc9fYQn0UsIWyGjGIJE9dkg=
|
||||
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
|
||||
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
|
||||
github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc=
|
||||
github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4=
|
||||
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
|
||||
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
|
||||
github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4=
|
||||
github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY=
|
||||
github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0=
|
||||
github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0=
|
||||
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
|
||||
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
|
||||
github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ=
|
||||
github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8=
|
||||
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
|
||||
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
|
||||
github.com/go-openapi/swag/fileutils v0.25.1 h1:rSRXapjQequt7kqalKXdcpIegIShhTPXx7yw0kek2uU=
|
||||
@@ -79,8 +77,12 @@ github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3
|
||||
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
|
||||
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
|
||||
github.com/go-openapi/validate v0.25.0 h1:JD9eGX81hDTjoY3WOzh6WqxVBVl7xjsLnvDo1GL5WPU=
|
||||
github.com/go-openapi/validate v0.25.0/go.mod h1:SUY7vKrN5FiwK6LyvSwKjDfLNirSfWwHNgxd2l29Mmw=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
|
||||
github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw=
|
||||
github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
@@ -194,8 +196,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
||||
github.com/wwt/guac v1.3.2 h1:sH6OFGa/1tBs7ieWBVlZe7t6F5JAOWBry/tqQL/Vup4=
|
||||
github.com/wwt/guac v1.3.2/go.mod h1:eKm+NrnK7A88l4UBEcYNpZQGMpZRryYKoz4D/0/n1C0=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
|
||||
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
@@ -212,13 +214,13 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
goauthentik.io/api/v3 v3.2025120.2 h1:FOUJCEI+qqBXmeB7DfStwfB6SXuoLUQrubX4AXsaf/E=
|
||||
goauthentik.io/api/v3 v3.2025120.2/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
goauthentik.io/api/v3 v3.2025120.3 h1:a/azcVyRED1sCfp2sbtv8Bl9ChR6BHRsb5rQHc449xQ=
|
||||
goauthentik.io/api/v3 v3.2025120.3/go.mod h1:82lqAz4jxzl6Cg0YDbhNtvvTG2rm6605ZhdJFnbbsl8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab h1:628ME69lBm9C6JY2wXhAph/yjN3jezx1z7BIDLUwxjo=
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
@@ -228,15 +230,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -247,8 +249,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -260,8 +262,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
@@ -279,7 +281,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
layeh.com/radius v0.0.0-20231213012653-1006025d24f8 h1:orYXpi6BJZdvgytfHH4ybOe4wHnLbbS71Cmd8mWdZjs=
|
||||
layeh.com/radius v0.0.0-20231213012653-1006025d24f8/go.mod h1:QRf+8aRqXc019kHkpcs/CTgyWXFzf+bxlsyuo2nAl1o=
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:a13297bfdd45d9702badd83b45c6db1f8fee638bba6ece0c359ce51260577bbe AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:27e1c927a07ed2c7295d39941d6d881424739dbde9ae3055d0d3013699ed35e8 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/ldap ./cmd/ldap
|
||||
|
||||
# Stage 2: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:9b4cedf932e97194f1825124830f2eec14254d90162dad28f97e505971543115
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:40a1f32b318c9d4a488580bf156c408ec2e076898d40ada9256b7c219af79e19
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
@@ -38,14 +38,14 @@ function check_if_root {
|
||||
chown -R authentik:authentik /media /certs "${PROMETHEUS_MULTIPROC_DIR}"
|
||||
chmod ug+rwx /media
|
||||
chmod ug+rx /certs
|
||||
exec chpst -u authentik:$GROUP env HOME=/authentik PATH="$PATH" $1
|
||||
exec chpst -u authentik:$GROUP env HOME=/authentik $1
|
||||
}
|
||||
|
||||
function run_authentik {
|
||||
if [[ -x "$(command -v authentik)" ]]; then
|
||||
exec authentik $@
|
||||
else
|
||||
exec env GOFIPS140=latest CGO_ENABLED=1 go run -v ./cmd/server/ $@
|
||||
exec go run -v ./cmd/server/ $@
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
8
lifecycle/aws/package-lock.json
generated
8
lifecycle/aws/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1031.1",
|
||||
"aws-cdk": "^2.1031.2",
|
||||
"cross-env": "^10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -24,9 +24,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aws-cdk": {
|
||||
"version": "2.1031.1",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1031.1.tgz",
|
||||
"integrity": "sha512-y/ovZDtOKvgJFtwE7NDFKRgJB3stgUUrAu6b31icuFO8+vnZ+4BIt0McQfbVR8ky5QOj2TW1G1sk55k8Yv+BnQ==",
|
||||
"version": "2.1031.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1031.2.tgz",
|
||||
"integrity": "sha512-FI8XkslwC1Vatjdu5MXu2ww++FcZPkPt45/DJklApxMF+aGcCKOuLf+COc12QYK88GOrLBeCED6lDjNc9m/ueA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"aws-cdk": "^2.1031.1",
|
||||
"aws-cdk": "^2.1031.2",
|
||||
"cross-env": "^10.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-28 00:10+0000\n"
|
||||
"POT-Creation-Date: 2025-11-13 03:19+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -446,6 +446,7 @@ msgid "Remove temporary users created by SAML Sources."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/core/templates/if/error.html
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Go home"
|
||||
msgstr ""
|
||||
|
||||
@@ -477,6 +478,26 @@ msgstr ""
|
||||
msgid "ecdsa"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "RSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Elliptic Curve"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "DSA"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed25519"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "Ed448"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/crypto/models.py
|
||||
msgid "PEM-encoded Certificate data"
|
||||
msgstr ""
|
||||
@@ -1473,6 +1494,10 @@ msgstr ""
|
||||
msgid "Not you?"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/policies/templates/policies/denied.html
|
||||
msgid "Request has been denied."
|
||||
msgstr ""
|
||||
@@ -1716,8 +1741,8 @@ msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
msgid ""
|
||||
"Tokens not valid on or after current time + this value (Format: hours=1;"
|
||||
"minutes=2;seconds=3)."
|
||||
"Tokens not valid on or after current time + this value (Format: "
|
||||
"hours=1;minutes=2;seconds=3)."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/oauth2/models.py
|
||||
@@ -2028,16 +2053,6 @@ msgstr ""
|
||||
msgid "ACS URL"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Service Provider Binding"
|
||||
msgstr ""
|
||||
@@ -2048,6 +2063,16 @@ msgid ""
|
||||
"Provider."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Value of the audience restriction field of the assertion. When left empty, "
|
||||
"no audience restriction will be added."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "Also known as EntityID"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid "SLS URL"
|
||||
msgstr ""
|
||||
@@ -2098,20 +2123,20 @@ msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Assertion valid not before current time + this value (Format: hours=-1;"
|
||||
"minutes=-2;seconds=-3)."
|
||||
"Assertion valid not before current time + this value (Format: "
|
||||
"hours=-1;minutes=-2;seconds=-3)."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Assertion not valid on or after current time + this value (Format: hours=1;"
|
||||
"minutes=2;seconds=3)."
|
||||
"Assertion not valid on or after current time + this value (Format: "
|
||||
"hours=1;minutes=2;seconds=3)."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py
|
||||
msgid ""
|
||||
"Session not valid on or after current time + this value (Format: hours=1;"
|
||||
"minutes=2;seconds=3)."
|
||||
"Session not valid on or after current time + this value (Format: "
|
||||
"hours=1;minutes=2;seconds=3)."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/providers/saml/models.py authentik/sources/saml/models.py
|
||||
@@ -3956,8 +3981,8 @@ msgstr ""
|
||||
#: authentik/stages/user_login/models.py
|
||||
msgid ""
|
||||
"When set to a non-zero value, authentik will save a cookie with a longer "
|
||||
"expiry,to remember the device the user is logging in from. (Format: hours=-1;"
|
||||
"minutes=-2;seconds=-3)"
|
||||
"expiry,to remember the device the user is logging in from. (Format: "
|
||||
"hours=-1;minutes=-2;seconds=-3)"
|
||||
msgstr ""
|
||||
|
||||
#: authentik/stages/user_login/models.py
|
||||
@@ -4077,8 +4102,8 @@ msgstr ""
|
||||
|
||||
#: authentik/tenants/models.py
|
||||
msgid ""
|
||||
"Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,"
|
||||
"seconds=2)."
|
||||
"Events will be deleted after this duration.(Format: "
|
||||
"weeks=3;days=2;hours=3,seconds=2)."
|
||||
msgstr ""
|
||||
|
||||
#: authentik/tenants/models.py
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
BIN
locale/ja/LC_MESSAGES/django.mo
Normal file
BIN
locale/ja/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
4365
locale/ja/LC_MESSAGES/django.po
Normal file
4365
locale/ja/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -473,7 +473,7 @@ class PostgresChannelLayerReceiver:
|
||||
"""
|
||||
DELETE
|
||||
FROM {table}
|
||||
WHERE {table}.{channel} IN (%s)
|
||||
WHERE {table}.{channel} = ANY(%s)
|
||||
AND {table}.{expires} >= %s
|
||||
RETURNING {table}.{id}, {table}.{channel}, {table}.{message}
|
||||
"""
|
||||
@@ -484,7 +484,7 @@ class PostgresChannelLayerReceiver:
|
||||
expires=sql.Identifier("expires"),
|
||||
message=sql.Identifier("message"),
|
||||
),
|
||||
(tuple(self._subscribed_to), now()),
|
||||
(list(self._subscribed_to), now()),
|
||||
)
|
||||
async for row in cursor:
|
||||
message_id, channel, message = row
|
||||
|
||||
@@ -61,6 +61,7 @@ def raise_connection_error(func: Callable[P, R]) -> Callable[P, R]:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except DATABASE_ERRORS as exc:
|
||||
logger.warning("Database error encountered", exc=exc)
|
||||
raise ConnectionError(str(exc)) from exc # type: ignore[no-untyped-call]
|
||||
|
||||
return wrapper
|
||||
@@ -239,15 +240,18 @@ class _PostgresConsumer(Consumer):
|
||||
self.in_processing: set[str] = set()
|
||||
self.prefetch = prefetch
|
||||
self.misses = 0
|
||||
# We have two different connections here. One for locks and one for listening to
|
||||
# notifications. We can't use the same connection for both as the listen connection might
|
||||
# be blocked with pending notifications. We also can't use a Django connection as we can't
|
||||
# be sure we'll get the same one every time to be able to release locks from the same
|
||||
# connection.
|
||||
self._locks_connection: DatabaseWrapper | None = None
|
||||
self._listen_connection: DatabaseWrapper | None = None
|
||||
self.postgres_channel = channel_name(self.queue_name, ChannelIdentifier.ENQUEUE)
|
||||
|
||||
# Override because dramatiq doesn't allow us setting this manually
|
||||
self.timeout = Conf().worker["consumer_listen_timeout"]
|
||||
|
||||
self.lock_purge_interval = timedelta(seconds=Conf().lock_purge_interval)
|
||||
self.lock_purge_last_run = timezone.now()
|
||||
|
||||
self.task_purge_interval = timedelta(seconds=Conf().task_purge_interval)
|
||||
self.task_purge_last_run = timezone.now() - self.task_purge_interval
|
||||
|
||||
@@ -258,14 +262,17 @@ class _PostgresConsumer(Consumer):
|
||||
self.scheduler_interval = timedelta(seconds=Conf().scheduler_interval)
|
||||
self.scheduler_last_run = timezone.now() - self.scheduler_interval
|
||||
|
||||
@property
|
||||
def connection(self) -> DatabaseWrapper:
|
||||
return cast(DatabaseWrapper, connections[self.db_alias])
|
||||
|
||||
@property
|
||||
def query_set(self) -> QuerySet[TaskBase]:
|
||||
return self.broker.query_set
|
||||
|
||||
@property
|
||||
def locks_connection(self) -> DatabaseWrapper:
|
||||
if self._locks_connection is not None and self._locks_connection.is_usable():
|
||||
return self._locks_connection
|
||||
self._locks_connection = cast(DatabaseWrapper, connections.create_connection(self.db_alias))
|
||||
return self._locks_connection
|
||||
|
||||
@property
|
||||
def listen_connection(self) -> DatabaseWrapper:
|
||||
if self._listen_connection is not None and self._listen_connection.is_usable():
|
||||
@@ -320,21 +327,40 @@ class _PostgresConsumer(Consumer):
|
||||
self.logger.debug("Message already consumed by self", message_id=message_id)
|
||||
return None
|
||||
|
||||
lock_result = (
|
||||
self.query_set.filter(message_id=message_id)
|
||||
.exclude(state__in=(TaskState.DONE, TaskState.REJECTED))
|
||||
.exclude(eta__gte=timezone.now() + timedelta(seconds=self.timeout))
|
||||
.extra(
|
||||
where=["pg_try_advisory_lock(%s)"],
|
||||
params=[self._get_message_lock_id(message_id)],
|
||||
with self.locks_connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
sql.SQL(
|
||||
"""
|
||||
UPDATE {table}
|
||||
SET {state} = %(state)s, {mtime} = %(mtime)s
|
||||
WHERE
|
||||
{table}.{message_id} = %(message_id)s
|
||||
AND
|
||||
{table}.{state} != ALL(%(excluded_states)s)
|
||||
AND
|
||||
({table}.{eta} < %(maximum_eta)s OR {table}.{eta} IS NULL)
|
||||
AND
|
||||
pg_try_advisory_lock(%(lock_id)s)
|
||||
"""
|
||||
).format(
|
||||
table=sql.Identifier(self.query_set.model._meta.db_table),
|
||||
state=sql.Identifier("state"),
|
||||
mtime=sql.Identifier("mtime"),
|
||||
message_id=sql.Identifier("message_id"),
|
||||
eta=sql.Identifier("eta"),
|
||||
),
|
||||
{
|
||||
"state": TaskState.CONSUMED.value,
|
||||
"mtime": timezone.now(),
|
||||
"message_id": message_id,
|
||||
"excluded_states": [TaskState.DONE.value, TaskState.REJECTED.value],
|
||||
"maximum_eta": timezone.now() + timedelta(seconds=self.timeout),
|
||||
"lock_id": self._get_message_lock_id(message_id),
|
||||
},
|
||||
)
|
||||
.update(
|
||||
state=TaskState.CONSUMED,
|
||||
mtime=timezone.now(),
|
||||
)
|
||||
)
|
||||
if lock_result != 1:
|
||||
return None
|
||||
if cursor.rowcount != 1:
|
||||
self._unlock_message(message_id)
|
||||
return None
|
||||
|
||||
task: TaskBase | None = (
|
||||
self.query_set.defer(None).defer("result").filter(message_id=message_id).first()
|
||||
@@ -405,9 +431,10 @@ class _PostgresConsumer(Consumer):
|
||||
def _unlock_message(self, message_id: str) -> bool:
|
||||
self.logger.debug("Unlocking message", message_id=message_id)
|
||||
try:
|
||||
with self.connection.cursor() as cursor:
|
||||
with self.locks_connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT pg_advisory_unlock(%s)", (self._get_message_lock_id(message_id),)
|
||||
"SELECT pg_advisory_unlock(%s)",
|
||||
(self._get_message_lock_id(message_id),),
|
||||
)
|
||||
return True
|
||||
except DATABASE_ERRORS:
|
||||
@@ -420,7 +447,7 @@ class _PostgresConsumer(Consumer):
|
||||
self.in_processing.remove(str(message.message_id))
|
||||
except KeyError:
|
||||
pass
|
||||
self._unlock_message(str(message.message_id))
|
||||
self.to_unlock.add(str(message.message_id))
|
||||
task = message.options.pop("task", None)
|
||||
self.query_set.filter(
|
||||
message_id=message.message_id,
|
||||
@@ -453,7 +480,6 @@ class _PostgresConsumer(Consumer):
|
||||
for message in messages:
|
||||
self.to_unlock.add(str(message.message_id))
|
||||
self.in_processing.remove(str(message.message_id))
|
||||
self._purge_locks()
|
||||
|
||||
def _scheduler(self) -> None:
|
||||
if not self.scheduler:
|
||||
@@ -464,8 +490,6 @@ class _PostgresConsumer(Consumer):
|
||||
self.schedule_last_run = timezone.now()
|
||||
|
||||
def _purge_locks(self) -> None:
|
||||
if timezone.now() - self.lock_purge_last_run < self.lock_purge_interval:
|
||||
return
|
||||
while True:
|
||||
try:
|
||||
message_id = self.to_unlock.pop()
|
||||
@@ -473,7 +497,6 @@ class _PostgresConsumer(Consumer):
|
||||
break
|
||||
if not self._unlock_message(str(message_id)):
|
||||
return
|
||||
self.lock_purge_last_run = timezone.now()
|
||||
|
||||
def _auto_purge(self) -> None:
|
||||
if timezone.now() - self.task_purge_last_run < self.task_purge_interval:
|
||||
@@ -492,15 +515,17 @@ class _PostgresConsumer(Consumer):
|
||||
try:
|
||||
self._purge_locks()
|
||||
finally:
|
||||
try:
|
||||
self.connection.close()
|
||||
except DATABASE_ERRORS:
|
||||
pass
|
||||
finally:
|
||||
if self._listen_connection is not None:
|
||||
conn = self._listen_connection
|
||||
self._listen_connection = None
|
||||
try:
|
||||
conn.close()
|
||||
except DATABASE_ERRORS:
|
||||
pass
|
||||
if self._locks_connection is not None:
|
||||
conn = self._locks_connection
|
||||
self._locks_connection = None
|
||||
try:
|
||||
conn.close()
|
||||
except DATABASE_ERRORS:
|
||||
pass
|
||||
if self._listen_connection is not None:
|
||||
conn = self._listen_connection
|
||||
self._listen_connection = None
|
||||
try:
|
||||
conn.close()
|
||||
except DATABASE_ERRORS:
|
||||
pass
|
||||
|
||||
@@ -63,10 +63,6 @@ class Conf:
|
||||
def task_model(self) -> str:
|
||||
return cast(str, self.conf["task_model"])
|
||||
|
||||
@property
|
||||
def lock_purge_interval(self) -> int:
|
||||
return cast(int, self.conf.get("lock_purge_interval", 60))
|
||||
|
||||
@property
|
||||
def task_purge_interval(self) -> int:
|
||||
# 24 hours
|
||||
|
||||
12
packages/docusaurus-config/package-lock.json
generated
12
packages/docusaurus-config/package-lock.json
generated
@@ -4646,9 +4646,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"version": "19.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4656,9 +4656,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -912,9 +912,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
164
packages/eslint-config/package-lock.json
generated
164
packages/eslint-config/package-lock.json
generated
@@ -383,21 +383,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
|
||||
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0"
|
||||
"@eslint/core": "^0.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
|
||||
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
@@ -430,9 +430,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
|
||||
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
||||
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -451,12 +451,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
|
||||
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -731,17 +731,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
|
||||
"integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz",
|
||||
"integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/type-utils": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"@typescript-eslint/scope-manager": "8.46.4",
|
||||
"@typescript-eslint/type-utils": "8.46.4",
|
||||
"@typescript-eslint/utils": "8.46.4",
|
||||
"@typescript-eslint/visitor-keys": "8.46.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -755,7 +755,7 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"@typescript-eslint/parser": "^8.46.4",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
@@ -771,16 +771,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz",
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz",
|
||||
"integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"@typescript-eslint/scope-manager": "8.46.4",
|
||||
"@typescript-eslint/types": "8.46.4",
|
||||
"@typescript-eslint/typescript-estree": "8.46.4",
|
||||
"@typescript-eslint/visitor-keys": "8.46.4",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -796,14 +796,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
|
||||
"integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz",
|
||||
"integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.2",
|
||||
"@typescript-eslint/types": "^8.46.2",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.4",
|
||||
"@typescript-eslint/types": "^8.46.4",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -818,14 +818,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
|
||||
"integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz",
|
||||
"integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2"
|
||||
"@typescript-eslint/types": "8.46.4",
|
||||
"@typescript-eslint/visitor-keys": "8.46.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -836,9 +836,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
|
||||
"integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz",
|
||||
"integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -853,15 +853,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz",
|
||||
"integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz",
|
||||
"integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.4",
|
||||
"@typescript-eslint/typescript-estree": "8.46.4",
|
||||
"@typescript-eslint/utils": "8.46.4",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
@@ -878,9 +878,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
|
||||
"integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz",
|
||||
"integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -892,16 +892,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
|
||||
"integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz",
|
||||
"integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.46.2",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/visitor-keys": "8.46.2",
|
||||
"@typescript-eslint/project-service": "8.46.4",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.4",
|
||||
"@typescript-eslint/types": "8.46.4",
|
||||
"@typescript-eslint/visitor-keys": "8.46.4",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -960,16 +960,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
|
||||
"integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz",
|
||||
"integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2"
|
||||
"@typescript-eslint/scope-manager": "8.46.4",
|
||||
"@typescript-eslint/types": "8.46.4",
|
||||
"@typescript-eslint/typescript-estree": "8.46.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -984,13 +984,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
|
||||
"integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz",
|
||||
"integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.4",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1923,19 +1923,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.1",
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.38.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
@@ -5077,16 +5077,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz",
|
||||
"integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==",
|
||||
"version": "8.46.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz",
|
||||
"integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.46.2",
|
||||
"@typescript-eslint/parser": "8.46.2",
|
||||
"@typescript-eslint/typescript-estree": "8.46.2",
|
||||
"@typescript-eslint/utils": "8.46.2"
|
||||
"@typescript-eslint/eslint-plugin": "8.46.4",
|
||||
"@typescript-eslint/parser": "8.46.4",
|
||||
"@typescript-eslint/typescript-estree": "8.46.4",
|
||||
"@typescript-eslint/utils": "8.46.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
||||
48
packages/prettier-config/package-lock.json
generated
48
packages/prettier-config/package-lock.json
generated
@@ -172,21 +172,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
|
||||
"integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0"
|
||||
"@eslint/core": "^0.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
|
||||
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.15"
|
||||
@@ -231,9 +231,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
|
||||
"integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
||||
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -252,12 +252,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
|
||||
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -388,9 +388,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -692,19 +692,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.38.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.1",
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.38.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
|
||||
@@ -17,7 +17,7 @@ COPY web .
|
||||
RUN npm run build-proxy
|
||||
|
||||
# Stage 2: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:a13297bfdd45d9702badd83b45c6db1f8fee638bba6ece0c359ce51260577bbe AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:27e1c927a07ed2c7295d39941d6d881424739dbde9ae3055d0d3013699ed35e8 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -47,7 +47,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/proxy ./cmd/proxy
|
||||
|
||||
# Stage 3: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:9b4cedf932e97194f1825124830f2eec14254d90162dad28f97e505971543115
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:40a1f32b318c9d4a488580bf156c408ec2e076898d40ada9256b7c219af79e19
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:a13297bfdd45d9702badd83b45c6db1f8fee638bba6ece0c359ce51260577bbe AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:27e1c927a07ed2c7295d39941d6d881424739dbde9ae3055d0d3013699ed35e8 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Stage 1: Build
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:a13297bfdd45d9702badd83b45c6db1f8fee638bba6ece0c359ce51260577bbe AS builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.25.4-trixie@sha256:27e1c927a07ed2c7295d39941d6d881424739dbde9ae3055d0d3013699ed35e8 AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -31,7 +31,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
|
||||
go build -o /go/radius ./cmd/radius
|
||||
|
||||
# Stage 2: Run
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:9b4cedf932e97194f1825124830f2eec14254d90162dad28f97e505971543115
|
||||
FROM ghcr.io/goauthentik/fips-debian:trixie-slim-fips@sha256:40a1f32b318c9d4a488580bf156c408ec2e076898d40ada9256b7c219af79e19
|
||||
|
||||
ARG VERSION
|
||||
ARG GIT_BUILD_HASH
|
||||
@@ -5,11 +5,11 @@ services:
|
||||
restart: never
|
||||
network_mode: none
|
||||
volumes:
|
||||
- ${LOCAL_PROJECT_DIR:-../../}:/local
|
||||
- ../../:/local
|
||||
|
||||
gen:
|
||||
image: docker.io/openapitools/openapi-generator-cli:v7.16.0
|
||||
restart: never
|
||||
network_mode: none
|
||||
volumes:
|
||||
- ${LOCAL_PROJECT_DIR:-../../}:/local
|
||||
- ../../:/local
|
||||
|
||||
@@ -75,5 +75,5 @@ base = {
|
||||
},
|
||||
}
|
||||
|
||||
with open("lifecycle/container/docker-compose.yml", "w") as _compose:
|
||||
with open("docker-compose.yml", "w") as _compose:
|
||||
safe_dump(base, _compose)
|
||||
|
||||
@@ -5,29 +5,29 @@ hash="$(git rev-parse HEAD || openssl rand -base64 36 | sha256sum)"
|
||||
AUTHENTIK_IMAGE="xghcr.io/goauthentik/server"
|
||||
AUTHENTIK_TAG="$(echo "$hash" | cut -c1-15)"
|
||||
|
||||
if [ -f lifecycle/container/.env ]; then
|
||||
if [ -f .env ]; then
|
||||
echo "Existing .env file, aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo PG_PASS="$(openssl rand -base64 36 | tr -d '\n')" >lifecycle/container/.env
|
||||
echo AUTHENTIK_SECRET_KEY="$(openssl rand -base64 60 | tr -d '\n')" >>lifecycle/container/.env
|
||||
echo PG_PASS="$(openssl rand -base64 36 | tr -d '\n')" >.env
|
||||
echo AUTHENTIK_SECRET_KEY="$(openssl rand -base64 60 | tr -d '\n')" >>.env
|
||||
export COMPOSE_PROJECT_NAME="authentik-test-${AUTHENTIK_TAG}"
|
||||
|
||||
if [[ -v BUILD ]]; then
|
||||
echo AUTHENTIK_IMAGE="${AUTHENTIK_IMAGE}" >>lifecycle/container/.env
|
||||
echo AUTHENTIK_TAG="${AUTHENTIK_TAG}" >>lifecycle/container/.env
|
||||
echo AUTHENTIK_IMAGE="${AUTHENTIK_IMAGE}" >>.env
|
||||
echo AUTHENTIK_TAG="${AUTHENTIK_TAG}" >>.env
|
||||
|
||||
# Ensure buildx is installed
|
||||
docker buildx install
|
||||
# For release builds we have an empty client here as we use the NPM package
|
||||
mkdir -p ./gen-ts-api
|
||||
touch lifecycle/container/.env
|
||||
touch .env
|
||||
|
||||
docker build -t "${AUTHENTIK_IMAGE}:${AUTHENTIK_TAG}" .
|
||||
fi
|
||||
|
||||
docker compose -f lifecycle/container/docker-compose.yml up --no-start
|
||||
docker compose -f lifecycle/container/docker-compose.yml start postgresql redis
|
||||
docker compose -f lifecycle/container/docker-compose.yml run -u root server test-all
|
||||
docker compose -f lifecycle/container/docker-compose.yml down -v
|
||||
docker compose up --no-start
|
||||
docker compose start postgresql
|
||||
docker compose run -u root server test-all
|
||||
docker compose down -v
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
services:
|
||||
chromium:
|
||||
image: docker.io/selenium/standalone-chromium:141.0
|
||||
image: docker.io/selenium/standalone-chromium:142.0
|
||||
shm_size: 2g
|
||||
network_mode: host
|
||||
restart: always
|
||||
mailpit:
|
||||
image: docker.io/axllent/mailpit:v1.27.10
|
||||
image: docker.io/axllent/mailpit:v1.27.11
|
||||
ports:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
enablePasswordDB: true
|
||||
issuer: http://127.0.0.1:5556/dex
|
||||
issuer: http://{{ .Env.AK_HOST }}:5556/dex
|
||||
logger:
|
||||
level: debug
|
||||
staticClients:
|
||||
|
||||
@@ -110,8 +110,8 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
|
||||
wait = WebDriverWait(identification_stage, self.wait_timeout)
|
||||
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "#enroll")))
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "#enroll").click()
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "a[name='enroll']")))
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "a[name='enroll']").click()
|
||||
|
||||
# First prompt stage
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
@@ -193,7 +193,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
|
||||
self.assertEqual(
|
||||
"Continue to confirm this email address.",
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "[data-test-id='stage-heading']").text,
|
||||
)
|
||||
|
||||
# Back on the main tab, confirm
|
||||
|
||||
@@ -26,8 +26,8 @@ class TestFlowsRecovery(SeleniumTestCase):
|
||||
identification_stage = self.get_shadow_root("ak-stage-identification", flow_executor)
|
||||
wait = WebDriverWait(identification_stage, self.wait_timeout)
|
||||
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "#recovery")))
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "#recovery").click()
|
||||
wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "a[name='recovery']")))
|
||||
identification_stage.find_element(By.CSS_SELECTOR, "a[name='recovery']").click()
|
||||
|
||||
# First prompt stage
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
|
||||
@@ -158,7 +158,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
|
||||
self.assertIn(
|
||||
app.name,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "[data-test-id='stage-heading']").text,
|
||||
)
|
||||
self.assertEqual(
|
||||
"GitHub Compatibility: Access you Email addresses",
|
||||
@@ -228,8 +228,10 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--github").click()
|
||||
self.login()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")))
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[data-test-id='card-title']"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[data-test-id='card-title']").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
||||
@@ -327,7 +327,7 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
|
||||
self.assertIn(
|
||||
app.name,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "[data-test-id='stage-heading']").text,
|
||||
)
|
||||
consent_stage.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
@@ -407,9 +407,11 @@ class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||
self.login()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")))
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[data-test-id='card-title']"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[data-test-id='card-title']").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""test OAuth2 OpenID Provider flow"""
|
||||
|
||||
from json import loads
|
||||
from json import dumps
|
||||
from time import sleep
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
@@ -146,6 +146,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
|
||||
self.driver.get("http://localhost:9009")
|
||||
self.login()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-flow-executor")))
|
||||
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
@@ -153,26 +154,63 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
|
||||
self.assertIn(
|
||||
app.name,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "[data-test-id='stage-heading']").text,
|
||||
)
|
||||
|
||||
current_url = self.driver.current_url
|
||||
|
||||
consent_stage.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
"[type=submit]",
|
||||
).click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
|
||||
self.wait.until(ec.text_to_be_present_in_element((By.CSS_SELECTOR, "pre"), "{"))
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
self.wait.until(ec.url_changes(current_url))
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["nickname"], self.user.username)
|
||||
self.assertEqual(body["IDTokenClaims"]["amr"], ["pwd"])
|
||||
self.assertEqual(body["UserInfo"]["nickname"], self.user.username)
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["name"], self.user.name)
|
||||
self.assertEqual(body["UserInfo"]["name"], self.user.name)
|
||||
self.assertEqual(
|
||||
body.get("IDTokenClaims", {}).get("nickname"),
|
||||
self.user.username,
|
||||
f"IDTokenClaims.nickname mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["email"], self.user.email)
|
||||
self.assertEqual(body["UserInfo"]["email"], self.user.email)
|
||||
self.assertEqual(
|
||||
body.get("IDTokenClaims", {}).get("amr"),
|
||||
["pwd"],
|
||||
f"IDTokenClaims.amr mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body.get("IDTokenClaims", {}).get("name"),
|
||||
self.user.name,
|
||||
f"IDTokenClaims.name mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body.get("IDTokenClaims", {}).get("email"),
|
||||
self.user.email,
|
||||
f"IDTokenClaims.email mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
# UserInfo assertions
|
||||
self.assertEqual(
|
||||
body.get("UserInfo", {}).get("nickname"),
|
||||
self.user.username,
|
||||
f"UserInfo.nickname mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body.get("UserInfo", {}).get("name"),
|
||||
self.user.name,
|
||||
f"UserInfo.name mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body.get("UserInfo", {}).get("email"),
|
||||
self.user.email,
|
||||
f"UserInfo.email mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
@@ -227,25 +265,56 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
|
||||
self.assertIn(
|
||||
app.name,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "[data-test-id='stage-heading']").text,
|
||||
)
|
||||
|
||||
current_url = self.driver.current_url
|
||||
|
||||
consent_stage.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
"[type=submit]",
|
||||
).click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
|
||||
self.wait.until(ec.text_to_be_present_in_element((By.CSS_SELECTOR, "pre"), "{"))
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
self.wait.until(ec.url_changes(current_url))
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["nickname"], self.user.username)
|
||||
self.assertEqual(body["UserInfo"]["nickname"], self.user.username)
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["name"], self.user.name)
|
||||
self.assertEqual(body["UserInfo"]["name"], self.user.name)
|
||||
id_token_claims = body.get("IDTokenClaims", {})
|
||||
user_info = body.get("UserInfo", {})
|
||||
|
||||
self.assertEqual(body["IDTokenClaims"]["email"], self.user.email)
|
||||
self.assertEqual(body["UserInfo"]["email"], self.user.email)
|
||||
self.assertEqual(
|
||||
id_token_claims.get("nickname"),
|
||||
self.user.username,
|
||||
f"IDTokenClaims.nickname mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
self.assertEqual(
|
||||
user_info.get("nickname"),
|
||||
self.user.username,
|
||||
f"UserInfo.nickname mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
id_token_claims.get("name"),
|
||||
self.user.name,
|
||||
f"IDTokenClaims.name mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
self.assertEqual(
|
||||
user_info.get("name"),
|
||||
self.user.name,
|
||||
f"UserInfo.name mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
id_token_claims.get("email"),
|
||||
self.user.email,
|
||||
f"IDTokenClaims.email mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
self.assertEqual(
|
||||
user_info.get("email"),
|
||||
self.user.email,
|
||||
f"UserInfo.email mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
@@ -297,8 +366,10 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
self.setup_client()
|
||||
self.driver.get("http://localhost:9009")
|
||||
self.login()
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")))
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[data-test-id='card-title']"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[data-test-id='card-title']").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""test OAuth2 OpenID Provider flow"""
|
||||
|
||||
from json import loads
|
||||
from json import dumps
|
||||
from time import sleep
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
@@ -149,12 +149,29 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
self.driver.get("http://localhost:9009/implicit/")
|
||||
self.wait.until(ec.title_contains("authentik"))
|
||||
self.login()
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
|
||||
self.wait.until(ec.text_to_be_present_in_element((By.CSS_SELECTOR, "pre"), "{"))
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
self.assertEqual(body["profile"]["nickname"], self.user.username)
|
||||
self.assertEqual(body["profile"]["name"], self.user.name)
|
||||
self.assertEqual(body["profile"]["email"], self.user.email)
|
||||
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
profile = body.get("profile", {})
|
||||
|
||||
self.assertEqual(
|
||||
profile.get("nickname"),
|
||||
self.user.username,
|
||||
f"Nickname mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
profile.get("name"),
|
||||
self.user.name,
|
||||
f"Name mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
profile.get("email"),
|
||||
self.user.email,
|
||||
f"Email mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
@@ -211,20 +228,40 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
|
||||
self.assertIn(
|
||||
app.name,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "[data-test-id='stage-heading']").text,
|
||||
)
|
||||
|
||||
current_url = self.driver.current_url
|
||||
|
||||
consent_stage.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
"[type=submit]",
|
||||
).click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
|
||||
self.wait.until(ec.text_to_be_present_in_element((By.CSS_SELECTOR, "pre"), "{"))
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
self.wait.until(ec.url_changes(current_url))
|
||||
|
||||
self.assertEqual(body["profile"]["nickname"], self.user.username)
|
||||
self.assertEqual(body["profile"]["name"], self.user.name)
|
||||
self.assertEqual(body["profile"]["email"], self.user.email)
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
profile = body.get("profile", {})
|
||||
|
||||
self.assertEqual(
|
||||
profile.get("nickname"),
|
||||
self.user.username,
|
||||
f"Nickname mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
profile.get("name"),
|
||||
self.user.name,
|
||||
f"Name mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
profile.get("email"),
|
||||
self.user.email,
|
||||
f"Email mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
@@ -278,8 +315,10 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
|
||||
self.driver.get("http://localhost:9009/implicit/")
|
||||
self.wait.until(ec.title_contains("authentik"))
|
||||
self.login()
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")))
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[data-test-id='card-title']"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[data-test-id='card-title']").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from base64 import b64encode
|
||||
from dataclasses import asdict
|
||||
from json import loads
|
||||
from json import dumps
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from unittest.case import skip, skipUnless
|
||||
@@ -94,24 +94,39 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
self.login()
|
||||
sleep(1)
|
||||
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
body = loads(full_body_text)
|
||||
body = self.parse_json_content()
|
||||
headers = body.get("headers", {})
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username])
|
||||
self.assertEqual(body["headers"]["X-Foo"], ["bar"])
|
||||
raw_jwt: str = body["headers"]["X-Authentik-Jwt"][0]
|
||||
self.assertEqual(
|
||||
headers.get("X-Authentik-Username"),
|
||||
[self.user.username],
|
||||
f"X-Authentik-Username header mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
self.assertEqual(
|
||||
headers.get("X-Foo"),
|
||||
["bar"],
|
||||
f"X-Foo header mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
raw_jwt: str = headers.get("X-Authentik-Jwt", [None])[0]
|
||||
jwt = decode(raw_jwt, options={"verify_signature": False})
|
||||
|
||||
self.assertIsNotNone(jwt["sid"])
|
||||
self.assertIsNotNone(jwt["ak_proxy"])
|
||||
self.assertIsNotNone(jwt["sid"], "Missing 'sid' in JWT")
|
||||
self.assertIsNotNone(jwt["ak_proxy"], "Missing 'ak_proxy' in JWT")
|
||||
|
||||
self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor)
|
||||
flow_card = self.get_shadow_root("ak-flow-card", session_end_stage)
|
||||
title = flow_card.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text
|
||||
self.assertIn("You've logged out of", title)
|
||||
|
||||
self.assertIn(
|
||||
"You've logged out of",
|
||||
title,
|
||||
f"Logout title mismatch at {self.driver.current_url}: {title}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
@@ -167,20 +182,37 @@ class TestProviderProxy(SeleniumTestCase):
|
||||
self.login()
|
||||
sleep(1)
|
||||
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
body = loads(full_body_text)
|
||||
body = self.parse_json_content()
|
||||
headers = body.get("headers", {})
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
self.assertEqual(
|
||||
headers.get("X-Authentik-Username"),
|
||||
[self.user.username],
|
||||
f"X-Authentik-Username header mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username])
|
||||
auth_header = b64encode(f"{cred}:{cred}".encode()).decode()
|
||||
self.assertEqual(body["headers"]["Authorization"], [f"Basic {auth_header}"])
|
||||
|
||||
self.assertEqual(
|
||||
headers.get("Authorization"),
|
||||
[f"Basic {auth_header}"],
|
||||
f"Authorization header mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
session_end_stage = self.get_shadow_root("ak-stage-session-end", flow_executor)
|
||||
flow_card = self.get_shadow_root("ak-flow-card", session_end_stage)
|
||||
title = flow_card.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text
|
||||
self.assertIn("You've logged out of", title)
|
||||
|
||||
self.assertIn(
|
||||
"You've logged out of",
|
||||
title,
|
||||
f"Logout title mismatch at {self.driver.current_url}: {title}",
|
||||
)
|
||||
|
||||
|
||||
# TODO: Fix flaky test
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Proxy and Outpost e2e tests"""
|
||||
|
||||
from json import loads
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
from unittest import skip
|
||||
@@ -101,10 +101,14 @@ class TestProviderProxyForward(SeleniumTestCase):
|
||||
self.login()
|
||||
sleep(1)
|
||||
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
body = loads(full_body_text)
|
||||
body_json = self.parse_json_content()
|
||||
snippet = dumps(body_json, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username])
|
||||
self.assertEqual(
|
||||
body_json.get("headers", {}).get("X-Authentik-Username"),
|
||||
[self.user.username],
|
||||
f"X-Authentik-Username header mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
@@ -137,10 +141,14 @@ class TestProviderProxyForward(SeleniumTestCase):
|
||||
self.login()
|
||||
sleep(1)
|
||||
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
body = loads(full_body_text)
|
||||
body_json = self.parse_json_content()
|
||||
snippet = dumps(body_json, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username])
|
||||
self.assertEqual(
|
||||
body_json.get("headers", {}).get("X-Authentik-Username"),
|
||||
[self.user.username],
|
||||
f"X-Authentik-Username header mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
@@ -171,10 +179,14 @@ class TestProviderProxyForward(SeleniumTestCase):
|
||||
self.login()
|
||||
sleep(1)
|
||||
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
body = loads(full_body_text)
|
||||
body_json = self.parse_json_content()
|
||||
snippet = dumps(body_json, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username])
|
||||
self.assertEqual(
|
||||
body_json.get("headers", {}).get("X-Authentik-Username"),
|
||||
[self.user.username],
|
||||
f"X-Authentik-Username header mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
@@ -208,10 +220,14 @@ class TestProviderProxyForward(SeleniumTestCase):
|
||||
self.login()
|
||||
sleep(1)
|
||||
|
||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
body = loads(full_body_text)
|
||||
body_json = self.parse_json_content()
|
||||
snippet = dumps(body_json, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
self.assertEqual(body["headers"]["X-Authentik-Username"], [self.user.username])
|
||||
self.assertEqual(
|
||||
body_json.get("headers", {}).get("X-Authentik-Username"),
|
||||
[self.user.username],
|
||||
f"X-Authentik-Username header mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.driver.get("http://localhost/outpost.goauthentik.io/sign_out")
|
||||
sleep(2)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""test SAML Provider flow"""
|
||||
|
||||
from json import loads
|
||||
from json import dumps
|
||||
from time import sleep
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
@@ -86,33 +86,44 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.login()
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
attrs = body.get("attr", {})
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"),
|
||||
[self.user.name],
|
||||
f"Claim 'name' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"][
|
||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
],
|
||||
attrs.get("http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"),
|
||||
[self.user.username],
|
||||
f"Claim 'windowsaccountname' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/username"),
|
||||
[self.user.username],
|
||||
f"Claim 'saml/username' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/uid"),
|
||||
[str(self.user.pk)],
|
||||
f"Claim 'saml/uid' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"),
|
||||
[self.user.email],
|
||||
f"Claim 'emailaddress' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"),
|
||||
[self.user.email],
|
||||
f"Claim 'upn' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@@ -154,33 +165,44 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.login()
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
attrs = body.get("attr", {})
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"),
|
||||
[self.user.name],
|
||||
f"Claim 'name' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"][
|
||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
],
|
||||
attrs.get("http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"),
|
||||
[self.user.username],
|
||||
f"Claim 'windowsaccountname' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/username"),
|
||||
[self.user.username],
|
||||
f"Claim 'saml/username' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/uid"),
|
||||
[str(self.user.pk)],
|
||||
f"Claim 'saml/uid' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"),
|
||||
[self.user.email],
|
||||
f"Claim 'emailaddress' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"),
|
||||
[self.user.email],
|
||||
f"Claim 'upn' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@@ -228,7 +250,8 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
|
||||
self.assertIn(
|
||||
app.name,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "[data-test-id='stage-heading']").text,
|
||||
"Consent stage header mismatch",
|
||||
)
|
||||
consent_stage.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
@@ -237,33 +260,44 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
attrs = body.get("attr", {})
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"),
|
||||
[self.user.name],
|
||||
f"Claim 'name' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"][
|
||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
],
|
||||
attrs.get("http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"),
|
||||
[self.user.username],
|
||||
f"Claim 'windowsaccountname' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/username"),
|
||||
[self.user.username],
|
||||
f"Claim 'saml/username' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/uid"),
|
||||
[str(self.user.pk)],
|
||||
f"Claim 'saml/uid' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"),
|
||||
[self.user.email],
|
||||
f"Claim 'emailaddress' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"),
|
||||
[self.user.email],
|
||||
f"Claim 'upn' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@@ -311,7 +345,7 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
|
||||
self.assertIn(
|
||||
app.name,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "[data-test-id='stage-heading']").text,
|
||||
)
|
||||
consent_stage.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
@@ -320,33 +354,44 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
attrs = body.get("attr", {})
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"),
|
||||
[self.user.name],
|
||||
f"Claim 'name' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"][
|
||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
],
|
||||
attrs.get("http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"),
|
||||
[self.user.username],
|
||||
f"Claim 'windowsaccountname' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/username"),
|
||||
[self.user.username],
|
||||
f"Claim 'username' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/uid"),
|
||||
[str(self.user.pk)],
|
||||
f"Claim 'uid' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"),
|
||||
[self.user.email],
|
||||
f"Claim 'emailaddress' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"),
|
||||
[self.user.email],
|
||||
f"Claim 'upn' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@@ -394,33 +439,44 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
sleep(1)
|
||||
self.wait_for_url("http://localhost:9009/")
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
attrs = body.get("attr", {})
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"),
|
||||
[self.user.name],
|
||||
f"Claim 'name' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"][
|
||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
],
|
||||
attrs.get("http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"),
|
||||
[self.user.username],
|
||||
f"Claim 'windowsaccountname' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/username"),
|
||||
[self.user.username],
|
||||
f"Claim 'username' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/uid"),
|
||||
[str(self.user.pk)],
|
||||
f"Claim 'uid' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"),
|
||||
[self.user.email],
|
||||
f"Claim 'emailaddress' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"),
|
||||
[self.user.email],
|
||||
f"Claim 'upn' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@@ -465,9 +521,12 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
self.driver.get("http://localhost:9009/")
|
||||
self.login()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")))
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[data-test-id='card-title']"))
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[data-test-id='card-title']").text,
|
||||
"Permission denied",
|
||||
)
|
||||
|
||||
@@ -589,31 +648,42 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
|
||||
self.wait_for_url(f"http://{self.host}:9009/")
|
||||
|
||||
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
body = self.parse_json_content()
|
||||
snippet = dumps(body, indent=2)[:500].replace("\n", " ")
|
||||
attrs = body.get("attr", {})
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"),
|
||||
[self.user.name],
|
||||
f"Claim 'name' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"][
|
||||
"http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"
|
||||
],
|
||||
attrs.get("http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname"),
|
||||
[self.user.username],
|
||||
f"Claim 'windowsaccountname' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/username"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/username"),
|
||||
[self.user.username],
|
||||
f"Claim 'username' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.goauthentik.io/2021/02/saml/uid"],
|
||||
attrs.get("http://schemas.goauthentik.io/2021/02/saml/uid"),
|
||||
[str(self.user.pk)],
|
||||
f"Claim 'uid' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"),
|
||||
[self.user.email],
|
||||
f"Claim 'emailaddress' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
body["attr"]["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"],
|
||||
attrs.get("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"),
|
||||
[self.user.email],
|
||||
f"Claim 'upn' mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
@@ -109,11 +109,11 @@ class TestSourceOAuth1(SeleniumTestCase):
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
(By.CSS_SELECTOR, "fieldset[name='login-sources'] button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
By.CSS_SELECTOR, "fieldset[name='login-sources'] button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""test OAuth Source"""
|
||||
|
||||
from json import loads
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
@@ -16,7 +15,10 @@ from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.stages.identification.models import IdentificationStage
|
||||
from tests.e2e.utils import SeleniumTestCase, retry
|
||||
from tests.e2e.utils import NoSuchElementException, SeleniumTestCase, TimeoutException, retry
|
||||
|
||||
MAX_REFRESH_RETRIES = 5
|
||||
INTERFACE_TIMEOUT = 10
|
||||
|
||||
|
||||
class TestSourceOAuth2(SeleniumTestCase):
|
||||
@@ -27,7 +29,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
self.slug = generate_id()
|
||||
super().setUp()
|
||||
self.run_container(
|
||||
image="ghcr.io/dexidp/dex:v2.28.1",
|
||||
image="ghcr.io/dexidp/dex:v2.44.0",
|
||||
ports={"5556": "5556"},
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
|
||||
@@ -35,6 +37,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
start_period=1 * 1_000 * 1_000_000,
|
||||
),
|
||||
environment={
|
||||
"AK_HOST": self.host,
|
||||
"AK_REDIRECT_URL": self.url(
|
||||
"authentik_sources_oauth:oauth-client-callback",
|
||||
source_slug=self.slug,
|
||||
@@ -48,6 +51,69 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def find_settings_tab_panel(self, tab_name: str, panel_content_selector: str):
|
||||
"""Find a settings tab panel by name"""
|
||||
url_after_login = self.driver.current_url
|
||||
|
||||
user_settings_url = self.if_user_url("/settings")
|
||||
hash_route = ';%7B"page"%3A"page-' + tab_name + '"%7D'
|
||||
|
||||
self.driver.get(user_settings_url + hash_route)
|
||||
|
||||
# A refresh is required because the hash change doesn't always trigger a reload.
|
||||
self.driver.refresh()
|
||||
|
||||
try:
|
||||
self.wait.until(ec.url_contains(user_settings_url))
|
||||
except TimeoutException:
|
||||
self.fail(
|
||||
f"Timed out waiting for user settings page"
|
||||
f"Initial URL after OAuth linking: {url_after_login} "
|
||||
f"Current URL: {self.driver.current_url} "
|
||||
f"Expected URL: {user_settings_url})"
|
||||
)
|
||||
|
||||
try:
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "ak-interface-user")))
|
||||
except TimeoutException:
|
||||
context = self.driver.find_element(By.TAG_NAME, "body")
|
||||
inner_html = context.get_attribute("innerHTML") or ""
|
||||
|
||||
snippet = context.text.strip()[:1000].replace("\n", " ")
|
||||
|
||||
self.fail(
|
||||
f"Timed out waiting for element text to appear at {self.driver.current_url}. "
|
||||
f"Current content: {snippet or '<empty>'}"
|
||||
f"{inner_html or '<empty>'}"
|
||||
)
|
||||
|
||||
interface = self.driver.find_element(By.CSS_SELECTOR, "ak-interface-user").shadow_root
|
||||
|
||||
interface_wait = WebDriverWait(interface, INTERFACE_TIMEOUT)
|
||||
|
||||
try:
|
||||
interface_wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "ak-interface-user-presentation"))
|
||||
)
|
||||
except TimeoutException:
|
||||
snippet = context.text.strip()[:1000].replace("\n", " ")
|
||||
self.fail(
|
||||
f"Timed out waiting for element text to appear at {self.driver.current_url}. "
|
||||
f"Current content: {snippet or '<empty>'}"
|
||||
)
|
||||
|
||||
interface_presentation = interface.find_element(
|
||||
By.CSS_SELECTOR, "ak-interface-user-presentation"
|
||||
).shadow_root
|
||||
|
||||
user_settings = interface_presentation.find_element(
|
||||
By.CSS_SELECTOR, "ak-user-settings"
|
||||
).shadow_root
|
||||
|
||||
tab_panel = user_settings.find_element(By.CSS_SELECTOR, panel_content_selector).shadow_root
|
||||
|
||||
return tab_panel
|
||||
|
||||
def create_objects(self):
|
||||
"""Create required objects"""
|
||||
# Bootstrap all needed objects
|
||||
@@ -60,9 +126,9 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
authentication_flow=authentication_flow,
|
||||
enrollment_flow=enrollment_flow,
|
||||
provider_type="openidconnect",
|
||||
authorization_url="http://127.0.0.1:5556/dex/auth",
|
||||
access_token_url="http://127.0.0.1:5556/dex/token",
|
||||
profile_url="http://127.0.0.1:5556/dex/userinfo",
|
||||
authorization_url=f"http://{self.host}:5556/dex/auth",
|
||||
access_token_url=f"http://{self.host}:5556/dex/token",
|
||||
profile_url=f"http://{self.host}:5556/dex/userinfo",
|
||||
consumer_key="example-app",
|
||||
consumer_secret=self.client_secret,
|
||||
)
|
||||
@@ -70,6 +136,28 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
ident_stage.sources.set([source])
|
||||
ident_stage.save()
|
||||
|
||||
def login_via_oauth_provider(self):
|
||||
"""Perform login at the OAuth provider (Dex)"""
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
|
||||
initial_provider_url = self.driver.current_url
|
||||
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]")))
|
||||
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
self.wait.until(ec.url_changes(initial_provider_url))
|
||||
|
||||
self.assertNotEqual(
|
||||
initial_provider_url,
|
||||
self.driver.current_url,
|
||||
"Expected to be redirected after login at OAuth provider",
|
||||
)
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
@@ -91,22 +179,14 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
(By.CSS_SELECTOR, "fieldset[name='login-sources'] button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
By.CSS_SELECTOR, "fieldset[name='login-sources'] button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]")))
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
self.login_via_oauth_provider()
|
||||
|
||||
# At this point we've been redirected back
|
||||
# and we're asked for the username
|
||||
@@ -135,25 +215,16 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
(By.CSS_SELECTOR, "fieldset[name='login-sources'] button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
By.CSS_SELECTOR, "fieldset[name='login-sources'] button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
self.login_via_oauth_provider()
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]")))
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
# Wait until we've logged in
|
||||
self.wait_for_url(self.if_user_url())
|
||||
self.wait.until(ec.url_matches(self.if_user_url()))
|
||||
|
||||
self.assert_user(User(username="foo", name="admin", email="admin@example.com"))
|
||||
|
||||
@@ -167,30 +238,93 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
"default/flow-default-source-enrollment.yaml",
|
||||
"default/flow-default-source-pre-authentication.yaml",
|
||||
)
|
||||
def test_oauth_link(self):
|
||||
"""test OAuth Source link OIDC"""
|
||||
def test_oauth_link(self) -> None:
|
||||
"""
|
||||
Test OAuth Source link OIDC
|
||||
|
||||
This test will enroll the user via OAuth, then log in as admin and link the OAuth
|
||||
source to the admin user.
|
||||
"""
|
||||
self.create_objects()
|
||||
self.driver.get(self.live_server_url)
|
||||
self.login()
|
||||
|
||||
# Ensure that a stable session is created before linking.
|
||||
sleep(3)
|
||||
|
||||
self.driver.get(
|
||||
self.url("authentik_sources_oauth:oauth-client-login", source_slug=self.slug)
|
||||
)
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
self.login_via_oauth_provider()
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]")))
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
post_login_expected_url = self.if_user_url("/settings;page-sources")
|
||||
|
||||
self.driver.get(self.url("authentik_api:usersourceconnection-list") + "?format=json")
|
||||
body_json = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||
results = body_json["results"]
|
||||
self.assertEqual(len(results), 1)
|
||||
connection = results[0]
|
||||
self.assertEqual(connection["source_obj"]["slug"], self.slug)
|
||||
self.assertEqual(connection["user"], self.user.pk)
|
||||
self.assertEqual(
|
||||
self.driver.current_url,
|
||||
post_login_expected_url,
|
||||
"Expected to be redirected to user settings after linking OAuth source",
|
||||
)
|
||||
|
||||
selector = f"[data-test-id=source-settings-list-item][data-slug='{self.slug}']"
|
||||
sourceElement = None
|
||||
|
||||
for attempt in range(MAX_REFRESH_RETRIES):
|
||||
source_settings_tab_panel = self.find_settings_tab_panel(
|
||||
"sources", "ak-user-settings-source"
|
||||
)
|
||||
|
||||
try:
|
||||
sourceElement = source_settings_tab_panel.find_element(By.CSS_SELECTOR, selector)
|
||||
except NoSuchElementException:
|
||||
sourceElement = None
|
||||
|
||||
if sourceElement:
|
||||
break
|
||||
|
||||
if attempt < MAX_REFRESH_RETRIES - 1:
|
||||
self.logger.debug(
|
||||
f"[Attempt {attempt + 1}/{MAX_REFRESH_RETRIES}] No results yet, sleeping 1s… "
|
||||
f"(Current URL: {self.driver.current_url})"
|
||||
)
|
||||
|
||||
sleep(1)
|
||||
|
||||
if not sourceElement:
|
||||
context = self.driver.find_element(By.TAG_NAME, "body")
|
||||
inner_html = context.get_attribute("innerHTML") or ""
|
||||
|
||||
snippet = context.text.strip()[:1000].replace("\n", " ")
|
||||
|
||||
self.fail(
|
||||
f"Selector '{selector}' not found at {self.driver.current_url}"
|
||||
f" after {MAX_REFRESH_RETRIES} retries. "
|
||||
f"Current content: {snippet or '<empty>'}"
|
||||
f"{inner_html or '<empty>'}"
|
||||
)
|
||||
|
||||
data_source_component_attribute = sourceElement.get_attribute("data-source-component")
|
||||
|
||||
self.assertIsNotNone(
|
||||
data_source_component_attribute,
|
||||
f"Source Component not found in source element at {self.driver.current_url}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
data_source_component_attribute,
|
||||
"ak-user-settings-source-oauth",
|
||||
"Unexpected source component",
|
||||
)
|
||||
|
||||
connection_user_pk_attribute = sourceElement.get_attribute("data-connection-user-pk")
|
||||
|
||||
self.assertIsNotNone(
|
||||
connection_user_pk_attribute,
|
||||
f"Connection User PK not found in source element at {self.driver.current_url}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
int(connection_user_pk_attribute),
|
||||
self.user.pk,
|
||||
f"Unexpected user {self.driver.current_url}",
|
||||
)
|
||||
|
||||
@@ -104,6 +104,15 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
},
|
||||
)
|
||||
|
||||
def login_via_saml_source(self):
|
||||
"""Perform login at the SAML IDP"""
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
self.wait_for_url(self.if_user_url())
|
||||
|
||||
@retry()
|
||||
@apply_blueprint(
|
||||
"default/flow-default-authentication-flow.yaml",
|
||||
@@ -149,21 +158,14 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
(By.CSS_SELECTOR, "fieldset[name='login-sources'] button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
By.CSS_SELECTOR, "fieldset[name='login-sources'] button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url())
|
||||
self.login_via_saml_source()
|
||||
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
@@ -218,11 +220,11 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
(By.CSS_SELECTOR, "fieldset[name='login-sources'] button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
By.CSS_SELECTOR, "fieldset[name='login-sources'] button"
|
||||
).click()
|
||||
sleep(1)
|
||||
|
||||
@@ -231,21 +233,14 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
self.assertIn(
|
||||
source.name,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text,
|
||||
consent_stage.find_element(By.CSS_SELECTOR, "[data-test-id='stage-heading']").text,
|
||||
)
|
||||
consent_stage.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
"[type=submit]",
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url())
|
||||
self.login_via_saml_source()
|
||||
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
@@ -300,21 +295,14 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
(By.CSS_SELECTOR, "fieldset[name='login-sources'] button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
By.CSS_SELECTOR, "fieldset[name='login-sources'] button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url())
|
||||
self.login_via_saml_source()
|
||||
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
@@ -369,21 +357,14 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
(By.CSS_SELECTOR, "fieldset[name='login-sources'] button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
By.CSS_SELECTOR, "fieldset[name='login-sources'] button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url())
|
||||
self.login_via_saml_source()
|
||||
|
||||
self.assert_user(
|
||||
User.objects.exclude(username="akadmin")
|
||||
@@ -403,21 +384,14 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button")
|
||||
(By.CSS_SELECTOR, "fieldset[name='login-sources'] button")
|
||||
)
|
||||
)
|
||||
identification_stage.find_element(
|
||||
By.CSS_SELECTOR, ".pf-c-login__main-footer-links-item > button"
|
||||
By.CSS_SELECTOR, "fieldset[name='login-sources'] button"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the username field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "username")))
|
||||
self.driver.find_element(By.ID, "username").send_keys("user1")
|
||||
self.driver.find_element(By.ID, "password").send_keys("user1pass")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.if_user_url())
|
||||
self.login_via_saml_source()
|
||||
|
||||
# sleep(999999)
|
||||
self.assert_user(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""authentik e2e testing utilities"""
|
||||
|
||||
import json
|
||||
import socket
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache, wraps
|
||||
from json import JSONDecodeError, dumps, loads
|
||||
from os import environ, getenv
|
||||
from sys import stderr
|
||||
from time import sleep
|
||||
@@ -22,7 +22,12 @@ from docker.errors import DockerException
|
||||
from docker.models.containers import Container
|
||||
from docker.models.networks import Network
|
||||
from selenium import webdriver
|
||||
from selenium.common.exceptions import NoSuchElementException, TimeoutException, WebDriverException
|
||||
from selenium.common.exceptions import (
|
||||
NoSuchElementException,
|
||||
NoSuchShadowRootException,
|
||||
TimeoutException,
|
||||
WebDriverException,
|
||||
)
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.remote.command import Command
|
||||
@@ -40,6 +45,9 @@ from authentik.root.test_runner import get_docker_tag
|
||||
|
||||
IS_CI = "CI" in environ
|
||||
RETRIES = int(environ.get("RETRIES", "3")) if IS_CI else 1
|
||||
SHADOW_ROOT_RETRIES = 5
|
||||
|
||||
JSONType = dict[str, Any] | list[Any] | str | int | float | bool | None
|
||||
|
||||
|
||||
def get_local_ip() -> str:
|
||||
@@ -206,14 +214,16 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
print("::endgroup::")
|
||||
self.driver.quit()
|
||||
|
||||
def wait_for_url(self, desired_url):
|
||||
def wait_for_url(self, desired_url: str):
|
||||
"""Wait until URL is `desired_url`."""
|
||||
|
||||
self.wait.until(
|
||||
lambda driver: driver.current_url == desired_url,
|
||||
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}",
|
||||
f"URL {self.driver.current_url} doesn't match expected URL {desired_url}. "
|
||||
f"HTML: {self.driver.page_source[:1000]}",
|
||||
)
|
||||
|
||||
def url(self, view, query: dict | None = None, **kwargs) -> str:
|
||||
def url(self, view: str, query: dict | None = None, **kwargs) -> str:
|
||||
"""reverse `view` with `**kwargs` into full URL using live_server_url"""
|
||||
url = self.live_server_url + reverse(view, kwargs=kwargs)
|
||||
if query:
|
||||
@@ -227,14 +237,112 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
return f"{url}#{path}"
|
||||
return url
|
||||
|
||||
def parse_json_content(
|
||||
self, context: WebElement | None = None, timeout: float | None = 10
|
||||
) -> JSONType:
|
||||
"""
|
||||
Parse JSON from a Selenium element's text content.
|
||||
|
||||
If `context` is not provided, defaults to the <body> element.
|
||||
Raises a clear test failure if the element isn't found, the text doesn't appear
|
||||
within `timeout` seconds, or the text is not valid JSON.
|
||||
"""
|
||||
|
||||
try:
|
||||
if context is None:
|
||||
context = self.driver.find_element(By.TAG_NAME, "body")
|
||||
except NoSuchElementException:
|
||||
self.fail(
|
||||
f"No element found (defaulted to <body>). Current URL: {self.driver.current_url}"
|
||||
)
|
||||
|
||||
wait_timeout = timeout or self.wait_timeout
|
||||
wait = WebDriverWait(context, wait_timeout)
|
||||
|
||||
try:
|
||||
wait.until(lambda d: len(d.text.strip()) != 0)
|
||||
except TimeoutException:
|
||||
snippet = context.text.strip()[:500].replace("\n", " ")
|
||||
self.fail(
|
||||
f"Timed out waiting for element text to appear at {self.driver.current_url}. "
|
||||
f"Current content: {snippet or '<empty>'}"
|
||||
)
|
||||
|
||||
body_text = context.text.strip()
|
||||
inner_html = context.get_attribute("innerHTML") or ""
|
||||
|
||||
if "redirecting" in inner_html.lower():
|
||||
try:
|
||||
wait.until(lambda d: "redirecting" not in d.get_attribute("innerHTML").lower())
|
||||
except TimeoutException:
|
||||
snippet = context.text.strip()[:500].replace("\n", " ")
|
||||
inner_html = context.get_attribute("innerHTML") or ""
|
||||
|
||||
self.fail(
|
||||
f"Timed out waiting for redirect to finish at {self.driver.current_url}. "
|
||||
f"Current content: {snippet or '<empty>'}"
|
||||
f"{inner_html or '<empty>'}"
|
||||
)
|
||||
|
||||
inner_html = context.get_attribute("innerHTML") or ""
|
||||
body_text = context.text.strip()
|
||||
|
||||
snippet = body_text[:500].replace("\n", " ")
|
||||
|
||||
if not body_text.startswith("{") and not body_text.startswith("["):
|
||||
self.fail(
|
||||
f"Expected JSON content but got non-JSON text at {self.driver.current_url}: "
|
||||
f"{snippet or '<empty>'}"
|
||||
f"{inner_html or '<empty>'}"
|
||||
)
|
||||
|
||||
try:
|
||||
body_json = loads(body_text)
|
||||
except JSONDecodeError as e:
|
||||
self.fail(
|
||||
f"Expected JSON but got invalid content at {self.driver.current_url}: "
|
||||
f"{snippet or '<empty>'}"
|
||||
f"{inner_html or '<empty>'}"
|
||||
f"(JSON error: {e})"
|
||||
)
|
||||
|
||||
return body_json
|
||||
|
||||
def get_shadow_root(
|
||||
self, selector: str, container: WebElement | WebDriver | None = None
|
||||
self, selector: str, container: WebElement | WebDriver | None = None, timeout: float = 10
|
||||
) -> WebElement:
|
||||
"""Get shadow root element's inner shadowRoot"""
|
||||
"""Get the shadow root of a web component specified by `selector`."""
|
||||
if not container:
|
||||
container = self.driver
|
||||
el = container.find_element(By.CSS_SELECTOR, selector)
|
||||
return el.shadow_root
|
||||
wait = WebDriverWait(container, timeout)
|
||||
host: WebElement | None = None
|
||||
|
||||
try:
|
||||
host = wait.until(lambda c: c.find_element(By.CSS_SELECTOR, selector))
|
||||
except TimeoutException:
|
||||
self.fail(f"Timed out waiting for shadow host {selector} to appear")
|
||||
|
||||
attempts = 0
|
||||
|
||||
while attempts < SHADOW_ROOT_RETRIES:
|
||||
try:
|
||||
return host.shadow_root
|
||||
except NoSuchShadowRootException:
|
||||
attempts += 1
|
||||
sleep(0.2)
|
||||
# re-find host in case it was re-attached
|
||||
try:
|
||||
host = container.find_element(By.CSS_SELECTOR, selector)
|
||||
except NoSuchElementException:
|
||||
# loop and retry finding host
|
||||
pass
|
||||
|
||||
inner_html = host.get_attribute("innerHTML") or "<no host>"
|
||||
|
||||
raise RuntimeError(
|
||||
f"Failed to obtain shadow root for {selector} after {attempts} attempts. "
|
||||
f"Host innerHTML: {inner_html}"
|
||||
)
|
||||
|
||||
def shady_dom(self) -> WebElement:
|
||||
class wrapper:
|
||||
@@ -249,7 +357,7 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
return wrapper(self.driver)
|
||||
|
||||
def login(self, shadow_dom=True):
|
||||
"""Do entire login flow"""
|
||||
"""Perform the entire authentik login flow."""
|
||||
|
||||
if shadow_dom:
|
||||
flow_executor = self.get_shadow_root("ak-flow-executor")
|
||||
@@ -287,13 +395,42 @@ class SeleniumTestCase(DockerTestCase, StaticLiveServerTestCase):
|
||||
|
||||
def assert_user(self, expected_user: User):
|
||||
"""Check users/me API and assert it matches expected_user"""
|
||||
self.driver.get(self.url("authentik_api:user-me") + "?format=json")
|
||||
user_json = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||
user = UserSerializer(data=json.loads(user_json)["user"])
|
||||
|
||||
expected_url = self.url("authentik_api:user-me") + "?format=json"
|
||||
self.driver.get(expected_url)
|
||||
|
||||
self.wait.until(lambda d: d.current_url == expected_url)
|
||||
|
||||
user_json = self.parse_json_content()
|
||||
data = user_json.get("user")
|
||||
snippet = dumps(user_json, indent=2)[:500].replace("\n", " ")
|
||||
|
||||
self.assertIsNotNone(
|
||||
data,
|
||||
f"Missing 'user' key in response at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
user = UserSerializer(data=data)
|
||||
|
||||
user.is_valid()
|
||||
self.assertEqual(user["username"].value, expected_user.username)
|
||||
self.assertEqual(user["name"].value, expected_user.name)
|
||||
self.assertEqual(user["email"].value, expected_user.email)
|
||||
|
||||
self.assertEqual(
|
||||
user["username"].value,
|
||||
expected_user.username,
|
||||
f"Username mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
user["name"].value,
|
||||
expected_user.name,
|
||||
f"Name mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
user["email"].value,
|
||||
expected_user.email,
|
||||
f"Email mismatch at {self.driver.current_url}: {snippet}",
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user