Compare commits
3 Commits
interfaces
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
63cfbb721c | |||
2b74a1f03b | |||
093573f89a |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2023.4.1
|
current_version = 2023.2.3
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||||
|
@ -6,4 +6,3 @@ dist/**
|
|||||||
build/**
|
build/**
|
||||||
build_docs/**
|
build_docs/**
|
||||||
Dockerfile
|
Dockerfile
|
||||||
authentik/enterprise
|
|
||||||
|
@ -7,14 +7,8 @@ charset = utf-8
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.html]
|
[html]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.{yaml,yml}]
|
[yaml]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.go]
|
|
||||||
indent_style = tab
|
|
||||||
|
|
||||||
[Makefile]
|
|
||||||
indent_style = tab
|
|
||||||
|
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ""
|
title: ''
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ""
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
@ -11,7 +12,6 @@ A clear and concise description of what the bug is.
|
|||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
@ -27,9 +27,8 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
Output of docker-compose logs or kubectl logs respectively
|
Output of docker-compose logs or kubectl logs respectively
|
||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
|
- authentik version: [e.g. 2021.8.5]
|
||||||
- authentik version: [e.g. 2021.8.5]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
5
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea for this project
|
about: Suggest an idea for this project
|
||||||
title: ""
|
title: ''
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ""
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
10
.github/ISSUE_TEMPLATE/question.md
vendored
10
.github/ISSUE_TEMPLATE/question.md
vendored
@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Question
|
name: Question
|
||||||
about: Ask a question about a feature or specific configuration
|
about: Ask a question about a feature or specific configuration
|
||||||
title: ""
|
title: ''
|
||||||
labels: question
|
labels: question
|
||||||
assignees: ""
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe your question/**
|
**Describe your question/**
|
||||||
@ -19,9 +20,8 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
Output of docker-compose logs or kubectl logs respectively
|
Output of docker-compose logs or kubectl logs respectively
|
||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
|
- authentik version: [e.g. 2021.8.5]
|
||||||
- authentik version: [e.g. 2021.8.5]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: "Comment usage instructions on PRs"
|
name: 'Comment usage instructions on PRs'
|
||||||
description: "Comment usage instructions on PRs"
|
description: 'Comment usage instructions on PRs'
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
@ -17,7 +17,7 @@ runs:
|
|||||||
id: fc
|
id: fc
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
comment-author: "github-actions[bot]"
|
comment-author: 'github-actions[bot]'
|
||||||
body-includes: authentik PR Installation instructions
|
body-includes: authentik PR Installation instructions
|
||||||
- name: Create or update comment
|
- name: Create or update comment
|
||||||
uses: peter-evans/create-or-update-comment@v2
|
uses: peter-evans/create-or-update-comment@v2
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: "Prepare docker environment variables"
|
name: 'Prepare docker environment variables'
|
||||||
description: "Prepare docker environment variables"
|
description: 'Prepare docker environment variables'
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
shouldBuild:
|
shouldBuild:
|
||||||
|
18
.github/actions/setup/action.yml
vendored
18
.github/actions/setup/action.yml
vendored
@ -1,10 +1,5 @@
|
|||||||
name: "Setup authentik testing environment"
|
name: 'Setup authentik testing environment'
|
||||||
description: "Setup authentik testing environment"
|
description: 'Setup authentik testing environment'
|
||||||
|
|
||||||
inputs:
|
|
||||||
postgresql_tag:
|
|
||||||
description: "Optional postgresql image tag"
|
|
||||||
default: "12"
|
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
@ -18,18 +13,17 @@ runs:
|
|||||||
- name: Setup python and restore poetry
|
- name: Setup python and restore poetry
|
||||||
uses: actions/setup-python@v3
|
uses: actions/setup-python@v3
|
||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
python-version: '3.11'
|
||||||
cache: "poetry"
|
cache: 'poetry'
|
||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v3.1.0
|
uses: actions/setup-node@v3.1.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Setup dependencies
|
- name: Setup dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
export PSQL_TAG=${{ inputs.postgresql_tag }}
|
|
||||||
docker-compose -f .github/actions/setup/docker-compose.yml up -d
|
docker-compose -f .github/actions/setup/docker-compose.yml up -d
|
||||||
poetry env use python3.11
|
poetry env use python3.11
|
||||||
poetry install
|
poetry install
|
||||||
|
10
.github/actions/setup/docker-compose.yml
vendored
10
.github/actions/setup/docker-compose.yml
vendored
@ -1,23 +1,23 @@
|
|||||||
version: "3.7"
|
version: '3.7'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgresql:
|
postgresql:
|
||||||
container_name: postgres
|
container_name: postgres
|
||||||
image: library/postgres:${PSQL_TAG:-12}
|
image: library/postgres:12
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/var/lib/postgresql/data
|
- db-data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: authentik
|
POSTGRES_USER: authentik
|
||||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||||
POSTGRES_DB: authentik
|
POSTGRES_DB: authentik
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
restart: always
|
restart: always
|
||||||
redis:
|
redis:
|
||||||
container_name: redis
|
container_name: redis
|
||||||
image: library/redis
|
image: library/redis
|
||||||
ports:
|
ports:
|
||||||
- 6379:6379
|
- 6379:6379
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
11
.github/codecov.yml
vendored
11
.github/codecov.yml
vendored
@ -1,10 +1,3 @@
|
|||||||
coverage:
|
coverage:
|
||||||
status:
|
precision: 2
|
||||||
project:
|
round: up
|
||||||
default:
|
|
||||||
target: auto
|
|
||||||
# adjust accordingly based on how flaky your tests are
|
|
||||||
# this allows a 1% drop from the previous base commit coverage
|
|
||||||
threshold: 1%
|
|
||||||
notify:
|
|
||||||
after_n_builds: 3
|
|
||||||
|
1
.github/codespell-dictionary.txt
vendored
1
.github/codespell-dictionary.txt
vendored
@ -1 +0,0 @@
|
|||||||
authentic->authentik
|
|
120
.github/dependabot.yml
vendored
120
.github/dependabot.yml
vendored
@ -1,62 +1,62 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "04:00"
|
time: "04:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
reviewers:
|
reviewers:
|
||||||
- "@goauthentik/core"
|
- "@goauthentik/core"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "ci:"
|
prefix: "ci:"
|
||||||
- package-ecosystem: gomod
|
- package-ecosystem: gomod
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "04:00"
|
time: "04:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
reviewers:
|
reviewers:
|
||||||
- "@goauthentik/core"
|
- "@goauthentik/core"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "core:"
|
prefix: "core:"
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/web"
|
directory: "/web"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "04:00"
|
time: "04:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
reviewers:
|
reviewers:
|
||||||
- "@goauthentik/core"
|
- "@goauthentik/core"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "web:"
|
prefix: "web:"
|
||||||
- package-ecosystem: npm
|
- package-ecosystem: npm
|
||||||
directory: "/website"
|
directory: "/website"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "04:00"
|
time: "04:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
reviewers:
|
reviewers:
|
||||||
- "@goauthentik/core"
|
- "@goauthentik/core"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "website:"
|
prefix: "website:"
|
||||||
- package-ecosystem: pip
|
- package-ecosystem: pip
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "04:00"
|
time: "04:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
reviewers:
|
reviewers:
|
||||||
- "@goauthentik/core"
|
- "@goauthentik/core"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "core:"
|
prefix: "core:"
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: docker
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
time: "04:00"
|
time: "04:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
reviewers:
|
reviewers:
|
||||||
- "@goauthentik/core"
|
- "@goauthentik/core"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "core:"
|
prefix: "core:"
|
||||||
|
13
.github/pull_request_template.md
vendored
13
.github/pull_request_template.md
vendored
@ -5,20 +5,15 @@ Please check the [Contributing guidelines](https://github.com/goauthentik/authen
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
# Details
|
# Details
|
||||||
|
* **Does this resolve an issue?**
|
||||||
- **Does this resolve an issue?**
|
Resolves #
|
||||||
Resolves #
|
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
* Adds feature which does x, y, and z.
|
||||||
- Adds feature which does x, y, and z.
|
|
||||||
|
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
* Adds breaking change which causes \<issue\>.
|
||||||
- Adds breaking change which causes \<issue\>.
|
|
||||||
|
|
||||||
## Additional
|
## Additional
|
||||||
|
|
||||||
Any further notes or comments you want to make.
|
Any further notes or comments you want to make.
|
||||||
|
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@ -16,4 +16,3 @@ markComment: >
|
|||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
for your contributions.
|
for your contributions.
|
||||||
only: issues
|
|
||||||
|
4
.github/transifex.yml
vendored
4
.github/transifex.yml
vendored
@ -6,11 +6,11 @@ git:
|
|||||||
source_language: en
|
source_language: en
|
||||||
source_file: web/src/locales/en.po
|
source_file: web/src/locales/en.po
|
||||||
# path expression to translation files, must contain <lang> placeholder
|
# path expression to translation files, must contain <lang> placeholder
|
||||||
translation_files_expression: "web/src/locales/<lang>.po"
|
translation_files_expression: 'web/src/locales/<lang>.po'
|
||||||
- filter_type: file
|
- filter_type: file
|
||||||
# all supported i18n types: https://docs.transifex.com/formats
|
# all supported i18n types: https://docs.transifex.com/formats
|
||||||
file_format: PO
|
file_format: PO
|
||||||
source_language: en
|
source_language: en
|
||||||
source_file: locale/en/LC_MESSAGES/django.po
|
source_file: locale/en/LC_MESSAGES/django.po
|
||||||
# path expression to translation files, must contain <lang> placeholder
|
# path expression to translation files, must contain <lang> placeholder
|
||||||
translation_files_expression: "locale/<lang>/LC_MESSAGES/django.po"
|
translation_files_expression: 'locale/<lang>/LC_MESSAGES/django.po'
|
||||||
|
25
.github/workflows/ci-main.yml
vendored
25
.github/workflows/ci-main.yml
vendored
@ -23,14 +23,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
job:
|
job:
|
||||||
- bandit
|
|
||||||
- black
|
|
||||||
- codespell
|
|
||||||
- isort
|
|
||||||
- pending-migrations
|
|
||||||
- pylint
|
- pylint
|
||||||
|
- black
|
||||||
|
- isort
|
||||||
|
- bandit
|
||||||
- pyright
|
- pyright
|
||||||
- ruff
|
- pending-migrations
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -61,7 +59,7 @@ jobs:
|
|||||||
cp authentik/lib/default.yml local.env.yml
|
cp authentik/lib/default.yml local.env.yml
|
||||||
cp -R .github ..
|
cp -R .github ..
|
||||||
cp -R scripts ..
|
cp -R scripts ..
|
||||||
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
|
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||||
rm -rf .github/ scripts/
|
rm -rf .github/ scripts/
|
||||||
mv ../.github ../scripts .
|
mv ../.github ../scripts .
|
||||||
- name: Setup authentik env (ensure stable deps are installed)
|
- name: Setup authentik env (ensure stable deps are installed)
|
||||||
@ -81,21 +79,12 @@ jobs:
|
|||||||
- name: migrate to latest
|
- name: migrate to latest
|
||||||
run: poetry run python -m lifecycle.migrate
|
run: poetry run python -m lifecycle.migrate
|
||||||
test-unittest:
|
test-unittest:
|
||||||
name: test-unittest - PostgreSQL ${{ matrix.psql }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
psql:
|
|
||||||
- 11-alpine
|
|
||||||
- 12-alpine
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
with:
|
|
||||||
postgresql_tag: ${{ matrix.psql }}
|
|
||||||
- name: run unittest
|
- name: run unittest
|
||||||
run: |
|
run: |
|
||||||
poetry run make test
|
poetry run make test
|
||||||
@ -139,8 +128,6 @@ jobs:
|
|||||||
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
||||||
- name: ldap
|
- name: ldap
|
||||||
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
||||||
- name: radius
|
|
||||||
glob: tests/e2e/test_provider_radius*
|
|
||||||
- name: flows
|
- name: flows
|
||||||
glob: tests/e2e/test_flows*
|
glob: tests/e2e/test_flows*
|
||||||
steps:
|
steps:
|
||||||
@ -212,7 +199,6 @@ jobs:
|
|||||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}
|
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}
|
||||||
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.sha }}
|
|
||||||
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }}
|
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }}
|
||||||
build-args: |
|
build-args: |
|
||||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
@ -254,7 +240,6 @@ jobs:
|
|||||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-arm64
|
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-arm64
|
||||||
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.sha }}-arm64
|
|
||||||
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }}-arm64
|
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }}-arm64
|
||||||
build-args: |
|
build-args: |
|
||||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
|
26
.github/workflows/ci-outpost.yml
vendored
26
.github/workflows/ci-outpost.yml
vendored
@ -15,9 +15,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "^1.17"
|
||||||
- name: Prepare and generate API
|
- name: Prepare and generate API
|
||||||
run: |
|
run: |
|
||||||
# Create folder structure for go embeds
|
# Create folder structure for go embeds
|
||||||
@ -34,9 +34,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "^1.17"
|
||||||
- name: Generate API
|
- name: Generate API
|
||||||
run: make gen-client-go
|
run: make gen-client-go
|
||||||
- name: Go unittests
|
- name: Go unittests
|
||||||
@ -59,7 +59,8 @@ jobs:
|
|||||||
type:
|
type:
|
||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
- radius
|
arch:
|
||||||
|
- 'linux/amd64'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -92,7 +93,7 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
|
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: ${{ matrix.arch }}
|
||||||
context: .
|
context: .
|
||||||
build-binary:
|
build-binary:
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
@ -105,18 +106,17 @@ jobs:
|
|||||||
type:
|
type:
|
||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
- radius
|
|
||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [amd64, arm64]
|
goarch: [amd64, arm64]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "^1.17"
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Generate API
|
- name: Generate API
|
||||||
run: make gen-client-go
|
run: make gen-client-go
|
||||||
@ -131,3 +131,7 @@ jobs:
|
|||||||
export GOOS=${{ matrix.goos }}
|
export GOOS=${{ matrix.goos }}
|
||||||
export GOARCH=${{ matrix.goarch }}
|
export GOARCH=${{ matrix.goarch }}
|
||||||
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
|
go build -tags=outpost_static_embed -v -o ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }} ./cmd/${{ matrix.type }}
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||||
|
path: ./authentik-outpost-${{ matrix.type }}_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||||
|
20
.github/workflows/ci-web.yml
vendored
20
.github/workflows/ci-web.yml
vendored
@ -17,8 +17,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@ -33,8 +33,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@ -49,8 +49,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@ -65,8 +65,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
run: |
|
run: |
|
||||||
@ -97,8 +97,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- working-directory: web/
|
- working-directory: web/
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
30
.github/workflows/ci-website.yml
vendored
30
.github/workflows/ci-website.yml
vendored
@ -17,8 +17,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: website/package-lock.json
|
cache-dependency-path: website/package-lock.json
|
||||||
- working-directory: website/
|
- working-directory: website/
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@ -31,40 +31,18 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: website/package-lock.json
|
cache-dependency-path: website/package-lock.json
|
||||||
- working-directory: website/
|
- working-directory: website/
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: test
|
- name: test
|
||||||
working-directory: website/
|
working-directory: website/
|
||||||
run: npm test
|
run: npm test
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: ${{ matrix.job }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
job:
|
|
||||||
- build
|
|
||||||
- build-docs-only
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- uses: actions/setup-node@v3.6.0
|
|
||||||
with:
|
|
||||||
node-version: "20"
|
|
||||||
cache: "npm"
|
|
||||||
cache-dependency-path: website/package-lock.json
|
|
||||||
- working-directory: website/
|
|
||||||
run: npm ci
|
|
||||||
- name: build
|
|
||||||
working-directory: website/
|
|
||||||
run: npm run ${{ matrix.job }}
|
|
||||||
ci-website-mark:
|
ci-website-mark:
|
||||||
needs:
|
needs:
|
||||||
- lint-prettier
|
- lint-prettier
|
||||||
- test
|
- test
|
||||||
- build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: echo mark
|
- run: echo mark
|
||||||
|
58
.github/workflows/codeql-analysis.yml
vendored
58
.github/workflows/codeql-analysis.yml
vendored
@ -2,12 +2,12 @@ name: "CodeQL"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, "*", next, version*]
|
branches: [ main, '*', next, version* ]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
# The branches below must be a subset of the branches above
|
||||||
branches: [main]
|
branches: [ main ]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "30 6 * * 5"
|
- cron: '30 6 * * 5'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
@ -21,40 +21,40 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: ["go", "javascript", "python"]
|
language: [ 'go', 'javascript', 'python' ]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||||
# Learn more:
|
# Learn more:
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
# By default, queries listed here will override any specified in a config file.
|
# By default, queries listed here will override any specified in a config file.
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||||
# and modify them (or add more) to build your code if your project
|
# and modify them (or add more) to build your code if your project
|
||||||
# uses a compiled language
|
# uses a compiled language
|
||||||
|
|
||||||
#- run: |
|
#- run: |
|
||||||
# make bootstrap
|
# make bootstrap
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v2
|
||||||
|
4
.github/workflows/ghcr-retention.yml
vendored
4
.github/workflows/ghcr-retention.yml
vendored
@ -2,7 +2,7 @@ name: ghcr-retention
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *" # every day at midnight
|
- cron: '0 0 * * *' # every day at midnight
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@ -11,7 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Delete 'dev' containers older than a week
|
- name: Delete 'dev' containers older than a week
|
||||||
uses: snok/container-retention-policy@v2
|
uses: snok/container-retention-policy@v1
|
||||||
with:
|
with:
|
||||||
image-names: dev-server,dev-ldap,dev-proxy
|
image-names: dev-server,dev-ldap,dev-proxy
|
||||||
cut-off: One week ago UTC
|
cut-off: One week ago UTC
|
||||||
|
18
.github/workflows/release-publish.yml
vendored
18
.github/workflows/release-publish.yml
vendored
@ -52,12 +52,11 @@ jobs:
|
|||||||
type:
|
type:
|
||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
- radius
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "^1.17"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2.1.0
|
uses: docker/setup-qemu-action@v2.1.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
@ -100,18 +99,17 @@ jobs:
|
|||||||
type:
|
type:
|
||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
- radius
|
|
||||||
goos: [linux, darwin]
|
goos: [linux, darwin]
|
||||||
goarch: [amd64, arm64]
|
goarch: [amd64, arm64]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "^1.17"
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Build web
|
- name: Build web
|
||||||
working-directory: web/
|
working-directory: web/
|
||||||
@ -173,5 +171,5 @@ jobs:
|
|||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
with:
|
with:
|
||||||
version: authentik@${{ steps.ev.outputs.version }}
|
version: authentik@${{ steps.ev.outputs.version }}
|
||||||
sourcemaps: "./web/dist"
|
sourcemaps: './web/dist'
|
||||||
url_prefix: "~/static/dist"
|
url_prefix: '~/static/dist'
|
||||||
|
2
.github/workflows/release-tag.yml
vendored
2
.github/workflows/release-tag.yml
vendored
@ -3,7 +3,7 @@ name: authentik-on-tag
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "version/*"
|
- 'version/*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
8
.github/workflows/translation-compile.yml
vendored
8
.github/workflows/translation-compile.yml
vendored
@ -1,12 +1,12 @@
|
|||||||
name: authentik-backend-translate-compile
|
name: authentik-backend-translate-compile
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [ main ]
|
||||||
paths:
|
paths:
|
||||||
- "/locale/"
|
- '/locale/'
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "/locale/"
|
- '/locale/'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: run compile
|
- name: run compile
|
||||||
run: poetry run ./manage.py compilemessages
|
run: poetry run ./manage.py compilemessages
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v5
|
uses: peter-evans/create-pull-request@v4
|
||||||
id: cpr
|
id: cpr
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
|
12
.github/workflows/web-api-publish.yml
vendored
12
.github/workflows/web-api-publish.yml
vendored
@ -1,9 +1,9 @@
|
|||||||
name: authentik-web-api-publish
|
name: authentik-web-api-publish
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [ main ]
|
||||||
paths:
|
paths:
|
||||||
- "schema.yml"
|
- 'schema.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -14,8 +14,8 @@ jobs:
|
|||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: "20"
|
node-version: '18'
|
||||||
registry-url: "https://registry.npmjs.org"
|
registry-url: 'https://registry.npmjs.org'
|
||||||
- name: Generate API Client
|
- name: Generate API Client
|
||||||
run: make gen-client-ts
|
run: make gen-client-ts
|
||||||
- name: Publish package
|
- name: Publish package
|
||||||
@ -30,7 +30,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||||
npm i @goauthentik/api@$VERSION
|
npm i @goauthentik/api@$VERSION
|
||||||
- uses: peter-evans/create-pull-request@v5
|
- uses: peter-evans/create-pull-request@v4
|
||||||
id: cpr
|
id: cpr
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
@ -42,7 +42,7 @@ jobs:
|
|||||||
signoff: true
|
signoff: true
|
||||||
team-reviewers: "@goauthentik/core"
|
team-reviewers: "@goauthentik/core"
|
||||||
author: authentik bot <github-bot@goauthentik.io>
|
author: authentik bot <github-bot@goauthentik.io>
|
||||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
- uses: peter-evans/enable-pull-request-automerge@v2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -200,6 +200,3 @@ media/
|
|||||||
.idea/
|
.idea/
|
||||||
/gen-*/
|
/gen-*/
|
||||||
data/
|
data/
|
||||||
|
|
||||||
# Local Netlify folder
|
|
||||||
.netlify
|
|
||||||
|
20
.vscode/extensions.json
vendored
20
.vscode/extensions.json
vendored
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"bashmish.es6-string-css",
|
|
||||||
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"golang.go",
|
|
||||||
"Gruntfuggly.todo-tree",
|
|
||||||
"mechatroner.rainbow-csv",
|
|
||||||
"ms-python.black-formatter",
|
|
||||||
"ms-python.isort",
|
|
||||||
"ms-python.pylint",
|
|
||||||
"ms-python.python",
|
|
||||||
"ms-python.vscode-pylance",
|
|
||||||
"redhat.vscode-yaml",
|
|
||||||
"Tobermory.es6-string-html",
|
|
||||||
"unifiedjs.vscode-mdx"
|
|
||||||
]
|
|
||||||
}
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -16,8 +16,7 @@
|
|||||||
"passwordless",
|
"passwordless",
|
||||||
"kubernetes",
|
"kubernetes",
|
||||||
"sso",
|
"sso",
|
||||||
"slo",
|
"slo"
|
||||||
"scim",
|
|
||||||
],
|
],
|
||||||
"python.linting.pylintEnabled": true,
|
"python.linting.pylintEnabled": true,
|
||||||
"todo-tree.tree.showCountsInTree": true,
|
"todo-tree.tree.showCountsInTree": true,
|
||||||
|
@ -20,7 +20,6 @@ The following is a set of guidelines for contributing to authentik and its compo
|
|||||||
- [Reporting Bugs](#reporting-bugs)
|
- [Reporting Bugs](#reporting-bugs)
|
||||||
- [Suggesting Enhancements](#suggesting-enhancements)
|
- [Suggesting Enhancements](#suggesting-enhancements)
|
||||||
- [Your First Code Contribution](#your-first-code-contribution)
|
- [Your First Code Contribution](#your-first-code-contribution)
|
||||||
- [Help with the Docs](#help-with-the-docs)
|
|
||||||
- [Pull Requests](#pull-requests)
|
- [Pull Requests](#pull-requests)
|
||||||
|
|
||||||
[Styleguides](#styleguides)
|
[Styleguides](#styleguides)
|
||||||
@ -136,9 +135,6 @@ authentik can be run locally, all though depending on which part you want to wor
|
|||||||
|
|
||||||
This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github)
|
This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github)
|
||||||
|
|
||||||
### Help with the Docs
|
|
||||||
Contributions to the technical documentation are greatly appreciated. Open a PR if you have improvements to make or new content to add. If you have questions or suggestions about the documentation, open an Issue. No contribution is too small.
|
|
||||||
|
|
||||||
### Pull Requests
|
### Pull Requests
|
||||||
|
|
||||||
The process described here has several goals:
|
The process described here has several goals:
|
||||||
@ -158,19 +154,12 @@ While the prerequisites above must be satisfied prior to having your pull reques
|
|||||||
|
|
||||||
## Styleguides
|
## Styleguides
|
||||||
|
|
||||||
### PR naming
|
|
||||||
|
|
||||||
- Use the format of `<package>: <verb> <description>`
|
|
||||||
- See [here](#authentik-packages) for `package`
|
|
||||||
- Example: `providers/saml2: fix parsing of requests`
|
|
||||||
|
|
||||||
### Git Commit Messages
|
### Git Commit Messages
|
||||||
|
|
||||||
- Use the format of `<package>: <verb> <description>`
|
- Use the format of `<package>: <verb> <description>`
|
||||||
- See [here](#authentik-packages) for `package`
|
- See [here](#authentik-packages) for `package`
|
||||||
- Example: `providers/saml2: fix parsing of requests`
|
- Example: `providers/saml2: fix parsing of requests`
|
||||||
- Reference issues and pull requests liberally after the first line
|
- Reference issues and pull requests liberally after the first line
|
||||||
- Naming of commits within a PR does not need to adhere to the guidelines as we squash merge PRs
|
|
||||||
|
|
||||||
### Python Styleguide
|
### Python Styleguide
|
||||||
|
|
||||||
|
18
Dockerfile
18
Dockerfile
@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Build website
|
# Stage 1: Build website
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/node:20 as website-builder
|
FROM --platform=${BUILDPLATFORM} docker.io/node:18 as website-builder
|
||||||
|
|
||||||
COPY ./website /work/website/
|
COPY ./website /work/website/
|
||||||
COPY ./blueprints /work/blueprints/
|
COPY ./blueprints /work/blueprints/
|
||||||
@ -10,7 +10,7 @@ WORKDIR /work/website
|
|||||||
RUN npm ci && npm run build-docs-only
|
RUN npm ci && npm run build-docs-only
|
||||||
|
|
||||||
# Stage 2: Build webui
|
# Stage 2: Build webui
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/node:20 as web-builder
|
FROM --platform=${BUILDPLATFORM} docker.io/node:18 as web-builder
|
||||||
|
|
||||||
COPY ./web /work/web/
|
COPY ./web /work/web/
|
||||||
COPY ./website /work/website/
|
COPY ./website /work/website/
|
||||||
@ -20,7 +20,7 @@ WORKDIR /work/web
|
|||||||
RUN npm ci && npm run build
|
RUN npm ci && npm run build
|
||||||
|
|
||||||
# Stage 3: Poetry to requirements.txt export
|
# Stage 3: Poetry to requirements.txt export
|
||||||
FROM docker.io/python:3.11.3-slim-bullseye AS poetry-locker
|
FROM docker.io/python:3.11.2-slim-bullseye AS poetry-locker
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
COPY ./pyproject.toml /work
|
COPY ./pyproject.toml /work
|
||||||
@ -31,7 +31,7 @@ RUN pip install --no-cache-dir poetry && \
|
|||||||
poetry export -f requirements.txt --dev --output requirements-dev.txt
|
poetry export -f requirements.txt --dev --output requirements-dev.txt
|
||||||
|
|
||||||
# Stage 4: Build go proxy
|
# Stage 4: Build go proxy
|
||||||
FROM docker.io/golang:1.20.3-bullseye AS go-builder
|
FROM docker.io/golang:1.20.1-bullseye AS go-builder
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ COPY ./go.sum /work/go.sum
|
|||||||
RUN go build -o /work/authentik ./cmd/server/
|
RUN go build -o /work/authentik ./cmd/server/
|
||||||
|
|
||||||
# Stage 5: MaxMind GeoIP
|
# Stage 5: MaxMind GeoIP
|
||||||
FROM docker.io/maxmindinc/geoipupdate:v5.0 as geoip
|
FROM docker.io/maxmindinc/geoipupdate:v4.10 as geoip
|
||||||
|
|
||||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
|
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
|
||||||
ENV GEOIPUPDATE_VERBOSE="true"
|
ENV GEOIPUPDATE_VERBOSE="true"
|
||||||
@ -62,7 +62,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
|||||||
"
|
"
|
||||||
|
|
||||||
# Stage 6: Run
|
# Stage 6: Run
|
||||||
FROM docker.io/python:3.11.3-slim-bullseye AS final-image
|
FROM docker.io/python:3.11.2-slim-bullseye AS final-image
|
||||||
|
|
||||||
LABEL org.opencontainers.image.url https://goauthentik.io
|
LABEL org.opencontainers.image.url https://goauthentik.io
|
||||||
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
|
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
|
||||||
@ -83,7 +83,7 @@ RUN apt-get update && \
|
|||||||
# Required for runtime
|
# Required for runtime
|
||||||
apt-get install -y --no-install-recommends libxmlsec1-openssl libmaxminddb0 && \
|
apt-get install -y --no-install-recommends libxmlsec1-openssl libmaxminddb0 && \
|
||||||
# Required for bootstrap & healtcheck
|
# Required for bootstrap & healtcheck
|
||||||
apt-get install -y --no-install-recommends runit && \
|
apt-get install -y --no-install-recommends curl runit && \
|
||||||
pip install --no-cache-dir -r /requirements.txt && \
|
pip install --no-cache-dir -r /requirements.txt && \
|
||||||
apt-get remove --purge -y build-essential pkg-config libxmlsec1-dev && \
|
apt-get remove --purge -y build-essential pkg-config libxmlsec1-dev && \
|
||||||
apt-get autoremove --purge -y && \
|
apt-get autoremove --purge -y && \
|
||||||
@ -96,13 +96,13 @@ RUN apt-get update && \
|
|||||||
|
|
||||||
COPY ./authentik/ /authentik
|
COPY ./authentik/ /authentik
|
||||||
COPY ./pyproject.toml /
|
COPY ./pyproject.toml /
|
||||||
COPY ./schemas /schemas
|
COPY ./xml /xml
|
||||||
COPY ./locale /locale
|
COPY ./locale /locale
|
||||||
COPY ./tests /tests
|
COPY ./tests /tests
|
||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
COPY ./blueprints /blueprints
|
COPY ./blueprints /blueprints
|
||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
COPY --from=go-builder /work/authentik /bin/authentik
|
COPY --from=go-builder /work/authentik /authentik-proxy
|
||||||
COPY --from=web-builder /work/web/dist/ /web/dist/
|
COPY --from=web-builder /work/web/dist/ /web/dist/
|
||||||
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
||||||
COPY --from=website-builder /work/website/help/ /website/help/
|
COPY --from=website-builder /work/website/help/ /website/help/
|
||||||
|
9
LICENSE
9
LICENSE
@ -1,11 +1,6 @@
|
|||||||
Copyright (c) 2023 Jens Langhammer
|
MIT License
|
||||||
|
|
||||||
Portions of this software are licensed as follows:
|
Copyright (c) 2022 Jens Langhammer
|
||||||
* All content residing under the "website/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license".
|
|
||||||
* All content that resides under the "authentik/enterprise/" directory of this repository, if that directory exists, is licensed under the license defined in "authentik/enterprise/LICENSE".
|
|
||||||
* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license.
|
|
||||||
* All third party components incorporated into the authentik are licensed under the original license provided by the owner of the applicable component.
|
|
||||||
* Content outside of the above mentioned directories or restrictions above is available under the "MIT" license as defined below.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
71
Makefile
71
Makefile
@ -3,21 +3,6 @@ PWD = $(shell pwd)
|
|||||||
UID = $(shell id -u)
|
UID = $(shell id -u)
|
||||||
GID = $(shell id -g)
|
GID = $(shell id -g)
|
||||||
NPM_VERSION = $(shell python -m scripts.npm_version)
|
NPM_VERSION = $(shell python -m scripts.npm_version)
|
||||||
PY_SOURCES = authentik tests scripts lifecycle
|
|
||||||
|
|
||||||
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
|
|
||||||
-I .github/codespell-words.txt \
|
|
||||||
-S 'web/src/locales/**' \
|
|
||||||
authentik \
|
|
||||||
internal \
|
|
||||||
cmd \
|
|
||||||
web/src \
|
|
||||||
website/src \
|
|
||||||
website/blog \
|
|
||||||
website/developer-docs \
|
|
||||||
website/docs \
|
|
||||||
website/integrations \
|
|
||||||
website/src
|
|
||||||
|
|
||||||
all: lint-fix lint test gen web
|
all: lint-fix lint test gen web
|
||||||
|
|
||||||
@ -39,19 +24,28 @@ test:
|
|||||||
coverage report
|
coverage report
|
||||||
|
|
||||||
lint-fix:
|
lint-fix:
|
||||||
isort authentik $(PY_SOURCES)
|
isort authentik tests scripts lifecycle
|
||||||
black authentik $(PY_SOURCES)
|
black authentik tests scripts lifecycle
|
||||||
ruff authentik $(PY_SOURCES)
|
codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \
|
||||||
codespell -w $(CODESPELL_ARGS)
|
authentik \
|
||||||
|
internal \
|
||||||
|
cmd \
|
||||||
|
web/src \
|
||||||
|
website/src \
|
||||||
|
website/docs \
|
||||||
|
website/developer-docs
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
pylint $(PY_SOURCES)
|
pylint authentik tests lifecycle
|
||||||
bandit -r $(PY_SOURCES) -x node_modules
|
bandit -r authentik tests lifecycle -x node_modules
|
||||||
golangci-lint run -v
|
golangci-lint run -v
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
python -m lifecycle.migrate
|
python -m lifecycle.migrate
|
||||||
|
|
||||||
|
run:
|
||||||
|
go run -v ./cmd/server/
|
||||||
|
|
||||||
i18n-extract: i18n-extract-core web-extract
|
i18n-extract: i18n-extract-core web-extract
|
||||||
|
|
||||||
i18n-extract-core:
|
i18n-extract-core:
|
||||||
@ -65,20 +59,15 @@ gen-build:
|
|||||||
AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json
|
AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json
|
||||||
AUTHENTIK_DEBUG=true ak spectacular --file schema.yml
|
AUTHENTIK_DEBUG=true ak spectacular --file schema.yml
|
||||||
|
|
||||||
gen-changelog:
|
|
||||||
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
|
|
||||||
npx prettier --write changelog.md
|
|
||||||
|
|
||||||
gen-diff:
|
gen-diff:
|
||||||
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > old_schema.yml
|
git show $(shell git describe --abbrev=0):schema.yml > old_schema.yml
|
||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-diff:2.1.0-beta.6 \
|
docker.io/openapitools/openapi-diff:2.1.0-beta.3 \
|
||||||
--markdown /local/diff.md \
|
--markdown /local/diff.md \
|
||||||
/local/old_schema.yml /local/schema.yml
|
/local/old_schema.yml /local/schema.yml
|
||||||
rm old_schema.yml
|
rm old_schema.yml
|
||||||
npx prettier --write diff.md
|
|
||||||
|
|
||||||
gen-clean:
|
gen-clean:
|
||||||
rm -rf web/api/src/
|
rm -rf web/api/src/
|
||||||
@ -88,7 +77,7 @@ gen-client-ts:
|
|||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
|
docker.io/openapitools/openapi-generator-cli:v6.0.0 generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g typescript-fetch \
|
-g typescript-fetch \
|
||||||
-o /local/gen-ts-api \
|
-o /local/gen-ts-api \
|
||||||
@ -101,21 +90,20 @@ gen-client-ts:
|
|||||||
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api
|
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api
|
||||||
|
|
||||||
gen-client-go:
|
gen-client-go:
|
||||||
mkdir -p ./gen-go-api ./gen-go-api/templates
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
|
||||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./gen-go-api/config.yaml
|
mkdir -p templates
|
||||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./gen-go-api/templates/README.mustache
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache
|
||||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O ./gen-go-api/templates/go.mod.mustache
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache
|
||||||
cp schema.yml ./gen-go-api/
|
|
||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}/gen-go-api:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
|
docker.io/openapitools/openapi-generator-cli:v6.0.0 generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g go \
|
-g go \
|
||||||
-o /local/ \
|
-o /local/gen-go-api \
|
||||||
-c /local/config.yaml
|
-c /local/config.yaml
|
||||||
go mod edit -replace goauthentik.io/api/v3=./gen-go-api
|
go mod edit -replace goauthentik.io/api/v3=./gen-go-api
|
||||||
rm -rf ./gen-go-api/config.yaml ./gen-go-api/templates/
|
rm -rf config.yaml ./templates/
|
||||||
|
|
||||||
gen-dev-config:
|
gen-dev-config:
|
||||||
python -m scripts.generate_config
|
python -m scripts.generate_config
|
||||||
@ -173,6 +161,7 @@ website-watch:
|
|||||||
|
|
||||||
# These targets are use by GitHub actions to allow usage of matrix
|
# These targets are use by GitHub actions to allow usage of matrix
|
||||||
# which makes the YAML File a lot smaller
|
# which makes the YAML File a lot smaller
|
||||||
|
PY_SOURCES=authentik tests lifecycle
|
||||||
ci--meta-debug:
|
ci--meta-debug:
|
||||||
python -V
|
python -V
|
||||||
node --version
|
node --version
|
||||||
@ -183,12 +172,6 @@ ci-pylint: ci--meta-debug
|
|||||||
ci-black: ci--meta-debug
|
ci-black: ci--meta-debug
|
||||||
black --check $(PY_SOURCES)
|
black --check $(PY_SOURCES)
|
||||||
|
|
||||||
ci-ruff: ci--meta-debug
|
|
||||||
ruff check $(PY_SOURCES)
|
|
||||||
|
|
||||||
ci-codespell: ci--meta-debug
|
|
||||||
codespell $(CODESPELL_ARGS) -s
|
|
||||||
|
|
||||||
ci-isort: ci--meta-debug
|
ci-isort: ci--meta-debug
|
||||||
isort --check $(PY_SOURCES)
|
isort --check $(PY_SOURCES)
|
||||||
|
|
||||||
|
12
README.md
12
README.md
@ -15,13 +15,13 @@
|
|||||||
|
|
||||||
## What is authentik?
|
## What is authentik?
|
||||||
|
|
||||||
Authentik is an open-source Identity Provider that emphasizes flexibility and versatility. It can be seamlessly integrated into existing environments to support new protocols. Authentik is also a great solution for implementing sign-up, recovery, and other similar features in your application, saving you the hassle of dealing with them.
|
authentik is an open-source Identity Provider focused on flexibility and versatility. You can use authentik in an existing environment to add support for new protocols. authentik is also a great solution for implementing signup/recovery/etc in your application, so you don't have to deal with it.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For small/test setups it is recommended to use Docker Compose; refer to the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github).
|
For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github)
|
||||||
|
|
||||||
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github).
|
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github)
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@ -32,15 +32,15 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See [Developer Documentation](https://goauthentik.io/developer-docs/?utm_source=github)
|
See [Development Documentation](https://goauthentik.io/developer-docs/?utm_source=github)
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
See [SECURITY.md](SECURITY.md)
|
See [SECURITY.md](SECURITY.md)
|
||||||
|
|
||||||
## Adoption and Contributions
|
## Support
|
||||||
|
|
||||||
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [CONTRIBUTING.md file](./CONTRIBUTING.md).
|
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR!
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ Authentik takes security very seriously. We follow the rules of [responsible dis
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| --------- | ------------------ |
|
| --------- | ------------------ |
|
||||||
| 2023.2.x | :white_check_mark: |
|
| 2022.12.x | :white_check_mark: |
|
||||||
| 2023.3.x | :white_check_mark: |
|
| 2023.1.x | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from os import environ
|
from os import environ
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
__version__ = "2023.4.1"
|
__version__ = "2023.2.3"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,7 +18,6 @@ from authentik.core.api.utils import PassiveSerializer
|
|||||||
from authentik.lib.utils.reflection import get_env
|
from authentik.lib.utils.reflection import get_env
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||||
from authentik.outposts.models import Outpost
|
from authentik.outposts.models import Outpost
|
||||||
from authentik.tenants.utils import get_tenant
|
|
||||||
|
|
||||||
|
|
||||||
class RuntimeDict(TypedDict):
|
class RuntimeDict(TypedDict):
|
||||||
@ -78,7 +77,7 @@ class SystemSerializer(PassiveSerializer):
|
|||||||
|
|
||||||
def get_tenant(self, request: Request) -> str:
|
def get_tenant(self, request: Request) -> str:
|
||||||
"""Currently active tenant"""
|
"""Currently active tenant"""
|
||||||
return str(get_tenant(request))
|
return str(request._request.tenant)
|
||||||
|
|
||||||
def get_server_time(self, request: Request) -> datetime:
|
def get_server_time(self, request: Request) -> datetime:
|
||||||
"""Current server time"""
|
"""Current server time"""
|
||||||
|
@ -9,7 +9,6 @@ from authentik.blueprints.tests import reconcile_app
|
|||||||
from authentik.core.models import Group, User
|
from authentik.core.models import Group, User
|
||||||
from authentik.core.tasks import clean_expired_models
|
from authentik.core.tasks import clean_expired_models
|
||||||
from authentik.events.monitored_tasks import TaskResultStatus
|
from authentik.events.monitored_tasks import TaskResultStatus
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
|
|
||||||
|
|
||||||
class TestAdminAPI(TestCase):
|
class TestAdminAPI(TestCase):
|
||||||
@ -17,8 +16,8 @@ class TestAdminAPI(TestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.create(username=generate_id())
|
self.user = User.objects.create(username="test-user")
|
||||||
self.group = Group.objects.create(name=generate_id(), is_superuser=True)
|
self.group = Group.objects.create(name="superusers", is_superuser=True)
|
||||||
self.group.users.add(self.user)
|
self.group.users.add(self.user)
|
||||||
self.group.save()
|
self.group.save()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
@ -7,13 +7,82 @@ API Browser - {{ tenant.branding_title }}
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/standalone/api-browser/index.js' %}?version={{ version }}" type="module"></script>
|
<script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script>
|
||||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
|
<script>
|
||||||
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
|
function getCookie(name) {
|
||||||
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
let cookieValue = "";
|
||||||
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
if (document.cookie && document.cookie !== "") {
|
||||||
|
const cookies = document.cookie.split(";");
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
// Does this cookie string begin with the name we want?
|
||||||
|
if (cookie.substring(0, name.length + 1) === name + "=") {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
window.addEventListener('DOMContentLoaded', (event) => {
|
||||||
|
const rapidocEl = document.querySelector('rapi-doc');
|
||||||
|
rapidocEl.addEventListener('before-try', (e) => {
|
||||||
|
e.detail.request.headers.append('X-authentik-CSRF', getCookie("authentik_csrf"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
img.logo {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 0.5rem 1.5rem 0.5rem;
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-api-browser schemaPath="{{ path }}"></ak-api-browser>
|
<rapi-doc
|
||||||
|
spec-url="{{ path }}"
|
||||||
|
heading-text=""
|
||||||
|
theme="light"
|
||||||
|
render-style="read"
|
||||||
|
default-schema-tab="schema"
|
||||||
|
primary-color="#fd4b2d"
|
||||||
|
nav-bg-color="#212427"
|
||||||
|
bg-color="#000000"
|
||||||
|
text-color="#000000"
|
||||||
|
nav-text-color="#ffffff"
|
||||||
|
nav-hover-bg-color="#3c3f42"
|
||||||
|
nav-accent-color="#4f5255"
|
||||||
|
nav-hover-text-color="#ffffff"
|
||||||
|
use-path-in-nav-bar="true"
|
||||||
|
nav-item-spacing="relaxed"
|
||||||
|
allow-server-selection="false"
|
||||||
|
show-header="false"
|
||||||
|
allow-spec-url-load="false"
|
||||||
|
allow-spec-file-load="false">
|
||||||
|
<div slot="nav-logo">
|
||||||
|
<img alt="authentik Logo" class="logo" src="{% static 'dist/assets/icons/icon_left_brand.png' %}" />
|
||||||
|
</div>
|
||||||
|
</rapi-doc>
|
||||||
|
<script>
|
||||||
|
const rapidoc = document.querySelector("rapi-doc");
|
||||||
|
const matcher = window.matchMedia("(prefers-color-scheme: light)");
|
||||||
|
const changer = (ev) => {
|
||||||
|
const style = getComputedStyle(document.documentElement);
|
||||||
|
let bg, text = "";
|
||||||
|
if (matcher.matches) {
|
||||||
|
bg = style.getPropertyValue('--pf-global--BackgroundColor--light-300');
|
||||||
|
text = style.getPropertyValue('--pf-global--Color--300');
|
||||||
|
} else {
|
||||||
|
bg = style.getPropertyValue('--ak-dark-background');
|
||||||
|
text = style.getPropertyValue('--ak-dark-foreground');
|
||||||
|
}
|
||||||
|
rapidoc.attributes.getNamedItem("bg-color").value = bg.trim();
|
||||||
|
rapidoc.attributes.getNamedItem("text-color").value = text.trim();
|
||||||
|
rapidoc.requestUpdate();
|
||||||
|
};
|
||||||
|
matcher.addEventListener("change", changer);
|
||||||
|
window.addEventListener("load", changer);
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -4,7 +4,6 @@ from base64 import b64encode
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
|
||||||
from rest_framework.exceptions import AuthenticationFailed
|
from rest_framework.exceptions import AuthenticationFailed
|
||||||
|
|
||||||
from authentik.api.authentication import bearer_auth
|
from authentik.api.authentication import bearer_auth
|
||||||
@ -69,7 +68,6 @@ class TestAPIAuth(TestCase):
|
|||||||
user=create_test_admin_user(),
|
user=create_test_admin_user(),
|
||||||
provider=provider,
|
provider=provider,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
auth_time=timezone.now(),
|
|
||||||
_scope=SCOPE_AUTHENTIK_API,
|
_scope=SCOPE_AUTHENTIK_API,
|
||||||
_id_token=json.dumps({}),
|
_id_token=json.dumps({}),
|
||||||
)
|
)
|
||||||
@ -84,7 +82,6 @@ class TestAPIAuth(TestCase):
|
|||||||
user=create_test_admin_user(),
|
user=create_test_admin_user(),
|
||||||
provider=provider,
|
provider=provider,
|
||||||
token=generate_id(),
|
token=generate_id(),
|
||||||
auth_time=timezone.now(),
|
|
||||||
_scope="",
|
_scope="",
|
||||||
_id_token=json.dumps({}),
|
_id_token=json.dumps({}),
|
||||||
)
|
)
|
||||||
|
@ -4,7 +4,6 @@ from guardian.shortcuts import assign_perm
|
|||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application, User
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
|
|
||||||
|
|
||||||
class TestAPIDecorators(APITestCase):
|
class TestAPIDecorators(APITestCase):
|
||||||
@ -17,7 +16,7 @@ class TestAPIDecorators(APITestCase):
|
|||||||
def test_obj_perm_denied(self):
|
def test_obj_perm_denied(self):
|
||||||
"""Test object perm denied"""
|
"""Test object perm denied"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
app = Application.objects.create(name="denied", slug="denied")
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
|
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
|
||||||
)
|
)
|
||||||
@ -26,7 +25,7 @@ class TestAPIDecorators(APITestCase):
|
|||||||
def test_other_perm_denied(self):
|
def test_other_perm_denied(self):
|
||||||
"""Test other perm denied"""
|
"""Test other perm denied"""
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
app = Application.objects.create(name=generate_id(), slug=generate_id())
|
app = Application.objects.create(name="denied", slug="denied")
|
||||||
assign_perm("authentik_core.view_application", self.user, app)
|
assign_perm("authentik_core.view_application", self.user, app)
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
|
reverse("authentik_api:application-metrics", kwargs={"slug": app.slug})
|
||||||
|
@ -29,7 +29,6 @@ class Capabilities(models.TextChoices):
|
|||||||
CAN_GEO_IP = "can_geo_ip"
|
CAN_GEO_IP = "can_geo_ip"
|
||||||
CAN_IMPERSONATE = "can_impersonate"
|
CAN_IMPERSONATE = "can_impersonate"
|
||||||
CAN_DEBUG = "can_debug"
|
CAN_DEBUG = "can_debug"
|
||||||
IS_ENTERPRISE = "is_enterprise"
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorReportingConfigSerializer(PassiveSerializer):
|
class ErrorReportingConfigSerializer(PassiveSerializer):
|
||||||
@ -71,8 +70,6 @@ class ConfigView(APIView):
|
|||||||
caps.append(Capabilities.CAN_IMPERSONATE)
|
caps.append(Capabilities.CAN_IMPERSONATE)
|
||||||
if settings.DEBUG: # pragma: no cover
|
if settings.DEBUG: # pragma: no cover
|
||||||
caps.append(Capabilities.CAN_DEBUG)
|
caps.append(Capabilities.CAN_DEBUG)
|
||||||
if "authentik.enterprise" in settings.INSTALLED_APPS:
|
|
||||||
caps.append(Capabilities.IS_ENTERPRISE)
|
|
||||||
return caps
|
return caps
|
||||||
|
|
||||||
def get_config(self) -> ConfigSerializer:
|
def get_config(self) -> ConfigSerializer:
|
||||||
|
@ -33,7 +33,6 @@ from authentik.flows.api.flows import FlowViewSet
|
|||||||
from authentik.flows.api.stages import StageViewSet
|
from authentik.flows.api.stages import StageViewSet
|
||||||
from authentik.flows.views.executor import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
from authentik.flows.views.inspector import FlowInspectorView
|
from authentik.flows.views.inspector import FlowInspectorView
|
||||||
from authentik.interfaces.api import InterfaceViewSet
|
|
||||||
from authentik.outposts.api.outposts import OutpostViewSet
|
from authentik.outposts.api.outposts import OutpostViewSet
|
||||||
from authentik.outposts.api.service_connections import (
|
from authentik.outposts.api.service_connections import (
|
||||||
DockerServiceConnectionViewSet,
|
DockerServiceConnectionViewSet,
|
||||||
@ -57,11 +56,8 @@ from authentik.providers.oauth2.api.tokens import (
|
|||||||
RefreshTokenViewSet,
|
RefreshTokenViewSet,
|
||||||
)
|
)
|
||||||
from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
|
from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
|
||||||
from authentik.providers.radius.api import RadiusOutpostConfigViewSet, RadiusProviderViewSet
|
|
||||||
from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet
|
from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet
|
||||||
from authentik.providers.saml.api.providers import SAMLProviderViewSet
|
from authentik.providers.saml.api.providers import SAMLProviderViewSet
|
||||||
from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet
|
|
||||||
from authentik.providers.scim.api.providers import SCIMProviderViewSet
|
|
||||||
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||||
from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
||||||
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet
|
from authentik.sources.oauth.api.source_connection import UserOAuthSourceConnectionViewSet
|
||||||
@ -124,15 +120,12 @@ router.register("core/user_consent", UserConsentViewSet)
|
|||||||
router.register("core/tokens", TokenViewSet)
|
router.register("core/tokens", TokenViewSet)
|
||||||
router.register("core/tenants", TenantViewSet)
|
router.register("core/tenants", TenantViewSet)
|
||||||
|
|
||||||
router.register("interfaces", InterfaceViewSet)
|
|
||||||
|
|
||||||
router.register("outposts/instances", OutpostViewSet)
|
router.register("outposts/instances", OutpostViewSet)
|
||||||
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
|
router.register("outposts/service_connections/all", ServiceConnectionViewSet)
|
||||||
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
router.register("outposts/service_connections/docker", DockerServiceConnectionViewSet)
|
||||||
router.register("outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet)
|
router.register("outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet)
|
||||||
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
|
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
|
||||||
router.register("outposts/ldap", LDAPOutpostConfigViewSet)
|
router.register("outposts/ldap", LDAPOutpostConfigViewSet)
|
||||||
router.register("outposts/radius", RadiusOutpostConfigViewSet)
|
|
||||||
|
|
||||||
router.register("flows/instances", FlowViewSet)
|
router.register("flows/instances", FlowViewSet)
|
||||||
router.register("flows/bindings", FlowStageBindingViewSet)
|
router.register("flows/bindings", FlowStageBindingViewSet)
|
||||||
@ -170,8 +163,6 @@ router.register("providers/ldap", LDAPProviderViewSet)
|
|||||||
router.register("providers/proxy", ProxyProviderViewSet)
|
router.register("providers/proxy", ProxyProviderViewSet)
|
||||||
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
||||||
router.register("providers/saml", SAMLProviderViewSet)
|
router.register("providers/saml", SAMLProviderViewSet)
|
||||||
router.register("providers/scim", SCIMProviderViewSet)
|
|
||||||
router.register("providers/radius", RadiusProviderViewSet)
|
|
||||||
|
|
||||||
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
|
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
|
||||||
router.register("oauth2/refresh_tokens", RefreshTokenViewSet)
|
router.register("oauth2/refresh_tokens", RefreshTokenViewSet)
|
||||||
@ -182,7 +173,6 @@ router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
|||||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||||
router.register("propertymappings/scope", ScopeMappingViewSet)
|
router.register("propertymappings/scope", ScopeMappingViewSet)
|
||||||
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
||||||
router.register("propertymappings/scim", SCIMMappingViewSet)
|
|
||||||
|
|
||||||
router.register("authenticators/all", DeviceViewSet, basename="device")
|
router.register("authenticators/all", DeviceViewSet, basename="device")
|
||||||
router.register("authenticators/duo", DuoDeviceViewSet)
|
router.register("authenticators/duo", DuoDeviceViewSet)
|
||||||
|
@ -55,11 +55,11 @@ class AuthentikBlueprintsConfig(ManagedAppConfig):
|
|||||||
"""Load v1 tasks"""
|
"""Load v1 tasks"""
|
||||||
self.import_module("authentik.blueprints.v1.tasks")
|
self.import_module("authentik.blueprints.v1.tasks")
|
||||||
|
|
||||||
def reconcile_blueprints_discovery(self):
|
def reconcile_blueprints_discover(self):
|
||||||
"""Run blueprint discovery"""
|
"""Run blueprint discovery"""
|
||||||
from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints
|
from authentik.blueprints.v1.tasks import blueprints_discover, clear_failed_blueprints
|
||||||
|
|
||||||
blueprints_discovery.delay()
|
blueprints_discover.delay()
|
||||||
clear_failed_blueprints.delay()
|
clear_failed_blueprints.delay()
|
||||||
|
|
||||||
def import_models(self):
|
def import_models(self):
|
||||||
|
@ -19,8 +19,10 @@ class Command(BaseCommand):
|
|||||||
for blueprint_path in options.get("blueprints", []):
|
for blueprint_path in options.get("blueprints", []):
|
||||||
content = BlueprintInstance(path=blueprint_path).retrieve()
|
content = BlueprintInstance(path=blueprint_path).retrieve()
|
||||||
importer = Importer(content)
|
importer = Importer(content)
|
||||||
valid, _ = importer.validate()
|
valid, logs = importer.validate()
|
||||||
if not valid:
|
if not valid:
|
||||||
|
for log in logs:
|
||||||
|
getattr(LOGGER, log.pop("log_level"))(**log)
|
||||||
self.stderr.write("blueprint invalid")
|
self.stderr.write("blueprint invalid")
|
||||||
sys_exit(1)
|
sys_exit(1)
|
||||||
importer.apply()
|
importer.apply()
|
||||||
|
@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
from dacite.core import from_dict
|
from dacite.core import from_dict
|
||||||
from django.apps.registry import Apps
|
from django.apps.registry import Apps
|
||||||
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
from yaml import load
|
from yaml import load
|
||||||
@ -14,7 +15,7 @@ from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_SYSTEM
|
|||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
def check_blueprint_v1_file(BlueprintInstance: type, path: Path):
|
def check_blueprint_v1_file(BlueprintInstance: type["BlueprintInstance"], path: Path):
|
||||||
"""Check if blueprint should be imported"""
|
"""Check if blueprint should be imported"""
|
||||||
from authentik.blueprints.models import BlueprintInstanceStatus
|
from authentik.blueprints.models import BlueprintInstanceStatus
|
||||||
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata
|
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata
|
||||||
|
@ -5,7 +5,7 @@ from authentik.lib.utils.time import fqdn_rand
|
|||||||
|
|
||||||
CELERY_BEAT_SCHEDULE = {
|
CELERY_BEAT_SCHEDULE = {
|
||||||
"blueprints_v1_discover": {
|
"blueprints_v1_discover": {
|
||||||
"task": "authentik.blueprints.v1.tasks.blueprints_discovery",
|
"task": "authentik.blueprints.v1.tasks.blueprints_discover",
|
||||||
"schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
|
"schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
|
||||||
"options": {"queue": "authentik_scheduled"},
|
"options": {"queue": "authentik_scheduled"},
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Blueprint helpers"""
|
"""Blueprint helpers"""
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from pathlib import Path
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
@ -44,3 +45,13 @@ def reconcile_app(app_name: str):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return wrapper_outer
|
return wrapper_outer
|
||||||
|
|
||||||
|
|
||||||
|
def load_yaml_fixture(path: str, **kwargs) -> str:
|
||||||
|
"""Load yaml fixture, optionally formatting it with kwargs"""
|
||||||
|
with open(Path(__file__).resolve().parent / Path(path), "r", encoding="utf-8") as _fixture:
|
||||||
|
fixture = _fixture.read()
|
||||||
|
try:
|
||||||
|
return fixture % kwargs
|
||||||
|
except TypeError:
|
||||||
|
return fixture
|
||||||
|
@ -3,12 +3,12 @@ from os import environ
|
|||||||
|
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
|
from authentik.blueprints.tests import load_yaml_fixture
|
||||||
from authentik.blueprints.v1.exporter import FlowExporter
|
from authentik.blueprints.v1.exporter import FlowExporter
|
||||||
from authentik.blueprints.v1.importer import Importer, transaction_rollback
|
from authentik.blueprints.v1.importer import Importer, transaction_rollback
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.tests.utils import load_fixture
|
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
@ -113,14 +113,14 @@ class TestBlueprintsV1(TransactionTestCase):
|
|||||||
"""Test export and import it twice"""
|
"""Test export and import it twice"""
|
||||||
count_initial = Prompt.objects.filter(field_key="username").count()
|
count_initial = Prompt.objects.filter(field_key="username").count()
|
||||||
|
|
||||||
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
|
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml"))
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
|
|
||||||
count_before = Prompt.objects.filter(field_key="username").count()
|
count_before = Prompt.objects.filter(field_key="username").count()
|
||||||
self.assertEqual(count_initial + 1, count_before)
|
self.assertEqual(count_initial + 1, count_before)
|
||||||
|
|
||||||
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
|
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml"))
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
|
|
||||||
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
|
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
|
||||||
@ -130,7 +130,7 @@ class TestBlueprintsV1(TransactionTestCase):
|
|||||||
ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete()
|
ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete()
|
||||||
Group.objects.filter(name="test").delete()
|
Group.objects.filter(name="test").delete()
|
||||||
environ["foo"] = generate_id()
|
environ["foo"] = generate_id()
|
||||||
importer = Importer(load_fixture("fixtures/tags.yaml"), {"bar": "baz"})
|
importer = Importer(load_yaml_fixture("fixtures/tags.yaml"), {"bar": "baz"})
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first()
|
policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first()
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"""Test blueprints v1"""
|
"""Test blueprints v1"""
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
|
from authentik.blueprints.tests import load_yaml_fixture
|
||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.tests.utils import load_fixture
|
|
||||||
|
|
||||||
|
|
||||||
class TestBlueprintsV1Conditions(TransactionTestCase):
|
class TestBlueprintsV1Conditions(TransactionTestCase):
|
||||||
@ -14,7 +14,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
|
|||||||
"""Test conditions fulfilled"""
|
"""Test conditions fulfilled"""
|
||||||
flow_slug1 = generate_id()
|
flow_slug1 = generate_id()
|
||||||
flow_slug2 = generate_id()
|
flow_slug2 = generate_id()
|
||||||
import_yaml = load_fixture(
|
import_yaml = load_yaml_fixture(
|
||||||
"fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
"fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
|
|||||||
"""Test conditions not fulfilled"""
|
"""Test conditions not fulfilled"""
|
||||||
flow_slug1 = generate_id()
|
flow_slug1 = generate_id()
|
||||||
flow_slug2 = generate_id()
|
flow_slug2 = generate_id()
|
||||||
import_yaml = load_fixture(
|
import_yaml = load_yaml_fixture(
|
||||||
"fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
"fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"""Test blueprints v1"""
|
"""Test blueprints v1"""
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
|
from authentik.blueprints.tests import load_yaml_fixture
|
||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.tests.utils import load_fixture
|
|
||||||
|
|
||||||
|
|
||||||
class TestBlueprintsV1State(TransactionTestCase):
|
class TestBlueprintsV1State(TransactionTestCase):
|
||||||
@ -13,7 +13,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
|||||||
def test_state_present(self):
|
def test_state_present(self):
|
||||||
"""Test state present"""
|
"""Test state present"""
|
||||||
flow_slug = generate_id()
|
flow_slug = generate_id()
|
||||||
import_yaml = load_fixture("fixtures/state_present.yaml", id=flow_slug)
|
import_yaml = load_yaml_fixture("fixtures/state_present.yaml", id=flow_slug)
|
||||||
|
|
||||||
importer = Importer(import_yaml)
|
importer = Importer(import_yaml)
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
@ -39,7 +39,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
|||||||
def test_state_created(self):
|
def test_state_created(self):
|
||||||
"""Test state created"""
|
"""Test state created"""
|
||||||
flow_slug = generate_id()
|
flow_slug = generate_id()
|
||||||
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
|
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug)
|
||||||
|
|
||||||
importer = Importer(import_yaml)
|
importer = Importer(import_yaml)
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
@ -65,7 +65,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
|||||||
def test_state_absent(self):
|
def test_state_absent(self):
|
||||||
"""Test state absent"""
|
"""Test state absent"""
|
||||||
flow_slug = generate_id()
|
flow_slug = generate_id()
|
||||||
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
|
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug)
|
||||||
|
|
||||||
importer = Importer(import_yaml)
|
importer = Importer(import_yaml)
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
@ -74,7 +74,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
|||||||
flow: Flow = Flow.objects.filter(slug=flow_slug).first()
|
flow: Flow = Flow.objects.filter(slug=flow_slug).first()
|
||||||
self.assertEqual(flow.slug, flow_slug)
|
self.assertEqual(flow.slug, flow_slug)
|
||||||
|
|
||||||
import_yaml = load_fixture("fixtures/state_absent.yaml", id=flow_slug)
|
import_yaml = load_yaml_fixture("fixtures/state_absent.yaml", id=flow_slug)
|
||||||
importer = Importer(import_yaml)
|
importer = Importer(import_yaml)
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
|
@ -6,7 +6,7 @@ from django.test import TransactionTestCase
|
|||||||
from yaml import dump
|
from yaml import dump
|
||||||
|
|
||||||
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
|
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
|
||||||
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discovery, blueprints_find
|
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discover, blueprints_find
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
|||||||
file.seek(0)
|
file.seek(0)
|
||||||
file_hash = sha512(file.read().encode()).hexdigest()
|
file_hash = sha512(file.read().encode()).hexdigest()
|
||||||
file.flush()
|
file.flush()
|
||||||
blueprints_discovery() # pylint: disable=no-value-for-parameter
|
blueprints_discover() # pylint: disable=no-value-for-parameter
|
||||||
instance = BlueprintInstance.objects.filter(name=blueprint_id).first()
|
instance = BlueprintInstance.objects.filter(name=blueprint_id).first()
|
||||||
self.assertEqual(instance.last_applied_hash, file_hash)
|
self.assertEqual(instance.last_applied_hash, file_hash)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -81,7 +81,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
file.flush()
|
file.flush()
|
||||||
blueprints_discovery() # pylint: disable=no-value-for-parameter
|
blueprints_discover() # pylint: disable=no-value-for-parameter
|
||||||
blueprint = BlueprintInstance.objects.filter(name="foo").first()
|
blueprint = BlueprintInstance.objects.filter(name="foo").first()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
blueprint.last_applied_hash,
|
blueprint.last_applied_hash,
|
||||||
@ -106,7 +106,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
file.flush()
|
file.flush()
|
||||||
blueprints_discovery() # pylint: disable=no-value-for-parameter
|
blueprints_discover() # pylint: disable=no-value-for-parameter
|
||||||
blueprint.refresh_from_db()
|
blueprint.refresh_from_db()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
blueprint.last_applied_hash,
|
blueprint.last_applied_hash,
|
||||||
|
@ -40,10 +40,6 @@ from authentik.lib.models import SerializerModel
|
|||||||
from authentik.outposts.models import OutpostServiceConnection
|
from authentik.outposts.models import OutpostServiceConnection
|
||||||
from authentik.policies.models import Policy, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBindingModel
|
||||||
|
|
||||||
# Context set when the serializer is created in a blueprint context
|
|
||||||
# Update website/developer-docs/blueprints/v1/models.md when used
|
|
||||||
SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry"
|
|
||||||
|
|
||||||
|
|
||||||
def is_model_allowed(model: type[Model]) -> bool:
|
def is_model_allowed(model: type[Model]) -> bool:
|
||||||
"""Check if model is allowed"""
|
"""Check if model is allowed"""
|
||||||
@ -162,12 +158,7 @@ class Importer:
|
|||||||
raise EntryInvalidError(f"Model {model} not allowed")
|
raise EntryInvalidError(f"Model {model} not allowed")
|
||||||
if issubclass(model, BaseMetaModel):
|
if issubclass(model, BaseMetaModel):
|
||||||
serializer_class: type[Serializer] = model.serializer()
|
serializer_class: type[Serializer] = model.serializer()
|
||||||
serializer = serializer_class(
|
serializer = serializer_class(data=entry.get_attrs(self.__import))
|
||||||
data=entry.get_attrs(self.__import),
|
|
||||||
context={
|
|
||||||
SERIALIZER_CONTEXT_BLUEPRINT: entry,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
@ -226,12 +217,7 @@ class Importer:
|
|||||||
always_merger.merge(full_data, updated_identifiers)
|
always_merger.merge(full_data, updated_identifiers)
|
||||||
serializer_kwargs["data"] = full_data
|
serializer_kwargs["data"] = full_data
|
||||||
|
|
||||||
serializer: Serializer = model().serializer(
|
serializer: Serializer = model().serializer(**serializer_kwargs)
|
||||||
context={
|
|
||||||
SERIALIZER_CONTEXT_BLUEPRINT: entry,
|
|
||||||
},
|
|
||||||
**serializer_kwargs,
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
|
@ -76,7 +76,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
|
|||||||
return
|
return
|
||||||
if isinstance(event, FileCreatedEvent):
|
if isinstance(event, FileCreatedEvent):
|
||||||
LOGGER.debug("new blueprint file created, starting discovery")
|
LOGGER.debug("new blueprint file created, starting discovery")
|
||||||
blueprints_discovery.delay()
|
blueprints_discover.delay()
|
||||||
if isinstance(event, FileModifiedEvent):
|
if isinstance(event, FileModifiedEvent):
|
||||||
path = Path(event.src_path)
|
path = Path(event.src_path)
|
||||||
root = Path(CONFIG.y("blueprints_dir")).absolute()
|
root = Path(CONFIG.y("blueprints_dir")).absolute()
|
||||||
@ -122,7 +122,7 @@ def blueprints_find():
|
|||||||
)
|
)
|
||||||
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
|
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
|
||||||
blueprints.append(blueprint)
|
blueprints.append(blueprint)
|
||||||
LOGGER.debug(
|
LOGGER.info(
|
||||||
"parsed & loaded blueprint",
|
"parsed & loaded blueprint",
|
||||||
hash=file_hash,
|
hash=file_hash,
|
||||||
path=str(path),
|
path=str(path),
|
||||||
@ -134,7 +134,7 @@ def blueprints_find():
|
|||||||
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
|
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
|
||||||
)
|
)
|
||||||
@prefill_task
|
@prefill_task
|
||||||
def blueprints_discovery(self: MonitoredTask):
|
def blueprints_discover(self: MonitoredTask):
|
||||||
"""Find blueprints and check if they need to be created in the database"""
|
"""Find blueprints and check if they need to be created in the database"""
|
||||||
count = 0
|
count = 0
|
||||||
for blueprint in blueprints_find():
|
for blueprint in blueprints_find():
|
||||||
|
@ -37,6 +37,7 @@ from authentik.lib.utils.file import (
|
|||||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.types import PolicyResult
|
from authentik.policies.types import PolicyResult
|
||||||
|
from authentik.stages.user_login.stage import USER_LOGIN_AUTHENTICATED
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -185,6 +186,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
if superuser_full_list and request.user.is_superuser:
|
if superuser_full_list and request.user.is_superuser:
|
||||||
return super().list(request)
|
return super().list(request)
|
||||||
|
|
||||||
|
# To prevent the user from having to double login when prompt is set to login
|
||||||
|
# and the user has just signed it. This session variable is set in the UserLoginStage
|
||||||
|
# and is (quite hackily) removed from the session in applications's API's List method
|
||||||
|
self.request.session.pop(USER_LOGIN_AUTHENTICATED, None)
|
||||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||||
self.paginate_queryset(queryset)
|
self.paginate_queryset(queryset)
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ from authentik.core.models import Group, User
|
|||||||
class GroupMemberSerializer(ModelSerializer):
|
class GroupMemberSerializer(ModelSerializer):
|
||||||
"""Stripped down user serializer to show relevant users for groups"""
|
"""Stripped down user serializer to show relevant users for groups"""
|
||||||
|
|
||||||
|
avatar = CharField(read_only=True)
|
||||||
attributes = JSONField(validators=[is_dict], required=False)
|
attributes = JSONField(validators=[is_dict], required=False)
|
||||||
uid = CharField(read_only=True)
|
uid = CharField(read_only=True)
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ class GroupMemberSerializer(ModelSerializer):
|
|||||||
"is_active",
|
"is_active",
|
||||||
"last_login",
|
"last_login",
|
||||||
"email",
|
"email",
|
||||||
|
"avatar",
|
||||||
"attributes",
|
"attributes",
|
||||||
"uid",
|
"uid",
|
||||||
]
|
]
|
||||||
|
@ -35,7 +35,6 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
"authentication_flow",
|
|
||||||
"authorization_flow",
|
"authorization_flow",
|
||||||
"property_mappings",
|
"property_mappings",
|
||||||
"component",
|
"component",
|
||||||
@ -45,9 +44,6 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
"verbose_name_plural",
|
"verbose_name_plural",
|
||||||
"meta_model_name",
|
"meta_model_name",
|
||||||
]
|
]
|
||||||
extra_kwargs = {
|
|
||||||
"authorization_flow": {"required": True, "allow_null": False},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderViewSet(
|
class ProviderViewSet(
|
||||||
|
@ -206,6 +206,5 @@ class UserSourceConnectionViewSet(
|
|||||||
queryset = UserSourceConnection.objects.all()
|
queryset = UserSourceConnection.objects.all()
|
||||||
serializer_class = UserSourceConnectionSerializer
|
serializer_class = UserSourceConnectionSerializer
|
||||||
permission_classes = [OwnerSuperuserPermissions]
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
filterset_fields = ["user"]
|
|
||||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
ordering = ["pk"]
|
ordering = ["pk"]
|
||||||
|
@ -16,7 +16,6 @@ from rest_framework.viewsets import ModelViewSet
|
|||||||
from authentik.api.authorization import OwnerSuperuserPermissions
|
from authentik.api.authorization import OwnerSuperuserPermissions
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.blueprints.api import ManagedSerializer
|
from authentik.blueprints.api import ManagedSerializer
|
||||||
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
@ -30,21 +29,10 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
|||||||
|
|
||||||
user_obj = UserSerializer(required=False, source="user", read_only=True)
|
user_obj = UserSerializer(required=False, source="user", read_only=True)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
|
||||||
self.fields["key"] = CharField()
|
|
||||||
|
|
||||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||||
"""Ensure only API or App password tokens are created."""
|
"""Ensure only API or App password tokens are created."""
|
||||||
request: Request = self.context.get("request")
|
request: Request = self.context["request"]
|
||||||
if not request:
|
attrs.setdefault("user", request.user)
|
||||||
if "user" not in attrs:
|
|
||||||
raise ValidationError("Missing user")
|
|
||||||
if "intent" not in attrs:
|
|
||||||
raise ValidationError("Missing intent")
|
|
||||||
else:
|
|
||||||
attrs.setdefault("user", request.user)
|
|
||||||
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
||||||
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
||||||
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
||||||
|
@ -10,6 +10,7 @@ from django.db.models.functions import ExtractHour
|
|||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
from django.urls import reverse_lazy
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
@ -37,7 +38,6 @@ from rest_framework.request import Request
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import (
|
from rest_framework.serializers import (
|
||||||
BooleanField,
|
BooleanField,
|
||||||
DateTimeField,
|
|
||||||
ListSerializer,
|
ListSerializer,
|
||||||
ModelSerializer,
|
ModelSerializer,
|
||||||
PrimaryKeyRelatedField,
|
PrimaryKeyRelatedField,
|
||||||
@ -67,16 +67,13 @@ from authentik.core.models import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
|
||||||
from authentik.flows.models import FlowToken
|
from authentik.flows.models import FlowToken
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.views import reverse_interface
|
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.stages.email.tasks import send_mails
|
from authentik.stages.email.tasks import send_mails
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
from authentik.tenants.utils import get_tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -212,9 +209,8 @@ class UserMetricsSerializer(PassiveSerializer):
|
|||||||
def get_logins(self, _):
|
def get_logins(self, _):
|
||||||
"""Get successful logins per 8 hours for the last 7 days"""
|
"""Get successful logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
request = self.context["request"]
|
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(request.user, "authentik_events.view_event").filter(
|
get_objects_for_user(user, "authentik_events.view_event").filter(
|
||||||
action=EventAction.LOGIN, user__pk=user.pk
|
action=EventAction.LOGIN, user__pk=user.pk
|
||||||
)
|
)
|
||||||
# 3 data points per day, so 8 hour spans
|
# 3 data points per day, so 8 hour spans
|
||||||
@ -225,9 +221,8 @@ class UserMetricsSerializer(PassiveSerializer):
|
|||||||
def get_logins_failed(self, _):
|
def get_logins_failed(self, _):
|
||||||
"""Get failed logins per 8 hours for the last 7 days"""
|
"""Get failed logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
request = self.context["request"]
|
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(request.user, "authentik_events.view_event").filter(
|
get_objects_for_user(user, "authentik_events.view_event").filter(
|
||||||
action=EventAction.LOGIN_FAILED, context__username=user.username
|
action=EventAction.LOGIN_FAILED, context__username=user.username
|
||||||
)
|
)
|
||||||
# 3 data points per day, so 8 hour spans
|
# 3 data points per day, so 8 hour spans
|
||||||
@ -238,9 +233,8 @@ class UserMetricsSerializer(PassiveSerializer):
|
|||||||
def get_authorizations(self, _):
|
def get_authorizations(self, _):
|
||||||
"""Get failed logins per 8 hours for the last 7 days"""
|
"""Get failed logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
request = self.context["request"]
|
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(request.user, "authentik_events.view_event").filter(
|
get_objects_for_user(user, "authentik_events.view_event").filter(
|
||||||
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
|
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
|
||||||
)
|
)
|
||||||
# 3 data points per day, so 8 hour spans
|
# 3 data points per day, so 8 hour spans
|
||||||
@ -322,7 +316,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
|
def _create_recovery_link(self) -> tuple[Optional[str], Optional[Token]]:
|
||||||
"""Create a recovery link (when the current tenant has a recovery flow set),
|
"""Create a recovery link (when the current tenant has a recovery flow set),
|
||||||
that can either be shown to an admin or sent to the user directly"""
|
that can either be shown to an admin or sent to the user directly"""
|
||||||
tenant = get_tenant(self.request)
|
tenant: Tenant = self.request._request.tenant
|
||||||
# Check that there is a recovery flow, if not return an error
|
# Check that there is a recovery flow, if not return an error
|
||||||
flow = tenant.flow_recovery
|
flow = tenant.flow_recovery
|
||||||
if not flow:
|
if not flow:
|
||||||
@ -331,16 +325,12 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
user: User = self.get_object()
|
user: User = self.get_object()
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
try:
|
plan = planner.plan(
|
||||||
plan = planner.plan(
|
self.request._request,
|
||||||
self.request._request,
|
{
|
||||||
{
|
PLAN_CONTEXT_PENDING_USER: user,
|
||||||
PLAN_CONTEXT_PENDING_USER: user,
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
except FlowNonApplicableException:
|
|
||||||
LOGGER.warning("Recovery flow not applicable to user")
|
|
||||||
return None, None
|
|
||||||
token, __ = FlowToken.objects.update_or_create(
|
token, __ = FlowToken.objects.update_or_create(
|
||||||
identifier=f"{user.uid}-password-reset",
|
identifier=f"{user.uid}-password-reset",
|
||||||
defaults={
|
defaults={
|
||||||
@ -351,12 +341,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
)
|
)
|
||||||
querystring = urlencode({QS_KEY_TOKEN: token.key})
|
querystring = urlencode({QS_KEY_TOKEN: token.key})
|
||||||
link = self.request.build_absolute_uri(
|
link = self.request.build_absolute_uri(
|
||||||
reverse_interface(
|
reverse_lazy("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
self.request,
|
+ f"?{querystring}"
|
||||||
InterfaceType.FLOW,
|
|
||||||
flow_slug=flow.slug,
|
|
||||||
),
|
|
||||||
+f"?{querystring}",
|
|
||||||
)
|
)
|
||||||
return link, token
|
return link, token
|
||||||
|
|
||||||
@ -367,11 +353,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
{
|
{
|
||||||
"name": CharField(required=True),
|
"name": CharField(required=True),
|
||||||
"create_group": BooleanField(default=False),
|
"create_group": BooleanField(default=False),
|
||||||
"expiring": BooleanField(default=True),
|
|
||||||
"expires": DateTimeField(
|
|
||||||
required=False,
|
|
||||||
help_text="If not provided, valid for 360 days",
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
responses={
|
responses={
|
||||||
@ -392,20 +373,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""Create a new user account that is marked as a service account"""
|
"""Create a new user account that is marked as a service account"""
|
||||||
username = request.data.get("name")
|
username = request.data.get("name")
|
||||||
create_group = request.data.get("create_group", False)
|
create_group = request.data.get("create_group", False)
|
||||||
expiring = request.data.get("expiring", True)
|
|
||||||
expires = request.data.get("expires", now() + timedelta(days=360))
|
|
||||||
|
|
||||||
with atomic():
|
with atomic():
|
||||||
try:
|
try:
|
||||||
user: User = User.objects.create(
|
user = User.objects.create(
|
||||||
username=username,
|
username=username,
|
||||||
name=username,
|
name=username,
|
||||||
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: expiring},
|
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||||
path=USER_PATH_SERVICE_ACCOUNT,
|
path=USER_PATH_SERVICE_ACCOUNT,
|
||||||
)
|
)
|
||||||
user.set_unusable_password()
|
|
||||||
user.save()
|
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
"user_uid": user.uid,
|
"user_uid": user.uid,
|
||||||
@ -421,8 +396,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
identifier=slugify(f"service-account-{username}-password"),
|
identifier=slugify(f"service-account-{username}-password"),
|
||||||
intent=TokenIntents.INTENT_APP_PASSWORD,
|
intent=TokenIntents.INTENT_APP_PASSWORD,
|
||||||
user=user,
|
user=user,
|
||||||
expires=expires,
|
expires=now() + timedelta(days=360),
|
||||||
expiring=expiring,
|
|
||||||
)
|
)
|
||||||
response["token"] = token.key
|
response["token"] = token.key
|
||||||
return Response(response)
|
return Response(response)
|
||||||
@ -479,9 +453,8 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
def metrics(self, request: Request, pk: int) -> Response:
|
def metrics(self, request: Request, pk: int) -> Response:
|
||||||
"""User metrics per 1h"""
|
"""User metrics per 1h"""
|
||||||
user: User = self.get_object()
|
user: User = self.get_object()
|
||||||
serializer = UserMetricsSerializer(instance={})
|
serializer = UserMetricsSerializer(True)
|
||||||
serializer.context["user"] = user
|
serializer.context["user"] = user
|
||||||
serializer.context["request"] = request
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@permission_required("authentik_core.reset_user_password")
|
@permission_required("authentik_core.reset_user_password")
|
||||||
|
@ -11,7 +11,6 @@ class AuthentikCoreConfig(ManagedAppConfig):
|
|||||||
label = "authentik_core"
|
label = "authentik_core"
|
||||||
verbose_name = "authentik Core"
|
verbose_name = "authentik Core"
|
||||||
mountpoint = ""
|
mountpoint = ""
|
||||||
ws_mountpoint = "authentik.core.urls"
|
|
||||||
default = True
|
default = True
|
||||||
|
|
||||||
def reconcile_load_core_signals(self):
|
def reconcile_load_core_signals(self):
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
"""Property Mapping Evaluator"""
|
"""Property Mapping Evaluator"""
|
||||||
from typing import Any, Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from prometheus_client import Histogram
|
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
@ -11,24 +10,15 @@ from authentik.lib.expression.evaluator import BaseEvaluator
|
|||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
|
|
||||||
PROPERTY_MAPPING_TIME = Histogram(
|
|
||||||
"authentik_property_mapping_execution_time",
|
|
||||||
"Evaluation time of property mappings",
|
|
||||||
["mapping_name"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingEvaluator(BaseEvaluator):
|
class PropertyMappingEvaluator(BaseEvaluator):
|
||||||
"""Custom Evaluator that adds some different context variables."""
|
"""Custom Evaluator that adds some different context variables."""
|
||||||
|
|
||||||
dry_run: bool
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
model: Model,
|
model: Model,
|
||||||
user: Optional[User] = None,
|
user: Optional[User] = None,
|
||||||
request: Optional[HttpRequest] = None,
|
request: Optional[HttpRequest] = None,
|
||||||
dry_run: Optional[bool] = False,
|
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
if hasattr(model, "name"):
|
if hasattr(model, "name"):
|
||||||
@ -45,13 +35,9 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||||||
req.http_request = request
|
req.http_request = request
|
||||||
self._context["request"] = req
|
self._context["request"] = req
|
||||||
self._context.update(**kwargs)
|
self._context.update(**kwargs)
|
||||||
self.dry_run = dry_run
|
|
||||||
|
|
||||||
def handle_error(self, exc: Exception, expression_source: str):
|
def handle_error(self, exc: Exception, expression_source: str):
|
||||||
"""Exception Handler"""
|
"""Exception Handler"""
|
||||||
# For dry-run requests we don't save exceptions
|
|
||||||
if self.dry_run:
|
|
||||||
return
|
|
||||||
error_string = exception_to_string(exc)
|
error_string = exception_to_string(exc)
|
||||||
event = Event.new(
|
event = Event.new(
|
||||||
EventAction.PROPERTY_MAPPING_EXCEPTION,
|
EventAction.PROPERTY_MAPPING_EXCEPTION,
|
||||||
@ -63,7 +49,3 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||||||
event.from_http(req.http_request, req.user)
|
event.from_http(req.http_request, req.user)
|
||||||
return
|
return
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
def evaluate(self, *args, **kwargs) -> Any:
|
|
||||||
with PROPERTY_MAPPING_TIME.labels(mapping_name=self._filename).time():
|
|
||||||
return super().evaluate(*args, **kwargs)
|
|
||||||
|
@ -18,13 +18,13 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
akadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||||
username="akadmin",
|
username="akadmin", email="root@localhost", name="authentik Default Admin"
|
||||||
email=environ.get("AUTHENTIK_BOOTSTRAP_EMAIL", "root@localhost"),
|
|
||||||
name="authentik Default Admin",
|
|
||||||
)
|
)
|
||||||
password = None
|
password = None
|
||||||
if "TF_BUILD" in environ or settings.TEST:
|
if "TF_BUILD" in environ or settings.TEST:
|
||||||
password = "akadmin" # noqa # nosec
|
password = "akadmin" # noqa # nosec
|
||||||
|
if "AK_ADMIN_PASS" in environ:
|
||||||
|
password = environ["AK_ADMIN_PASS"]
|
||||||
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
if "AUTHENTIK_BOOTSTRAP_PASSWORD" in environ:
|
||||||
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
password = environ["AUTHENTIK_BOOTSTRAP_PASSWORD"]
|
||||||
if password:
|
if password:
|
||||||
|
@ -46,9 +46,13 @@ def create_default_user_token(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
|||||||
akadmin = User.objects.using(db_alias).filter(username="akadmin")
|
akadmin = User.objects.using(db_alias).filter(username="akadmin")
|
||||||
if not akadmin.exists():
|
if not akadmin.exists():
|
||||||
return
|
return
|
||||||
if "AUTHENTIK_BOOTSTRAP_TOKEN" not in environ:
|
key = None
|
||||||
|
if "AK_ADMIN_TOKEN" in environ:
|
||||||
|
key = environ["AK_ADMIN_TOKEN"]
|
||||||
|
if "AUTHENTIK_BOOTSTRAP_TOKEN" in environ:
|
||||||
|
key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"]
|
||||||
|
if not key:
|
||||||
return
|
return
|
||||||
key = environ["AUTHENTIK_BOOTSTRAP_TOKEN"]
|
|
||||||
Token.objects.using(db_alias).create(
|
Token.objects.using(db_alias).create(
|
||||||
identifier="authentik-bootstrap-token",
|
identifier="authentik-bootstrap-token",
|
||||||
user=akadmin.first(),
|
user=akadmin.first(),
|
||||||
@ -182,9 +186,7 @@ class Migration(migrations.Migration):
|
|||||||
model_name="application",
|
model_name="application",
|
||||||
name="meta_launch_url",
|
name="meta_launch_url",
|
||||||
field=models.TextField(
|
field=models.TextField(
|
||||||
blank=True,
|
blank=True, default="", validators=[authentik.lib.models.DomainlessURLValidator()]
|
||||||
default="",
|
|
||||||
validators=[authentik.lib.models.DomainlessFormattedURLValidator()],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
migrations.RunPython(
|
migrations.RunPython(
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-03-02 21:32
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
|
|
||||||
("authentik_core", "0024_source_icon"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="provider",
|
|
||||||
name="authorization_flow",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
help_text="Flow used when authorizing this provider.",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
related_name="provider_authorization",
|
|
||||||
to="authentik_flows.flow",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,26 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-03-07 13:41
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
from authentik.lib.migrations import fallback_names
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0025_alter_provider_authorization_flow"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(fallback_names("authentik_core", "propertymapping", "name")),
|
|
||||||
migrations.RunPython(fallback_names("authentik_core", "provider", "name")),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="propertymapping",
|
|
||||||
name="name",
|
|
||||||
field=models.TextField(unique=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="provider",
|
|
||||||
name="name",
|
|
||||||
field=models.TextField(unique=True),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-03-19 21:57
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("authentik_core", "0026_alter_propertymapping_name_alter_provider_name"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="user",
|
|
||||||
name="uuid",
|
|
||||||
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-03-23 21:44
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
|
|
||||||
("authentik_core", "0027_alter_user_uuid"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="provider",
|
|
||||||
name="authentication_flow",
|
|
||||||
field=models.ForeignKey(
|
|
||||||
help_text="Flow used for authentication when the associated application is accessed by an un-authenticated user.",
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="provider_authentication",
|
|
||||||
to="authentik_flows.flow",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -22,18 +22,14 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.blueprints.models import ManagedModel
|
from authentik.blueprints.models import ManagedModel
|
||||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||||
|
from authentik.core.signals import password_changed
|
||||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||||
from authentik.lib.avatars import get_avatar
|
from authentik.lib.avatars import get_avatar
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.models import (
|
from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel
|
||||||
CreatedUpdatedModel,
|
|
||||||
DomainlessFormattedURLValidator,
|
|
||||||
SerializerModel,
|
|
||||||
)
|
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
from authentik.tenants.utils import get_tenant
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||||
@ -147,7 +143,7 @@ class UserManager(DjangoUserManager):
|
|||||||
class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
||||||
"""Custom User model to allow easier adding of user-based settings"""
|
"""Custom User model to allow easier adding of user-based settings"""
|
||||||
|
|
||||||
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
|
uuid = models.UUIDField(default=uuid4, editable=False)
|
||||||
name = models.TextField(help_text=_("User's display name."))
|
name = models.TextField(help_text=_("User's display name."))
|
||||||
path = models.TextField(default="users")
|
path = models.TextField(default="users")
|
||||||
|
|
||||||
@ -169,7 +165,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||||||
including the users attributes"""
|
including the users attributes"""
|
||||||
final_attributes = {}
|
final_attributes = {}
|
||||||
if request and hasattr(request, "tenant"):
|
if request and hasattr(request, "tenant"):
|
||||||
always_merger.merge(final_attributes, get_tenant(request).attributes)
|
always_merger.merge(final_attributes, request.tenant.attributes)
|
||||||
for group in self.ak_groups.all().order_by("name"):
|
for group in self.ak_groups.all().order_by("name"):
|
||||||
always_merger.merge(final_attributes, group.attributes)
|
always_merger.merge(final_attributes, group.attributes)
|
||||||
always_merger.merge(final_attributes, self.attributes)
|
always_merger.merge(final_attributes, self.attributes)
|
||||||
@ -193,8 +189,6 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||||||
|
|
||||||
def set_password(self, raw_password, signal=True):
|
def set_password(self, raw_password, signal=True):
|
||||||
if self.pk and signal:
|
if self.pk and signal:
|
||||||
from authentik.core.signals import password_changed
|
|
||||||
|
|
||||||
password_changed.send(sender=self, user=self, password=raw_password)
|
password_changed.send(sender=self, user=self, password=raw_password)
|
||||||
self.password_change_date = now()
|
self.password_change_date = now()
|
||||||
return super().set_password(raw_password)
|
return super().set_password(raw_password)
|
||||||
@ -228,7 +222,7 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
LOGGER.warning("Failed to get default locale", exc=exc)
|
LOGGER.warning("Failed to get default locale", exc=exc)
|
||||||
if request:
|
if request:
|
||||||
return get_tenant(request).default_locale
|
return request.tenant.locale
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -248,23 +242,11 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
|||||||
class Provider(SerializerModel):
|
class Provider(SerializerModel):
|
||||||
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
||||||
|
|
||||||
name = models.TextField(unique=True)
|
name = models.TextField()
|
||||||
|
|
||||||
authentication_flow = models.ForeignKey(
|
|
||||||
"authentik_flows.Flow",
|
|
||||||
null=True,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
help_text=_(
|
|
||||||
"Flow used for authentication when the associated application is accessed by an "
|
|
||||||
"un-authenticated user."
|
|
||||||
),
|
|
||||||
related_name="provider_authentication",
|
|
||||||
)
|
|
||||||
|
|
||||||
authorization_flow = models.ForeignKey(
|
authorization_flow = models.ForeignKey(
|
||||||
"authentik_flows.Flow",
|
"authentik_flows.Flow",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
null=True,
|
|
||||||
help_text=_("Flow used when authorizing this provider."),
|
help_text=_("Flow used when authorizing this provider."),
|
||||||
related_name="provider_authorization",
|
related_name="provider_authorization",
|
||||||
)
|
)
|
||||||
@ -307,7 +289,7 @@ class Application(SerializerModel, PolicyBindingModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
meta_launch_url = models.TextField(
|
meta_launch_url = models.TextField(
|
||||||
default="", blank=True, validators=[DomainlessFormattedURLValidator()]
|
default="", blank=True, validators=[DomainlessURLValidator()]
|
||||||
)
|
)
|
||||||
|
|
||||||
open_in_new_tab = models.BooleanField(
|
open_in_new_tab = models.BooleanField(
|
||||||
@ -624,7 +606,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
|||||||
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
||||||
|
|
||||||
pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
pm_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
name = models.TextField(unique=True)
|
name = models.TextField()
|
||||||
expression = models.TextField()
|
expression = models.TextField()
|
||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
@ -647,7 +629,7 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
|||||||
try:
|
try:
|
||||||
return evaluator.evaluate(self.expression)
|
return evaluator.evaluate(self.expression)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise PropertyMappingExpressionException(exc) from exc
|
raise PropertyMappingExpressionException(str(exc)) from exc
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Property Mapping {self.name}"
|
return f"Property Mapping {self.name}"
|
||||||
|
@ -10,25 +10,25 @@ from django.db.models.signals import post_save, pre_delete
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.http.request import HttpRequest
|
from django.http.request import HttpRequest
|
||||||
|
|
||||||
from authentik.core.models import Application, AuthenticatedSession
|
|
||||||
|
|
||||||
# Arguments: user: User, password: str
|
# Arguments: user: User, password: str
|
||||||
password_changed = Signal()
|
password_changed = Signal()
|
||||||
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
|
# Arguments: credentials: dict[str, any], request: HttpRequest, stage: Stage
|
||||||
login_failed = Signal()
|
login_failed = Signal()
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.core.models import User
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=Application)
|
@receiver(post_save)
|
||||||
def post_save_application(sender: type[Model], instance, created: bool, **_):
|
def post_save_application(sender: type[Model], instance, created: bool, **_):
|
||||||
"""Clear user's application cache upon application creation"""
|
"""Clear user's application cache upon application creation"""
|
||||||
from authentik.core.api.applications import user_app_cache_key
|
from authentik.core.api.applications import user_app_cache_key
|
||||||
|
from authentik.core.models import Application
|
||||||
|
|
||||||
|
if sender != Application:
|
||||||
|
return
|
||||||
if not created: # pragma: no cover
|
if not created: # pragma: no cover
|
||||||
return
|
return
|
||||||
|
|
||||||
# Also delete user application cache
|
# Also delete user application cache
|
||||||
keys = cache.keys(user_app_cache_key("*"))
|
keys = cache.keys(user_app_cache_key("*"))
|
||||||
cache.delete_many(keys)
|
cache.delete_many(keys)
|
||||||
@ -37,6 +37,7 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
|
|||||||
@receiver(user_logged_in)
|
@receiver(user_logged_in)
|
||||||
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
|
def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
|
||||||
"""Create an AuthenticatedSession from request"""
|
"""Create an AuthenticatedSession from request"""
|
||||||
|
from authentik.core.models import AuthenticatedSession
|
||||||
|
|
||||||
session = AuthenticatedSession.from_request(request, user)
|
session = AuthenticatedSession.from_request(request, user)
|
||||||
if session:
|
if session:
|
||||||
@ -46,11 +47,18 @@ def user_logged_in_session(sender, request: HttpRequest, user: "User", **_):
|
|||||||
@receiver(user_logged_out)
|
@receiver(user_logged_out)
|
||||||
def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
|
def user_logged_out_session(sender, request: HttpRequest, user: "User", **_):
|
||||||
"""Delete AuthenticatedSession if it exists"""
|
"""Delete AuthenticatedSession if it exists"""
|
||||||
|
from authentik.core.models import AuthenticatedSession
|
||||||
|
|
||||||
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()
|
AuthenticatedSession.objects.filter(session_key=request.session.session_key).delete()
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
@receiver(pre_delete)
|
||||||
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_):
|
||||||
"""Delete session when authenticated session is deleted"""
|
"""Delete session when authenticated session is deleted"""
|
||||||
|
from authentik.core.models import AuthenticatedSession
|
||||||
|
|
||||||
|
if sender != AuthenticatedSession:
|
||||||
|
return
|
||||||
|
|
||||||
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
cache_key = f"{KEY_PREFIX}{instance.session_key}"
|
||||||
cache.delete(cache_key)
|
cache.delete(cache_key)
|
||||||
|
@ -25,8 +25,7 @@ from authentik.flows.planner import (
|
|||||||
)
|
)
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||||
from authentik.interfaces.models import InterfaceType
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
from authentik.interfaces.views import redirect_to_default_interface
|
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.denied import AccessDeniedResponse
|
from authentik.policies.denied import AccessDeniedResponse
|
||||||
from authentik.policies.utils import delete_none_keys
|
from authentik.policies.utils import delete_none_keys
|
||||||
@ -227,7 +226,7 @@ class SourceFlowManager:
|
|||||||
# Ensure redirect is carried through when user was trying to
|
# Ensure redirect is carried through when user was trying to
|
||||||
# authorize application
|
# authorize application
|
||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:root-redirect"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
@ -254,9 +253,9 @@ class SourceFlowManager:
|
|||||||
for stage in stages:
|
for stage in stages:
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
self.request.session[SESSION_KEY_PLAN] = plan
|
self.request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_to_default_interface(
|
return redirect_with_qs(
|
||||||
self.request,
|
"authentik_core:if-flow",
|
||||||
InterfaceType.FLOW,
|
self.request.GET,
|
||||||
flow_slug=flow.slug,
|
flow_slug=flow.slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -300,9 +299,8 @@ class SourceFlowManager:
|
|||||||
_("Successfully linked %(source)s!" % {"source": self.source.name}),
|
_("Successfully linked %(source)s!" % {"source": self.source.name}),
|
||||||
)
|
)
|
||||||
return redirect(
|
return redirect(
|
||||||
# Not ideal that we don't directly redirect to the configured user interface
|
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_core:root-redirect",
|
"authentik_core:if-user",
|
||||||
)
|
)
|
||||||
+ f"#/settings;page-{self.source.slug}"
|
+ f"#/settings;page-{self.source.slug}"
|
||||||
)
|
)
|
||||||
|
@ -9,13 +9,15 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
||||||
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}">
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
<script src="{% static 'dist/poly.js' %}" type="module"></script>
|
||||||
<script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script>
|
|
||||||
<script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% extends "base/skeleton.html" %}
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
|
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
|
||||||
@ -14,6 +15,19 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
<ak-interface-admin>
|
<ak-interface-admin>
|
||||||
<ak-loading></ak-loading>
|
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||||
|
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||||
|
<div class="pf-c-empty-state__content">
|
||||||
|
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">
|
||||||
|
{% trans "Loading..." %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</ak-interface-admin>
|
</ak-interface-admin>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% extends "base/skeleton.html" %}
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@ -30,6 +31,19 @@ window.authentik.flow = {
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
<ak-flow-executor>
|
<ak-flow-executor>
|
||||||
<ak-loading></ak-loading>
|
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||||
|
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||||
|
<div class="pf-c-empty-state__content">
|
||||||
|
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">
|
||||||
|
{% trans "Loading..." %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</ak-flow-executor>
|
</ak-flow-executor>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% extends "base/skeleton.html" %}
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
|
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
|
||||||
@ -14,6 +15,19 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
<ak-interface-user>
|
<ak-interface-user>
|
||||||
<ak-loading></ak-loading>
|
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||||
|
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||||
|
<div class="pf-c-empty-state__content">
|
||||||
|
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">
|
||||||
|
{% trans "Loading..." %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</ak-interface-user>
|
</ak-interface-user>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -37,22 +37,6 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
order=0,
|
order=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_formatted_launch_url(self):
|
|
||||||
"""Test formatted launch URL"""
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
self.assertEqual(
|
|
||||||
self.client.patch(
|
|
||||||
reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}),
|
|
||||||
{"meta_launch_url": "https://%(username)s-test.test.goauthentik.io/%(username)s"},
|
|
||||||
).status_code,
|
|
||||||
200,
|
|
||||||
)
|
|
||||||
self.allowed.refresh_from_db()
|
|
||||||
self.assertEqual(
|
|
||||||
self.allowed.get_launch_url(self.user),
|
|
||||||
f"https://{self.user.username}-test.test.goauthentik.io/{self.user.username}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_set_icon(self):
|
def test_set_icon(self):
|
||||||
"""Test set_icon"""
|
"""Test set_icon"""
|
||||||
file = ContentFile(b"text", "name")
|
file = ContentFile(b"text", "name")
|
||||||
@ -129,7 +113,6 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
"provider_obj": {
|
"provider_obj": {
|
||||||
"assigned_application_name": "allowed",
|
"assigned_application_name": "allowed",
|
||||||
"assigned_application_slug": "allowed",
|
"assigned_application_slug": "allowed",
|
||||||
"authentication_flow": None,
|
|
||||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||||
"component": "ak-provider-oauth2-form",
|
"component": "ak-provider-oauth2-form",
|
||||||
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
||||||
@ -179,7 +162,6 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
"provider_obj": {
|
"provider_obj": {
|
||||||
"assigned_application_name": "allowed",
|
"assigned_application_name": "allowed",
|
||||||
"assigned_application_slug": "allowed",
|
"assigned_application_slug": "allowed",
|
||||||
"authentication_flow": None,
|
|
||||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||||
"component": "ak-provider-oauth2-form",
|
"component": "ak-provider-oauth2-form",
|
||||||
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
||||||
|
@ -59,6 +59,4 @@ class TestImpersonation(TestCase):
|
|||||||
self.client.force_login(self.other_user)
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
||||||
self.assertRedirects(
|
self.assertRedirects(response, reverse("authentik_core:if-user"))
|
||||||
response, reverse("authentik_interfaces:if", kwargs={"if_name": "user"})
|
|
||||||
)
|
|
||||||
|
@ -4,10 +4,7 @@ from guardian.shortcuts import get_anonymous_user
|
|||||||
|
|
||||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||||
from authentik.core.models import PropertyMapping
|
from authentik.core.models import PropertyMapping
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
|
||||||
|
|
||||||
|
|
||||||
class TestPropertyMappings(TestCase):
|
class TestPropertyMappings(TestCase):
|
||||||
@ -15,24 +12,23 @@ class TestPropertyMappings(TestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = create_test_admin_user()
|
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
def test_expression(self):
|
def test_expression(self):
|
||||||
"""Test expression"""
|
"""Test expression"""
|
||||||
mapping = PropertyMapping.objects.create(name=generate_id(), expression="return 'test'")
|
mapping = PropertyMapping.objects.create(name="test", expression="return 'test'")
|
||||||
self.assertEqual(mapping.evaluate(None, None), "test")
|
self.assertEqual(mapping.evaluate(None, None), "test")
|
||||||
|
|
||||||
def test_expression_syntax(self):
|
def test_expression_syntax(self):
|
||||||
"""Test expression syntax error"""
|
"""Test expression syntax error"""
|
||||||
mapping = PropertyMapping.objects.create(name=generate_id(), expression="-")
|
mapping = PropertyMapping.objects.create(name="test", expression="-")
|
||||||
with self.assertRaises(PropertyMappingExpressionException):
|
with self.assertRaises(PropertyMappingExpressionException):
|
||||||
mapping.evaluate(None, None)
|
mapping.evaluate(None, None)
|
||||||
|
|
||||||
def test_expression_error_general(self):
|
def test_expression_error_general(self):
|
||||||
"""Test expression error"""
|
"""Test expression error"""
|
||||||
expr = "return aaa"
|
expr = "return aaa"
|
||||||
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr)
|
mapping = PropertyMapping.objects.create(name="test", expression=expr)
|
||||||
with self.assertRaises(PropertyMappingExpressionException):
|
with self.assertRaises(PropertyMappingExpressionException):
|
||||||
mapping.evaluate(None, None)
|
mapping.evaluate(None, None)
|
||||||
events = Event.objects.filter(
|
events = Event.objects.filter(
|
||||||
@ -45,7 +41,7 @@ class TestPropertyMappings(TestCase):
|
|||||||
"""Test expression error (with user and http request"""
|
"""Test expression error (with user and http request"""
|
||||||
expr = "return aaa"
|
expr = "return aaa"
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr)
|
mapping = PropertyMapping.objects.create(name="test", expression=expr)
|
||||||
with self.assertRaises(PropertyMappingExpressionException):
|
with self.assertRaises(PropertyMappingExpressionException):
|
||||||
mapping.evaluate(get_anonymous_user(), request)
|
mapping.evaluate(get_anonymous_user(), request)
|
||||||
events = Event.objects.filter(
|
events = Event.objects.filter(
|
||||||
@ -56,23 +52,3 @@ class TestPropertyMappings(TestCase):
|
|||||||
event = events.first()
|
event = events.first()
|
||||||
self.assertEqual(event.user["username"], "AnonymousUser")
|
self.assertEqual(event.user["username"], "AnonymousUser")
|
||||||
self.assertEqual(event.client_ip, "127.0.0.1")
|
self.assertEqual(event.client_ip, "127.0.0.1")
|
||||||
|
|
||||||
def test_call_policy(self):
|
|
||||||
"""test ak_call_policy"""
|
|
||||||
expr = ExpressionPolicy.objects.create(
|
|
||||||
name=generate_id(),
|
|
||||||
execution_logging=True,
|
|
||||||
expression="return request.http_request.path",
|
|
||||||
)
|
|
||||||
http_request = self.factory.get("/")
|
|
||||||
tmpl = (
|
|
||||||
"""
|
|
||||||
res = ak_call_policy('%s')
|
|
||||||
result = [request.http_request.path, res.raw_result]
|
|
||||||
return result
|
|
||||||
"""
|
|
||||||
% expr.name
|
|
||||||
)
|
|
||||||
evaluator = PropertyMapping(expression=tmpl, name=generate_id())
|
|
||||||
res = evaluator.evaluate(self.user, http_request)
|
|
||||||
self.assertEqual(res, ["/", "/"])
|
|
||||||
|
@ -5,7 +5,6 @@ from django.urls.base import reverse
|
|||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.api.tokens import TokenSerializer
|
|
||||||
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
|
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
@ -100,16 +99,3 @@ class TestTokenAPI(APITestCase):
|
|||||||
self.assertEqual(len(body["results"]), 2)
|
self.assertEqual(len(body["results"]), 2)
|
||||||
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
||||||
self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier)
|
self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier)
|
||||||
|
|
||||||
def test_serializer_no_request(self):
|
|
||||||
"""Test serializer without request"""
|
|
||||||
self.assertTrue(
|
|
||||||
TokenSerializer(
|
|
||||||
data={
|
|
||||||
"identifier": generate_id(),
|
|
||||||
"intent": TokenIntents.INTENT_APP_PASSWORD,
|
|
||||||
"key": generate_id(),
|
|
||||||
"user": self.user.pk,
|
|
||||||
}
|
|
||||||
).is_valid(raise_exception=True)
|
|
||||||
)
|
|
||||||
|
@ -1,19 +1,11 @@
|
|||||||
"""Test Users API"""
|
"""Test Users API"""
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import (
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
USER_ATTRIBUTE_SA,
|
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
|
||||||
AuthenticatedSession,
|
|
||||||
Token,
|
|
||||||
User,
|
|
||||||
)
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
||||||
from authentik.flows.models import FlowDesignation
|
from authentik.flows.models import FlowDesignation
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
@ -138,71 +130,7 @@ class TestUsersAPI(APITestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||||
user_filter = User.objects.filter(
|
|
||||||
username="test-sa",
|
|
||||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
|
|
||||||
)
|
|
||||||
self.assertTrue(user_filter.exists())
|
|
||||||
user: User = user_filter.first()
|
|
||||||
self.assertFalse(user.has_usable_password())
|
|
||||||
|
|
||||||
token_filter = Token.objects.filter(user=user)
|
|
||||||
self.assertTrue(token_filter.exists())
|
|
||||||
self.assertTrue(token_filter.first().expiring)
|
|
||||||
|
|
||||||
def test_service_account_no_expire(self):
|
|
||||||
"""Service account creation without token expiration"""
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("authentik_api:user-service-account"),
|
|
||||||
data={
|
|
||||||
"name": "test-sa",
|
|
||||||
"create_group": True,
|
|
||||||
"expiring": False,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
user_filter = User.objects.filter(
|
|
||||||
username="test-sa",
|
|
||||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: False, USER_ATTRIBUTE_SA: True},
|
|
||||||
)
|
|
||||||
self.assertTrue(user_filter.exists())
|
|
||||||
user: User = user_filter.first()
|
|
||||||
self.assertFalse(user.has_usable_password())
|
|
||||||
|
|
||||||
token_filter = Token.objects.filter(user=user)
|
|
||||||
self.assertTrue(token_filter.exists())
|
|
||||||
self.assertFalse(token_filter.first().expiring)
|
|
||||||
|
|
||||||
def test_service_account_with_custom_expire(self):
|
|
||||||
"""Service account creation with custom token expiration date"""
|
|
||||||
self.client.force_login(self.admin)
|
|
||||||
expire_on = datetime(2050, 11, 11, 11, 11, 11).astimezone()
|
|
||||||
response = self.client.post(
|
|
||||||
reverse("authentik_api:user-service-account"),
|
|
||||||
data={
|
|
||||||
"name": "test-sa",
|
|
||||||
"create_group": True,
|
|
||||||
"expires": expire_on.isoformat(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
user_filter = User.objects.filter(
|
|
||||||
username="test-sa",
|
|
||||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
|
|
||||||
)
|
|
||||||
self.assertTrue(user_filter.exists())
|
|
||||||
user: User = user_filter.first()
|
|
||||||
self.assertFalse(user.has_usable_password())
|
|
||||||
|
|
||||||
token_filter = Token.objects.filter(user=user)
|
|
||||||
self.assertTrue(token_filter.exists())
|
|
||||||
token = token_filter.first()
|
|
||||||
self.assertTrue(token.expiring)
|
|
||||||
self.assertEqual(token.expires, expire_on)
|
|
||||||
|
|
||||||
def test_service_account_invalid(self):
|
def test_service_account_invalid(self):
|
||||||
"""Service account creation (twice with same name, expect error)"""
|
"""Service account creation (twice with same name, expect error)"""
|
||||||
@ -215,19 +143,7 @@ class TestUsersAPI(APITestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||||
user_filter = User.objects.filter(
|
|
||||||
username="test-sa",
|
|
||||||
attributes={USER_ATTRIBUTE_TOKEN_EXPIRING: True, USER_ATTRIBUTE_SA: True},
|
|
||||||
)
|
|
||||||
self.assertTrue(user_filter.exists())
|
|
||||||
user: User = user_filter.first()
|
|
||||||
self.assertFalse(user.has_usable_password())
|
|
||||||
|
|
||||||
token_filter = Token.objects.filter(user=user)
|
|
||||||
self.assertTrue(token_filter.exists())
|
|
||||||
self.assertTrue(token_filter.first().expiring)
|
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse("authentik_api:user-service-account"),
|
reverse("authentik_api:user-service-account"),
|
||||||
data={
|
data={
|
||||||
|
@ -27,6 +27,6 @@ class UserSettingSerializer(PassiveSerializer):
|
|||||||
|
|
||||||
object_uid = CharField()
|
object_uid = CharField()
|
||||||
component = CharField()
|
component = CharField()
|
||||||
title = CharField(required=True)
|
title = CharField()
|
||||||
configure_url = CharField(required=False)
|
configure_url = CharField(required=False)
|
||||||
icon_url = CharField(required=False)
|
icon_url = CharField(required=False)
|
||||||
|
@ -1,32 +1,21 @@
|
|||||||
"""authentik URL Configuration"""
|
"""authentik URL Configuration"""
|
||||||
from channels.auth import AuthMiddleware
|
|
||||||
from channels.sessions import CookieMiddleware
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
|
from django.views.generic import RedirectView
|
||||||
|
|
||||||
from authentik.core.views import apps, impersonate
|
from authentik.core.views import apps, impersonate
|
||||||
from authentik.core.views.debug import AccessDeniedView
|
from authentik.core.views.debug import AccessDeniedView
|
||||||
|
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
|
||||||
from authentik.core.views.session import EndSessionView
|
from authentik.core.views.session import EndSessionView
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.views import RedirectToInterface
|
|
||||||
from authentik.root.asgi_middleware import SessionMiddleware
|
|
||||||
from authentik.root.messages.consumer import MessageConsumer
|
|
||||||
|
|
||||||
|
|
||||||
def placeholder_view(request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
"""Empty view used as placeholder
|
|
||||||
|
|
||||||
(Mounted to websocket endpoints and used by e2e tests)"""
|
|
||||||
return HttpResponse(status_code=200)
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"",
|
"",
|
||||||
login_required(RedirectToInterface.as_view(type=InterfaceType.USER)),
|
login_required(
|
||||||
|
RedirectView.as_view(pattern_name="authentik_core:if-user", query_string=True)
|
||||||
|
),
|
||||||
name="root-redirect",
|
name="root-redirect",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
@ -47,22 +36,31 @@ urlpatterns = [
|
|||||||
name="impersonate-end",
|
name="impersonate-end",
|
||||||
),
|
),
|
||||||
# Interfaces
|
# Interfaces
|
||||||
|
path(
|
||||||
|
"if/admin/",
|
||||||
|
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/admin.html")),
|
||||||
|
name="if-admin",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"if/user/",
|
||||||
|
ensure_csrf_cookie(InterfaceView.as_view(template_name="if/user.html")),
|
||||||
|
name="if-user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"if/flow/<slug:flow_slug>/",
|
||||||
|
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
||||||
|
name="if-flow",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"if/session-end/<slug:application_slug>/",
|
"if/session-end/<slug:application_slug>/",
|
||||||
ensure_csrf_cookie(EndSessionView.as_view()),
|
ensure_csrf_cookie(EndSessionView.as_view()),
|
||||||
name="if-session-end",
|
name="if-session-end",
|
||||||
),
|
),
|
||||||
# Fallback for WS
|
# Fallback for WS
|
||||||
path("ws/outpost/<uuid:pk>/", placeholder_view),
|
path("ws/outpost/<uuid:pk>/", InterfaceView.as_view(template_name="if/admin.html")),
|
||||||
path(
|
path(
|
||||||
"ws/client/",
|
"ws/client/",
|
||||||
placeholder_view,
|
InterfaceView.as_view(template_name="if/admin.html"),
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
websocket_urlpatterns = [
|
|
||||||
path(
|
|
||||||
"ws/client/", CookieMiddleware(SessionMiddleware(AuthMiddleware(MessageConsumer.as_asgi())))
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -11,22 +11,16 @@ from authentik.flows.challenge import (
|
|||||||
HttpChallengeResponse,
|
HttpChallengeResponse,
|
||||||
RedirectChallenge,
|
RedirectChallenge,
|
||||||
)
|
)
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.models import in_memory_stage
|
||||||
from authentik.flows.models import FlowDesignation, in_memory_stage
|
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import (
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
SESSION_KEY_APPLICATION_PRE,
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
SESSION_KEY_PLAN,
|
|
||||||
ToDefaultFlow,
|
|
||||||
)
|
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.views import redirect_to_default_interface
|
|
||||||
from authentik.stages.consent.stage import (
|
from authentik.stages.consent.stage import (
|
||||||
PLAN_CONTEXT_CONSENT_HEADER,
|
PLAN_CONTEXT_CONSENT_HEADER,
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
PLAN_CONTEXT_CONSENT_PERMISSIONS,
|
||||||
)
|
)
|
||||||
from authentik.tenants.utils import get_tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
|
|
||||||
class RedirectToAppLaunch(View):
|
class RedirectToAppLaunch(View):
|
||||||
@ -41,27 +35,24 @@ class RedirectToAppLaunch(View):
|
|||||||
# Check if we're authenticated already, saves us the flow run
|
# Check if we're authenticated already, saves us the flow run
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
return HttpResponseRedirect(app.get_launch_url(request.user))
|
return HttpResponseRedirect(app.get_launch_url(request.user))
|
||||||
self.request.session[SESSION_KEY_APPLICATION_PRE] = app
|
|
||||||
# otherwise, do a custom flow plan that includes the application that's
|
# otherwise, do a custom flow plan that includes the application that's
|
||||||
# being accessed, to improve usability
|
# being accessed, to improve usability
|
||||||
flow = ToDefaultFlow(request=request, designation=FlowDesignation.AUTHENTICATION).get_flow()
|
tenant: Tenant = request.tenant
|
||||||
|
flow = tenant.flow_authentication
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
try:
|
plan = planner.plan(
|
||||||
plan = planner.plan(
|
request,
|
||||||
request,
|
{
|
||||||
{
|
PLAN_CONTEXT_APPLICATION: app,
|
||||||
PLAN_CONTEXT_APPLICATION: app,
|
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
||||||
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
|
% {"application": app.name},
|
||||||
% {"application": app.name},
|
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS: [],
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
except FlowNonApplicableException:
|
|
||||||
raise Http404
|
|
||||||
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
plan.insert_stage(in_memory_stage(RedirectToAppStage))
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_to_default_interface(request, InterfaceType.FLOW, flow_slug=flow.slug)
|
return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug)
|
||||||
|
|
||||||
|
|
||||||
class RedirectToAppStage(ChallengeStageView):
|
class RedirectToAppStage(ChallengeStageView):
|
||||||
|
@ -35,7 +35,7 @@ class ImpersonateInitView(View):
|
|||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||||
|
|
||||||
return redirect("authentik_core:root-redirect")
|
return redirect("authentik_core:if-user")
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateEndView(View):
|
class ImpersonateEndView(View):
|
||||||
@ -48,7 +48,7 @@ class ImpersonateEndView(View):
|
|||||||
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||||
):
|
):
|
||||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||||
return redirect("authentik_core:root-redirect")
|
return redirect("authentik_core:if-user")
|
||||||
|
|
||||||
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
|
36
authentik/core/views/interface.py
Normal file
36
authentik/core/views/interface.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""Interface views"""
|
||||||
|
from json import dumps
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.views.generic.base import TemplateView
|
||||||
|
from rest_framework.request import Request
|
||||||
|
|
||||||
|
from authentik import get_build_hash
|
||||||
|
from authentik.admin.tasks import LOCAL_VERSION
|
||||||
|
from authentik.api.v3.config import ConfigView
|
||||||
|
from authentik.flows.models import Flow
|
||||||
|
from authentik.tenants.api import CurrentTenantSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class InterfaceView(TemplateView):
|
||||||
|
"""Base interface view"""
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
kwargs["config_json"] = dumps(ConfigView(request=Request(self.request)).get_config().data)
|
||||||
|
kwargs["tenant_json"] = dumps(CurrentTenantSerializer(self.request.tenant).data)
|
||||||
|
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
|
||||||
|
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
|
||||||
|
kwargs["build"] = get_build_hash()
|
||||||
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class FlowInterfaceView(InterfaceView):
|
||||||
|
"""Flow interface"""
|
||||||
|
|
||||||
|
template_name = "if/flow.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
||||||
|
kwargs["inspector"] = "inspector" in self.request.GET
|
||||||
|
return super().get_context_data(**kwargs)
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
The authentik Enterprise Edition (EE) license (the “EE License”)
|
|
||||||
Copyright (c) 2022-present Authentik Security Inc.
|
|
||||||
|
|
||||||
With regard to the authentik Software:
|
|
||||||
|
|
||||||
This software and associated documentation files (the "Software") may only be
|
|
||||||
used in production, if you (and any entity that you represent) have agreed to,
|
|
||||||
and are in compliance with, the Authentik Subscription Terms of Service, available
|
|
||||||
at https://goauthentik.io/legal/terms (the "EE Terms"), or other
|
|
||||||
agreement governing the use of the Software, as agreed by you and authentik Security Inc,
|
|
||||||
and otherwise have a valid authentik Enterprise Edition subscription for the
|
|
||||||
correct number of user seats. Subject to the foregoing sentence, you are free to
|
|
||||||
modify this Software and publish patches to the Software. You agree that Authentik
|
|
||||||
Security Inc. and/or its licensors (as applicable) retain all right, title and interest
|
|
||||||
in and to all such modifications and/or patches, and all such modifications and/or
|
|
||||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
|
||||||
exploited with a valid authentik Enterprise Edition subscription for the correct
|
|
||||||
number of user seats. Notwithstanding the foregoing, you may copy and modify
|
|
||||||
the Software for development and testing purposes, without requiring a
|
|
||||||
subscription. You agree that Authentik Security Inc. and/or its
|
|
||||||
licensors (as applicable) retain all right, title and interest in
|
|
||||||
and to all such modifications. You are not granted any other rights
|
|
||||||
beyond what is expressly stated herein. Subject to the
|
|
||||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
|
||||||
and/or sell the Software.
|
|
||||||
|
|
||||||
This EE License applies only to the part of this Software that is not
|
|
||||||
distributed as part of authentik Open Source (OSS). Any part of this Software
|
|
||||||
distributed as part of authentik OSS or is served client-side as an image, font,
|
|
||||||
cascading stylesheet (CSS), file which produces or is compiled, arranged,
|
|
||||||
augmented, or combined into client-side JavaScript, in whole or in part, is
|
|
||||||
copyrighted under the MIT license. The full text of this EE License shall
|
|
||||||
be included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
For all third party components incorporated into the authentik Software, those
|
|
||||||
components are licensed under the original license provided by the owner of the
|
|
||||||
applicable component.
|
|
@ -1,11 +0,0 @@
|
|||||||
"""Enterprise app config"""
|
|
||||||
from authentik.blueprints.apps import ManagedAppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AuthentikEnterpriseConfig(ManagedAppConfig):
|
|
||||||
"""Enterprise app config"""
|
|
||||||
|
|
||||||
name = "authentik.enterprise"
|
|
||||||
label = "authentik_enterprise"
|
|
||||||
verbose_name = "authentik Enterprise"
|
|
||||||
default = True
|
|
@ -1 +0,0 @@
|
|||||||
"""Enterprise additional settings"""
|
|
@ -7,14 +7,13 @@ from django.conf import settings
|
|||||||
from django.contrib.sessions.models import Session
|
from django.contrib.sessions.models import Session
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import m2m_changed, post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django_otp.plugins.otp_static.models import StaticToken
|
from django_otp.plugins.otp_static.models import StaticToken
|
||||||
from guardian.models import UserObjectPermission
|
from guardian.models import UserObjectPermission
|
||||||
|
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
AuthenticatedSession,
|
AuthenticatedSession,
|
||||||
Group,
|
|
||||||
PropertyMapping,
|
PropertyMapping,
|
||||||
Provider,
|
Provider,
|
||||||
Source,
|
Source,
|
||||||
@ -29,7 +28,6 @@ from authentik.lib.utils.errors import exception_to_string
|
|||||||
from authentik.outposts.models import OutpostServiceConnection
|
from authentik.outposts.models import OutpostServiceConnection
|
||||||
from authentik.policies.models import Policy, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBindingModel
|
||||||
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
|
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken
|
||||||
from authentik.providers.scim.models import SCIMGroup, SCIMUser
|
|
||||||
|
|
||||||
IGNORED_MODELS = (
|
IGNORED_MODELS = (
|
||||||
Event,
|
Event,
|
||||||
@ -50,8 +48,6 @@ IGNORED_MODELS = (
|
|||||||
AuthorizationCode,
|
AuthorizationCode,
|
||||||
AccessToken,
|
AccessToken,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
SCIMUser,
|
|
||||||
SCIMGroup,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -62,13 +58,6 @@ def should_log_model(model: Model) -> bool:
|
|||||||
return model.__class__ not in IGNORED_MODELS
|
return model.__class__ not in IGNORED_MODELS
|
||||||
|
|
||||||
|
|
||||||
def should_log_m2m(model: Model) -> bool:
|
|
||||||
"""Return true if m2m operation should be logged"""
|
|
||||||
if model.__class__ in [User, Group]:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class EventNewThread(Thread):
|
class EventNewThread(Thread):
|
||||||
"""Create Event in background thread"""
|
"""Create Event in background thread"""
|
||||||
|
|
||||||
@ -107,7 +96,6 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
|
post_save_handler = partial(self.post_save_handler, user=request.user, request=request)
|
||||||
pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
|
pre_delete_handler = partial(self.pre_delete_handler, user=request.user, request=request)
|
||||||
m2m_changed_handler = partial(self.m2m_changed_handler, user=request.user, request=request)
|
|
||||||
post_save.connect(
|
post_save.connect(
|
||||||
post_save_handler,
|
post_save_handler,
|
||||||
dispatch_uid=request.request_id,
|
dispatch_uid=request.request_id,
|
||||||
@ -118,11 +106,6 @@ class AuditMiddleware:
|
|||||||
dispatch_uid=request.request_id,
|
dispatch_uid=request.request_id,
|
||||||
weak=False,
|
weak=False,
|
||||||
)
|
)
|
||||||
m2m_changed.connect(
|
|
||||||
m2m_changed_handler,
|
|
||||||
dispatch_uid=request.request_id,
|
|
||||||
weak=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
def disconnect(self, request: HttpRequest):
|
def disconnect(self, request: HttpRequest):
|
||||||
"""Disconnect signals"""
|
"""Disconnect signals"""
|
||||||
@ -130,7 +113,6 @@ class AuditMiddleware:
|
|||||||
return
|
return
|
||||||
post_save.disconnect(dispatch_uid=request.request_id)
|
post_save.disconnect(dispatch_uid=request.request_id)
|
||||||
pre_delete.disconnect(dispatch_uid=request.request_id)
|
pre_delete.disconnect(dispatch_uid=request.request_id)
|
||||||
m2m_changed.disconnect(dispatch_uid=request.request_id)
|
|
||||||
|
|
||||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
self.connect(request)
|
self.connect(request)
|
||||||
@ -185,20 +167,3 @@ class AuditMiddleware:
|
|||||||
user=user,
|
user=user,
|
||||||
model=model_to_dict(instance),
|
model=model_to_dict(instance),
|
||||||
).run()
|
).run()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def m2m_changed_handler(
|
|
||||||
user: User, request: HttpRequest, sender, instance: Model, action: str, **_
|
|
||||||
):
|
|
||||||
"""Signal handler for all object's m2m_changed"""
|
|
||||||
if action not in ["pre_add", "pre_remove", "post_clear"]:
|
|
||||||
return
|
|
||||||
if not should_log_m2m(instance):
|
|
||||||
return
|
|
||||||
|
|
||||||
EventNewThread(
|
|
||||||
EventAction.MODEL_UPDATED,
|
|
||||||
request,
|
|
||||||
user=user,
|
|
||||||
model=model_to_dict(instance),
|
|
||||||
).run()
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.apps.registry import Apps
|
from django.apps.registry import Apps
|
||||||
@ -11,7 +12,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|||||||
|
|
||||||
import authentik.events.models
|
import authentik.events.models
|
||||||
import authentik.lib.models
|
import authentik.lib.models
|
||||||
from authentik.lib.migrations import progress_bar
|
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
|
||||||
|
|
||||||
|
|
||||||
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
@ -42,6 +43,49 @@ def token_view_to_secret_view(apps: Apps, schema_editor: BaseDatabaseSchemaEdito
|
|||||||
Event.objects.using(db_alias).bulk_update(events, ["context", "action"])
|
Event.objects.using(db_alias).bulk_update(events, ["context", "action"])
|
||||||
|
|
||||||
|
|
||||||
|
# Taken from https://stackoverflow.com/questions/3173320/text-progress-bar-in-the-console
|
||||||
|
def progress_bar(
|
||||||
|
iterable: Iterable,
|
||||||
|
prefix="Writing: ",
|
||||||
|
suffix=" finished",
|
||||||
|
decimals=1,
|
||||||
|
length=100,
|
||||||
|
fill="█",
|
||||||
|
print_end="\r",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Call in a loop to create terminal progress bar
|
||||||
|
@params:
|
||||||
|
iteration - Required : current iteration (Int)
|
||||||
|
total - Required : total iterations (Int)
|
||||||
|
prefix - Optional : prefix string (Str)
|
||||||
|
suffix - Optional : suffix string (Str)
|
||||||
|
decimals - Optional : positive number of decimals in percent complete (Int)
|
||||||
|
length - Optional : character length of bar (Int)
|
||||||
|
fill - Optional : bar fill character (Str)
|
||||||
|
print_end - Optional : end character (e.g. "\r", "\r\n") (Str)
|
||||||
|
"""
|
||||||
|
total = len(iterable)
|
||||||
|
if total < 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
def print_progress_bar(iteration):
|
||||||
|
"""Progress Bar Printing Function"""
|
||||||
|
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
|
||||||
|
filledLength = int(length * iteration // total)
|
||||||
|
bar = fill * filledLength + "-" * (length - filledLength)
|
||||||
|
print(f"\r{prefix} |{bar}| {percent}% {suffix}", end=print_end)
|
||||||
|
|
||||||
|
# Initial Call
|
||||||
|
print_progress_bar(0)
|
||||||
|
# Update Progress Bar
|
||||||
|
for i, item in enumerate(iterable):
|
||||||
|
yield item
|
||||||
|
print_progress_bar(i + 1)
|
||||||
|
# Print New Line on Complete
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def update_expires(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
db_alias = schema_editor.connection.alias
|
db_alias = schema_editor.connection.alias
|
||||||
Event = apps.get_model("authentik_events", "event")
|
Event = apps.get_model("authentik_events", "event")
|
||||||
|
@ -41,7 +41,8 @@ from authentik.lib.utils.http import get_client_ip, get_http_session
|
|||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
from authentik.tenants.utils import get_fallback_tenant, get_tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
from authentik.tenants.utils import DEFAULT_TENANT
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -56,7 +57,7 @@ def default_event_duration():
|
|||||||
|
|
||||||
def default_tenant():
|
def default_tenant():
|
||||||
"""Get a default value for tenant"""
|
"""Get a default value for tenant"""
|
||||||
return sanitize_dict(model_to_dict(get_fallback_tenant()))
|
return sanitize_dict(model_to_dict(DEFAULT_TENANT))
|
||||||
|
|
||||||
|
|
||||||
class NotificationTransportError(SentryIgnoredException):
|
class NotificationTransportError(SentryIgnoredException):
|
||||||
@ -213,20 +214,13 @@ class Event(SerializerModel, ExpiringModel):
|
|||||||
Events independently from requests.
|
Events independently from requests.
|
||||||
`user` arguments optionally overrides user from requests."""
|
`user` arguments optionally overrides user from requests."""
|
||||||
if request:
|
if request:
|
||||||
from authentik.flows.views.executor import QS_QUERY
|
|
||||||
|
|
||||||
self.context["http_request"] = {
|
self.context["http_request"] = {
|
||||||
"path": request.path,
|
"path": request.path,
|
||||||
"method": request.method,
|
"method": request.method,
|
||||||
"args": QueryDict(request.META.get("QUERY_STRING", "")),
|
"args": QueryDict(request.META.get("QUERY_STRING", "")),
|
||||||
}
|
}
|
||||||
# Special case for events created during flow execution
|
|
||||||
# since they keep the http query within a wrapped query
|
|
||||||
if QS_QUERY in self.context["http_request"]["args"]:
|
|
||||||
wrapped = self.context["http_request"]["args"][QS_QUERY]
|
|
||||||
self.context["http_request"]["args"] = QueryDict(wrapped)
|
|
||||||
if hasattr(request, "tenant"):
|
if hasattr(request, "tenant"):
|
||||||
tenant = get_tenant(request)
|
tenant: Tenant = request.tenant
|
||||||
# Because self.created only gets set on save, we can't use it's value here
|
# Because self.created only gets set on save, we can't use it's value here
|
||||||
# hence we set self.created to now and then use it
|
# hence we set self.created to now and then use it
|
||||||
self.created = now()
|
self.created = now()
|
||||||
|
@ -41,7 +41,7 @@ class TaskResult:
|
|||||||
|
|
||||||
def with_error(self, exc: Exception) -> "TaskResult":
|
def with_error(self, exc: Exception) -> "TaskResult":
|
||||||
"""Since errors might not always be pickle-able, set the traceback"""
|
"""Since errors might not always be pickle-able, set the traceback"""
|
||||||
self.messages.append(exception_to_string(exc))
|
self.messages.append(str(exc))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
@ -111,7 +111,6 @@ class MonitoredTask(Task):
|
|||||||
_result: Optional[TaskResult]
|
_result: Optional[TaskResult]
|
||||||
|
|
||||||
_uid: Optional[str]
|
_uid: Optional[str]
|
||||||
start: Optional[float] = None
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
@ -119,6 +118,7 @@ class MonitoredTask(Task):
|
|||||||
self._uid = None
|
self._uid = None
|
||||||
self._result = None
|
self._result = None
|
||||||
self.result_timeout_hours = 6
|
self.result_timeout_hours = 6
|
||||||
|
self.start = default_timer()
|
||||||
|
|
||||||
def set_uid(self, uid: str):
|
def set_uid(self, uid: str):
|
||||||
"""Set UID, so in the case of an unexpected error its saved correctly"""
|
"""Set UID, so in the case of an unexpected error its saved correctly"""
|
||||||
@ -128,10 +128,6 @@ class MonitoredTask(Task):
|
|||||||
"""Set result for current run, will overwrite previous result."""
|
"""Set result for current run, will overwrite previous result."""
|
||||||
self._result = result
|
self._result = result
|
||||||
|
|
||||||
def before_start(self, task_id, args, kwargs):
|
|
||||||
self.start = default_timer()
|
|
||||||
return super().before_start(task_id, args, kwargs)
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def after_return(self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo):
|
def after_return(self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo):
|
||||||
super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
|
super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
|
||||||
@ -142,7 +138,7 @@ class MonitoredTask(Task):
|
|||||||
info = TaskInfo(
|
info = TaskInfo(
|
||||||
task_name=self.__name__,
|
task_name=self.__name__,
|
||||||
task_description=self.__doc__,
|
task_description=self.__doc__,
|
||||||
start_timestamp=self.start or default_timer(),
|
start_timestamp=self.start,
|
||||||
finish_timestamp=default_timer(),
|
finish_timestamp=default_timer(),
|
||||||
finish_time=datetime.now(),
|
finish_time=datetime.now(),
|
||||||
result=self._result,
|
result=self._result,
|
||||||
@ -166,7 +162,7 @@ class MonitoredTask(Task):
|
|||||||
TaskInfo(
|
TaskInfo(
|
||||||
task_name=self.__name__,
|
task_name=self.__name__,
|
||||||
task_description=self.__doc__,
|
task_description=self.__doc__,
|
||||||
start_timestamp=self.start or default_timer(),
|
start_timestamp=self.start,
|
||||||
finish_timestamp=default_timer(),
|
finish_timestamp=default_timer(),
|
||||||
finish_time=datetime.now(),
|
finish_time=datetime.now(),
|
||||||
result=self._result,
|
result=self._result,
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
"""Flow Binding API Views"""
|
"""Flow Binding API Views"""
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
@ -15,13 +12,6 @@ class FlowStageBindingSerializer(ModelSerializer):
|
|||||||
|
|
||||||
stage_obj = StageSerializer(read_only=True, source="stage")
|
stage_obj = StageSerializer(read_only=True, source="stage")
|
||||||
|
|
||||||
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
evaluate_on_plan = attrs.get("evaluate_on_plan", False)
|
|
||||||
re_evaluate_policies = attrs.get("re_evaluate_policies", True)
|
|
||||||
if not evaluate_on_plan and not re_evaluate_policies:
|
|
||||||
raise ValidationError("Either evaluation on plan or evaluation on run must be enabled")
|
|
||||||
return super().validate(attrs)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FlowStageBinding
|
model = FlowStageBinding
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -25,8 +25,6 @@ from authentik.flows.exceptions import FlowNonApplicableException
|
|||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.flows.planner import CACHE_PREFIX, PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
from authentik.flows.planner import CACHE_PREFIX, PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||||
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_HISTORY, SESSION_KEY_PLAN
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.views import reverse_interface
|
|
||||||
from authentik.lib.utils.file import (
|
from authentik.lib.utils.file import (
|
||||||
FilePathSerializer,
|
FilePathSerializer,
|
||||||
FileUploadSerializer,
|
FileUploadSerializer,
|
||||||
@ -296,11 +294,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"link": request._request.build_absolute_uri(
|
"link": request._request.build_absolute_uri(
|
||||||
reverse_interface(
|
reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
request,
|
|
||||||
InterfaceType.FLOW,
|
|
||||||
flow_slug=flow.slug,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-02-25 15:51
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("authentik_flows", "0024_flow_authentication"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="flowstagebinding",
|
|
||||||
name="evaluate_on_plan",
|
|
||||||
field=models.BooleanField(
|
|
||||||
default=False, help_text="Evaluate policies during the Flow planning process."
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="flowstagebinding",
|
|
||||||
name="re_evaluate_policies",
|
|
||||||
field=models.BooleanField(
|
|
||||||
default=True, help_text="Evaluate policies when the Stage is present to the user."
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -211,11 +211,14 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
|||||||
stage = InheritanceForeignKey(Stage, on_delete=models.CASCADE)
|
stage = InheritanceForeignKey(Stage, on_delete=models.CASCADE)
|
||||||
|
|
||||||
evaluate_on_plan = models.BooleanField(
|
evaluate_on_plan = models.BooleanField(
|
||||||
default=False,
|
default=True,
|
||||||
help_text=_("Evaluate policies during the Flow planning process."),
|
help_text=_(
|
||||||
|
"Evaluate policies during the Flow planning process. "
|
||||||
|
"Disable this for input-based policies."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
re_evaluate_policies = models.BooleanField(
|
re_evaluate_policies = models.BooleanField(
|
||||||
default=True,
|
default=False,
|
||||||
help_text=_("Evaluate policies when the Stage is present to the user."),
|
help_text=_("Evaluate policies when the Stage is present to the user."),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -271,15 +274,6 @@ class ConfigurableStage(models.Model):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class FriendlyNamedStage(models.Model):
|
|
||||||
"""Abstract base class for a Stage that can have a user friendly name configured."""
|
|
||||||
|
|
||||||
friendly_name = models.TextField(null=True)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
class FlowToken(Token):
|
class FlowToken(Token):
|
||||||
"""Subclass of a standard Token, stores the currently active flow plan upon creation.
|
"""Subclass of a standard Token, stores the currently active flow plan upon creation.
|
||||||
Can be used to later resume a flow."""
|
Can be used to later resume a flow."""
|
||||||
|
@ -147,6 +147,7 @@ class FlowPlanner:
|
|||||||
) -> FlowPlan:
|
) -> FlowPlan:
|
||||||
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
|
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
|
||||||
and return ordered list"""
|
and return ordered list"""
|
||||||
|
self._check_authentication(request)
|
||||||
with Hub.current.start_span(
|
with Hub.current.start_span(
|
||||||
op="authentik.flow.planner.plan", description=self.flow.slug
|
op="authentik.flow.planner.plan", description=self.flow.slug
|
||||||
) as span:
|
) as span:
|
||||||
@ -164,12 +165,6 @@ class FlowPlanner:
|
|||||||
user = default_context[PLAN_CONTEXT_PENDING_USER]
|
user = default_context[PLAN_CONTEXT_PENDING_USER]
|
||||||
else:
|
else:
|
||||||
user = request.user
|
user = request.user
|
||||||
# We only need to check the flow authentication if it's planned without a user
|
|
||||||
# in the context, as a user in the context can only be set via the explicit code API
|
|
||||||
# or if a flow is restarted due to `invalid_response_action` being set to
|
|
||||||
# `restart_with_context`, which can only happen if the user was already authorized
|
|
||||||
# to use the flow
|
|
||||||
self._check_authentication(request)
|
|
||||||
# First off, check the flow's direct policy bindings
|
# First off, check the flow's direct policy bindings
|
||||||
# to make sure the user even has access to the flow
|
# to make sure the user even has access to the flow
|
||||||
engine = PolicyEngine(self.flow, user, request)
|
engine = PolicyEngine(self.flow, user, request)
|
||||||
@ -266,6 +261,7 @@ class FlowPlanner:
|
|||||||
marker = ReevaluateMarker(binding=binding)
|
marker = ReevaluateMarker(binding=binding)
|
||||||
if stage:
|
if stage:
|
||||||
plan.append(binding, marker)
|
plan.append(binding, marker)
|
||||||
|
HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug)
|
||||||
self._logger.debug(
|
self._logger.debug(
|
||||||
"f(plan): finished building",
|
"f(plan): finished building",
|
||||||
)
|
)
|
||||||
|
@ -7,7 +7,6 @@ from django.http.request import QueryDict
|
|||||||
from django.http.response import HttpResponse
|
from django.http.response import HttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.generic.base import View
|
from django.views.generic.base import View
|
||||||
from prometheus_client import Histogram
|
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
@ -32,11 +31,6 @@ if TYPE_CHECKING:
|
|||||||
from authentik.flows.views.executor import FlowExecutorView
|
from authentik.flows.views.executor import FlowExecutorView
|
||||||
|
|
||||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
|
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
|
||||||
HIST_FLOWS_STAGE_TIME = Histogram(
|
|
||||||
"authentik_flows_stage_time",
|
|
||||||
"Duration taken by different parts of stages",
|
|
||||||
["stage_type", "method"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StageView(View):
|
class StageView(View):
|
||||||
@ -115,24 +109,14 @@ class ChallengeStageView(StageView):
|
|||||||
keep_context=keep_context,
|
keep_context=keep_context,
|
||||||
)
|
)
|
||||||
return self.executor.restart_flow(keep_context)
|
return self.executor.restart_flow(keep_context)
|
||||||
with (
|
with Hub.current.start_span(
|
||||||
Hub.current.start_span(
|
op="authentik.flow.stage.challenge_invalid",
|
||||||
op="authentik.flow.stage.challenge_invalid",
|
description=self.__class__.__name__,
|
||||||
description=self.__class__.__name__,
|
|
||||||
),
|
|
||||||
HIST_FLOWS_STAGE_TIME.labels(
|
|
||||||
stage_type=self.__class__.__name__, method="challenge_invalid"
|
|
||||||
).time(),
|
|
||||||
):
|
):
|
||||||
return self.challenge_invalid(challenge)
|
return self.challenge_invalid(challenge)
|
||||||
with (
|
with Hub.current.start_span(
|
||||||
Hub.current.start_span(
|
op="authentik.flow.stage.challenge_valid",
|
||||||
op="authentik.flow.stage.challenge_valid",
|
description=self.__class__.__name__,
|
||||||
description=self.__class__.__name__,
|
|
||||||
),
|
|
||||||
HIST_FLOWS_STAGE_TIME.labels(
|
|
||||||
stage_type=self.__class__.__name__, method="challenge_valid"
|
|
||||||
).time(),
|
|
||||||
):
|
):
|
||||||
return self.challenge_valid(challenge)
|
return self.challenge_valid(challenge)
|
||||||
|
|
||||||
@ -151,14 +135,9 @@ class ChallengeStageView(StageView):
|
|||||||
return self.executor.flow.title
|
return self.executor.flow.title
|
||||||
|
|
||||||
def _get_challenge(self, *args, **kwargs) -> Challenge:
|
def _get_challenge(self, *args, **kwargs) -> Challenge:
|
||||||
with (
|
with Hub.current.start_span(
|
||||||
Hub.current.start_span(
|
op="authentik.flow.stage.get_challenge",
|
||||||
op="authentik.flow.stage.get_challenge",
|
description=self.__class__.__name__,
|
||||||
description=self.__class__.__name__,
|
|
||||||
),
|
|
||||||
HIST_FLOWS_STAGE_TIME.labels(
|
|
||||||
stage_type=self.__class__.__name__, method="get_challenge"
|
|
||||||
).time(),
|
|
||||||
):
|
):
|
||||||
challenge = self.get_challenge(*args, **kwargs)
|
challenge = self.get_challenge(*args, **kwargs)
|
||||||
with Hub.current.start_span(
|
with Hub.current.start_span(
|
||||||
@ -231,7 +210,7 @@ class AccessDeniedChallengeView(ChallengeStageView):
|
|||||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||||
return AccessDeniedChallenge(
|
return AccessDeniedChallenge(
|
||||||
data={
|
data={
|
||||||
"error_message": str(self.error_message or "Unknown error"),
|
"error_message": self.error_message or "Unknown error",
|
||||||
"type": ChallengeTypes.NATIVE.value,
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
"component": "ak-stage-access-denied",
|
"component": "ak-stage-access-denied",
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,10 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik.core.models import Application
|
|
||||||
from authentik.core.tests.utils import create_test_flow
|
from authentik.core.tests.utils import create_test_flow
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
from authentik.flows.planner import FlowPlan
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.tests import reverse_interface
|
|
||||||
from authentik.lib.generators import generate_id
|
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
|
||||||
|
|
||||||
|
|
||||||
class TestHelperView(TestCase):
|
class TestHelperView(TestCase):
|
||||||
@ -23,44 +18,6 @@ class TestHelperView(TestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("authentik_flows:default-invalidation"),
|
reverse("authentik_flows:default-invalidation"),
|
||||||
)
|
)
|
||||||
expected_url = reverse_interface(
|
|
||||||
InterfaceType.FLOW,
|
|
||||||
flow_slug=flow.slug,
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(response.url, expected_url)
|
|
||||||
|
|
||||||
def test_default_view_app(self):
|
|
||||||
"""Test that ToDefaultFlow returns the expected URL (when accessing an application)"""
|
|
||||||
Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete()
|
|
||||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
|
||||||
self.client.session[SESSION_KEY_APPLICATION_PRE] = Application(
|
|
||||||
name=generate_id(),
|
|
||||||
slug=generate_id(),
|
|
||||||
provider=OAuth2Provider(
|
|
||||||
name=generate_id(),
|
|
||||||
authentication_flow=flow,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_flows:default-authentication"),
|
|
||||||
)
|
|
||||||
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(response.url, expected_url)
|
|
||||||
|
|
||||||
def test_default_view_app_no_provider(self):
|
|
||||||
"""Test that ToDefaultFlow returns the expected URL
|
|
||||||
(when accessing an application, without a provider)"""
|
|
||||||
Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete()
|
|
||||||
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
|
||||||
self.client.session[SESSION_KEY_APPLICATION_PRE] = Application(
|
|
||||||
name=generate_id(),
|
|
||||||
slug=generate_id(),
|
|
||||||
)
|
|
||||||
response = self.client.get(
|
|
||||||
reverse("authentik_flows:default-authentication"),
|
|
||||||
)
|
|
||||||
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.url, expected_url)
|
self.assertEqual(response.url, expected_url)
|
||||||
@ -77,9 +34,6 @@ class TestHelperView(TestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse("authentik_flows:default-invalidation"),
|
reverse("authentik_flows:default-invalidation"),
|
||||||
)
|
)
|
||||||
expected_url = reverse_interface(
|
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
InterfaceType.FLOW,
|
|
||||||
flow_slug=flow.slug,
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.url, expected_url)
|
self.assertEqual(response.url, expected_url)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user