Compare commits
18 Commits
interfaces
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
918355e1c9 | |||
c07a48a3ec | |||
e1bae1240f | |||
37bd62d291 | |||
ac63db0136 | |||
5cdf3a09a9 | |||
3e17adf33f | |||
8392916c84 | |||
7e75a48fd0 | |||
d69d84e48c | |||
78cc8fa498 | |||
0fcdf5e968 | |||
f05997740f | |||
1aff300171 | |||
ffb98eaa75 | |||
5c1db432f0 | |||
07fd4daa3e | |||
aa80babfff |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2023.4.1
|
current_version = 2023.4.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
|
|
||||||
|
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:
|
||||||
|
12
.github/actions/setup/action.yml
vendored
12
.github/actions/setup/action.yml
vendored
@ -1,5 +1,5 @@
|
|||||||
name: "Setup authentik testing environment"
|
name: 'Setup authentik testing environment'
|
||||||
description: "Setup authentik testing environment"
|
description: 'Setup authentik testing environment'
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
postgresql_tag:
|
postgresql_tag:
|
||||||
@ -18,13 +18,13 @@ 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
|
||||||
|
8
.github/actions/setup/docker-compose.yml
vendored
8
.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:${PSQL_TAG:-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:
|
||||||
|
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.
|
||||||
|
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'
|
||||||
|
13
.github/workflows/ci-main.yml
vendored
13
.github/workflows/ci-main.yml
vendored
@ -23,14 +23,13 @@ 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
|
||||||
|
- codespell
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -212,7 +211,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 +252,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 }}
|
||||||
|
16
.github/workflows/ci-outpost.yml
vendored
16
.github/workflows/ci-outpost.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
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
|
||||||
@ -36,7 +36,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
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
|
||||||
@ -60,6 +60,8 @@ jobs:
|
|||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
- radius
|
- radius
|
||||||
|
arch:
|
||||||
|
- "linux/amd64"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -92,7 +94,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
|
||||||
@ -112,10 +114,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
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
|
||||||
@ -131,3 +133,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
|
||||||
|
12
.github/workflows/ci-website.yml
vendored
12
.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,8 +31,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
|
||||||
@ -52,8 +52,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
|
||||||
|
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
|
||||||
|
9
.github/workflows/ghcr-retention.yml
vendored
9
.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:
|
||||||
@ -10,6 +10,11 @@ jobs:
|
|||||||
name: Delete old unused container images
|
name: Delete old unused container images
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- id: generate_token
|
||||||
|
uses: tibdex/github-app-token@v1
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
- name: Delete 'dev' containers older than a week
|
- name: Delete 'dev' containers older than a week
|
||||||
uses: snok/container-retention-policy@v2
|
uses: snok/container-retention-policy@v2
|
||||||
with:
|
with:
|
||||||
@ -18,5 +23,5 @@ jobs:
|
|||||||
account-type: org
|
account-type: org
|
||||||
org-name: goauthentik
|
org-name: goauthentik
|
||||||
untagged-only: false
|
untagged-only: false
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
skip-tags: gh-next,gh-main
|
skip-tags: gh-next,gh-main
|
||||||
|
12
.github/workflows/release-publish.yml
vendored
12
.github/workflows/release-publish.yml
vendored
@ -57,7 +57,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
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
|
||||||
@ -107,11 +107,11 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
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 +173,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'
|
||||||
|
11
.github/workflows/release-tag.yml
vendored
11
.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:
|
||||||
@ -22,18 +22,23 @@ jobs:
|
|||||||
docker-compose up --no-start
|
docker-compose up --no-start
|
||||||
docker-compose start postgresql redis
|
docker-compose start postgresql redis
|
||||||
docker-compose run -u root server test-all
|
docker-compose run -u root server test-all
|
||||||
|
- id: generate_token
|
||||||
|
uses: tibdex/github-app-token@v1
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
- name: Extract version number
|
- name: Extract version number
|
||||||
id: get_version
|
id: get_version
|
||||||
uses: actions/github-script@v6
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
github-token: ${{ steps.generate_token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
|
return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1.1.4
|
uses: actions/create-release@v1.1.4
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ github.ref }}
|
tag_name: ${{ github.ref }}
|
||||||
release_name: Release ${{ steps.get_version.outputs.result }}
|
release_name: Release ${{ steps.get_version.outputs.result }}
|
||||||
|
15
.github/workflows/translation-compile.yml
vendored
15
.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:
|
||||||
@ -18,9 +18,14 @@ jobs:
|
|||||||
compile:
|
compile:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- id: generate_token
|
||||||
|
uses: tibdex/github-app-token@v1
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
- name: run compile
|
- name: run compile
|
||||||
@ -29,7 +34,7 @@ jobs:
|
|||||||
uses: peter-evans/create-pull-request@v5
|
uses: peter-evans/create-pull-request@v5
|
||||||
id: cpr
|
id: cpr
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
branch: compile-backend-translation
|
branch: compile-backend-translation
|
||||||
commit-message: "core: compile backend translations"
|
commit-message: "core: compile backend translations"
|
||||||
title: "core: compile backend translations"
|
title: "core: compile backend translations"
|
||||||
|
19
.github/workflows/web-api-publish.yml
vendored
19
.github/workflows/web-api-publish.yml
vendored
@ -1,21 +1,26 @@
|
|||||||
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:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- id: generate_token
|
||||||
|
uses: tibdex/github-app-token@v1
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ steps.generate_token.outputs.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
|
||||||
@ -33,7 +38,7 @@ jobs:
|
|||||||
- uses: peter-evans/create-pull-request@v5
|
- uses: peter-evans/create-pull-request@v5
|
||||||
id: cpr
|
id: cpr
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
branch: update-web-api-client
|
branch: update-web-api-client
|
||||||
commit-message: "web: bump API Client version"
|
commit-message: "web: bump API Client version"
|
||||||
title: "web: bump API Client version"
|
title: "web: bump API Client version"
|
||||||
@ -44,6 +49,6 @@ jobs:
|
|||||||
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@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||||
merge-method: squash
|
merge-method: squash
|
||||||
|
@ -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/
|
||||||
@ -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 && \
|
||||||
|
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
|
||||||
|
14
Makefile
14
Makefile
@ -3,7 +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 \
|
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
|
||||||
-I .github/codespell-words.txt \
|
-I .github/codespell-words.txt \
|
||||||
@ -39,14 +38,13 @@ 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 -w $(CODESPELL_ARGS)
|
codespell -w $(CODESPELL_ARGS)
|
||||||
|
|
||||||
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:
|
||||||
@ -173,6 +171,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,9 +182,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
|
ci-codespell: ci--meta-debug
|
||||||
codespell $(CODESPELL_ARGS) -s
|
codespell $(CODESPELL_ARGS) -s
|
||||||
|
|
||||||
|
@ -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.4.3"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""authentik administration overview"""
|
"""authentik administration overview"""
|
||||||
import os
|
|
||||||
import platform
|
import platform
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sys import version as python_version
|
from sys import version as python_version
|
||||||
@ -18,7 +17,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):
|
||||||
@ -35,7 +33,6 @@ class RuntimeDict(TypedDict):
|
|||||||
class SystemSerializer(PassiveSerializer):
|
class SystemSerializer(PassiveSerializer):
|
||||||
"""Get system information."""
|
"""Get system information."""
|
||||||
|
|
||||||
env = SerializerMethodField()
|
|
||||||
http_headers = SerializerMethodField()
|
http_headers = SerializerMethodField()
|
||||||
http_host = SerializerMethodField()
|
http_host = SerializerMethodField()
|
||||||
http_is_secure = SerializerMethodField()
|
http_is_secure = SerializerMethodField()
|
||||||
@ -44,10 +41,6 @@ class SystemSerializer(PassiveSerializer):
|
|||||||
server_time = SerializerMethodField()
|
server_time = SerializerMethodField()
|
||||||
embedded_outpost_host = SerializerMethodField()
|
embedded_outpost_host = SerializerMethodField()
|
||||||
|
|
||||||
def get_env(self, request: Request) -> dict[str, str]:
|
|
||||||
"""Get Environment"""
|
|
||||||
return os.environ.copy()
|
|
||||||
|
|
||||||
def get_http_headers(self, request: Request) -> dict[str, str]:
|
def get_http_headers(self, request: Request) -> dict[str, str]:
|
||||||
"""Get HTTP Request headers"""
|
"""Get HTTP Request headers"""
|
||||||
headers = {}
|
headers = {}
|
||||||
@ -78,7 +71,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"""
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""API Authentication"""
|
"""API Authentication"""
|
||||||
|
from hmac import compare_digest
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -78,7 +79,7 @@ def token_secret_key(value: str) -> Optional[User]:
|
|||||||
and return the service account for the managed outpost"""
|
and return the service account for the managed outpost"""
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||||
|
|
||||||
if value != settings.SECRET_KEY:
|
if not compare_digest(value, settings.SECRET_KEY):
|
||||||
return None
|
return None
|
||||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||||
if not outposts:
|
if not outposts:
|
||||||
|
@ -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,
|
||||||
@ -124,8 +123,6 @@ 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)
|
||||||
|
@ -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
|
||||||
|
@ -82,7 +82,10 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
|||||||
def retrieve_file(self) -> str:
|
def retrieve_file(self) -> str:
|
||||||
"""Get blueprint from path"""
|
"""Get blueprint from path"""
|
||||||
try:
|
try:
|
||||||
full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path))
|
base = Path(CONFIG.y("blueprints_dir"))
|
||||||
|
full_path = base.joinpath(Path(self.path)).resolve()
|
||||||
|
if not str(full_path).startswith(str(base.resolve())):
|
||||||
|
raise BlueprintRetrievalFailed("Invalid blueprint path")
|
||||||
with full_path.open("r", encoding="utf-8") as _file:
|
with full_path.open("r", encoding="utf-8") as _file:
|
||||||
return _file.read()
|
return _file.read()
|
||||||
except (IOError, OSError) as exc:
|
except (IOError, OSError) as exc:
|
||||||
|
@ -1,34 +1,15 @@
|
|||||||
"""authentik managed models tests"""
|
"""authentik managed models tests"""
|
||||||
from typing import Callable, Type
|
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from authentik.blueprints.v1.importer import is_model_allowed
|
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
|
|
||||||
class TestModels(TestCase):
|
class TestModels(TestCase):
|
||||||
"""Test Models"""
|
"""Test Models"""
|
||||||
|
|
||||||
|
def test_retrieve_file(self):
|
||||||
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
|
"""Test retrieve_file"""
|
||||||
"""Test serializer"""
|
instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts")
|
||||||
|
with self.assertRaises(BlueprintRetrievalFailed):
|
||||||
def tester(self: TestModels):
|
instance.retrieve()
|
||||||
if test_model._meta.abstract: # pragma: no cover
|
|
||||||
return
|
|
||||||
model_class = test_model()
|
|
||||||
self.assertTrue(isinstance(model_class, SerializerModel))
|
|
||||||
self.assertIsNotNone(model_class.serializer)
|
|
||||||
|
|
||||||
return tester
|
|
||||||
|
|
||||||
|
|
||||||
for app in apps.get_app_configs():
|
|
||||||
if not app.label.startswith("authentik"):
|
|
||||||
continue
|
|
||||||
for model in app.get_models():
|
|
||||||
if not is_model_allowed(model):
|
|
||||||
continue
|
|
||||||
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
|
|
||||||
|
34
authentik/blueprints/tests/test_serializer_models.py
Normal file
34
authentik/blueprints/tests/test_serializer_models.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
"""authentik managed models tests"""
|
||||||
|
from typing import Callable, Type
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from authentik.blueprints.v1.importer import is_model_allowed
|
||||||
|
from authentik.lib.models import SerializerModel
|
||||||
|
|
||||||
|
|
||||||
|
class TestModels(TestCase):
|
||||||
|
"""Test Models"""
|
||||||
|
|
||||||
|
|
||||||
|
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
|
||||||
|
"""Test serializer"""
|
||||||
|
|
||||||
|
def tester(self: TestModels):
|
||||||
|
if test_model._meta.abstract: # pragma: no cover
|
||||||
|
return
|
||||||
|
model_class = test_model()
|
||||||
|
self.assertTrue(isinstance(model_class, SerializerModel))
|
||||||
|
self.assertIsNotNone(model_class.serializer)
|
||||||
|
|
||||||
|
return tester
|
||||||
|
|
||||||
|
|
||||||
|
for app in apps.get_app_configs():
|
||||||
|
if not app.label.startswith("authentik"):
|
||||||
|
continue
|
||||||
|
for model in app.get_models():
|
||||||
|
if not is_model_allowed(model):
|
||||||
|
continue
|
||||||
|
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
|
@ -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
|
||||||
@ -66,17 +67,16 @@ from authentik.core.models import (
|
|||||||
TokenIntents,
|
TokenIntents,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
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.lib.config import CONFIG
|
||||||
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()
|
||||||
|
|
||||||
@ -322,7 +322,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:
|
||||||
@ -351,12 +351,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
|
||||||
|
|
||||||
@ -548,6 +544,58 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
send_mails(email_stage, message)
|
send_mails(email_stage, message)
|
||||||
return Response(status=204)
|
return Response(status=204)
|
||||||
|
|
||||||
|
@permission_required("authentik_core.impersonate")
|
||||||
|
@extend_schema(
|
||||||
|
request=OpenApiTypes.NONE,
|
||||||
|
responses={
|
||||||
|
"204": OpenApiResponse(description="Successfully started impersonation"),
|
||||||
|
"401": OpenApiResponse(description="Access denied"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=True, methods=["POST"])
|
||||||
|
def impersonate(self, request: Request, pk: int) -> Response:
|
||||||
|
"""Impersonate a user"""
|
||||||
|
if not CONFIG.y_bool("impersonation"):
|
||||||
|
LOGGER.debug("User attempted to impersonate", user=request.user)
|
||||||
|
return Response(status=401)
|
||||||
|
if not request.user.has_perm("impersonate"):
|
||||||
|
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
|
||||||
|
return Response(status=401)
|
||||||
|
|
||||||
|
user_to_be = self.get_object()
|
||||||
|
|
||||||
|
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||||
|
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
||||||
|
|
||||||
|
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||||
|
|
||||||
|
return Response(status=201)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=OpenApiTypes.NONE,
|
||||||
|
responses={
|
||||||
|
"204": OpenApiResponse(description="Successfully started impersonation"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=["GET"])
|
||||||
|
def impersonate_end(self, request: Request) -> Response:
|
||||||
|
"""End Impersonation a user"""
|
||||||
|
if (
|
||||||
|
SESSION_KEY_IMPERSONATE_USER not in request.session
|
||||||
|
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||||
|
):
|
||||||
|
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
|
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
|
del request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||||
|
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
|
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
||||||
|
|
||||||
|
return Response(status=204)
|
||||||
|
|
||||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
||||||
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
||||||
for backend in list(self.filter_backends):
|
for backend in list(self.filter_backends):
|
||||||
|
@ -33,7 +33,6 @@ from authentik.lib.models import (
|
|||||||
)
|
)
|
||||||
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"
|
||||||
@ -169,7 +168,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)
|
||||||
@ -228,7 +227,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
|
||||||
|
@ -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}"
|
||||||
)
|
)
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
"""impersonation tests"""
|
"""impersonation tests"""
|
||||||
from json import loads
|
from json import loads
|
||||||
|
|
||||||
from django.test.testcases import TestCase
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.core.tests.utils import create_test_admin_user
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
|
|
||||||
|
|
||||||
class TestImpersonation(TestCase):
|
class TestImpersonation(APITestCase):
|
||||||
"""impersonation tests"""
|
"""impersonation tests"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
@ -23,10 +23,10 @@ class TestImpersonation(TestCase):
|
|||||||
self.other_user.save()
|
self.other_user.save()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
self.client.get(
|
self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_core:impersonate-init",
|
"authentik_api:user-impersonate",
|
||||||
kwargs={"user_id": self.other_user.pk},
|
kwargs={"pk": self.other_user.pk},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ class TestImpersonation(TestCase):
|
|||||||
self.assertEqual(response_body["user"]["username"], self.other_user.username)
|
self.assertEqual(response_body["user"]["username"], self.other_user.username)
|
||||||
self.assertEqual(response_body["original"]["username"], self.user.username)
|
self.assertEqual(response_body["original"]["username"], self.user.username)
|
||||||
|
|
||||||
self.client.get(reverse("authentik_core:impersonate-end"))
|
self.client.get(reverse("authentik_api:user-impersonate-end"))
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
response_body = loads(response.content.decode())
|
response_body = loads(response.content.decode())
|
||||||
@ -46,9 +46,7 @@ class TestImpersonation(TestCase):
|
|||||||
"""test impersonation without permissions"""
|
"""test impersonation without permissions"""
|
||||||
self.client.force_login(self.other_user)
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
self.client.get(
|
self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}))
|
||||||
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_api:user-me"))
|
response = self.client.get(reverse("authentik_api:user-me"))
|
||||||
response_body = loads(response.content.decode())
|
response_body = loads(response.content.decode())
|
||||||
@ -58,7 +56,5 @@ class TestImpersonation(TestCase):
|
|||||||
"""test un-impersonation without impersonating first"""
|
"""test un-impersonation without impersonating first"""
|
||||||
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_api:user-impersonate-end"))
|
||||||
self.assertRedirects(
|
self.assertEqual(response.status_code, 204)
|
||||||
response, reverse("authentik_interfaces:if", kwargs={"if_name": "user"})
|
|
||||||
)
|
|
||||||
|
@ -3,30 +3,23 @@ from channels.auth import AuthMiddleware
|
|||||||
from channels.sessions import CookieMiddleware
|
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
|
||||||
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.asgi_middleware import SessionMiddleware
|
||||||
from authentik.root.messages.consumer import MessageConsumer
|
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(
|
||||||
@ -35,28 +28,32 @@ urlpatterns = [
|
|||||||
apps.RedirectToAppLaunch.as_view(),
|
apps.RedirectToAppLaunch.as_view(),
|
||||||
name="application-launch",
|
name="application-launch",
|
||||||
),
|
),
|
||||||
# Impersonation
|
|
||||||
path(
|
|
||||||
"-/impersonation/<int:user_id>/",
|
|
||||||
impersonate.ImpersonateInitView.as_view(),
|
|
||||||
name="impersonate-init",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"-/impersonation/end/",
|
|
||||||
impersonate.ImpersonateEndView.as_view(),
|
|
||||||
name="impersonate-end",
|
|
||||||
),
|
|
||||||
# Interfaces
|
# 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"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -20,13 +20,11 @@ from authentik.flows.views.executor import (
|
|||||||
SESSION_KEY_PLAN,
|
SESSION_KEY_PLAN,
|
||||||
ToDefaultFlow,
|
ToDefaultFlow,
|
||||||
)
|
)
|
||||||
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.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
|
|
||||||
|
|
||||||
|
|
||||||
class RedirectToAppLaunch(View):
|
class RedirectToAppLaunch(View):
|
||||||
@ -61,7 +59,7 @@ class RedirectToAppLaunch(View):
|
|||||||
raise Http404
|
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):
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
"""authentik impersonation views"""
|
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
|
||||||
from django.views import View
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.core.middleware import (
|
|
||||||
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
|
|
||||||
SESSION_KEY_IMPERSONATE_USER,
|
|
||||||
)
|
|
||||||
from authentik.core.models import User
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateInitView(View):
|
|
||||||
"""Initiate Impersonation"""
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
|
|
||||||
"""Impersonation handler, checks permissions"""
|
|
||||||
if not CONFIG.y_bool("impersonation"):
|
|
||||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
|
||||||
return HttpResponse("Unauthorized", status=401)
|
|
||||||
if not request.user.has_perm("impersonate"):
|
|
||||||
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
|
|
||||||
return HttpResponse("Unauthorized", status=401)
|
|
||||||
|
|
||||||
user_to_be = get_object_or_404(User, pk=user_id)
|
|
||||||
|
|
||||||
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
|
||||||
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
|
||||||
|
|
||||||
return redirect("authentik_core:root-redirect")
|
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateEndView(View):
|
|
||||||
"""End User impersonation"""
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
|
||||||
"""End Impersonation handler"""
|
|
||||||
if (
|
|
||||||
SESSION_KEY_IMPERSONATE_USER not in request.session
|
|
||||||
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
|
||||||
):
|
|
||||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
|
||||||
return redirect("authentik_core:root-redirect")
|
|
||||||
|
|
||||||
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
|
||||||
|
|
||||||
del request.session[SESSION_KEY_IMPERSONATE_USER]
|
|
||||||
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
|
||||||
|
|
||||||
return redirect("authentik_core:root-redirect")
|
|
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"""
|
|
@ -11,6 +11,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.events.models import EventAction, NotificationSeverity, TransportMode
|
||||||
from authentik.lib.migrations import progress_bar
|
from authentik.lib.migrations import progress_bar
|
||||||
|
|
||||||
|
|
||||||
|
@ -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):
|
||||||
@ -226,7 +227,7 @@ class Event(SerializerModel, ExpiringModel):
|
|||||||
wrapped = self.context["http_request"]["args"][QS_QUERY]
|
wrapped = self.context["http_request"]["args"][QS_QUERY]
|
||||||
self.context["http_request"]["args"] = QueryDict(wrapped)
|
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()
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -23,7 +23,8 @@ class DiagramElement:
|
|||||||
style: list[str] = field(default_factory=lambda: ["[", "]"])
|
style: list[str] = field(default_factory=lambda: ["[", "]"])
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}'
|
description = self.description.replace('"', "#quot;")
|
||||||
|
element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}'
|
||||||
if self.action is not None:
|
if self.action is not None:
|
||||||
if self.action != "":
|
if self.action != "":
|
||||||
element = f"--{self.action}--> {element}"
|
element = f"--{self.action}--> {element}"
|
||||||
|
@ -204,12 +204,12 @@ class ChallengeStageView(StageView):
|
|||||||
for field, errors in response.errors.items():
|
for field, errors in response.errors.items():
|
||||||
for error in errors:
|
for error in errors:
|
||||||
full_errors.setdefault(field, [])
|
full_errors.setdefault(field, [])
|
||||||
full_errors[field].append(
|
field_error = {
|
||||||
{
|
"string": str(error),
|
||||||
"string": str(error),
|
}
|
||||||
"code": error.code,
|
if hasattr(error, "code"):
|
||||||
}
|
field_error["code"] = error.code
|
||||||
)
|
full_errors[field].append(field_error)
|
||||||
challenge_response.initial_data["response_errors"] = full_errors
|
challenge_response.initial_data["response_errors"] = full_errors
|
||||||
if not challenge_response.is_valid():
|
if not challenge_response.is_valid():
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
|
@ -7,8 +7,6 @@ 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_APPLICATION_PRE, 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.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
|
|
||||||
@ -23,10 +21,7 @@ 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)
|
||||||
|
|
||||||
@ -77,9 +72,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)
|
||||||
|
@ -53,14 +53,12 @@ from authentik.flows.planner import (
|
|||||||
FlowPlanner,
|
FlowPlanner,
|
||||||
)
|
)
|
||||||
from authentik.flows.stage import AccessDeniedChallengeView, StageView
|
from authentik.flows.stage import AccessDeniedChallengeView, StageView
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.views import redirect_to_default_interface
|
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
||||||
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.tenants.utils import get_tenant
|
from authentik.tenants.models import Tenant
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
# Argument used to redirect user after login
|
# Argument used to redirect user after login
|
||||||
@ -481,7 +479,7 @@ class ToDefaultFlow(View):
|
|||||||
|
|
||||||
def get_flow(self) -> Flow:
|
def get_flow(self) -> Flow:
|
||||||
"""Get a flow for the selected designation"""
|
"""Get a flow for the selected designation"""
|
||||||
tenant = get_tenant(self.request)
|
tenant: Tenant = self.request.tenant
|
||||||
flow = None
|
flow = None
|
||||||
# First, attempt to get default flow from tenant
|
# First, attempt to get default flow from tenant
|
||||||
if self.designation == FlowDesignation.AUTHENTICATION:
|
if self.designation == FlowDesignation.AUTHENTICATION:
|
||||||
@ -514,7 +512,7 @@ class ToDefaultFlow(View):
|
|||||||
flow_slug=flow.slug,
|
flow_slug=flow.slug,
|
||||||
)
|
)
|
||||||
del self.request.session[SESSION_KEY_PLAN]
|
del self.request.session[SESSION_KEY_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)
|
||||||
|
|
||||||
|
|
||||||
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
|
def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse:
|
||||||
@ -585,8 +583,8 @@ class ConfigureFlowInitView(LoginRequiredMixin, View):
|
|||||||
LOGGER.warning("Flow not applicable to user")
|
LOGGER.warning("Flow not applicable to user")
|
||||||
raise Http404
|
raise Http404
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
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=stage.configure_flow.slug,
|
flow_slug=stage.configure_flow.slug,
|
||||||
)
|
)
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
"""interfaces API"""
|
|
||||||
from rest_framework.serializers import ModelSerializer
|
|
||||||
from rest_framework.viewsets import ModelViewSet
|
|
||||||
|
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
|
||||||
from authentik.interfaces.models import Interface
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceSerializer(ModelSerializer):
|
|
||||||
"""Interface serializer"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Interface
|
|
||||||
fields = [
|
|
||||||
"interface_uuid",
|
|
||||||
"url_name",
|
|
||||||
"type",
|
|
||||||
"template",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceViewSet(UsedByMixin, ModelViewSet):
|
|
||||||
"""Interface serializer"""
|
|
||||||
|
|
||||||
queryset = Interface.objects.all()
|
|
||||||
serializer_class = InterfaceSerializer
|
|
||||||
filterset_fields = ["url_name", "type", "template"]
|
|
||||||
search_fields = ["url_name", "type", "template"]
|
|
@ -1,12 +0,0 @@
|
|||||||
"""authentik interfaces app config"""
|
|
||||||
from authentik.blueprints.apps import ManagedAppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class AuthentikInterfacesConfig(ManagedAppConfig):
|
|
||||||
"""authentik interfaces app config"""
|
|
||||||
|
|
||||||
name = "authentik.interfaces"
|
|
||||||
label = "authentik_interfaces"
|
|
||||||
verbose_name = "authentik Interfaces"
|
|
||||||
mountpoint = "if/"
|
|
||||||
default = True
|
|
@ -1,36 +0,0 @@
|
|||||||
# Generated by Django 4.1.7 on 2023-02-16 11:01
|
|
||||||
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = []
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Interface",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"interface_uuid",
|
|
||||||
models.UUIDField(
|
|
||||||
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("url_name", models.SlugField(unique=True)),
|
|
||||||
(
|
|
||||||
"type",
|
|
||||||
models.TextField(
|
|
||||||
choices=[("user", "User"), ("admin", "Admin"), ("flow", "Flow")]
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("template", models.TextField()),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,33 +0,0 @@
|
|||||||
"""Interface models"""
|
|
||||||
from typing import Type
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from rest_framework.serializers import BaseSerializer
|
|
||||||
|
|
||||||
from authentik.lib.models import SerializerModel
|
|
||||||
|
|
||||||
|
|
||||||
class InterfaceType(models.TextChoices):
|
|
||||||
"""Interface types"""
|
|
||||||
|
|
||||||
USER = "user"
|
|
||||||
ADMIN = "admin"
|
|
||||||
FLOW = "flow"
|
|
||||||
|
|
||||||
|
|
||||||
class Interface(SerializerModel):
|
|
||||||
"""Interface"""
|
|
||||||
|
|
||||||
interface_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
|
||||||
|
|
||||||
url_name = models.SlugField(unique=True)
|
|
||||||
|
|
||||||
type = models.TextField(choices=InterfaceType.choices)
|
|
||||||
template = models.TextField()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def serializer(self) -> Type[BaseSerializer]:
|
|
||||||
from authentik.interfaces.api import InterfaceSerializer
|
|
||||||
|
|
||||||
return InterfaceSerializer
|
|
@ -1,12 +0,0 @@
|
|||||||
"""Interface tests"""
|
|
||||||
from django.test import RequestFactory
|
|
||||||
|
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.views import reverse_interface as full_reverse_interface
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_interface(interface_type: InterfaceType, **kwargs):
|
|
||||||
"""reverse_interface wrapper for tests"""
|
|
||||||
factory = RequestFactory()
|
|
||||||
request = factory.get("/")
|
|
||||||
return full_reverse_interface(request, interface_type, **kwargs)
|
|
@ -1,14 +0,0 @@
|
|||||||
"""Interface urls"""
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from authentik.interfaces.views import InterfaceView
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path(
|
|
||||||
"<slug:if_name>/",
|
|
||||||
InterfaceView.as_view(),
|
|
||||||
kwargs={"flow_slug": None},
|
|
||||||
name="if",
|
|
||||||
),
|
|
||||||
path("<slug:if_name>/<slug:flow_slug>/", InterfaceView.as_view(), name="if"),
|
|
||||||
]
|
|
@ -1,113 +0,0 @@
|
|||||||
"""Interface views"""
|
|
||||||
from json import dumps
|
|
||||||
from typing import Any, Optional
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
from django.http import Http404, HttpRequest, HttpResponse, QueryDict
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
|
||||||
from django.template import Template, TemplateSyntaxError, engines
|
|
||||||
from django.template.response import TemplateResponse
|
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.views import View
|
|
||||||
from django.views.decorators.cache import cache_page
|
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
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.interfaces.models import Interface, InterfaceType
|
|
||||||
from authentik.lib.utils.urls import reverse_with_qs
|
|
||||||
from authentik.tenants.api import CurrentTenantSerializer
|
|
||||||
from authentik.tenants.utils import get_tenant
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
def template_from_string(template_string: str) -> Template:
|
|
||||||
"""Render template from string"""
|
|
||||||
chain = []
|
|
||||||
engine_list = engines.all()
|
|
||||||
for engine in engine_list:
|
|
||||||
try:
|
|
||||||
return engine.from_string(template_string)
|
|
||||||
except TemplateSyntaxError as exc:
|
|
||||||
chain.append(exc)
|
|
||||||
raise TemplateSyntaxError(template_string, chain=chain)
|
|
||||||
|
|
||||||
|
|
||||||
def redirect_to_default_interface(request: HttpRequest, interface_type: InterfaceType, **kwargs):
|
|
||||||
"""Shortcut to inline redirect to default interface,
|
|
||||||
keeping GET parameters of the passed request"""
|
|
||||||
return RedirectToInterface.as_view(type=interface_type)(request, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_interface(
|
|
||||||
request: HttpRequest, interface_type: InterfaceType, query: Optional[QueryDict] = None, **kwargs
|
|
||||||
):
|
|
||||||
"""Reverse URL to configured default interface"""
|
|
||||||
tenant = get_tenant(request)
|
|
||||||
interface: Interface = None
|
|
||||||
|
|
||||||
if interface_type == InterfaceType.USER:
|
|
||||||
interface = tenant.interface_user
|
|
||||||
if interface_type == InterfaceType.ADMIN:
|
|
||||||
interface = tenant.interface_admin
|
|
||||||
if interface_type == InterfaceType.FLOW:
|
|
||||||
interface = tenant.interface_flow
|
|
||||||
|
|
||||||
if not interface:
|
|
||||||
LOGGER.warning("No interface found", type=interface_type, tenant=tenant)
|
|
||||||
raise Http404()
|
|
||||||
kwargs["if_name"] = interface.url_name
|
|
||||||
return reverse_with_qs(
|
|
||||||
"authentik_interfaces:if",
|
|
||||||
query=query or request.GET,
|
|
||||||
kwargs=kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RedirectToInterface(View):
|
|
||||||
"""Redirect to tenant's configured view for specified type"""
|
|
||||||
|
|
||||||
type: Optional[InterfaceType] = None
|
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest, **kwargs: Any) -> HttpResponse:
|
|
||||||
target = reverse_interface(request, self.type, **kwargs)
|
|
||||||
if self.request.GET:
|
|
||||||
target += "?" + urlencode(self.request.GET.items())
|
|
||||||
return redirect(target)
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(ensure_csrf_cookie, name="dispatch")
|
|
||||||
@method_decorator(cache_page(60 * 10), name="dispatch")
|
|
||||||
class InterfaceView(View):
|
|
||||||
"""General interface view"""
|
|
||||||
|
|
||||||
def get_context_data(self) -> dict[str, Any]:
|
|
||||||
"""Get template context"""
|
|
||||||
return {
|
|
||||||
"config_json": dumps(ConfigView(request=Request(self.request)).get_config().data),
|
|
||||||
"tenant_json": dumps(CurrentTenantSerializer(get_tenant(self.request)).data),
|
|
||||||
"version_family": f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}",
|
|
||||||
"version_subdomain": f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}",
|
|
||||||
"build": get_build_hash(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def type_flow(self, context: dict[str, Any]):
|
|
||||||
"""Special handling for flow interfaces"""
|
|
||||||
if self.kwargs.get("flow_slug", None) is None:
|
|
||||||
raise Http404()
|
|
||||||
context["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
|
|
||||||
context["inspector"] = "inspector" in self.request.GET
|
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest, if_name: str, **kwargs: Any) -> HttpResponse:
|
|
||||||
context = self.get_context_data()
|
|
||||||
# TODO: Cache
|
|
||||||
interface: Interface = get_object_or_404(Interface, url_name=if_name)
|
|
||||||
if interface.type == InterfaceType.FLOW:
|
|
||||||
self.type_flow(context)
|
|
||||||
template = template_from_string(interface.template)
|
|
||||||
return TemplateResponse(request, template, context)
|
|
@ -5,18 +5,25 @@ postgresql:
|
|||||||
name: authentik
|
name: authentik
|
||||||
user: authentik
|
user: authentik
|
||||||
port: 5432
|
port: 5432
|
||||||
password: 'env://POSTGRES_PASSWORD'
|
password: "env://POSTGRES_PASSWORD"
|
||||||
use_pgbouncer: false
|
use_pgbouncer: false
|
||||||
|
|
||||||
listen:
|
listen:
|
||||||
listen_http: 0.0.0.0:9000
|
listen_http: 0.0.0.0:9000
|
||||||
listen_https: 0.0.0.0:9443
|
listen_https: 0.0.0.0:9443
|
||||||
listen_metrics: 0.0.0.0:9300
|
listen_metrics: 0.0.0.0:9300
|
||||||
|
trusted_proxy_cidrs:
|
||||||
|
- 127.0.0.0/8
|
||||||
|
- 10.0.0.0/8
|
||||||
|
- 172.16.0.0/12
|
||||||
|
- 192.168.0.0/16
|
||||||
|
- fe80::/10
|
||||||
|
- ::1/128
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
port: 6379
|
port: 6379
|
||||||
password: ''
|
password: ""
|
||||||
tls: false
|
tls: false
|
||||||
tls_reqs: "none"
|
tls_reqs: "none"
|
||||||
db: 0
|
db: 0
|
||||||
|
@ -16,10 +16,12 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
|
def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
|
||||||
"""Attempt to get the client's IP by checking common HTTP Headers.
|
"""Attempt to get the client's IP by checking common HTTP Headers.
|
||||||
Returns none if no IP Could be found"""
|
Returns none if no IP Could be found
|
||||||
|
|
||||||
|
No additional validation is done here as requests are expected to only arrive here
|
||||||
|
via the go proxy, which deals with validating these headers for us"""
|
||||||
headers = (
|
headers = (
|
||||||
"HTTP_X_FORWARDED_FOR",
|
"HTTP_X_FORWARDED_FOR",
|
||||||
"HTTP_X_REAL_IP",
|
|
||||||
"REMOTE_ADDR",
|
"REMOTE_ADDR",
|
||||||
)
|
)
|
||||||
for _header in headers:
|
for _header in headers:
|
||||||
|
@ -13,6 +13,7 @@ from paramiko.ssh_exception import SSHException
|
|||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
from yaml import safe_dump
|
from yaml import safe_dump
|
||||||
|
|
||||||
|
from authentik import __version__
|
||||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||||
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException
|
||||||
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException
|
||||||
|
@ -22,7 +22,7 @@ from kubernetes.client import (
|
|||||||
V1SecurityContext,
|
V1SecurityContext,
|
||||||
)
|
)
|
||||||
|
|
||||||
from authentik import get_full_version
|
from authentik import __version__, get_full_version
|
||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||||
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
|
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
|
||||||
|
@ -12,7 +12,6 @@ from authentik.lib.utils.http import get_http_session
|
|||||||
from authentik.policies.models import Policy
|
from authentik.policies.models import Policy
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||||
from authentik.tenants.utils import get_tenant
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
RE_LOWER = re.compile("[a-z]")
|
RE_LOWER = re.compile("[a-z]")
|
||||||
@ -144,8 +143,7 @@ class PasswordPolicy(Policy):
|
|||||||
user_inputs.append(request.user.name)
|
user_inputs.append(request.user.name)
|
||||||
user_inputs.append(request.user.email)
|
user_inputs.append(request.user.email)
|
||||||
if request.http_request:
|
if request.http_request:
|
||||||
tenant = get_tenant(request.http_request)
|
user_inputs.append(request.http_request.tenant.branding_title)
|
||||||
user_inputs.append(tenant.branding_title)
|
|
||||||
# Only calculate result for the first 100 characters, as with over 100 char
|
# Only calculate result for the first 100 characters, as with over 100 char
|
||||||
# long passwords we can be reasonably sure that they'll surpass the score anyways
|
# long passwords we can be reasonably sure that they'll surpass the score anyways
|
||||||
# See https://github.com/dropbox/zxcvbn#runtime-latency
|
# See https://github.com/dropbox/zxcvbn#runtime-latency
|
||||||
|
@ -59,7 +59,6 @@ class TestPasswordPolicyFlow(FlowTestCase):
|
|||||||
"label": "PASSWORD_LABEL",
|
"label": "PASSWORD_LABEL",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
"placeholder": "PASSWORD_PLACEHOLDER",
|
"placeholder": "PASSWORD_PLACEHOLDER",
|
||||||
"initial_value": "",
|
|
||||||
"required": True,
|
"required": True,
|
||||||
"type": "password",
|
"type": "password",
|
||||||
"sub_text": "",
|
"sub_text": "",
|
||||||
|
@ -132,9 +132,9 @@ class TestPolicyProcess(TestCase):
|
|||||||
)
|
)
|
||||||
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
|
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
|
||||||
|
|
||||||
http_request = self.factory.get(reverse("authentik_core:impersonate-end"))
|
http_request = self.factory.get(reverse("authentik_api:user-impersonate-end"))
|
||||||
http_request.user = self.user
|
http_request.user = self.user
|
||||||
http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end"))
|
http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end"))
|
||||||
|
|
||||||
request = PolicyRequest(self.user)
|
request = PolicyRequest(self.user)
|
||||||
request.set_http_request(http_request)
|
request.set_http_request(http_request)
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
# Generated by Django 3.1 on 2020-08-18 15:59
|
# Generated by Django 3.1 on 2020-08-18 15:59
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.apps.registry import Apps
|
||||||
from django.conf import settings
|
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
|
||||||
|
|
||||||
import authentik.core.models
|
import authentik.core.models
|
||||||
import authentik.lib.generators
|
import authentik.lib.generators
|
||||||
|
@ -39,9 +39,8 @@ class TesOAuth2DeviceInit(OAuthTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
res.url,
|
res.url,
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_interfaces:if",
|
"authentik_core:if-flow",
|
||||||
kwargs={
|
kwargs={
|
||||||
"if_name": "flow",
|
|
||||||
"flow_slug": self.device_flow.slug,
|
"flow_slug": self.device_flow.slug,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -69,9 +68,8 @@ class TesOAuth2DeviceInit(OAuthTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
res.url,
|
res.url,
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_interfaces:if",
|
"authentik_core:if-flow",
|
||||||
kwargs={
|
kwargs={
|
||||||
"if_name": "flow",
|
|
||||||
"flow_slug": self.provider.authorization_flow.slug,
|
"flow_slug": self.provider.authorization_flow.slug,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -29,9 +29,8 @@ from authentik.flows.models import in_memory_stage
|
|||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
||||||
from authentik.flows.stage import StageView
|
from authentik.flows.stage import StageView
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.views import redirect_to_default_interface
|
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
from authentik.lib.views import bad_request_message
|
from authentik.lib.views import bad_request_message
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
from authentik.policies.views import PolicyAccessView, RequestValidationError
|
from authentik.policies.views import PolicyAccessView, RequestValidationError
|
||||||
@ -69,7 +68,7 @@ from authentik.stages.consent.stage import (
|
|||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
PLAN_CONTEXT_PARAMS = "goauthentik.io/providers/oauth2/params"
|
PLAN_CONTEXT_PARAMS = "params"
|
||||||
SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
|
SESSION_KEY_LAST_LOGIN_UID = "authentik/providers/oauth2/last_login_uid"
|
||||||
|
|
||||||
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
|
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSENT, PROMPT_LOGIN}
|
||||||
@ -405,9 +404,9 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
|||||||
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
|
plan.append_stage(in_memory_stage(OAuthFulfillmentStage))
|
||||||
|
|
||||||
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=self.provider.authorization_flow.slug,
|
flow_slug=self.provider.authorization_flow.slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from authentik.flows.stage import ChallengeStageView
|
|||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.providers.oauth2.models import DeviceToken
|
from authentik.providers.oauth2.models import DeviceToken
|
||||||
|
|
||||||
PLAN_CONTEXT_DEVICE = "goauthentik.io/providers/oauth2/device"
|
PLAN_CONTEXT_DEVICE = "device"
|
||||||
|
|
||||||
|
|
||||||
class OAuthDeviceCodeFinishChallenge(Challenge):
|
class OAuthDeviceCodeFinishChallenge(Challenge):
|
||||||
|
@ -15,8 +15,7 @@ from authentik.flows.models import in_memory_stage
|
|||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import 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.providers.oauth2.models import DeviceToken, OAuth2Provider
|
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
|
||||||
from authentik.providers.oauth2.views.device_finish import (
|
from authentik.providers.oauth2.views.device_finish import (
|
||||||
PLAN_CONTEXT_DEVICE,
|
PLAN_CONTEXT_DEVICE,
|
||||||
@ -27,7 +26,7 @@ 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
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
QS_KEY_CODE = "code" # nosec
|
QS_KEY_CODE = "code" # nosec
|
||||||
@ -78,9 +77,9 @@ def validate_code(code: int, request: HttpRequest) -> Optional[HttpResponse]:
|
|||||||
return None
|
return None
|
||||||
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_to_default_interface(
|
return redirect_with_qs(
|
||||||
request,
|
"authentik_core:if-flow",
|
||||||
InterfaceType.FLOW,
|
request.GET,
|
||||||
flow_slug=token.provider.authorization_flow.slug,
|
flow_slug=token.provider.authorization_flow.slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -89,7 +88,7 @@ class DeviceEntryView(View):
|
|||||||
"""View used to initiate the device-code flow, url entered by endusers"""
|
"""View used to initiate the device-code flow, url entered by endusers"""
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
tenant = get_tenant(request)
|
tenant: Tenant = request.tenant
|
||||||
device_flow = tenant.flow_device_code
|
device_flow = tenant.flow_device_code
|
||||||
if not device_flow:
|
if not device_flow:
|
||||||
LOGGER.info("Tenant has no device code flow configured", tenant=tenant)
|
LOGGER.info("Tenant has no device code flow configured", tenant=tenant)
|
||||||
@ -111,9 +110,9 @@ class DeviceEntryView(View):
|
|||||||
plan.append_stage(in_memory_stage(OAuthDeviceCodeStage))
|
plan.append_stage(in_memory_stage(OAuthDeviceCodeStage))
|
||||||
|
|
||||||
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=device_flow.slug,
|
flow_slug=device_flow.slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL
|
from authentik.providers.oauth2.constants import SCOPE_GITHUB_ORG_READ, SCOPE_GITHUB_USER_EMAIL
|
||||||
from authentik.providers.oauth2.models import RefreshToken
|
from authentik.providers.oauth2.models import RefreshToken
|
||||||
from authentik.providers.oauth2.utils import protected_resource_view
|
from authentik.providers.oauth2.utils import protected_resource_view
|
||||||
from authentik.tenants.utils import get_tenant
|
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(csrf_exempt, name="dispatch")
|
@method_decorator(csrf_exempt, name="dispatch")
|
||||||
@ -77,7 +76,6 @@ class GitHubUserTeamsView(View):
|
|||||||
def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse:
|
def get(self, request: HttpRequest, token: RefreshToken) -> HttpResponse:
|
||||||
"""Emulate GitHub's /user/teams API Endpoint"""
|
"""Emulate GitHub's /user/teams API Endpoint"""
|
||||||
user = token.user
|
user = token.user
|
||||||
tenant = get_tenant(request)
|
|
||||||
|
|
||||||
orgs_response = []
|
orgs_response = []
|
||||||
for org in user.ak_groups.all():
|
for org in user.ak_groups.all():
|
||||||
@ -99,7 +97,7 @@ class GitHubUserTeamsView(View):
|
|||||||
"created_at": "",
|
"created_at": "",
|
||||||
"updated_at": "",
|
"updated_at": "",
|
||||||
"organization": {
|
"organization": {
|
||||||
"login": slugify(tenant.branding_title),
|
"login": slugify(request.tenant.branding_title),
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"node_id": "",
|
"node_id": "",
|
||||||
"url": "",
|
"url": "",
|
||||||
@ -111,7 +109,7 @@ class GitHubUserTeamsView(View):
|
|||||||
"public_members_url": "",
|
"public_members_url": "",
|
||||||
"avatar_url": "",
|
"avatar_url": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"name": tenant.branding_title,
|
"name": request.tenant.branding_title,
|
||||||
"company": "",
|
"company": "",
|
||||||
"blog": "",
|
"blog": "",
|
||||||
"location": "",
|
"location": "",
|
||||||
|
@ -29,6 +29,9 @@ from authentik.providers.oauth2.utils import cors_allow
|
|||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
PLAN_CONTEXT_PARAMS = "params"
|
||||||
|
PLAN_CONTEXT_SCOPES = "scopes"
|
||||||
|
|
||||||
|
|
||||||
class ProviderInfoView(View):
|
class ProviderInfoView(View):
|
||||||
"""OpenID-compliant Provider Info"""
|
"""OpenID-compliant Provider Info"""
|
||||||
|
@ -15,8 +15,7 @@ from authentik.flows.exceptions import FlowNonApplicableException
|
|||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
|
from authentik.flows.views.executor import SESSION_KEY_PLAN, SESSION_KEY_POST
|
||||||
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.views import PolicyAccessView
|
from authentik.policies.views import PolicyAccessView
|
||||||
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
from authentik.providers.saml.exceptions import CannotHandleAssertion
|
||||||
@ -77,9 +76,9 @@ class SAMLSSOView(PolicyAccessView):
|
|||||||
raise Http404
|
raise Http404
|
||||||
plan.append_stage(in_memory_stage(SAMLFlowFinalView))
|
plan.append_stage(in_memory_stage(SAMLFlowFinalView))
|
||||||
request.session[SESSION_KEY_PLAN] = plan
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
return redirect_to_default_interface(
|
return redirect_with_qs(
|
||||||
request,
|
"authentik_core:if-flow",
|
||||||
InterfaceType.FLOW,
|
request.GET,
|
||||||
flow_slug=self.provider.authorization_flow.slug,
|
flow_slug=self.provider.authorization_flow.slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,4 +22,4 @@ class UseTokenView(View):
|
|||||||
login(request, token.user, backend=BACKEND_INBUILT)
|
login(request, token.user, backend=BACKEND_INBUILT)
|
||||||
token.delete()
|
token.delete()
|
||||||
messages.warning(request, _("Used recovery-link to authenticate."))
|
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||||
return redirect("authentik_core:root-redirect")
|
return redirect("authentik_core:if-user")
|
||||||
|
@ -65,7 +65,6 @@ INSTALLED_APPS = [
|
|||||||
"authentik.admin",
|
"authentik.admin",
|
||||||
"authentik.api",
|
"authentik.api",
|
||||||
"authentik.crypto",
|
"authentik.crypto",
|
||||||
"authentik.interfaces",
|
|
||||||
"authentik.events",
|
"authentik.events",
|
||||||
"authentik.flows",
|
"authentik.flows",
|
||||||
"authentik.lib",
|
"authentik.lib",
|
||||||
@ -493,12 +492,3 @@ if DEBUG:
|
|||||||
INSTALLED_APPS.append("authentik.core")
|
INSTALLED_APPS.append("authentik.core")
|
||||||
|
|
||||||
CONFIG.log("info", "Booting authentik", version=__version__)
|
CONFIG.log("info", "Booting authentik", version=__version__)
|
||||||
|
|
||||||
# Attempt to load enterprise app, if available
|
|
||||||
try:
|
|
||||||
importlib.import_module("authentik.enterprise.apps")
|
|
||||||
CONFIG.log("info", "Enabled authentik enterprise")
|
|
||||||
INSTALLED_APPS.append("authentik.enterprise")
|
|
||||||
_update_settings("authentik.enterprise.settings")
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
@ -63,8 +63,8 @@ def ldap_sync_password(sender, user: User, password: str, **_):
|
|||||||
if not sources.exists():
|
if not sources.exists():
|
||||||
return
|
return
|
||||||
source = sources.first()
|
source = sources.first()
|
||||||
changer = LDAPPasswordChanger(source)
|
|
||||||
try:
|
try:
|
||||||
|
changer = LDAPPasswordChanger(source)
|
||||||
changer.change_password(user, password)
|
changer.change_password(user, password)
|
||||||
except LDAPOperationResult as exc:
|
except LDAPOperationResult as exc:
|
||||||
Event.new(
|
Event.new(
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
import django.contrib.postgres.fields
|
import django.contrib.postgres.fields
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import authentik.lib.generators
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
@ -32,8 +32,7 @@ from authentik.flows.planner import (
|
|||||||
)
|
)
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
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.providers.saml.utils.encoding import nice64
|
from authentik.providers.saml.utils.encoding import nice64
|
||||||
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat
|
||||||
@ -41,7 +40,11 @@ from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource
|
|||||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
||||||
from authentik.sources.saml.processors.request import RequestProcessor
|
from authentik.sources.saml.processors.request import RequestProcessor
|
||||||
from authentik.sources.saml.processors.response import ResponseProcessor
|
from authentik.sources.saml.processors.response import ResponseProcessor
|
||||||
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_HEADER, ConsentStageView
|
from authentik.stages.consent.stage import (
|
||||||
|
PLAN_CONTEXT_CONSENT_HEADER,
|
||||||
|
PLAN_CONTEXT_CONSENT_TITLE,
|
||||||
|
ConsentStageView,
|
||||||
|
)
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -73,7 +76,7 @@ class InitiateView(View):
|
|||||||
# 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(
|
||||||
{
|
{
|
||||||
@ -92,9 +95,9 @@ class InitiateView(View):
|
|||||||
for stage in stages_to_append:
|
for stage in stages_to_append:
|
||||||
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=source.pre_authentication_flow.slug,
|
flow_slug=source.pre_authentication_flow.slug,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -125,6 +128,7 @@ class InitiateView(View):
|
|||||||
injected_stages = []
|
injected_stages = []
|
||||||
plan_kwargs = {
|
plan_kwargs = {
|
||||||
PLAN_CONTEXT_TITLE: f"Redirecting to {source.name}...",
|
PLAN_CONTEXT_TITLE: f"Redirecting to {source.name}...",
|
||||||
|
PLAN_CONTEXT_CONSENT_TITLE: f"Redirecting to {source.name}...",
|
||||||
PLAN_CONTEXT_ATTRS: {
|
PLAN_CONTEXT_ATTRS: {
|
||||||
"SAMLRequest": saml_request,
|
"SAMLRequest": saml_request,
|
||||||
"RelayState": relay_state,
|
"RelayState": relay_state,
|
||||||
|
@ -10,6 +10,7 @@ from duo_client.admin import Admin
|
|||||||
from duo_client.auth import Auth
|
from duo_client.auth import Auth
|
||||||
from rest_framework.serializers import BaseSerializer, Serializer
|
from rest_framework.serializers import BaseSerializer, Serializer
|
||||||
|
|
||||||
|
from authentik import __version__
|
||||||
from authentik.core.types import UserSettingSerializer
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
from authentik.flows.models import ConfigurableStage, FriendlyNamedStage, Stage
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.models import SerializerModel
|
||||||
|
@ -17,7 +17,6 @@ from authentik.flows.challenge import (
|
|||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage
|
||||||
from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER
|
from authentik.stages.authenticator_totp.settings import OTP_TOTP_ISSUER
|
||||||
from authentik.tenants.utils import get_tenant
|
|
||||||
|
|
||||||
SESSION_TOTP_DEVICE = "totp_device"
|
SESSION_TOTP_DEVICE = "totp_device"
|
||||||
|
|
||||||
@ -58,7 +57,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
|
|||||||
data={
|
data={
|
||||||
"type": ChallengeTypes.NATIVE.value,
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
"config_url": device.config_url.replace(
|
"config_url": device.config_url.replace(
|
||||||
OTP_TOTP_ISSUER, quote(get_tenant(self.request).branding_title)
|
OTP_TOTP_ISSUER, quote(self.request.tenant.branding_title)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -33,7 +33,7 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate
|
|||||||
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
||||||
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
|
from authentik.stages.authenticator_webauthn.stage import SESSION_KEY_WEBAUTHN_CHALLENGE
|
||||||
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
||||||
from authentik.tenants.utils import get_tenant
|
from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_TITLE
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -134,6 +134,12 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
|
|||||||
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
||||||
if not device:
|
if not device:
|
||||||
raise ValidationError("Invalid device")
|
raise ValidationError("Invalid device")
|
||||||
|
# We can only check the device's user if the user we're given isn't anonymous
|
||||||
|
# as this validation is also used for password-less login where webauthn is the very first
|
||||||
|
# step done by a user. Only if this validation happens at a later stage we can check
|
||||||
|
# that the device belongs to the user
|
||||||
|
if not user.is_anonymous and device.user != user:
|
||||||
|
raise ValidationError("Invalid device")
|
||||||
|
|
||||||
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
|
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
|
||||||
|
|
||||||
@ -175,6 +181,8 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
|
|||||||
pushinfo = {
|
pushinfo = {
|
||||||
__("Domain"): stage_view.request.get_host(),
|
__("Domain"): stage_view.request.get_host(),
|
||||||
}
|
}
|
||||||
|
if PLAN_CONTEXT_CONSENT_TITLE in stage_view.executor.plan.context:
|
||||||
|
pushinfo[__("Title")] = stage_view.executor.plan.context[PLAN_CONTEXT_CONSENT_TITLE]
|
||||||
if SESSION_KEY_APPLICATION_PRE in stage_view.request.session:
|
if SESSION_KEY_APPLICATION_PRE in stage_view.request.session:
|
||||||
pushinfo[__("Application")] = stage_view.request.session.get(
|
pushinfo[__("Application")] = stage_view.request.session.get(
|
||||||
SESSION_KEY_APPLICATION_PRE, Application()
|
SESSION_KEY_APPLICATION_PRE, Application()
|
||||||
@ -188,7 +196,7 @@ def validate_challenge_duo(device_pk: int, stage_view: StageView, user: User) ->
|
|||||||
type=__(
|
type=__(
|
||||||
"%(brand_name)s Login request"
|
"%(brand_name)s Login request"
|
||||||
% {
|
% {
|
||||||
"brand_name": get_tenant(stage_view.request).branding_title,
|
"brand_name": stage_view.request.tenant.branding_title,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
display_username=user.username,
|
display_username=user.username,
|
||||||
|
@ -14,6 +14,7 @@ from rest_framework.serializers import ValidationError
|
|||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
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
|
||||||
|
from authentik.events.utils import cleanse_dict, sanitize_dict
|
||||||
from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
|
from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge
|
||||||
from authentik.flows.exceptions import FlowSkipStageException
|
from authentik.flows.exceptions import FlowSkipStageException
|
||||||
from authentik.flows.models import FlowDesignation, NotConfiguredAction, Stage
|
from authentik.flows.models import FlowDesignation, NotConfiguredAction, Stage
|
||||||
@ -36,9 +37,9 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME
|
|||||||
|
|
||||||
COOKIE_NAME_MFA = "authentik_mfa"
|
COOKIE_NAME_MFA = "authentik_mfa"
|
||||||
|
|
||||||
SESSION_KEY_STAGES = "authentik/stages/authenticator_validate/stages"
|
PLAN_CONTEXT_STAGES = "goauthentik.io/stages/authenticator_validate/stages"
|
||||||
SESSION_KEY_SELECTED_STAGE = "authentik/stages/authenticator_validate/selected_stage"
|
PLAN_CONTEXT_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage"
|
||||||
SESSION_KEY_DEVICE_CHALLENGES = "authentik/stages/authenticator_validate/device_challenges"
|
PLAN_CONTEXT_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges"
|
||||||
|
|
||||||
|
|
||||||
class SelectableStageSerializer(PassiveSerializer):
|
class SelectableStageSerializer(PassiveSerializer):
|
||||||
@ -72,8 +73,8 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||||||
component = CharField(default="ak-stage-authenticator-validate")
|
component = CharField(default="ak-stage-authenticator-validate")
|
||||||
|
|
||||||
def _challenge_allowed(self, classes: list):
|
def _challenge_allowed(self, classes: list):
|
||||||
device_challenges: list[dict] = self.stage.request.session.get(
|
device_challenges: list[dict] = self.stage.executor.plan.context.get(
|
||||||
SESSION_KEY_DEVICE_CHALLENGES, []
|
PLAN_CONTEXT_DEVICE_CHALLENGES, []
|
||||||
)
|
)
|
||||||
if not any(x["device_class"] in classes for x in device_challenges):
|
if not any(x["device_class"] in classes for x in device_challenges):
|
||||||
raise ValidationError("No compatible device class allowed")
|
raise ValidationError("No compatible device class allowed")
|
||||||
@ -103,7 +104,9 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||||||
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
|
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
|
||||||
# First check if the challenge is valid
|
# First check if the challenge is valid
|
||||||
allowed = False
|
allowed = False
|
||||||
for device_challenge in self.stage.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []):
|
for device_challenge in self.stage.executor.plan.context.get(
|
||||||
|
PLAN_CONTEXT_DEVICE_CHALLENGES, []
|
||||||
|
):
|
||||||
if device_challenge.get("device_class", "") == challenge.get(
|
if device_challenge.get("device_class", "") == challenge.get(
|
||||||
"device_class", ""
|
"device_class", ""
|
||||||
) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""):
|
) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""):
|
||||||
@ -121,11 +124,11 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||||||
|
|
||||||
def validate_selected_stage(self, stage_pk: str) -> str:
|
def validate_selected_stage(self, stage_pk: str) -> str:
|
||||||
"""Check that the selected stage is valid"""
|
"""Check that the selected stage is valid"""
|
||||||
stages = self.stage.request.session.get(SESSION_KEY_STAGES, [])
|
stages = self.stage.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
|
||||||
if not any(str(stage.pk) == stage_pk for stage in stages):
|
if not any(str(stage.pk) == stage_pk for stage in stages):
|
||||||
raise ValidationError("Selected stage is invalid")
|
raise ValidationError("Selected stage is invalid")
|
||||||
self.stage.logger.debug("Setting selected stage to ", stage=stage_pk)
|
self.stage.logger.debug("Setting selected stage to ", stage=stage_pk)
|
||||||
self.stage.request.session[SESSION_KEY_SELECTED_STAGE] = stage_pk
|
self.stage.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = stage_pk
|
||||||
return stage_pk
|
return stage_pk
|
||||||
|
|
||||||
def validate(self, attrs: dict):
|
def validate(self, attrs: dict):
|
||||||
@ -230,7 +233,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
else:
|
else:
|
||||||
self.logger.debug("No pending user, continuing")
|
self.logger.debug("No pending user, continuing")
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
self.request.session[SESSION_KEY_DEVICE_CHALLENGES] = challenges
|
self.executor.plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = challenges
|
||||||
|
|
||||||
# No allowed devices
|
# No allowed devices
|
||||||
if len(challenges) < 1:
|
if len(challenges) < 1:
|
||||||
@ -263,23 +266,23 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
if stage.configuration_stages.count() == 1:
|
if stage.configuration_stages.count() == 1:
|
||||||
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
|
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
|
||||||
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
|
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
|
||||||
self.request.session[SESSION_KEY_SELECTED_STAGE] = next_stage
|
self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage
|
||||||
# Because that normal execution only happens on post, we directly inject it here and
|
# Because that normal execution only happens on post, we directly inject it here and
|
||||||
# return it
|
# return it
|
||||||
self.executor.plan.insert_stage(next_stage)
|
self.executor.plan.insert_stage(next_stage)
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
|
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
|
||||||
self.request.session[SESSION_KEY_STAGES] = stages
|
self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages
|
||||||
return super().get(self.request, *args, **kwargs)
|
return super().get(self.request, *args, **kwargs)
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
res = super().post(request, *args, **kwargs)
|
res = super().post(request, *args, **kwargs)
|
||||||
if (
|
if (
|
||||||
SESSION_KEY_SELECTED_STAGE in self.request.session
|
PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
|
||||||
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
|
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
|
||||||
):
|
):
|
||||||
self.logger.debug("Got selected stage in session, running that")
|
self.logger.debug("Got selected stage in context, running that")
|
||||||
stage_pk = self.request.session.get(SESSION_KEY_SELECTED_STAGE)
|
stage_pk = self.executor.plan.context(PLAN_CONTEXT_SELECTED_STAGE)
|
||||||
# Because the foreign key to stage.configuration_stage points to
|
# Because the foreign key to stage.configuration_stage points to
|
||||||
# a base stage class, we need to do another lookup
|
# a base stage class, we need to do another lookup
|
||||||
stage = Stage.objects.get_subclass(pk=stage_pk)
|
stage = Stage.objects.get_subclass(pk=stage_pk)
|
||||||
@ -290,8 +293,8 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
def get_challenge(self) -> AuthenticatorValidationChallenge:
|
def get_challenge(self) -> AuthenticatorValidationChallenge:
|
||||||
challenges = self.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, [])
|
challenges = self.executor.plan.context.get(PLAN_CONTEXT_DEVICE_CHALLENGES, [])
|
||||||
stages = self.request.session.get(SESSION_KEY_STAGES, [])
|
stages = self.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
|
||||||
stage_challenges = []
|
stage_challenges = []
|
||||||
for stage in stages:
|
for stage in stages:
|
||||||
serializer = SelectableStageSerializer(
|
serializer = SelectableStageSerializer(
|
||||||
@ -306,6 +309,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
stage_challenges.append(serializer.data)
|
stage_challenges.append(serializer.data)
|
||||||
return AuthenticatorValidationChallenge(
|
return AuthenticatorValidationChallenge(
|
||||||
data={
|
data={
|
||||||
|
"component": "ak-stage-authenticator-validate",
|
||||||
"type": ChallengeTypes.NATIVE.value,
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
"device_challenges": challenges,
|
"device_challenges": challenges,
|
||||||
"configuration_stages": stage_challenges,
|
"configuration_stages": stage_challenges,
|
||||||
@ -381,12 +385,11 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||||||
self.logger.debug("Set user from user-less flow", user=webauthn_device.user)
|
self.logger.debug("Set user from user-less flow", user=webauthn_device.user)
|
||||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = webauthn_device.user
|
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = webauthn_device.user
|
||||||
self.executor.plan.context[PLAN_CONTEXT_METHOD] = "auth_webauthn_pwl"
|
self.executor.plan.context[PLAN_CONTEXT_METHOD] = "auth_webauthn_pwl"
|
||||||
self.executor.plan.context[PLAN_CONTEXT_METHOD_ARGS] = {
|
self.executor.plan.context[PLAN_CONTEXT_METHOD_ARGS] = cleanse_dict(
|
||||||
"device": webauthn_device,
|
sanitize_dict(
|
||||||
}
|
{
|
||||||
|
"device": webauthn_device,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
return self.set_valid_mfa_cookie(response.device)
|
return self.set_valid_mfa_cookie(response.device)
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self.request.session.pop(SESSION_KEY_STAGES, None)
|
|
||||||
self.request.session.pop(SESSION_KEY_SELECTED_STAGE, None)
|
|
||||||
self.request.session.pop(SESSION_KEY_DEVICE_CHALLENGES, None)
|
|
||||||
|
@ -19,7 +19,7 @@ from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, Duo
|
|||||||
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
|
from authentik.stages.authenticator_validate.challenge import validate_challenge_duo
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from authentik.stages.user_login.models import UserLoginStage
|
from authentik.stages.user_login.models import UserLoginStage
|
||||||
from authentik.tenants.utils import lookup_tenant_for_request
|
from authentik.tenants.utils import get_tenant_for_request
|
||||||
|
|
||||||
|
|
||||||
class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
||||||
@ -36,7 +36,7 @@ class AuthenticatorValidateStageDuoTests(FlowTestCase):
|
|||||||
middleware = SessionMiddleware(dummy_get_response)
|
middleware = SessionMiddleware(dummy_get_response)
|
||||||
middleware.process_request(request)
|
middleware.process_request(request)
|
||||||
request.session.save()
|
request.session.save()
|
||||||
setattr(request, "tenant", lookup_tenant_for_request(request))
|
setattr(request, "tenant", get_tenant_for_request(request))
|
||||||
|
|
||||||
stage = AuthenticatorDuoStage.objects.create(
|
stage = AuthenticatorDuoStage.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
|
@ -1,26 +1,19 @@
|
|||||||
"""Test validator stage"""
|
"""Test validator stage"""
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.exceptions import ValidationError
|
|
||||||
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
|
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
|
||||||
from authentik.flows.planner import FlowPlan
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.stage import StageView
|
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView
|
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id, generate_key
|
||||||
from authentik.lib.tests.utils import dummy_get_response
|
|
||||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||||
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from authentik.stages.authenticator_validate.stage import (
|
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
|
||||||
SESSION_KEY_DEVICE_CHALLENGES,
|
|
||||||
AuthenticatorValidationChallengeResponse,
|
|
||||||
)
|
|
||||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||||
|
|
||||||
|
|
||||||
@ -86,12 +79,17 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
|||||||
|
|
||||||
def test_validate_selected_challenge(self):
|
def test_validate_selected_challenge(self):
|
||||||
"""Test validate_selected_challenge"""
|
"""Test validate_selected_challenge"""
|
||||||
# Prepare request with session
|
flow = create_test_flow()
|
||||||
request = self.request_factory.get("/")
|
stage = AuthenticatorValidateStage.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||||
|
device_classes=[DeviceClasses.STATIC, DeviceClasses.TOTP],
|
||||||
|
)
|
||||||
|
|
||||||
middleware = SessionMiddleware(dummy_get_response)
|
session = self.client.session
|
||||||
middleware.process_request(request)
|
plan = FlowPlan(flow_pk=flow.pk.hex)
|
||||||
request.session[SESSION_KEY_DEVICE_CHALLENGES] = [
|
plan.append_stage(stage)
|
||||||
|
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
|
||||||
{
|
{
|
||||||
"device_class": "static",
|
"device_class": "static",
|
||||||
"device_uid": "1",
|
"device_uid": "1",
|
||||||
@ -101,23 +99,43 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
|||||||
"device_uid": "2",
|
"device_uid": "2",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
request.session.save()
|
session[SESSION_KEY_PLAN] = plan
|
||||||
|
session.save()
|
||||||
|
|
||||||
res = AuthenticatorValidationChallengeResponse()
|
response = self.client.post(
|
||||||
res.stage = StageView(FlowExecutorView())
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
res.stage.request = request
|
data={
|
||||||
with self.assertRaises(ValidationError):
|
"selected_challenge": {
|
||||||
res.validate_selected_challenge(
|
|
||||||
{
|
|
||||||
"device_class": "baz",
|
"device_class": "baz",
|
||||||
"device_uid": "quox",
|
"device_uid": "quox",
|
||||||
|
"challenge": {},
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
res.validate_selected_challenge(
|
)
|
||||||
{
|
self.assertStageResponse(
|
||||||
"device_class": "static",
|
response,
|
||||||
"device_uid": "1",
|
flow,
|
||||||
}
|
response_errors={
|
||||||
|
"selected_challenge": [{"string": "invalid challenge selected", "code": "invalid"}]
|
||||||
|
},
|
||||||
|
component="ak-stage-authenticator-validate",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
|
data={
|
||||||
|
"selected_challenge": {
|
||||||
|
"device_class": "static",
|
||||||
|
"device_uid": "1",
|
||||||
|
"challenge": {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertStageResponse(
|
||||||
|
response,
|
||||||
|
flow,
|
||||||
|
response_errors={"non_field_errors": [{"string": "Empty response", "code": "invalid"}]},
|
||||||
|
component="ak-stage-authenticator-validate",
|
||||||
)
|
)
|
||||||
|
|
||||||
@patch(
|
@patch(
|
||||||
|
@ -22,7 +22,7 @@ from authentik.stages.authenticator_validate.challenge import (
|
|||||||
)
|
)
|
||||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||||
from authentik.stages.authenticator_validate.stage import (
|
from authentik.stages.authenticator_validate.stage import (
|
||||||
SESSION_KEY_DEVICE_CHALLENGES,
|
PLAN_CONTEXT_DEVICE_CHALLENGES,
|
||||||
AuthenticatorValidateStageView,
|
AuthenticatorValidateStageView,
|
||||||
)
|
)
|
||||||
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
||||||
@ -211,14 +211,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
plan.append_stage(UserLoginStage(name=generate_id()))
|
plan.append_stage(UserLoginStage(name=generate_id()))
|
||||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||||
session[SESSION_KEY_PLAN] = plan
|
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
|
||||||
session[SESSION_KEY_DEVICE_CHALLENGES] = [
|
|
||||||
{
|
{
|
||||||
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
||||||
"device_uid": device.pk,
|
"device_uid": device.pk,
|
||||||
"challenge": {},
|
"challenge": {},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
||||||
)
|
)
|
||||||
@ -283,14 +283,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||||||
plan = FlowPlan(flow_pk=flow.pk.hex)
|
plan = FlowPlan(flow_pk=flow.pk.hex)
|
||||||
plan.append_stage(stage)
|
plan.append_stage(stage)
|
||||||
plan.append_stage(UserLoginStage(name=generate_id()))
|
plan.append_stage(UserLoginStage(name=generate_id()))
|
||||||
session[SESSION_KEY_PLAN] = plan
|
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
|
||||||
session[SESSION_KEY_DEVICE_CHALLENGES] = [
|
|
||||||
{
|
{
|
||||||
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
||||||
"device_uid": device.pk,
|
"device_uid": device.pk,
|
||||||
"challenge": {},
|
"challenge": {},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
session[SESSION_KEY_PLAN] = plan
|
||||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||||
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
||||||
)
|
)
|
||||||
|
@ -29,7 +29,6 @@ from authentik.flows.challenge import (
|
|||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
|
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
|
||||||
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
|
||||||
from authentik.tenants.utils import get_tenant
|
|
||||||
|
|
||||||
SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge"
|
SESSION_KEY_WEBAUTHN_CHALLENGE = "authentik/stages/authenticator_webauthn/challenge"
|
||||||
|
|
||||||
@ -93,7 +92,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||||||
|
|
||||||
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
|
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
|
||||||
rp_id=get_rp_id(self.request),
|
rp_id=get_rp_id(self.request),
|
||||||
rp_name=get_tenant(self.request).branding_title,
|
rp_name=self.request.tenant.branding_title,
|
||||||
user_id=user.uid,
|
user_id=user.uid,
|
||||||
user_name=user.username,
|
user_name=user.username,
|
||||||
user_display_name=user.name,
|
user_display_name=user.name,
|
||||||
|
@ -19,6 +19,7 @@ from authentik.lib.utils.time import timedelta_from_string
|
|||||||
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
|
from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
|
||||||
|
|
||||||
PLAN_CONTEXT_CONSENT = "consent"
|
PLAN_CONTEXT_CONSENT = "consent"
|
||||||
|
PLAN_CONTEXT_CONSENT_TITLE = "consent_title"
|
||||||
PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
|
PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
|
||||||
PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
|
PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
|
||||||
PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS = "consent_additional_permissions"
|
PLAN_CONTEXT_CONSENT_EXTRA_PERMISSIONS = "consent_additional_permissions"
|
||||||
@ -58,6 +59,8 @@ class ConsentStageView(ChallengeStageView):
|
|||||||
),
|
),
|
||||||
"token": token,
|
"token": token,
|
||||||
}
|
}
|
||||||
|
if PLAN_CONTEXT_CONSENT_TITLE in self.executor.plan.context:
|
||||||
|
data["title"] = self.executor.plan.context[PLAN_CONTEXT_CONSENT_TITLE]
|
||||||
if PLAN_CONTEXT_CONSENT_HEADER in self.executor.plan.context:
|
if PLAN_CONTEXT_CONSENT_HEADER in self.executor.plan.context:
|
||||||
data["header_text"] = self.executor.plan.context[PLAN_CONTEXT_CONSENT_HEADER]
|
data["header_text"] = self.executor.plan.context[PLAN_CONTEXT_CONSENT_HEADER]
|
||||||
challenge = ConsentChallenge(data=data)
|
challenge = ConsentChallenge(data=data)
|
||||||
|
@ -3,6 +3,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.urls import reverse
|
||||||
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
|
||||||
@ -15,8 +16,6 @@ from authentik.flows.models import FlowToken
|
|||||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
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
|
||||||
@ -48,10 +47,9 @@ class EmailStageView(ChallengeStageView):
|
|||||||
|
|
||||||
def get_full_url(self, **kwargs) -> str:
|
def get_full_url(self, **kwargs) -> str:
|
||||||
"""Get full URL to be used in template"""
|
"""Get full URL to be used in template"""
|
||||||
base_url = reverse_interface(
|
base_url = reverse(
|
||||||
self.request,
|
"authentik_core:if-flow",
|
||||||
InterfaceType.FLOW,
|
kwargs={"flow_slug": self.executor.flow.slug},
|
||||||
flow_slug=self.executor.flow.slug,
|
|
||||||
)
|
)
|
||||||
relative_url = f"{base_url}?{urlencode(kwargs)}"
|
relative_url = f"{base_url}?{urlencode(kwargs)}"
|
||||||
return self.request.build_absolute_uri(relative_url)
|
return self.request.build_absolute_uri(relative_url)
|
||||||
|
@ -7,7 +7,6 @@ from django.core.mail.backends.locmem import EmailBackend
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.blueprints.tests import apply_blueprint
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.flows.markers import StageMarker
|
from authentik.flows.markers import StageMarker
|
||||||
@ -30,7 +29,6 @@ class TestEmailStageSending(APITestCase):
|
|||||||
)
|
)
|
||||||
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||||
|
|
||||||
@apply_blueprint("system/interfaces.yaml")
|
|
||||||
def test_pending_user(self):
|
def test_pending_user(self):
|
||||||
"""Test with pending user"""
|
"""Test with pending user"""
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
@ -56,7 +54,6 @@ class TestEmailStageSending(APITestCase):
|
|||||||
self.assertEqual(event.context["to_email"], [self.user.email])
|
self.assertEqual(event.context["to_email"], [self.user.email])
|
||||||
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
self.assertEqual(event.context["from_email"], "system@authentik.local")
|
||||||
|
|
||||||
@apply_blueprint("system/interfaces.yaml")
|
|
||||||
def test_send_error(self):
|
def test_send_error(self):
|
||||||
"""Test error during sending (sending will be retried)"""
|
"""Test error during sending (sending will be retried)"""
|
||||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||||
|
@ -7,7 +7,6 @@ from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
|
|
||||||
from authentik.blueprints.tests import apply_blueprint
|
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||||
from authentik.flows.markers import StageMarker
|
from authentik.flows.markers import StageMarker
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
|
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
|
||||||
@ -75,7 +74,6 @@ class TestEmailStage(FlowTestCase):
|
|||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@apply_blueprint("system/interfaces.yaml")
|
|
||||||
@patch(
|
@patch(
|
||||||
"authentik.stages.email.models.EmailStage.backend_class",
|
"authentik.stages.email.models.EmailStage.backend_class",
|
||||||
PropertyMock(return_value=EmailBackend),
|
PropertyMock(return_value=EmailBackend),
|
||||||
@ -125,7 +123,6 @@ class TestEmailStage(FlowTestCase):
|
|||||||
with self.settings(EMAIL_HOST=host):
|
with self.settings(EMAIL_HOST=host):
|
||||||
self.assertEqual(EmailStage(use_global_settings=True).backend.host, host)
|
self.assertEqual(EmailStage(use_global_settings=True).backend.host, host)
|
||||||
|
|
||||||
@apply_blueprint("system/interfaces.yaml")
|
|
||||||
def test_token(self):
|
def test_token(self):
|
||||||
"""Test with token"""
|
"""Test with token"""
|
||||||
# Make sure token exists
|
# Make sure token exists
|
||||||
|
@ -26,9 +26,8 @@ from authentik.flows.models import FlowDesignation
|
|||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
|
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
|
||||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_GET
|
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_GET
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.views import reverse_interface
|
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
|
from authentik.lib.utils.urls import reverse_with_qs
|
||||||
from authentik.sources.oauth.types.apple import AppleLoginChallenge
|
from authentik.sources.oauth.types.apple import AppleLoginChallenge
|
||||||
from authentik.sources.plex.models import PlexAuthenticationChallenge
|
from authentik.sources.plex.models import PlexAuthenticationChallenge
|
||||||
from authentik.stages.identification.models import IdentificationStage
|
from authentik.stages.identification.models import IdentificationStage
|
||||||
@ -206,25 +205,22 @@ class IdentificationStageView(ChallengeStageView):
|
|||||||
get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET)
|
get_qs = self.request.session.get(SESSION_KEY_GET, self.request.GET)
|
||||||
# Check for related enrollment and recovery flow, add URL to view
|
# Check for related enrollment and recovery flow, add URL to view
|
||||||
if current_stage.enrollment_flow:
|
if current_stage.enrollment_flow:
|
||||||
challenge.initial_data["enroll_url"] = reverse_interface(
|
challenge.initial_data["enroll_url"] = reverse_with_qs(
|
||||||
self.request,
|
"authentik_core:if-flow",
|
||||||
InterfaceType.FLOW,
|
|
||||||
query=get_qs,
|
query=get_qs,
|
||||||
flow_slug=current_stage.enrollment_flow.slug,
|
kwargs={"flow_slug": current_stage.enrollment_flow.slug},
|
||||||
)
|
)
|
||||||
if current_stage.recovery_flow:
|
if current_stage.recovery_flow:
|
||||||
challenge.initial_data["recovery_url"] = reverse_interface(
|
challenge.initial_data["recovery_url"] = reverse_with_qs(
|
||||||
self.request,
|
"authentik_core:if-flow",
|
||||||
InterfaceType.FLOW,
|
|
||||||
query=get_qs,
|
query=get_qs,
|
||||||
flow_slug=current_stage.recovery_flow.slug,
|
kwargs={"flow_slug": current_stage.recovery_flow.slug},
|
||||||
)
|
)
|
||||||
if current_stage.passwordless_flow:
|
if current_stage.passwordless_flow:
|
||||||
challenge.initial_data["passwordless_url"] = reverse_interface(
|
challenge.initial_data["passwordless_url"] = reverse_with_qs(
|
||||||
self.request,
|
"authentik_core:if-flow",
|
||||||
InterfaceType.FLOW,
|
|
||||||
query=get_qs,
|
query=get_qs,
|
||||||
flow_slug=current_stage.passwordless_flow.slug,
|
kwargs={"flow_slug": current_stage.passwordless_flow.slug},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check all enabled source, add them if they have a UI Login button.
|
# Check all enabled source, add them if they have a UI Login button.
|
||||||
|
@ -5,8 +5,6 @@ from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
|||||||
from authentik.flows.challenge import ChallengeTypes
|
from authentik.flows.challenge import ChallengeTypes
|
||||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||||
from authentik.flows.tests import FlowTestCase
|
from authentik.flows.tests import FlowTestCase
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.tests import reverse_interface
|
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||||
from authentik.stages.password import BACKEND_INBUILT
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
@ -168,9 +166,9 @@ class TestIdentificationStage(FlowTestCase):
|
|||||||
component="ak-stage-identification",
|
component="ak-stage-identification",
|
||||||
user_fields=["email"],
|
user_fields=["email"],
|
||||||
password_fields=False,
|
password_fields=False,
|
||||||
enroll_url=reverse_interface(
|
enroll_url=reverse(
|
||||||
InterfaceType.FLOW,
|
"authentik_core:if-flow",
|
||||||
flow_slug=flow.slug,
|
kwargs={"flow_slug": flow.slug},
|
||||||
),
|
),
|
||||||
show_source_labels=False,
|
show_source_labels=False,
|
||||||
primary_action="Log in",
|
primary_action="Log in",
|
||||||
@ -206,9 +204,9 @@ class TestIdentificationStage(FlowTestCase):
|
|||||||
component="ak-stage-identification",
|
component="ak-stage-identification",
|
||||||
user_fields=["email"],
|
user_fields=["email"],
|
||||||
password_fields=False,
|
password_fields=False,
|
||||||
recovery_url=reverse_interface(
|
recovery_url=reverse(
|
||||||
InterfaceType.FLOW,
|
"authentik_core:if-flow",
|
||||||
flow_slug=flow.slug,
|
kwargs={"flow_slug": flow.slug},
|
||||||
),
|
),
|
||||||
show_source_labels=False,
|
show_source_labels=False,
|
||||||
primary_action="Log in",
|
primary_action="Log in",
|
||||||
|
@ -4,7 +4,7 @@ from django.apps.registry import Apps
|
|||||||
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 authentik.stages.password import BACKEND_APP_PASSWORD
|
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT
|
||||||
|
|
||||||
|
|
||||||
def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
def update_default_backends(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Generated by Django 3.2.6 on 2021-08-23 14:34
|
# Generated by Django 3.2.6 on 2021-08-23 14:34
|
||||||
|
import django.contrib.postgres.fields
|
||||||
from django.apps.registry import Apps
|
from django.apps.registry import Apps
|
||||||
from django.db import migrations
|
from django.db import migrations, models
|
||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
from authentik.stages.password import BACKEND_INBUILT
|
from authentik.stages.password import BACKEND_INBUILT
|
||||||
|
@ -5,6 +5,7 @@ from django.contrib.auth import _clean_credentials
|
|||||||
from django.contrib.auth.backends import BaseBackend
|
from django.contrib.auth.backends import BaseBackend
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from rest_framework.exceptions import ErrorDetail, ValidationError
|
from rest_framework.exceptions import ErrorDetail, ValidationError
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
@ -22,8 +23,6 @@ from authentik.flows.challenge import (
|
|||||||
from authentik.flows.models import Flow, FlowDesignation, Stage
|
from authentik.flows.models import Flow, FlowDesignation, Stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.interfaces.models import InterfaceType
|
|
||||||
from authentik.interfaces.views import reverse_interface
|
|
||||||
from authentik.lib.utils.reflection import path_to_class
|
from authentik.lib.utils.reflection import path_to_class
|
||||||
from authentik.stages.password.models import PasswordStage
|
from authentik.stages.password.models import PasswordStage
|
||||||
|
|
||||||
@ -96,12 +95,11 @@ class PasswordStageView(ChallengeStageView):
|
|||||||
"type": ChallengeTypes.NATIVE.value,
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY).first()
|
recovery_flow = Flow.objects.filter(designation=FlowDesignation.RECOVERY)
|
||||||
if recovery_flow:
|
if recovery_flow.exists():
|
||||||
recover_url = reverse_interface(
|
recover_url = reverse(
|
||||||
self.request,
|
"authentik_core:if-flow",
|
||||||
InterfaceType.FLOW,
|
kwargs={"flow_slug": recovery_flow.first().slug},
|
||||||
flow_slug=recovery_flow.slug,
|
|
||||||
)
|
)
|
||||||
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url)
|
challenge.initial_data["recovery_url"] = self.request.build_absolute_uri(recover_url)
|
||||||
return challenge
|
return challenge
|
||||||
|
@ -57,12 +57,10 @@ class PromptSerializer(ModelSerializer):
|
|||||||
"type",
|
"type",
|
||||||
"required",
|
"required",
|
||||||
"placeholder",
|
"placeholder",
|
||||||
"initial_value",
|
|
||||||
"order",
|
"order",
|
||||||
"promptstage_set",
|
"promptstage_set",
|
||||||
"sub_text",
|
"sub_text",
|
||||||
"placeholder_expression",
|
"placeholder_expression",
|
||||||
"initial_value_expression",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user