Compare commits
253 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
| e1bae1240f | |||
| 37bd62d291 | |||
| ac63db0136 | |||
| 5cdf3a09a9 | |||
| 3e17adf33f | |||
| 8392916c84 | |||
| 7e75a48fd0 | |||
| d69d84e48c | |||
| 78cc8fa498 | |||
| 0fcdf5e968 | |||
| f05997740f | |||
| 1aff300171 | |||
| ffb98eaa75 | |||
| 5c1db432f0 | |||
| 07fd4daa3e | |||
| 9faad8a055 | |||
| a94392808f | |||
| c4998e7dd4 | |||
| 1ab587d80e | |||
| 5715ffd845 | |||
| 8c3834e6b2 | |||
| f841586153 | |||
| b8b681250f | |||
| 3ab9ee5acc | |||
| 1a4c640835 | |||
| 38bf0ee740 | |||
| 520fb2fac1 | |||
| 95adc38ff4 | |||
| 55ad2d7eab | |||
| 8160663214 | |||
| aa80babfff | |||
| 6a700cb376 | |||
| e123afd9ee | |||
| 96e732e45b | |||
| 6349ab60e7 | |||
| 2b0749af6b | |||
| a5098364eb | |||
| 71820191a3 | |||
| c08c849fec | |||
| 6a74fa11c6 | |||
| 7841720acf | |||
| 67644ace87 | |||
| f84a10b59b | |||
| 200d6d6adf | |||
| d0f1ebfad3 | |||
| 7d849d7bd7 | |||
| f1dfe04786 | |||
| 4d7d2b8d3a | |||
| a6cc0f189c | |||
| 18a4eac527 | |||
| 6dd2e2b85f | |||
| 7bfea87864 | |||
| 1ca8feb5fc | |||
| c1615d044b | |||
| edc9d60e22 | |||
| e6b135d535 | |||
| 8cfad9a854 | |||
| 2237358633 | |||
| d15cd9ce5f | |||
| 62abe22673 | |||
| 8b78570597 | |||
| 549e4dcb94 | |||
| 1480ff6732 | |||
| 0e1000764d | |||
| 8dc9b43bb5 | |||
| 3ce0aa54c7 | |||
| b5888e79f5 | |||
| 25d779e879 | |||
| d1fbb85821 | |||
| ea307689d4 | |||
| 7a06c1685b | |||
| 977757f561 | |||
| c117d98e27 | |||
| 711e98d049 | |||
| f84c176bd0 | |||
| c4b11ca861 | |||
| 132a353b92 | |||
| bb464aad50 | |||
| ab27cd0a9a | |||
| 241280f2b5 | |||
| d110b5b661 | |||
| 8871a4acb2 | |||
| a1ad357abd | |||
| 81f9842797 | |||
| 712256cdfe | |||
| fb4808418c | |||
| 7c7bb9dc2e | |||
| 9a3809135e | |||
| de13265997 | |||
| 0228ea9a4c | |||
| faf986c231 | |||
| 315eae009f | |||
| 02f75a92ce | |||
| a92786e153 | |||
| 157c23946e | |||
| f6b33d65af | |||
| ce461631b5 | |||
| 2f106a9049 | |||
| 7038431e19 | |||
| 3fd9b53fe6 | |||
| e542783fec | |||
| adcd11b1f8 | |||
| 6192d01b7e | |||
| fd2677af1f | |||
| 5947c7b97e | |||
| 986d7bf714 | |||
| 6282e923d6 | |||
| 88b4125a6a | |||
| 208c2d1913 | |||
| 54dc0a46b4 | |||
| fc807744bf | |||
| 9666d407b4 | |||
| 75510ead84 | |||
| 73bf6fd530 | |||
| 2e5a33f0c2 | |||
| 8c33d13dff | |||
| a70de69228 | |||
| ab2d39dd2a | |||
| 2084156f1d | |||
| 1d2725825c | |||
| b9754f9c13 | |||
| bb2e5b4861 | |||
| 89abc99dc0 | |||
| f92c661d09 | |||
| 3468afc399 | |||
| a286ae276b | |||
| 4fdd978b57 | |||
| c52bd8c4b9 | |||
| ca5ae5f914 | |||
| 4604c92046 | |||
| 4218ece2a5 | |||
| 0d6481c4d5 | |||
| a7fc579202 | |||
| 5600261852 | |||
| 824737965d | |||
| 5476f517da | |||
| d38043fe72 | |||
| 102570c61a | |||
| 238e6e3f24 | |||
| 89c7e61769 | |||
| b097cf4d7e | |||
| 5c0d7f9a58 | |||
| 95b99e3e55 | |||
| 6437fbc814 | |||
| d6fa19a97f | |||
| 1957717160 | |||
| 94a93adb4b | |||
| 5d84f2a079 | |||
| 5b9f35a4a1 | |||
| b3dd87bbab | |||
| af7189953c | |||
| 35d2e9cd5f | |||
| 9a52d8db83 | |||
| 14f0034a0a | |||
| 20522558fe | |||
| f00ee5c174 | |||
| 95e24c9ec2 | |||
| 6b42e404bf | |||
| 9abd4b3e14 | |||
| 865138e7e7 | |||
| 7524413b22 | |||
| 70bdbfd5ef | |||
| 73a7c0c559 | |||
| cafff808ab | |||
| bbbbc2a718 | |||
| 1452f2680a | |||
| dd39aab1fb | |||
| 524fbd5838 | |||
| bb7c3456fa | |||
| b611fd10a2 | |||
| 65b1cbc010 | |||
| 119f64159b | |||
| 1352ed7e44 | |||
| 34ce85fcd1 | |||
| 977ae4f225 | |||
| a464ffe846 | |||
| 6757d43d33 | |||
| da3222df07 | |||
| 54cacd784c | |||
| 32840d3909 | |||
| eb78632853 | |||
| 4868d4a14d | |||
| 3f5effb1bc | |||
| 84c2da8a6e | |||
| 56744659e4 | |||
| bad7deb52a | |||
| 5748e19845 | |||
| 16a03160d0 | |||
| a566856b65 | |||
| 8b52d711e8 | |||
| 4da18b5f0c | |||
| 63e3f6545b | |||
| e35c3d19bc | |||
| ef028af7d1 | |||
| b69c26d485 | |||
| e13cfec84f | |||
| 97df7848a5 | |||
| e2d3a95c80 | |||
| bebf18f257 | |||
| 53e68b8540 | |||
| 9dbd54690c | |||
| 9e41b7d208 | |||
| 1c66d420c4 | |||
| 0ca913f8d4 | |||
| b97274058c | |||
| aef0333695 | |||
| c847b16b3e | |||
| e2e83f5631 | |||
| 8363016982 | |||
| 397b9845ec | |||
| b9da24c952 | |||
| 1053962bec | |||
| 19ff8129e5 | |||
| 40cdf6877d | |||
| 2a399cf8e8 | |||
| 345fa1bed6 | |||
| 70ffb6d49e | |||
| 3ecdcebd35 | |||
| 4f02c8ab98 | |||
| 41974c3f82 | |||
| 808f697423 | |||
| a9dc3ff0d8 | |||
| acde584cbd | |||
| df52116135 | |||
| eaf56f4f3f | |||
| fd9293e3e8 | |||
| 520de8d5b0 | |||
| bbdb0df42e | |||
| 9310d4cdc0 | |||
| 86f9056d3f | |||
| 5375637eda | |||
| 109f06c3ae | |||
| a3744da3a5 | |||
| ff1feb653b | |||
| 4a11d89a08 | |||
| 73d7b5f110 | |||
| 8b7a92068b | |||
| ff1532da13 | |||
| 6eafa2346d | |||
| 681644b854 | |||
| de4d388e0a | |||
| cbe2cb51e7 | |||
| 9176c71075 | |||
| 1c05e4ca09 | |||
| 2d55d3c743 | |||
| 0a9482b28a | |||
| 4b1440944e | |||
| 75794defc6 | |||
| 59a92dbacd | |||
| b81ddf2b80 | |||
| 9ccd1ce08b | |||
| 6f6d22da13 | |||
| 095850f038 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2023.2.2
|
current_version = 2023.4.2
|
||||||
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+)
|
||||||
|
|||||||
@ -7,8 +7,14 @@ charset = utf-8
|
|||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[html]
|
[*.html]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[yaml]
|
[*.{yaml,yml}]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|||||||
6
.github/actions/setup/action.yml
vendored
6
.github/actions/setup/action.yml
vendored
@ -1,6 +1,11 @@
|
|||||||
name: 'Setup authentik testing environment'
|
name: 'Setup authentik testing environment'
|
||||||
description: 'Setup authentik testing environment'
|
description: 'Setup authentik testing environment'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
postgresql_tag:
|
||||||
|
description: "Optional postgresql image tag"
|
||||||
|
default: "12"
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
@ -24,6 +29,7 @@ runs:
|
|||||||
- name: Setup dependencies
|
- name: Setup dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
export PSQL_TAG=${{ inputs.postgresql_tag }}
|
||||||
docker-compose -f .github/actions/setup/docker-compose.yml up -d
|
docker-compose -f .github/actions/setup/docker-compose.yml up -d
|
||||||
poetry env use python3.11
|
poetry env use python3.11
|
||||||
poetry install
|
poetry install
|
||||||
|
|||||||
2
.github/actions/setup/docker-compose.yml
vendored
2
.github/actions/setup/docker-compose.yml
vendored
@ -3,7 +3,7 @@ version: '3.7'
|
|||||||
services:
|
services:
|
||||||
postgresql:
|
postgresql:
|
||||||
container_name: postgres
|
container_name: postgres
|
||||||
image: library/postgres:12
|
image: library/postgres:${PSQL_TAG:-12}
|
||||||
volumes:
|
volumes:
|
||||||
- db-data:/var/lib/postgresql/data
|
- db-data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
11
.github/codecov.yml
vendored
11
.github/codecov.yml
vendored
@ -1,3 +1,10 @@
|
|||||||
coverage:
|
coverage:
|
||||||
precision: 2
|
status:
|
||||||
round: up
|
project:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
# adjust accordingly based on how flaky your tests are
|
||||||
|
# this allows a 1% drop from the previous base commit coverage
|
||||||
|
threshold: 1%
|
||||||
|
notify:
|
||||||
|
after_n_builds: 3
|
||||||
|
|||||||
1
.github/codespell-dictionary.txt
vendored
Normal file
1
.github/codespell-dictionary.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
authentic->authentik
|
||||||
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@ -16,3 +16,4 @@ markComment: >
|
|||||||
This issue has been automatically marked as stale because it has not had
|
This issue has been automatically marked as stale because it has not had
|
||||||
recent activity. It will be closed if no further activity occurs. Thank you
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
for your contributions.
|
for your contributions.
|
||||||
|
only: issues
|
||||||
|
|||||||
14
.github/workflows/ci-main.yml
vendored
14
.github/workflows/ci-main.yml
vendored
@ -29,6 +29,7 @@ jobs:
|
|||||||
- bandit
|
- bandit
|
||||||
- pyright
|
- pyright
|
||||||
- pending-migrations
|
- pending-migrations
|
||||||
|
- codespell
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -59,7 +60,7 @@ jobs:
|
|||||||
cp authentik/lib/default.yml local.env.yml
|
cp authentik/lib/default.yml local.env.yml
|
||||||
cp -R .github ..
|
cp -R .github ..
|
||||||
cp -R scripts ..
|
cp -R scripts ..
|
||||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
|
||||||
rm -rf .github/ scripts/
|
rm -rf .github/ scripts/
|
||||||
mv ../.github ../scripts .
|
mv ../.github ../scripts .
|
||||||
- name: Setup authentik env (ensure stable deps are installed)
|
- name: Setup authentik env (ensure stable deps are installed)
|
||||||
@ -79,12 +80,21 @@ jobs:
|
|||||||
- name: migrate to latest
|
- name: migrate to latest
|
||||||
run: poetry run python -m lifecycle.migrate
|
run: poetry run python -m lifecycle.migrate
|
||||||
test-unittest:
|
test-unittest:
|
||||||
|
name: test-unittest - PostgreSQL ${{ matrix.psql }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
psql:
|
||||||
|
- 11-alpine
|
||||||
|
- 12-alpine
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Setup authentik env
|
- name: Setup authentik env
|
||||||
uses: ./.github/actions/setup
|
uses: ./.github/actions/setup
|
||||||
|
with:
|
||||||
|
postgresql_tag: ${{ matrix.psql }}
|
||||||
- name: run unittest
|
- name: run unittest
|
||||||
run: |
|
run: |
|
||||||
poetry run make test
|
poetry run make test
|
||||||
@ -128,6 +138,8 @@ jobs:
|
|||||||
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
glob: tests/e2e/test_provider_saml* tests/e2e/test_source_saml*
|
||||||
- name: ldap
|
- name: ldap
|
||||||
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
glob: tests/e2e/test_provider_ldap* tests/e2e/test_source_ldap*
|
||||||
|
- name: radius
|
||||||
|
glob: tests/e2e/test_provider_radius*
|
||||||
- name: flows
|
- name: flows
|
||||||
glob: tests/e2e/test_flows*
|
glob: tests/e2e/test_flows*
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
14
.github/workflows/ci-outpost.yml
vendored
14
.github/workflows/ci-outpost.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "^1.17"
|
go-version: "^1.17"
|
||||||
- name: Prepare and generate API
|
- name: Prepare and generate API
|
||||||
@ -34,7 +34,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "^1.17"
|
go-version: "^1.17"
|
||||||
- name: Generate API
|
- name: Generate API
|
||||||
@ -59,8 +59,9 @@ jobs:
|
|||||||
type:
|
type:
|
||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
|
- radius
|
||||||
arch:
|
arch:
|
||||||
- 'linux/amd64'
|
- "linux/amd64"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@ -106,17 +107,18 @@ jobs:
|
|||||||
type:
|
type:
|
||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
|
- radius
|
||||||
goos: [linux]
|
goos: [linux]
|
||||||
goarch: [amd64, arm64]
|
goarch: [amd64, arm64]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "^1.17"
|
go-version: "^1.17"
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: "18"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
- name: Generate API
|
- name: Generate API
|
||||||
run: make gen-client-go
|
run: make gen-client-go
|
||||||
|
|||||||
22
.github/workflows/ci-website.yml
vendored
22
.github/workflows/ci-website.yml
vendored
@ -39,10 +39,32 @@ jobs:
|
|||||||
- name: test
|
- name: test
|
||||||
working-directory: website/
|
working-directory: website/
|
||||||
run: npm test
|
run: npm test
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: ${{ matrix.job }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- build
|
||||||
|
- build-docs-only
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3.6.0
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: website/package-lock.json
|
||||||
|
- working-directory: website/
|
||||||
|
run: npm ci
|
||||||
|
- name: build
|
||||||
|
working-directory: website/
|
||||||
|
run: npm run ${{ matrix.job }}
|
||||||
ci-website-mark:
|
ci-website-mark:
|
||||||
needs:
|
needs:
|
||||||
- lint-prettier
|
- lint-prettier
|
||||||
- test
|
- test
|
||||||
|
- build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: echo mark
|
- run: echo mark
|
||||||
|
|||||||
7
.github/workflows/ghcr-retention.yml
vendored
7
.github/workflows/ghcr-retention.yml
vendored
@ -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
|
||||||
|
|||||||
6
.github/workflows/release-publish.yml
vendored
6
.github/workflows/release-publish.yml
vendored
@ -52,9 +52,10 @@ jobs:
|
|||||||
type:
|
type:
|
||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
|
- radius
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "^1.17"
|
go-version: "^1.17"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
@ -99,11 +100,12 @@ jobs:
|
|||||||
type:
|
type:
|
||||||
- proxy
|
- proxy
|
||||||
- ldap
|
- ldap
|
||||||
|
- radius
|
||||||
goos: [linux, darwin]
|
goos: [linux, darwin]
|
||||||
goarch: [amd64, arm64]
|
goarch: [amd64, arm64]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "^1.17"
|
go-version: "^1.17"
|
||||||
- uses: actions/setup-node@v3.6.0
|
- uses: actions/setup-node@v3.6.0
|
||||||
|
|||||||
9
.github/workflows/release-tag.yml
vendored
9
.github/workflows/release-tag.yml
vendored
@ -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 }}
|
||||||
|
|||||||
11
.github/workflows/translation-compile.yml
vendored
11
.github/workflows/translation-compile.yml
vendored
@ -18,18 +18,23 @@ 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
|
||||||
run: poetry run ./manage.py compilemessages
|
run: poetry run ./manage.py compilemessages
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v4
|
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"
|
||||||
|
|||||||
15
.github/workflows/web-api-publish.yml
vendored
15
.github/workflows/web-api-publish.yml
vendored
@ -9,9 +9,14 @@ 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: '18'
|
node-version: '18'
|
||||||
@ -30,10 +35,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
|
||||||
npm i @goauthentik/api@$VERSION
|
npm i @goauthentik/api@$VERSION
|
||||||
- uses: peter-evans/create-pull-request@v4
|
- 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"
|
||||||
@ -42,8 +47,8 @@ jobs:
|
|||||||
signoff: true
|
signoff: true
|
||||||
team-reviewers: "@goauthentik/core"
|
team-reviewers: "@goauthentik/core"
|
||||||
author: authentik bot <github-bot@goauthentik.io>
|
author: authentik bot <github-bot@goauthentik.io>
|
||||||
- uses: peter-evans/enable-pull-request-automerge@v2
|
- 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
|
||||||
|
|||||||
@ -20,6 +20,7 @@ The following is a set of guidelines for contributing to authentik and its compo
|
|||||||
- [Reporting Bugs](#reporting-bugs)
|
- [Reporting Bugs](#reporting-bugs)
|
||||||
- [Suggesting Enhancements](#suggesting-enhancements)
|
- [Suggesting Enhancements](#suggesting-enhancements)
|
||||||
- [Your First Code Contribution](#your-first-code-contribution)
|
- [Your First Code Contribution](#your-first-code-contribution)
|
||||||
|
- [Help with the Docs](#help-with-the-docs)
|
||||||
- [Pull Requests](#pull-requests)
|
- [Pull Requests](#pull-requests)
|
||||||
|
|
||||||
[Styleguides](#styleguides)
|
[Styleguides](#styleguides)
|
||||||
@ -135,6 +136,9 @@ authentik can be run locally, all though depending on which part you want to wor
|
|||||||
|
|
||||||
This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github)
|
This is documented in the [developer docs](https://goauthentik.io/developer-docs/?utm_source=github)
|
||||||
|
|
||||||
|
### Help with the Docs
|
||||||
|
Contributions to the technical documentation are greatly appreciated. Open a PR if you have improvements to make or new content to add. If you have questions or suggestions about the documentation, open an Issue. No contribution is too small.
|
||||||
|
|
||||||
### Pull Requests
|
### Pull Requests
|
||||||
|
|
||||||
The process described here has several goals:
|
The process described here has several goals:
|
||||||
|
|||||||
10
Dockerfile
10
Dockerfile
@ -20,7 +20,7 @@ WORKDIR /work/web
|
|||||||
RUN npm ci && npm run build
|
RUN npm ci && npm run build
|
||||||
|
|
||||||
# Stage 3: Poetry to requirements.txt export
|
# Stage 3: Poetry to requirements.txt export
|
||||||
FROM docker.io/python:3.11.2-slim-bullseye AS poetry-locker
|
FROM docker.io/python:3.11.3-slim-bullseye AS poetry-locker
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
COPY ./pyproject.toml /work
|
COPY ./pyproject.toml /work
|
||||||
@ -31,7 +31,7 @@ RUN pip install --no-cache-dir poetry && \
|
|||||||
poetry export -f requirements.txt --dev --output requirements-dev.txt
|
poetry export -f requirements.txt --dev --output requirements-dev.txt
|
||||||
|
|
||||||
# Stage 4: Build go proxy
|
# Stage 4: Build go proxy
|
||||||
FROM docker.io/golang:1.20.2-bullseye AS go-builder
|
FROM docker.io/golang:1.20.3-bullseye AS go-builder
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ COPY ./go.sum /work/go.sum
|
|||||||
RUN go build -o /work/authentik ./cmd/server/
|
RUN go build -o /work/authentik ./cmd/server/
|
||||||
|
|
||||||
# Stage 5: MaxMind GeoIP
|
# Stage 5: MaxMind GeoIP
|
||||||
FROM docker.io/maxmindinc/geoipupdate:v4.10 as geoip
|
FROM docker.io/maxmindinc/geoipupdate:v5.0 as geoip
|
||||||
|
|
||||||
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
|
ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City"
|
||||||
ENV GEOIPUPDATE_VERBOSE="true"
|
ENV GEOIPUPDATE_VERBOSE="true"
|
||||||
@ -62,7 +62,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
|||||||
"
|
"
|
||||||
|
|
||||||
# Stage 6: Run
|
# Stage 6: Run
|
||||||
FROM docker.io/python:3.11.2-slim-bullseye AS final-image
|
FROM docker.io/python:3.11.3-slim-bullseye AS final-image
|
||||||
|
|
||||||
LABEL org.opencontainers.image.url https://goauthentik.io
|
LABEL org.opencontainers.image.url https://goauthentik.io
|
||||||
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
|
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
|
||||||
@ -102,7 +102,7 @@ COPY ./tests /tests
|
|||||||
COPY ./manage.py /
|
COPY ./manage.py /
|
||||||
COPY ./blueprints /blueprints
|
COPY ./blueprints /blueprints
|
||||||
COPY ./lifecycle/ /lifecycle
|
COPY ./lifecycle/ /lifecycle
|
||||||
COPY --from=go-builder /work/authentik /authentik-proxy
|
COPY --from=go-builder /work/authentik /bin/authentik
|
||||||
COPY --from=web-builder /work/web/dist/ /web/dist/
|
COPY --from=web-builder /work/web/dist/ /web/dist/
|
||||||
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
COPY --from=web-builder /work/web/authentik/ /web/authentik/
|
||||||
COPY --from=website-builder /work/website/help/ /website/help/
|
COPY --from=website-builder /work/website/help/ /website/help/
|
||||||
|
|||||||
57
Makefile
57
Makefile
@ -4,6 +4,20 @@ 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)
|
||||||
|
|
||||||
|
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
|
||||||
|
-I .github/codespell-words.txt \
|
||||||
|
-S 'web/src/locales/**' \
|
||||||
|
authentik \
|
||||||
|
internal \
|
||||||
|
cmd \
|
||||||
|
web/src \
|
||||||
|
website/src \
|
||||||
|
website/blog \
|
||||||
|
website/developer-docs \
|
||||||
|
website/docs \
|
||||||
|
website/integrations \
|
||||||
|
website/src
|
||||||
|
|
||||||
all: lint-fix lint test gen web
|
all: lint-fix lint test gen web
|
||||||
|
|
||||||
test-go:
|
test-go:
|
||||||
@ -26,14 +40,7 @@ test:
|
|||||||
lint-fix:
|
lint-fix:
|
||||||
isort authentik tests scripts lifecycle
|
isort authentik tests scripts lifecycle
|
||||||
black authentik tests scripts lifecycle
|
black authentik tests scripts lifecycle
|
||||||
codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w \
|
codespell -w $(CODESPELL_ARGS)
|
||||||
authentik \
|
|
||||||
internal \
|
|
||||||
cmd \
|
|
||||||
web/src \
|
|
||||||
website/src \
|
|
||||||
website/docs \
|
|
||||||
website/developer-docs
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
pylint authentik tests lifecycle
|
pylint authentik tests lifecycle
|
||||||
@ -43,9 +50,6 @@ lint:
|
|||||||
migrate:
|
migrate:
|
||||||
python -m lifecycle.migrate
|
python -m lifecycle.migrate
|
||||||
|
|
||||||
run:
|
|
||||||
go run -v ./cmd/server/
|
|
||||||
|
|
||||||
i18n-extract: i18n-extract-core web-extract
|
i18n-extract: i18n-extract-core web-extract
|
||||||
|
|
||||||
i18n-extract-core:
|
i18n-extract-core:
|
||||||
@ -59,15 +63,20 @@ gen-build:
|
|||||||
AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json
|
AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json
|
||||||
AUTHENTIK_DEBUG=true ak spectacular --file schema.yml
|
AUTHENTIK_DEBUG=true ak spectacular --file schema.yml
|
||||||
|
|
||||||
|
gen-changelog:
|
||||||
|
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
|
||||||
|
npx prettier --write changelog.md
|
||||||
|
|
||||||
gen-diff:
|
gen-diff:
|
||||||
git show $(shell git describe --abbrev=0):schema.yml > old_schema.yml
|
git show $(shell git describe --tags $(shell git rev-list --tags --max-count=1)):schema.yml > old_schema.yml
|
||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-diff:2.1.0-beta.3 \
|
docker.io/openapitools/openapi-diff:2.1.0-beta.6 \
|
||||||
--markdown /local/diff.md \
|
--markdown /local/diff.md \
|
||||||
/local/old_schema.yml /local/schema.yml
|
/local/old_schema.yml /local/schema.yml
|
||||||
rm old_schema.yml
|
rm old_schema.yml
|
||||||
|
npx prettier --write diff.md
|
||||||
|
|
||||||
gen-clean:
|
gen-clean:
|
||||||
rm -rf web/api/src/
|
rm -rf web/api/src/
|
||||||
@ -77,7 +86,7 @@ gen-client-ts:
|
|||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-generator-cli:v6.0.0 generate \
|
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g typescript-fetch \
|
-g typescript-fetch \
|
||||||
-o /local/gen-ts-api \
|
-o /local/gen-ts-api \
|
||||||
@ -90,20 +99,21 @@ gen-client-ts:
|
|||||||
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api
|
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api
|
||||||
|
|
||||||
gen-client-go:
|
gen-client-go:
|
||||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
|
mkdir -p ./gen-go-api ./gen-go-api/templates
|
||||||
mkdir -p templates
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./gen-go-api/config.yaml
|
||||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./gen-go-api/templates/README.mustache
|
||||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O ./gen-go-api/templates/go.mod.mustache
|
||||||
|
cp schema.yml ./gen-go-api/
|
||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}/gen-go-api:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
docker.io/openapitools/openapi-generator-cli:v6.0.0 generate \
|
docker.io/openapitools/openapi-generator-cli:v6.5.0 generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g go \
|
-g go \
|
||||||
-o /local/gen-go-api \
|
-o /local/ \
|
||||||
-c /local/config.yaml
|
-c /local/config.yaml
|
||||||
go mod edit -replace goauthentik.io/api/v3=./gen-go-api
|
go mod edit -replace goauthentik.io/api/v3=./gen-go-api
|
||||||
rm -rf config.yaml ./templates/
|
rm -rf ./gen-go-api/config.yaml ./gen-go-api/templates/
|
||||||
|
|
||||||
gen-dev-config:
|
gen-dev-config:
|
||||||
python -m scripts.generate_config
|
python -m scripts.generate_config
|
||||||
@ -172,6 +182,9 @@ ci-pylint: ci--meta-debug
|
|||||||
ci-black: ci--meta-debug
|
ci-black: ci--meta-debug
|
||||||
black --check $(PY_SOURCES)
|
black --check $(PY_SOURCES)
|
||||||
|
|
||||||
|
ci-codespell: ci--meta-debug
|
||||||
|
codespell $(CODESPELL_ARGS) -s
|
||||||
|
|
||||||
ci-isort: ci--meta-debug
|
ci-isort: ci--meta-debug
|
||||||
isort --check $(PY_SOURCES)
|
isort --check $(PY_SOURCES)
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@ -15,13 +15,13 @@
|
|||||||
|
|
||||||
## What is authentik?
|
## What is authentik?
|
||||||
|
|
||||||
authentik is an open-source Identity Provider focused on flexibility and versatility. You can use authentik in an existing environment to add support for new protocols. authentik is also a great solution for implementing signup/recovery/etc in your application, so you don't have to deal with it.
|
Authentik is an open-source Identity Provider that emphasizes flexibility and versatility. It can be seamlessly integrated into existing environments to support new protocols. Authentik is also a great solution for implementing sign-up, recovery, and other similar features in your application, saving you the hassle of dealing with them.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
For small/test setups it is recommended to use docker-compose, see the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github)
|
For small/test setups it is recommended to use Docker Compose; refer to the [documentation](https://goauthentik.io/docs/installation/docker-compose/?utm_source=github).
|
||||||
|
|
||||||
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github)
|
For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/helm). This is documented [here](https://goauthentik.io/docs/installation/kubernetes/?utm_source=github).
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@ -32,15 +32,15 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See [Development Documentation](https://goauthentik.io/developer-docs/?utm_source=github)
|
See [Developer Documentation](https://goauthentik.io/developer-docs/?utm_source=github)
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
See [SECURITY.md](SECURITY.md)
|
See [SECURITY.md](SECURITY.md)
|
||||||
|
|
||||||
## Support
|
## Adoption and Contributions
|
||||||
|
|
||||||
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR!
|
Your organization uses authentik? We'd love to add your logo to the readme and our website! Email us @ hello@goauthentik.io or open a GitHub Issue/PR! For more information on how to contribute to authentik, please refer to our [CONTRIBUTING.md file](./CONTRIBUTING.md).
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,8 @@ Authentik takes security very seriously. We follow the rules of [responsible dis
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| --------- | ------------------ |
|
| --------- | ------------------ |
|
||||||
| 2022.12.x | :white_check_mark: |
|
|
||||||
| 2023.1.x | :white_check_mark: |
|
|
||||||
| 2023.2.x | :white_check_mark: |
|
| 2023.2.x | :white_check_mark: |
|
||||||
|
| 2023.3.x | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
from os import environ
|
from os import environ
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
__version__ = "2023.2.2"
|
__version__ = "2023.4.2"
|
||||||
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
|
||||||
@ -34,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()
|
||||||
@ -43,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 = {}
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -7,82 +7,13 @@ API Browser - {{ tenant.branding_title }}
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script type="module" src="{% static 'dist/rapidoc-min.js' %}"></script>
|
<script src="{% static 'dist/standalone/api-browser/index.js' %}?version={{ version }}" type="module"></script>
|
||||||
<script>
|
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
|
||||||
function getCookie(name) {
|
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
|
||||||
let cookieValue = "";
|
<link rel="icon" href="{{ tenant.branding_favicon }}">
|
||||||
if (document.cookie && document.cookie !== "") {
|
<link rel="shortcut icon" href="{{ tenant.branding_favicon }}">
|
||||||
const cookies = document.cookie.split(";");
|
|
||||||
for (let i = 0; i < cookies.length; i++) {
|
|
||||||
const cookie = cookies[i].trim();
|
|
||||||
// Does this cookie string begin with the name we want?
|
|
||||||
if (cookie.substring(0, name.length + 1) === name + "=") {
|
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cookieValue;
|
|
||||||
}
|
|
||||||
window.addEventListener('DOMContentLoaded', (event) => {
|
|
||||||
const rapidocEl = document.querySelector('rapi-doc');
|
|
||||||
rapidocEl.addEventListener('before-try', (e) => {
|
|
||||||
e.detail.request.headers.append('X-authentik-CSRF', getCookie("authentik_csrf"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style>
|
|
||||||
img.logo {
|
|
||||||
width: 100%;
|
|
||||||
padding: 1rem 0.5rem 1.5rem 0.5rem;
|
|
||||||
min-height: 48px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<rapi-doc
|
<ak-api-browser schemaPath="{{ path }}"></ak-api-browser>
|
||||||
spec-url="{{ path }}"
|
|
||||||
heading-text=""
|
|
||||||
theme="light"
|
|
||||||
render-style="read"
|
|
||||||
default-schema-tab="schema"
|
|
||||||
primary-color="#fd4b2d"
|
|
||||||
nav-bg-color="#212427"
|
|
||||||
bg-color="#000000"
|
|
||||||
text-color="#000000"
|
|
||||||
nav-text-color="#ffffff"
|
|
||||||
nav-hover-bg-color="#3c3f42"
|
|
||||||
nav-accent-color="#4f5255"
|
|
||||||
nav-hover-text-color="#ffffff"
|
|
||||||
use-path-in-nav-bar="true"
|
|
||||||
nav-item-spacing="relaxed"
|
|
||||||
allow-server-selection="false"
|
|
||||||
show-header="false"
|
|
||||||
allow-spec-url-load="false"
|
|
||||||
allow-spec-file-load="false">
|
|
||||||
<div slot="nav-logo">
|
|
||||||
<img alt="authentik Logo" class="logo" src="{% static 'dist/assets/icons/icon_left_brand.png' %}" />
|
|
||||||
</div>
|
|
||||||
</rapi-doc>
|
|
||||||
<script>
|
|
||||||
const rapidoc = document.querySelector("rapi-doc");
|
|
||||||
const matcher = window.matchMedia("(prefers-color-scheme: light)");
|
|
||||||
const changer = (ev) => {
|
|
||||||
const style = getComputedStyle(document.documentElement);
|
|
||||||
let bg, text = "";
|
|
||||||
if (matcher.matches) {
|
|
||||||
bg = style.getPropertyValue('--pf-global--BackgroundColor--light-300');
|
|
||||||
text = style.getPropertyValue('--pf-global--Color--300');
|
|
||||||
} else {
|
|
||||||
bg = style.getPropertyValue('--ak-dark-background');
|
|
||||||
text = style.getPropertyValue('--ak-dark-foreground');
|
|
||||||
}
|
|
||||||
rapidoc.attributes.getNamedItem("bg-color").value = bg.trim();
|
|
||||||
rapidoc.attributes.getNamedItem("text-color").value = text.trim();
|
|
||||||
rapidoc.requestUpdate();
|
|
||||||
};
|
|
||||||
matcher.addEventListener("change", changer);
|
|
||||||
window.addEventListener("load", changer);
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -56,6 +56,7 @@ from authentik.providers.oauth2.api.tokens import (
|
|||||||
RefreshTokenViewSet,
|
RefreshTokenViewSet,
|
||||||
)
|
)
|
||||||
from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
|
from authentik.providers.proxy.api import ProxyOutpostConfigViewSet, ProxyProviderViewSet
|
||||||
|
from authentik.providers.radius.api import RadiusOutpostConfigViewSet, RadiusProviderViewSet
|
||||||
from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet
|
from authentik.providers.saml.api.property_mapping import SAMLPropertyMappingViewSet
|
||||||
from authentik.providers.saml.api.providers import SAMLProviderViewSet
|
from authentik.providers.saml.api.providers import SAMLProviderViewSet
|
||||||
from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet
|
from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet
|
||||||
@ -128,6 +129,7 @@ router.register("outposts/service_connections/docker", DockerServiceConnectionVi
|
|||||||
router.register("outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet)
|
router.register("outposts/service_connections/kubernetes", KubernetesServiceConnectionViewSet)
|
||||||
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
|
router.register("outposts/proxy", ProxyOutpostConfigViewSet)
|
||||||
router.register("outposts/ldap", LDAPOutpostConfigViewSet)
|
router.register("outposts/ldap", LDAPOutpostConfigViewSet)
|
||||||
|
router.register("outposts/radius", RadiusOutpostConfigViewSet)
|
||||||
|
|
||||||
router.register("flows/instances", FlowViewSet)
|
router.register("flows/instances", FlowViewSet)
|
||||||
router.register("flows/bindings", FlowStageBindingViewSet)
|
router.register("flows/bindings", FlowStageBindingViewSet)
|
||||||
@ -166,6 +168,7 @@ router.register("providers/proxy", ProxyProviderViewSet)
|
|||||||
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
router.register("providers/oauth2", OAuth2ProviderViewSet)
|
||||||
router.register("providers/saml", SAMLProviderViewSet)
|
router.register("providers/saml", SAMLProviderViewSet)
|
||||||
router.register("providers/scim", SCIMProviderViewSet)
|
router.register("providers/scim", SCIMProviderViewSet)
|
||||||
|
router.register("providers/radius", RadiusProviderViewSet)
|
||||||
|
|
||||||
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
|
router.register("oauth2/authorization_codes", AuthorizationCodeViewSet)
|
||||||
router.register("oauth2/refresh_tokens", RefreshTokenViewSet)
|
router.register("oauth2/refresh_tokens", RefreshTokenViewSet)
|
||||||
|
|||||||
@ -55,11 +55,11 @@ class AuthentikBlueprintsConfig(ManagedAppConfig):
|
|||||||
"""Load v1 tasks"""
|
"""Load v1 tasks"""
|
||||||
self.import_module("authentik.blueprints.v1.tasks")
|
self.import_module("authentik.blueprints.v1.tasks")
|
||||||
|
|
||||||
def reconcile_blueprints_discover(self):
|
def reconcile_blueprints_discovery(self):
|
||||||
"""Run blueprint discovery"""
|
"""Run blueprint discovery"""
|
||||||
from authentik.blueprints.v1.tasks import blueprints_discover, clear_failed_blueprints
|
from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints
|
||||||
|
|
||||||
blueprints_discover.delay()
|
blueprints_discovery.delay()
|
||||||
clear_failed_blueprints.delay()
|
clear_failed_blueprints.delay()
|
||||||
|
|
||||||
def import_models(self):
|
def import_models(self):
|
||||||
|
|||||||
@ -19,10 +19,8 @@ class Command(BaseCommand):
|
|||||||
for blueprint_path in options.get("blueprints", []):
|
for blueprint_path in options.get("blueprints", []):
|
||||||
content = BlueprintInstance(path=blueprint_path).retrieve()
|
content = BlueprintInstance(path=blueprint_path).retrieve()
|
||||||
importer = Importer(content)
|
importer = Importer(content)
|
||||||
valid, logs = importer.validate()
|
valid, _ = importer.validate()
|
||||||
if not valid:
|
if not valid:
|
||||||
for log in logs:
|
|
||||||
getattr(LOGGER, log.pop("log_level"))(**log)
|
|
||||||
self.stderr.write("blueprint invalid")
|
self.stderr.write("blueprint invalid")
|
||||||
sys_exit(1)
|
sys_exit(1)
|
||||||
importer.apply()
|
importer.apply()
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from authentik.lib.utils.time import fqdn_rand
|
|||||||
|
|
||||||
CELERY_BEAT_SCHEDULE = {
|
CELERY_BEAT_SCHEDULE = {
|
||||||
"blueprints_v1_discover": {
|
"blueprints_v1_discover": {
|
||||||
"task": "authentik.blueprints.v1.tasks.blueprints_discover",
|
"task": "authentik.blueprints.v1.tasks.blueprints_discovery",
|
||||||
"schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
|
"schedule": crontab(minute=fqdn_rand("blueprints_v1_discover"), hour="*"),
|
||||||
"options": {"queue": "authentik_scheduled"},
|
"options": {"queue": "authentik_scheduled"},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
"""Blueprint helpers"""
|
"""Blueprint helpers"""
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pathlib import Path
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
@ -45,13 +44,3 @@ def reconcile_app(app_name: str):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
return wrapper_outer
|
return wrapper_outer
|
||||||
|
|
||||||
|
|
||||||
def load_yaml_fixture(path: str, **kwargs) -> str:
|
|
||||||
"""Load yaml fixture, optionally formatting it with kwargs"""
|
|
||||||
with open(Path(__file__).resolve().parent / Path(path), "r", encoding="utf-8") as _fixture:
|
|
||||||
fixture = _fixture.read()
|
|
||||||
try:
|
|
||||||
return fixture % kwargs
|
|
||||||
except TypeError:
|
|
||||||
return fixture
|
|
||||||
|
|||||||
@ -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))
|
||||||
@ -3,12 +3,12 @@ from os import environ
|
|||||||
|
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
from authentik.blueprints.tests import load_yaml_fixture
|
|
||||||
from authentik.blueprints.v1.exporter import FlowExporter
|
from authentik.blueprints.v1.exporter import FlowExporter
|
||||||
from authentik.blueprints.v1.importer import Importer, transaction_rollback
|
from authentik.blueprints.v1.importer import Importer, transaction_rollback
|
||||||
from authentik.core.models import Group
|
from authentik.core.models import Group
|
||||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.lib.tests.utils import load_fixture
|
||||||
from authentik.policies.expression.models import ExpressionPolicy
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
@ -113,14 +113,14 @@ class TestBlueprintsV1(TransactionTestCase):
|
|||||||
"""Test export and import it twice"""
|
"""Test export and import it twice"""
|
||||||
count_initial = Prompt.objects.filter(field_key="username").count()
|
count_initial = Prompt.objects.filter(field_key="username").count()
|
||||||
|
|
||||||
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml"))
|
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
|
|
||||||
count_before = Prompt.objects.filter(field_key="username").count()
|
count_before = Prompt.objects.filter(field_key="username").count()
|
||||||
self.assertEqual(count_initial + 1, count_before)
|
self.assertEqual(count_initial + 1, count_before)
|
||||||
|
|
||||||
importer = Importer(load_yaml_fixture("fixtures/static_prompt_export.yaml"))
|
importer = Importer(load_fixture("fixtures/static_prompt_export.yaml"))
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
|
|
||||||
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
|
self.assertEqual(Prompt.objects.filter(field_key="username").count(), count_before)
|
||||||
@ -130,7 +130,7 @@ class TestBlueprintsV1(TransactionTestCase):
|
|||||||
ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete()
|
ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").delete()
|
||||||
Group.objects.filter(name="test").delete()
|
Group.objects.filter(name="test").delete()
|
||||||
environ["foo"] = generate_id()
|
environ["foo"] = generate_id()
|
||||||
importer = Importer(load_yaml_fixture("fixtures/tags.yaml"), {"bar": "baz"})
|
importer = Importer(load_fixture("fixtures/tags.yaml"), {"bar": "baz"})
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first()
|
policy = ExpressionPolicy.objects.filter(name="foo-bar-baz-qux").first()
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
"""Test blueprints v1"""
|
"""Test blueprints v1"""
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
from authentik.blueprints.tests import load_yaml_fixture
|
|
||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.lib.tests.utils import load_fixture
|
||||||
|
|
||||||
|
|
||||||
class TestBlueprintsV1Conditions(TransactionTestCase):
|
class TestBlueprintsV1Conditions(TransactionTestCase):
|
||||||
@ -14,7 +14,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
|
|||||||
"""Test conditions fulfilled"""
|
"""Test conditions fulfilled"""
|
||||||
flow_slug1 = generate_id()
|
flow_slug1 = generate_id()
|
||||||
flow_slug2 = generate_id()
|
flow_slug2 = generate_id()
|
||||||
import_yaml = load_yaml_fixture(
|
import_yaml = load_fixture(
|
||||||
"fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
"fixtures/conditions_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ class TestBlueprintsV1Conditions(TransactionTestCase):
|
|||||||
"""Test conditions not fulfilled"""
|
"""Test conditions not fulfilled"""
|
||||||
flow_slug1 = generate_id()
|
flow_slug1 = generate_id()
|
||||||
flow_slug2 = generate_id()
|
flow_slug2 = generate_id()
|
||||||
import_yaml = load_yaml_fixture(
|
import_yaml = load_fixture(
|
||||||
"fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
"fixtures/conditions_not_fulfilled.yaml", id1=flow_slug1, id2=flow_slug2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
"""Test blueprints v1"""
|
"""Test blueprints v1"""
|
||||||
from django.test import TransactionTestCase
|
from django.test import TransactionTestCase
|
||||||
|
|
||||||
from authentik.blueprints.tests import load_yaml_fixture
|
|
||||||
from authentik.blueprints.v1.importer import Importer
|
from authentik.blueprints.v1.importer import Importer
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.lib.tests.utils import load_fixture
|
||||||
|
|
||||||
|
|
||||||
class TestBlueprintsV1State(TransactionTestCase):
|
class TestBlueprintsV1State(TransactionTestCase):
|
||||||
@ -13,7 +13,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
|||||||
def test_state_present(self):
|
def test_state_present(self):
|
||||||
"""Test state present"""
|
"""Test state present"""
|
||||||
flow_slug = generate_id()
|
flow_slug = generate_id()
|
||||||
import_yaml = load_yaml_fixture("fixtures/state_present.yaml", id=flow_slug)
|
import_yaml = load_fixture("fixtures/state_present.yaml", id=flow_slug)
|
||||||
|
|
||||||
importer = Importer(import_yaml)
|
importer = Importer(import_yaml)
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
@ -39,7 +39,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
|||||||
def test_state_created(self):
|
def test_state_created(self):
|
||||||
"""Test state created"""
|
"""Test state created"""
|
||||||
flow_slug = generate_id()
|
flow_slug = generate_id()
|
||||||
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug)
|
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
|
||||||
|
|
||||||
importer = Importer(import_yaml)
|
importer = Importer(import_yaml)
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
@ -65,7 +65,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
|||||||
def test_state_absent(self):
|
def test_state_absent(self):
|
||||||
"""Test state absent"""
|
"""Test state absent"""
|
||||||
flow_slug = generate_id()
|
flow_slug = generate_id()
|
||||||
import_yaml = load_yaml_fixture("fixtures/state_created.yaml", id=flow_slug)
|
import_yaml = load_fixture("fixtures/state_created.yaml", id=flow_slug)
|
||||||
|
|
||||||
importer = Importer(import_yaml)
|
importer = Importer(import_yaml)
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
@ -74,7 +74,7 @@ class TestBlueprintsV1State(TransactionTestCase):
|
|||||||
flow: Flow = Flow.objects.filter(slug=flow_slug).first()
|
flow: Flow = Flow.objects.filter(slug=flow_slug).first()
|
||||||
self.assertEqual(flow.slug, flow_slug)
|
self.assertEqual(flow.slug, flow_slug)
|
||||||
|
|
||||||
import_yaml = load_yaml_fixture("fixtures/state_absent.yaml", id=flow_slug)
|
import_yaml = load_fixture("fixtures/state_absent.yaml", id=flow_slug)
|
||||||
importer = Importer(import_yaml)
|
importer = Importer(import_yaml)
|
||||||
self.assertTrue(importer.validate()[0])
|
self.assertTrue(importer.validate()[0])
|
||||||
self.assertTrue(importer.apply())
|
self.assertTrue(importer.apply())
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from django.test import TransactionTestCase
|
|||||||
from yaml import dump
|
from yaml import dump
|
||||||
|
|
||||||
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
|
from authentik.blueprints.models import BlueprintInstance, BlueprintInstanceStatus
|
||||||
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discover, blueprints_find
|
from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_discovery, blueprints_find
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
|||||||
file.seek(0)
|
file.seek(0)
|
||||||
file_hash = sha512(file.read().encode()).hexdigest()
|
file_hash = sha512(file.read().encode()).hexdigest()
|
||||||
file.flush()
|
file.flush()
|
||||||
blueprints_discover() # pylint: disable=no-value-for-parameter
|
blueprints_discovery() # pylint: disable=no-value-for-parameter
|
||||||
instance = BlueprintInstance.objects.filter(name=blueprint_id).first()
|
instance = BlueprintInstance.objects.filter(name=blueprint_id).first()
|
||||||
self.assertEqual(instance.last_applied_hash, file_hash)
|
self.assertEqual(instance.last_applied_hash, file_hash)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -81,7 +81,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
file.flush()
|
file.flush()
|
||||||
blueprints_discover() # pylint: disable=no-value-for-parameter
|
blueprints_discovery() # pylint: disable=no-value-for-parameter
|
||||||
blueprint = BlueprintInstance.objects.filter(name="foo").first()
|
blueprint = BlueprintInstance.objects.filter(name="foo").first()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
blueprint.last_applied_hash,
|
blueprint.last_applied_hash,
|
||||||
@ -106,7 +106,7 @@ class TestBlueprintsV1Tasks(TransactionTestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
file.flush()
|
file.flush()
|
||||||
blueprints_discover() # pylint: disable=no-value-for-parameter
|
blueprints_discovery() # pylint: disable=no-value-for-parameter
|
||||||
blueprint.refresh_from_db()
|
blueprint.refresh_from_db()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
blueprint.last_applied_hash,
|
blueprint.last_applied_hash,
|
||||||
|
|||||||
@ -40,6 +40,10 @@ from authentik.lib.models import SerializerModel
|
|||||||
from authentik.outposts.models import OutpostServiceConnection
|
from authentik.outposts.models import OutpostServiceConnection
|
||||||
from authentik.policies.models import Policy, PolicyBindingModel
|
from authentik.policies.models import Policy, PolicyBindingModel
|
||||||
|
|
||||||
|
# Context set when the serializer is created in a blueprint context
|
||||||
|
# Update website/developer-docs/blueprints/v1/models.md when used
|
||||||
|
SERIALIZER_CONTEXT_BLUEPRINT = "blueprint_entry"
|
||||||
|
|
||||||
|
|
||||||
def is_model_allowed(model: type[Model]) -> bool:
|
def is_model_allowed(model: type[Model]) -> bool:
|
||||||
"""Check if model is allowed"""
|
"""Check if model is allowed"""
|
||||||
@ -158,7 +162,12 @@ class Importer:
|
|||||||
raise EntryInvalidError(f"Model {model} not allowed")
|
raise EntryInvalidError(f"Model {model} not allowed")
|
||||||
if issubclass(model, BaseMetaModel):
|
if issubclass(model, BaseMetaModel):
|
||||||
serializer_class: type[Serializer] = model.serializer()
|
serializer_class: type[Serializer] = model.serializer()
|
||||||
serializer = serializer_class(data=entry.get_attrs(self.__import))
|
serializer = serializer_class(
|
||||||
|
data=entry.get_attrs(self.__import),
|
||||||
|
context={
|
||||||
|
SERIALIZER_CONTEXT_BLUEPRINT: entry,
|
||||||
|
},
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
@ -217,7 +226,12 @@ class Importer:
|
|||||||
always_merger.merge(full_data, updated_identifiers)
|
always_merger.merge(full_data, updated_identifiers)
|
||||||
serializer_kwargs["data"] = full_data
|
serializer_kwargs["data"] = full_data
|
||||||
|
|
||||||
serializer: Serializer = model().serializer(**serializer_kwargs)
|
serializer: Serializer = model().serializer(
|
||||||
|
context={
|
||||||
|
SERIALIZER_CONTEXT_BLUEPRINT: entry,
|
||||||
|
},
|
||||||
|
**serializer_kwargs,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
except ValidationError as exc:
|
except ValidationError as exc:
|
||||||
|
|||||||
@ -76,7 +76,7 @@ class BlueprintEventHandler(FileSystemEventHandler):
|
|||||||
return
|
return
|
||||||
if isinstance(event, FileCreatedEvent):
|
if isinstance(event, FileCreatedEvent):
|
||||||
LOGGER.debug("new blueprint file created, starting discovery")
|
LOGGER.debug("new blueprint file created, starting discovery")
|
||||||
blueprints_discover.delay()
|
blueprints_discovery.delay()
|
||||||
if isinstance(event, FileModifiedEvent):
|
if isinstance(event, FileModifiedEvent):
|
||||||
path = Path(event.src_path)
|
path = Path(event.src_path)
|
||||||
root = Path(CONFIG.y("blueprints_dir")).absolute()
|
root = Path(CONFIG.y("blueprints_dir")).absolute()
|
||||||
@ -122,7 +122,7 @@ def blueprints_find():
|
|||||||
)
|
)
|
||||||
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
|
blueprint.meta = from_dict(BlueprintMetadata, metadata) if metadata else None
|
||||||
blueprints.append(blueprint)
|
blueprints.append(blueprint)
|
||||||
LOGGER.info(
|
LOGGER.debug(
|
||||||
"parsed & loaded blueprint",
|
"parsed & loaded blueprint",
|
||||||
hash=file_hash,
|
hash=file_hash,
|
||||||
path=str(path),
|
path=str(path),
|
||||||
@ -134,7 +134,7 @@ def blueprints_find():
|
|||||||
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
|
throws=(DatabaseError, ProgrammingError, InternalError), base=MonitoredTask, bind=True
|
||||||
)
|
)
|
||||||
@prefill_task
|
@prefill_task
|
||||||
def blueprints_discover(self: MonitoredTask):
|
def blueprints_discovery(self: MonitoredTask):
|
||||||
"""Find blueprints and check if they need to be created in the database"""
|
"""Find blueprints and check if they need to be created in the database"""
|
||||||
count = 0
|
count = 0
|
||||||
for blueprint in blueprints_find():
|
for blueprint in blueprints_find():
|
||||||
|
|||||||
@ -35,6 +35,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
"pk",
|
"pk",
|
||||||
"name",
|
"name",
|
||||||
|
"authentication_flow",
|
||||||
"authorization_flow",
|
"authorization_flow",
|
||||||
"property_mappings",
|
"property_mappings",
|
||||||
"component",
|
"component",
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from rest_framework.viewsets import ModelViewSet
|
|||||||
from authentik.api.authorization import OwnerSuperuserPermissions
|
from authentik.api.authorization import OwnerSuperuserPermissions
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.blueprints.api import ManagedSerializer
|
from authentik.blueprints.api import ManagedSerializer
|
||||||
|
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
@ -29,6 +30,11 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
|||||||
|
|
||||||
user_obj = UserSerializer(required=False, source="user", read_only=True)
|
user_obj = UserSerializer(required=False, source="user", read_only=True)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
|
||||||
|
self.fields["key"] = CharField()
|
||||||
|
|
||||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||||
"""Ensure only API or App password tokens are created."""
|
"""Ensure only API or App password tokens are created."""
|
||||||
request: Request = self.context.get("request")
|
request: Request = self.context.get("request")
|
||||||
|
|||||||
@ -67,11 +67,12 @@ 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.lib.config import CONFIG
|
||||||
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
|
||||||
@ -211,8 +212,9 @@ class UserMetricsSerializer(PassiveSerializer):
|
|||||||
def get_logins(self, _):
|
def get_logins(self, _):
|
||||||
"""Get successful logins per 8 hours for the last 7 days"""
|
"""Get successful logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
|
request = self.context["request"]
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(user, "authentik_events.view_event").filter(
|
get_objects_for_user(request.user, "authentik_events.view_event").filter(
|
||||||
action=EventAction.LOGIN, user__pk=user.pk
|
action=EventAction.LOGIN, user__pk=user.pk
|
||||||
)
|
)
|
||||||
# 3 data points per day, so 8 hour spans
|
# 3 data points per day, so 8 hour spans
|
||||||
@ -223,8 +225,9 @@ class UserMetricsSerializer(PassiveSerializer):
|
|||||||
def get_logins_failed(self, _):
|
def get_logins_failed(self, _):
|
||||||
"""Get failed logins per 8 hours for the last 7 days"""
|
"""Get failed logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
|
request = self.context["request"]
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(user, "authentik_events.view_event").filter(
|
get_objects_for_user(request.user, "authentik_events.view_event").filter(
|
||||||
action=EventAction.LOGIN_FAILED, context__username=user.username
|
action=EventAction.LOGIN_FAILED, context__username=user.username
|
||||||
)
|
)
|
||||||
# 3 data points per day, so 8 hour spans
|
# 3 data points per day, so 8 hour spans
|
||||||
@ -235,8 +238,9 @@ class UserMetricsSerializer(PassiveSerializer):
|
|||||||
def get_authorizations(self, _):
|
def get_authorizations(self, _):
|
||||||
"""Get failed logins per 8 hours for the last 7 days"""
|
"""Get failed logins per 8 hours for the last 7 days"""
|
||||||
user = self.context["user"]
|
user = self.context["user"]
|
||||||
|
request = self.context["request"]
|
||||||
return (
|
return (
|
||||||
get_objects_for_user(user, "authentik_events.view_event").filter(
|
get_objects_for_user(request.user, "authentik_events.view_event").filter(
|
||||||
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
|
action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk
|
||||||
)
|
)
|
||||||
# 3 data points per day, so 8 hour spans
|
# 3 data points per day, so 8 hour spans
|
||||||
@ -471,8 +475,9 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
def metrics(self, request: Request, pk: int) -> Response:
|
def metrics(self, request: Request, pk: int) -> Response:
|
||||||
"""User metrics per 1h"""
|
"""User metrics per 1h"""
|
||||||
user: User = self.get_object()
|
user: User = self.get_object()
|
||||||
serializer = UserMetricsSerializer(True)
|
serializer = UserMetricsSerializer(instance={})
|
||||||
serializer.context["user"] = user
|
serializer.context["user"] = user
|
||||||
|
serializer.context["request"] = request
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@permission_required("authentik_core.reset_user_password")
|
@permission_required("authentik_core.reset_user_password")
|
||||||
@ -539,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):
|
||||||
|
|||||||
@ -11,6 +11,7 @@ class AuthentikCoreConfig(ManagedAppConfig):
|
|||||||
label = "authentik_core"
|
label = "authentik_core"
|
||||||
verbose_name = "authentik Core"
|
verbose_name = "authentik Core"
|
||||||
mountpoint = ""
|
mountpoint = ""
|
||||||
|
ws_mountpoint = "authentik.core.urls"
|
||||||
default = True
|
default = True
|
||||||
|
|
||||||
def reconcile_load_core_signals(self):
|
def reconcile_load_core_signals(self):
|
||||||
|
|||||||
@ -21,11 +21,14 @@ PROPERTY_MAPPING_TIME = Histogram(
|
|||||||
class PropertyMappingEvaluator(BaseEvaluator):
|
class PropertyMappingEvaluator(BaseEvaluator):
|
||||||
"""Custom Evaluator that adds some different context variables."""
|
"""Custom Evaluator that adds some different context variables."""
|
||||||
|
|
||||||
|
dry_run: bool
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
model: Model,
|
model: Model,
|
||||||
user: Optional[User] = None,
|
user: Optional[User] = None,
|
||||||
request: Optional[HttpRequest] = None,
|
request: Optional[HttpRequest] = None,
|
||||||
|
dry_run: Optional[bool] = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
if hasattr(model, "name"):
|
if hasattr(model, "name"):
|
||||||
@ -42,9 +45,13 @@ class PropertyMappingEvaluator(BaseEvaluator):
|
|||||||
req.http_request = request
|
req.http_request = request
|
||||||
self._context["request"] = req
|
self._context["request"] = req
|
||||||
self._context.update(**kwargs)
|
self._context.update(**kwargs)
|
||||||
|
self.dry_run = dry_run
|
||||||
|
|
||||||
def handle_error(self, exc: Exception, expression_source: str):
|
def handle_error(self, exc: Exception, expression_source: str):
|
||||||
"""Exception Handler"""
|
"""Exception Handler"""
|
||||||
|
# For dry-run requests we don't save exceptions
|
||||||
|
if self.dry_run:
|
||||||
|
return
|
||||||
error_string = exception_to_string(exc)
|
error_string = exception_to_string(exc)
|
||||||
event = Event.new(
|
event = Event.new(
|
||||||
EventAction.PROPERTY_MAPPING_EXCEPTION,
|
EventAction.PROPERTY_MAPPING_EXCEPTION,
|
||||||
|
|||||||
19
authentik/core/migrations/0027_alter_user_uuid.py
Normal file
19
authentik/core/migrations/0027_alter_user_uuid.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-03-19 21:57
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0026_alter_propertymapping_name_alter_provider_name"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="uuid",
|
||||||
|
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-03-23 21:44
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("authentik_flows", "0025_alter_flowstagebinding_evaluate_on_plan_and_more"),
|
||||||
|
("authentik_core", "0027_alter_user_uuid"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="provider",
|
||||||
|
name="authentication_flow",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
help_text="Flow used for authentication when the associated application is accessed by an un-authenticated user.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="provider_authentication",
|
||||||
|
to="authentik_flows.flow",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -146,7 +146,7 @@ class UserManager(DjangoUserManager):
|
|||||||
class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
||||||
"""Custom User model to allow easier adding of user-based settings"""
|
"""Custom User model to allow easier adding of user-based settings"""
|
||||||
|
|
||||||
uuid = models.UUIDField(default=uuid4, editable=False)
|
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
|
||||||
name = models.TextField(help_text=_("User's display name."))
|
name = models.TextField(help_text=_("User's display name."))
|
||||||
path = models.TextField(default="users")
|
path = models.TextField(default="users")
|
||||||
|
|
||||||
@ -249,6 +249,17 @@ class Provider(SerializerModel):
|
|||||||
|
|
||||||
name = models.TextField(unique=True)
|
name = models.TextField(unique=True)
|
||||||
|
|
||||||
|
authentication_flow = models.ForeignKey(
|
||||||
|
"authentik_flows.Flow",
|
||||||
|
null=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
help_text=_(
|
||||||
|
"Flow used for authentication when the associated application is accessed by an "
|
||||||
|
"un-authenticated user."
|
||||||
|
),
|
||||||
|
related_name="provider_authentication",
|
||||||
|
)
|
||||||
|
|
||||||
authorization_flow = models.ForeignKey(
|
authorization_flow = models.ForeignKey(
|
||||||
"authentik_flows.Flow",
|
"authentik_flows.Flow",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|||||||
@ -9,16 +9,13 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
||||||
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}">
|
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}">
|
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}">
|
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}">
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
|
||||||
<script src="{% static 'dist/poly.js' %}" type="module"></script>
|
<script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script>
|
||||||
|
<script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
<meta name="sentry-trace" content="{{ sentry_trace }}" />
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
{% extends "base/skeleton.html" %}
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
|
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
|
||||||
@ -15,19 +14,6 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
<ak-interface-admin>
|
<ak-interface-admin>
|
||||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
<ak-loading></ak-loading>
|
||||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
|
||||||
<div class="pf-c-empty-state__content">
|
|
||||||
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
|
||||||
<span class="pf-c-spinner__clipper"></span>
|
|
||||||
<span class="pf-c-spinner__lead-ball"></span>
|
|
||||||
<span class="pf-c-spinner__tail-ball"></span>
|
|
||||||
</span>
|
|
||||||
<h1 class="pf-c-title pf-m-lg">
|
|
||||||
{% trans "Loading..." %}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</ak-interface-admin>
|
</ak-interface-admin>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
{% extends "base/skeleton.html" %}
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
@ -31,19 +30,6 @@ window.authentik.flow = {
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
<ak-flow-executor>
|
<ak-flow-executor>
|
||||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
<ak-loading></ak-loading>
|
||||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
|
||||||
<div class="pf-c-empty-state__content">
|
|
||||||
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
|
||||||
<span class="pf-c-spinner__clipper"></span>
|
|
||||||
<span class="pf-c-spinner__lead-ball"></span>
|
|
||||||
<span class="pf-c-spinner__tail-ball"></span>
|
|
||||||
</span>
|
|
||||||
<h1 class="pf-c-title pf-m-lg">
|
|
||||||
{% trans "Loading..." %}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</ak-flow-executor>
|
</ak-flow-executor>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
{% extends "base/skeleton.html" %}
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
|
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
|
||||||
@ -15,19 +14,6 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<ak-message-container></ak-message-container>
|
<ak-message-container></ak-message-container>
|
||||||
<ak-interface-user>
|
<ak-interface-user>
|
||||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
<ak-loading></ak-loading>
|
||||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
|
||||||
<div class="pf-c-empty-state__content">
|
|
||||||
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
|
||||||
<span class="pf-c-spinner__clipper"></span>
|
|
||||||
<span class="pf-c-spinner__lead-ball"></span>
|
|
||||||
<span class="pf-c-spinner__tail-ball"></span>
|
|
||||||
</span>
|
|
||||||
<h1 class="pf-c-title pf-m-lg">
|
|
||||||
{% trans "Loading..." %}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</ak-interface-user>
|
</ak-interface-user>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -43,14 +43,14 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}),
|
reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}),
|
||||||
{"meta_launch_url": "https://%(username)s.test.goauthentik.io/%(username)s"},
|
{"meta_launch_url": "https://%(username)s-test.test.goauthentik.io/%(username)s"},
|
||||||
).status_code,
|
).status_code,
|
||||||
200,
|
200,
|
||||||
)
|
)
|
||||||
self.allowed.refresh_from_db()
|
self.allowed.refresh_from_db()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.allowed.get_launch_url(self.user),
|
self.allowed.get_launch_url(self.user),
|
||||||
f"https://{self.user.username}.test.goauthentik.io/{self.user.username}",
|
f"https://{self.user.username}-test.test.goauthentik.io/{self.user.username}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_set_icon(self):
|
def test_set_icon(self):
|
||||||
@ -129,6 +129,7 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
"provider_obj": {
|
"provider_obj": {
|
||||||
"assigned_application_name": "allowed",
|
"assigned_application_name": "allowed",
|
||||||
"assigned_application_slug": "allowed",
|
"assigned_application_slug": "allowed",
|
||||||
|
"authentication_flow": None,
|
||||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||||
"component": "ak-provider-oauth2-form",
|
"component": "ak-provider-oauth2-form",
|
||||||
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
||||||
@ -178,6 +179,7 @@ class TestApplicationsAPI(APITestCase):
|
|||||||
"provider_obj": {
|
"provider_obj": {
|
||||||
"assigned_application_name": "allowed",
|
"assigned_application_name": "allowed",
|
||||||
"assigned_application_slug": "allowed",
|
"assigned_application_slug": "allowed",
|
||||||
|
"authentication_flow": None,
|
||||||
"authorization_flow": str(self.provider.authorization_flow.pk),
|
"authorization_flow": str(self.provider.authorization_flow.pk),
|
||||||
"component": "ak-provider-oauth2-form",
|
"component": "ak-provider-oauth2-form",
|
||||||
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
"meta_model_name": "authentik_providers_oauth2.oauth2provider",
|
||||||
|
|||||||
@ -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,5 +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(response, reverse("authentik_core:if-user"))
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|||||||
@ -4,7 +4,10 @@ from guardian.shortcuts import get_anonymous_user
|
|||||||
|
|
||||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||||
from authentik.core.models import PropertyMapping
|
from authentik.core.models import PropertyMapping
|
||||||
|
from authentik.core.tests.utils import create_test_admin_user
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.policies.expression.models import ExpressionPolicy
|
||||||
|
|
||||||
|
|
||||||
class TestPropertyMappings(TestCase):
|
class TestPropertyMappings(TestCase):
|
||||||
@ -12,23 +15,24 @@ class TestPropertyMappings(TestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
self.user = create_test_admin_user()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
def test_expression(self):
|
def test_expression(self):
|
||||||
"""Test expression"""
|
"""Test expression"""
|
||||||
mapping = PropertyMapping.objects.create(name="test", expression="return 'test'")
|
mapping = PropertyMapping.objects.create(name=generate_id(), expression="return 'test'")
|
||||||
self.assertEqual(mapping.evaluate(None, None), "test")
|
self.assertEqual(mapping.evaluate(None, None), "test")
|
||||||
|
|
||||||
def test_expression_syntax(self):
|
def test_expression_syntax(self):
|
||||||
"""Test expression syntax error"""
|
"""Test expression syntax error"""
|
||||||
mapping = PropertyMapping.objects.create(name="test", expression="-")
|
mapping = PropertyMapping.objects.create(name=generate_id(), expression="-")
|
||||||
with self.assertRaises(PropertyMappingExpressionException):
|
with self.assertRaises(PropertyMappingExpressionException):
|
||||||
mapping.evaluate(None, None)
|
mapping.evaluate(None, None)
|
||||||
|
|
||||||
def test_expression_error_general(self):
|
def test_expression_error_general(self):
|
||||||
"""Test expression error"""
|
"""Test expression error"""
|
||||||
expr = "return aaa"
|
expr = "return aaa"
|
||||||
mapping = PropertyMapping.objects.create(name="test", expression=expr)
|
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr)
|
||||||
with self.assertRaises(PropertyMappingExpressionException):
|
with self.assertRaises(PropertyMappingExpressionException):
|
||||||
mapping.evaluate(None, None)
|
mapping.evaluate(None, None)
|
||||||
events = Event.objects.filter(
|
events = Event.objects.filter(
|
||||||
@ -41,7 +45,7 @@ class TestPropertyMappings(TestCase):
|
|||||||
"""Test expression error (with user and http request"""
|
"""Test expression error (with user and http request"""
|
||||||
expr = "return aaa"
|
expr = "return aaa"
|
||||||
request = self.factory.get("/")
|
request = self.factory.get("/")
|
||||||
mapping = PropertyMapping.objects.create(name="test", expression=expr)
|
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr)
|
||||||
with self.assertRaises(PropertyMappingExpressionException):
|
with self.assertRaises(PropertyMappingExpressionException):
|
||||||
mapping.evaluate(get_anonymous_user(), request)
|
mapping.evaluate(get_anonymous_user(), request)
|
||||||
events = Event.objects.filter(
|
events = Event.objects.filter(
|
||||||
@ -52,3 +56,23 @@ class TestPropertyMappings(TestCase):
|
|||||||
event = events.first()
|
event = events.first()
|
||||||
self.assertEqual(event.user["username"], "AnonymousUser")
|
self.assertEqual(event.user["username"], "AnonymousUser")
|
||||||
self.assertEqual(event.client_ip, "127.0.0.1")
|
self.assertEqual(event.client_ip, "127.0.0.1")
|
||||||
|
|
||||||
|
def test_call_policy(self):
|
||||||
|
"""test ak_call_policy"""
|
||||||
|
expr = ExpressionPolicy.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
execution_logging=True,
|
||||||
|
expression="return request.http_request.path",
|
||||||
|
)
|
||||||
|
http_request = self.factory.get("/")
|
||||||
|
tmpl = (
|
||||||
|
"""
|
||||||
|
res = ak_call_policy('%s')
|
||||||
|
result = [request.http_request.path, res.raw_result]
|
||||||
|
return result
|
||||||
|
"""
|
||||||
|
% expr.name
|
||||||
|
)
|
||||||
|
evaluator = PropertyMapping(expression=tmpl, name=generate_id())
|
||||||
|
res = evaluator.evaluate(self.user, http_request)
|
||||||
|
self.assertEqual(res, ["/", "/"])
|
||||||
|
|||||||
@ -27,6 +27,6 @@ class UserSettingSerializer(PassiveSerializer):
|
|||||||
|
|
||||||
object_uid = CharField()
|
object_uid = CharField()
|
||||||
component = CharField()
|
component = CharField()
|
||||||
title = CharField()
|
title = CharField(required=True)
|
||||||
configure_url = CharField(required=False)
|
configure_url = CharField(required=False)
|
||||||
icon_url = CharField(required=False)
|
icon_url = CharField(required=False)
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
"""authentik URL Configuration"""
|
"""authentik URL Configuration"""
|
||||||
|
from channels.auth import AuthMiddleware
|
||||||
|
from channels.sessions import CookieMiddleware
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.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 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.interface import FlowInterfaceView, InterfaceView
|
||||||
from authentik.core.views.session import EndSessionView
|
from authentik.core.views.session import EndSessionView
|
||||||
|
from authentik.root.asgi_middleware import SessionMiddleware
|
||||||
|
from authentik.root.messages.consumer import MessageConsumer
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
@ -24,17 +28,6 @@ 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(
|
path(
|
||||||
"if/admin/",
|
"if/admin/",
|
||||||
@ -64,6 +57,12 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
path(
|
||||||
|
"ws/client/", CookieMiddleware(SessionMiddleware(AuthMiddleware(MessageConsumer.as_asgi())))
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path("debug/policy/deny/", AccessDeniedView.as_view(), name="debug-policy-deny"),
|
path("debug/policy/deny/", AccessDeniedView.as_view(), name="debug-policy-deny"),
|
||||||
|
|||||||
@ -12,16 +12,19 @@ from authentik.flows.challenge import (
|
|||||||
RedirectChallenge,
|
RedirectChallenge,
|
||||||
)
|
)
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import FlowDesignation, in_memory_stage
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import (
|
||||||
|
SESSION_KEY_APPLICATION_PRE,
|
||||||
|
SESSION_KEY_PLAN,
|
||||||
|
ToDefaultFlow,
|
||||||
|
)
|
||||||
from authentik.lib.utils.urls import redirect_with_qs
|
from authentik.lib.utils.urls import redirect_with_qs
|
||||||
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.models import Tenant
|
|
||||||
|
|
||||||
|
|
||||||
class RedirectToAppLaunch(View):
|
class RedirectToAppLaunch(View):
|
||||||
@ -36,10 +39,10 @@ class RedirectToAppLaunch(View):
|
|||||||
# Check if we're authenticated already, saves us the flow run
|
# Check if we're authenticated already, saves us the flow run
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
return HttpResponseRedirect(app.get_launch_url(request.user))
|
return HttpResponseRedirect(app.get_launch_url(request.user))
|
||||||
|
self.request.session[SESSION_KEY_APPLICATION_PRE] = app
|
||||||
# otherwise, do a custom flow plan that includes the application that's
|
# otherwise, do a custom flow plan that includes the application that's
|
||||||
# being accessed, to improve usability
|
# being accessed, to improve usability
|
||||||
tenant: Tenant = request.tenant
|
flow = ToDefaultFlow(request=request, designation=FlowDesignation.AUTHENTICATION).get_flow()
|
||||||
flow = tenant.flow_authentication
|
|
||||||
planner = FlowPlanner(flow)
|
planner = FlowPlanner(flow)
|
||||||
planner.allow_empty_flows = True
|
planner.allow_empty_flows = True
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
"""authentik impersonation views"""
|
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
|
||||||
from django.views import View
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.core.middleware import (
|
|
||||||
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
|
|
||||||
SESSION_KEY_IMPERSONATE_USER,
|
|
||||||
)
|
|
||||||
from authentik.core.models import User
|
|
||||||
from authentik.events.models import Event, EventAction
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateInitView(View):
|
|
||||||
"""Initiate Impersonation"""
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
|
|
||||||
"""Impersonation handler, checks permissions"""
|
|
||||||
if not CONFIG.y_bool("impersonation"):
|
|
||||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
|
||||||
return HttpResponse("Unauthorized", status=401)
|
|
||||||
if not request.user.has_perm("impersonate"):
|
|
||||||
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
|
|
||||||
return HttpResponse("Unauthorized", status=401)
|
|
||||||
|
|
||||||
user_to_be = get_object_or_404(User, pk=user_id)
|
|
||||||
|
|
||||||
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
|
||||||
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
|
||||||
|
|
||||||
return redirect("authentik_core:if-user")
|
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateEndView(View):
|
|
||||||
"""End User impersonation"""
|
|
||||||
|
|
||||||
def get(self, request: HttpRequest) -> HttpResponse:
|
|
||||||
"""End Impersonation handler"""
|
|
||||||
if (
|
|
||||||
SESSION_KEY_IMPERSONATE_USER not in request.session
|
|
||||||
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
|
||||||
):
|
|
||||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
|
||||||
return redirect("authentik_core:if-user")
|
|
||||||
|
|
||||||
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
|
||||||
|
|
||||||
del request.session[SESSION_KEY_IMPERSONATE_USER]
|
|
||||||
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
|
||||||
|
|
||||||
return redirect("authentik_core:root-redirect")
|
|
||||||
@ -214,11 +214,18 @@ class Event(SerializerModel, ExpiringModel):
|
|||||||
Events independently from requests.
|
Events independently from requests.
|
||||||
`user` arguments optionally overrides user from requests."""
|
`user` arguments optionally overrides user from requests."""
|
||||||
if request:
|
if request:
|
||||||
|
from authentik.flows.views.executor import QS_QUERY
|
||||||
|
|
||||||
self.context["http_request"] = {
|
self.context["http_request"] = {
|
||||||
"path": request.path,
|
"path": request.path,
|
||||||
"method": request.method,
|
"method": request.method,
|
||||||
"args": QueryDict(request.META.get("QUERY_STRING", "")),
|
"args": QueryDict(request.META.get("QUERY_STRING", "")),
|
||||||
}
|
}
|
||||||
|
# Special case for events created during flow execution
|
||||||
|
# since they keep the http query within a wrapped query
|
||||||
|
if QS_QUERY in self.context["http_request"]["args"]:
|
||||||
|
wrapped = self.context["http_request"]["args"][QS_QUERY]
|
||||||
|
self.context["http_request"]["args"] = QueryDict(wrapped)
|
||||||
if hasattr(request, "tenant"):
|
if hasattr(request, "tenant"):
|
||||||
tenant: Tenant = request.tenant
|
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
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class TaskResult:
|
|||||||
|
|
||||||
def with_error(self, exc: Exception) -> "TaskResult":
|
def with_error(self, exc: Exception) -> "TaskResult":
|
||||||
"""Since errors might not always be pickle-able, set the traceback"""
|
"""Since errors might not always be pickle-able, set the traceback"""
|
||||||
self.messages.append(str(exc))
|
self.messages.append(exception_to_string(exc))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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}"
|
||||||
|
|||||||
@ -271,6 +271,15 @@ class ConfigurableStage(models.Model):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class FriendlyNamedStage(models.Model):
|
||||||
|
"""Abstract base class for a Stage that can have a user friendly name configured."""
|
||||||
|
|
||||||
|
friendly_name = models.TextField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class FlowToken(Token):
|
class FlowToken(Token):
|
||||||
"""Subclass of a standard Token, stores the currently active flow plan upon creation.
|
"""Subclass of a standard Token, stores the currently active flow plan upon creation.
|
||||||
Can be used to later resume a flow."""
|
Can be used to later resume a flow."""
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -2,10 +2,13 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_flow
|
from authentik.core.tests.utils import create_test_flow
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
from authentik.flows.planner import FlowPlan
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_PLAN
|
||||||
|
from authentik.lib.generators import generate_id
|
||||||
|
from authentik.providers.oauth2.models import OAuth2Provider
|
||||||
|
|
||||||
|
|
||||||
class TestHelperView(TestCase):
|
class TestHelperView(TestCase):
|
||||||
@ -22,6 +25,41 @@ class TestHelperView(TestCase):
|
|||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.assertEqual(response.url, expected_url)
|
self.assertEqual(response.url, expected_url)
|
||||||
|
|
||||||
|
def test_default_view_app(self):
|
||||||
|
"""Test that ToDefaultFlow returns the expected URL (when accessing an application)"""
|
||||||
|
Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete()
|
||||||
|
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||||
|
self.client.session[SESSION_KEY_APPLICATION_PRE] = Application(
|
||||||
|
name=generate_id(),
|
||||||
|
slug=generate_id(),
|
||||||
|
provider=OAuth2Provider(
|
||||||
|
name=generate_id(),
|
||||||
|
authentication_flow=flow,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_flows:default-authentication"),
|
||||||
|
)
|
||||||
|
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, expected_url)
|
||||||
|
|
||||||
|
def test_default_view_app_no_provider(self):
|
||||||
|
"""Test that ToDefaultFlow returns the expected URL
|
||||||
|
(when accessing an application, without a provider)"""
|
||||||
|
Flow.objects.filter(designation=FlowDesignation.AUTHENTICATION).delete()
|
||||||
|
flow = create_test_flow(FlowDesignation.AUTHENTICATION)
|
||||||
|
self.client.session[SESSION_KEY_APPLICATION_PRE] = Application(
|
||||||
|
name=generate_id(),
|
||||||
|
slug=generate_id(),
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_flows:default-authentication"),
|
||||||
|
)
|
||||||
|
expected_url = reverse("authentik_core:if-flow", kwargs={"flow_slug": flow.slug})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, expected_url)
|
||||||
|
|
||||||
def test_default_view_invalid_plan(self):
|
def test_default_view_invalid_plan(self):
|
||||||
"""Test that ToDefaultFlow returns the expected URL (with an invalid plan)"""
|
"""Test that ToDefaultFlow returns the expected URL (with an invalid plan)"""
|
||||||
Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete()
|
Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete()
|
||||||
|
|||||||
@ -22,6 +22,7 @@ from sentry_sdk.api import set_tag
|
|||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from structlog.stdlib import BoundLogger, get_logger
|
from structlog.stdlib import BoundLogger, get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import Application
|
||||||
from authentik.events.models import Event, EventAction, cleanse_dict
|
from authentik.events.models import Event, EventAction, cleanse_dict
|
||||||
from authentik.flows.challenge import (
|
from authentik.flows.challenge import (
|
||||||
Challenge,
|
Challenge,
|
||||||
@ -68,6 +69,7 @@ SESSION_KEY_GET = "authentik/flows/get"
|
|||||||
SESSION_KEY_POST = "authentik/flows/post"
|
SESSION_KEY_POST = "authentik/flows/post"
|
||||||
SESSION_KEY_HISTORY = "authentik/flows/history"
|
SESSION_KEY_HISTORY = "authentik/flows/history"
|
||||||
QS_KEY_TOKEN = "flow_token" # nosec
|
QS_KEY_TOKEN = "flow_token" # nosec
|
||||||
|
QS_QUERY = "query"
|
||||||
|
|
||||||
|
|
||||||
def challenge_types():
|
def challenge_types():
|
||||||
@ -172,7 +174,7 @@ class FlowExecutorView(APIView):
|
|||||||
op="authentik.flow.executor.dispatch", description=self.flow.slug
|
op="authentik.flow.executor.dispatch", description=self.flow.slug
|
||||||
) as span:
|
) as span:
|
||||||
span.set_data("authentik Flow", self.flow.slug)
|
span.set_data("authentik Flow", self.flow.slug)
|
||||||
get_params = QueryDict(request.GET.get("query", ""))
|
get_params = QueryDict(request.GET.get(QS_QUERY, ""))
|
||||||
if QS_KEY_TOKEN in get_params:
|
if QS_KEY_TOKEN in get_params:
|
||||||
plan = self._check_flow_token(get_params[QS_KEY_TOKEN])
|
plan = self._check_flow_token(get_params[QS_KEY_TOKEN])
|
||||||
if plan:
|
if plan:
|
||||||
@ -475,20 +477,32 @@ class ToDefaultFlow(View):
|
|||||||
LOGGER.debug("flow_by_policy: no flow found", filters=flow_filter)
|
LOGGER.debug("flow_by_policy: no flow found", filters=flow_filter)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
def get_flow(self) -> Flow:
|
||||||
tenant: Tenant = request.tenant
|
"""Get a flow for the selected designation"""
|
||||||
|
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:
|
||||||
flow = tenant.flow_authentication
|
flow = tenant.flow_authentication
|
||||||
if self.designation == FlowDesignation.INVALIDATION:
|
# Check if we have a default flow from application
|
||||||
|
application: Optional[Application] = self.request.session.get(
|
||||||
|
SESSION_KEY_APPLICATION_PRE
|
||||||
|
)
|
||||||
|
if application and application.provider and application.provider.authentication_flow:
|
||||||
|
flow = application.provider.authentication_flow
|
||||||
|
elif self.designation == FlowDesignation.INVALIDATION:
|
||||||
flow = tenant.flow_invalidation
|
flow = tenant.flow_invalidation
|
||||||
|
if flow:
|
||||||
|
return flow
|
||||||
# If no flow was set, get the first based on slug and policy
|
# If no flow was set, get the first based on slug and policy
|
||||||
if not flow:
|
flow = self.flow_by_policy(self.request, designation=self.designation)
|
||||||
flow = self.flow_by_policy(request, designation=self.designation)
|
if flow:
|
||||||
|
return flow
|
||||||
# If we still don't have a flow, 404
|
# If we still don't have a flow, 404
|
||||||
if not flow:
|
raise Http404
|
||||||
raise Http404
|
|
||||||
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
flow = self.get_flow()
|
||||||
# If user already has a pending plan, clear it so we don't have to later.
|
# If user already has a pending plan, clear it so we don't have to later.
|
||||||
if SESSION_KEY_PLAN in self.request.session:
|
if SESSION_KEY_PLAN in self.request.session:
|
||||||
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from typing import Any, Iterable, Optional
|
|||||||
from cachetools import TLRUCache, cached
|
from cachetools import TLRUCache, cached
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from django_otp import devices_for_user
|
from django_otp import devices_for_user
|
||||||
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
@ -16,7 +17,9 @@ from structlog.stdlib import get_logger
|
|||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event
|
from authentik.events.models import Event
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.models import Policy, PolicyBinding
|
||||||
|
from authentik.policies.process import PolicyProcess
|
||||||
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -37,19 +40,20 @@ class BaseEvaluator:
|
|||||||
# update website/docs/expressions/_objects.md
|
# update website/docs/expressions/_objects.md
|
||||||
# update website/docs/expressions/_functions.md
|
# update website/docs/expressions/_functions.md
|
||||||
self._globals = {
|
self._globals = {
|
||||||
"regex_match": BaseEvaluator.expr_regex_match,
|
"ak_call_policy": self.expr_func_call_policy,
|
||||||
"regex_replace": BaseEvaluator.expr_regex_replace,
|
"ak_create_event": self.expr_event_create,
|
||||||
"list_flatten": BaseEvaluator.expr_flatten,
|
|
||||||
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
||||||
|
"ak_logger": get_logger(self._filename).bind(),
|
||||||
"ak_user_by": BaseEvaluator.expr_user_by,
|
"ak_user_by": BaseEvaluator.expr_user_by,
|
||||||
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
|
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
|
||||||
"resolve_dns": BaseEvaluator.expr_resolve_dns,
|
|
||||||
"reverse_dns": BaseEvaluator.expr_reverse_dns,
|
|
||||||
"ak_create_event": self.expr_event_create,
|
|
||||||
"ak_logger": get_logger(self._filename).bind(),
|
|
||||||
"requests": get_http_session(),
|
|
||||||
"ip_address": ip_address,
|
"ip_address": ip_address,
|
||||||
"ip_network": ip_network,
|
"ip_network": ip_network,
|
||||||
|
"list_flatten": BaseEvaluator.expr_flatten,
|
||||||
|
"regex_match": BaseEvaluator.expr_regex_match,
|
||||||
|
"regex_replace": BaseEvaluator.expr_regex_replace,
|
||||||
|
"requests": get_http_session(),
|
||||||
|
"resolve_dns": BaseEvaluator.expr_resolve_dns,
|
||||||
|
"reverse_dns": BaseEvaluator.expr_reverse_dns,
|
||||||
}
|
}
|
||||||
self._context = {}
|
self._context = {}
|
||||||
|
|
||||||
@ -152,6 +156,19 @@ class BaseEvaluator:
|
|||||||
return
|
return
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
|
def expr_func_call_policy(self, name: str, **kwargs) -> PolicyResult:
|
||||||
|
"""Call policy by name, with current request"""
|
||||||
|
policy = Policy.objects.filter(name=name).select_subclasses().first()
|
||||||
|
if not policy:
|
||||||
|
raise ValueError(f"Policy '{name}' not found.")
|
||||||
|
user = self._context.get("user", get_anonymous_user())
|
||||||
|
req = PolicyRequest(user)
|
||||||
|
if "request" in self._context:
|
||||||
|
req = self._context["request"]
|
||||||
|
req.context.update(kwargs)
|
||||||
|
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
|
||||||
|
return proc.profiling_wrapper()
|
||||||
|
|
||||||
def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
|
def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
|
||||||
"""Wrap expression in a function, call it, and save the result as `result`"""
|
"""Wrap expression in a function, call it, and save the result as `result`"""
|
||||||
handler_signature = ",".join(params)
|
handler_signature = ",".join(params)
|
||||||
|
|||||||
@ -81,7 +81,8 @@ class DomainlessFormattedURLValidator(DomainlessURLValidator):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs) -> None:
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.host_re = r"([%\(\)a-zA-Z])+" + self.domain_re + self.domain_re
|
self.formatter_re = r"([%\(\)a-zA-Z])*"
|
||||||
|
self.host_re = "(" + self.formatter_re + self.hostname_re + self.domain_re + "|localhost)"
|
||||||
self.regex = _lazy_re_compile(
|
self.regex = _lazy_re_compile(
|
||||||
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
|
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
|
||||||
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
|
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
|
||||||
|
|||||||
@ -19,9 +19,12 @@ from rest_framework.exceptions import APIException
|
|||||||
from sentry_sdk import HttpTransport
|
from sentry_sdk import HttpTransport
|
||||||
from sentry_sdk import init as sentry_sdk_init
|
from sentry_sdk import init as sentry_sdk_init
|
||||||
from sentry_sdk.api import set_tag
|
from sentry_sdk.api import set_tag
|
||||||
|
from sentry_sdk.integrations.argv import ArgvIntegration
|
||||||
from sentry_sdk.integrations.celery import CeleryIntegration
|
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
from sentry_sdk.integrations.redis import RedisIntegration
|
from sentry_sdk.integrations.redis import RedisIntegration
|
||||||
|
from sentry_sdk.integrations.socket import SocketIntegration
|
||||||
|
from sentry_sdk.integrations.stdlib import StdlibIntegration
|
||||||
from sentry_sdk.integrations.threading import ThreadingIntegration
|
from sentry_sdk.integrations.threading import ThreadingIntegration
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
from websockets.exceptions import WebSocketException
|
from websockets.exceptions import WebSocketException
|
||||||
@ -61,10 +64,13 @@ def sentry_init(**sentry_init_kwargs):
|
|||||||
sentry_sdk_init(
|
sentry_sdk_init(
|
||||||
dsn=CONFIG.y("error_reporting.sentry_dsn"),
|
dsn=CONFIG.y("error_reporting.sentry_dsn"),
|
||||||
integrations=[
|
integrations=[
|
||||||
|
ArgvIntegration(),
|
||||||
|
StdlibIntegration(),
|
||||||
DjangoIntegration(transaction_style="function_name"),
|
DjangoIntegration(transaction_style="function_name"),
|
||||||
CeleryIntegration(),
|
CeleryIntegration(monitor_beat_tasks=True),
|
||||||
RedisIntegration(),
|
RedisIntegration(),
|
||||||
ThreadingIntegration(propagate_hub=True),
|
ThreadingIntegration(propagate_hub=True),
|
||||||
|
SocketIntegration(),
|
||||||
],
|
],
|
||||||
before_send=before_send,
|
before_send=before_send,
|
||||||
traces_sampler=traces_sampler,
|
traces_sampler=traces_sampler,
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
"""Test utils"""
|
"""Test utils"""
|
||||||
|
from inspect import currentframe
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from django.contrib.messages.middleware import MessageMiddleware
|
from django.contrib.messages.middleware import MessageMiddleware
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
@ -11,6 +14,21 @@ def dummy_get_response(request: HttpRequest): # pragma: no cover
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_fixture(path: str, **kwargs) -> str:
|
||||||
|
"""Load fixture, optionally formatting it with kwargs"""
|
||||||
|
current = currentframe()
|
||||||
|
parent = current.f_back
|
||||||
|
calling_file_path = parent.f_globals["__file__"]
|
||||||
|
with open(
|
||||||
|
Path(calling_file_path).resolve().parent / Path(path), "r", encoding="utf-8"
|
||||||
|
) as _fixture:
|
||||||
|
fixture = _fixture.read()
|
||||||
|
try:
|
||||||
|
return fixture % kwargs
|
||||||
|
except TypeError:
|
||||||
|
return fixture
|
||||||
|
|
||||||
|
|
||||||
def get_request(*args, user=None, **kwargs):
|
def get_request(*args, user=None, **kwargs):
|
||||||
"""Get a request with usable session"""
|
"""Get a request with usable session"""
|
||||||
request = RequestFactory().get(*args, **kwargs)
|
request = RequestFactory().get(*args, **kwargs)
|
||||||
|
|||||||
@ -38,13 +38,17 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
|
|||||||
if OUTPOST_REMOTE_IP_HEADER not in request.META or OUTPOST_TOKEN_HEADER not in request.META:
|
if OUTPOST_REMOTE_IP_HEADER not in request.META or OUTPOST_TOKEN_HEADER not in request.META:
|
||||||
return None
|
return None
|
||||||
fake_ip = request.META[OUTPOST_REMOTE_IP_HEADER]
|
fake_ip = request.META[OUTPOST_REMOTE_IP_HEADER]
|
||||||
tokens = Token.filter_not_expired(
|
token = (
|
||||||
key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API
|
Token.filter_not_expired(
|
||||||
|
key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API
|
||||||
|
)
|
||||||
|
.select_related("user")
|
||||||
|
.first()
|
||||||
)
|
)
|
||||||
if not tokens.exists():
|
if not token:
|
||||||
LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip)
|
LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip)
|
||||||
return None
|
return None
|
||||||
user = tokens.first().user
|
user = token.user
|
||||||
if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
if not user.group_attributes(request).get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Remote-IP override: user doesn't have permission",
|
"Remote-IP override: user doesn't have permission",
|
||||||
|
|||||||
@ -28,6 +28,7 @@ from authentik.outposts.models import (
|
|||||||
)
|
)
|
||||||
from authentik.providers.ldap.models import LDAPProvider
|
from authentik.providers.ldap.models import LDAPProvider
|
||||||
from authentik.providers.proxy.models import ProxyProvider
|
from authentik.providers.proxy.models import ProxyProvider
|
||||||
|
from authentik.providers.radius.models import RadiusProvider
|
||||||
|
|
||||||
|
|
||||||
class OutpostSerializer(ModelSerializer):
|
class OutpostSerializer(ModelSerializer):
|
||||||
@ -51,6 +52,7 @@ class OutpostSerializer(ModelSerializer):
|
|||||||
type_map = {
|
type_map = {
|
||||||
OutpostType.LDAP: LDAPProvider,
|
OutpostType.LDAP: LDAPProvider,
|
||||||
OutpostType.PROXY: ProxyProvider,
|
OutpostType.PROXY: ProxyProvider,
|
||||||
|
OutpostType.RADIUS: RadiusProvider,
|
||||||
None: Provider,
|
None: Provider,
|
||||||
}
|
}
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
|
|||||||
@ -24,6 +24,7 @@ class AuthentikOutpostConfig(ManagedAppConfig):
|
|||||||
label = "authentik_outposts"
|
label = "authentik_outposts"
|
||||||
verbose_name = "authentik Outpost"
|
verbose_name = "authentik Outpost"
|
||||||
default = True
|
default = True
|
||||||
|
ws_mountpoint = "authentik.outposts.urls"
|
||||||
|
|
||||||
def reconcile_load_outposts_signals(self):
|
def reconcile_load_outposts_signals(self):
|
||||||
"""Load outposts signals"""
|
"""Load outposts signals"""
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
|
|||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from kubernetes.client import (
|
from kubernetes.client import (
|
||||||
AppsV1Api,
|
AppsV1Api,
|
||||||
|
V1Capabilities,
|
||||||
V1Container,
|
V1Container,
|
||||||
V1ContainerPort,
|
V1ContainerPort,
|
||||||
V1Deployment,
|
V1Deployment,
|
||||||
@ -13,9 +14,12 @@ from kubernetes.client import (
|
|||||||
V1LabelSelector,
|
V1LabelSelector,
|
||||||
V1ObjectMeta,
|
V1ObjectMeta,
|
||||||
V1ObjectReference,
|
V1ObjectReference,
|
||||||
|
V1PodSecurityContext,
|
||||||
V1PodSpec,
|
V1PodSpec,
|
||||||
V1PodTemplateSpec,
|
V1PodTemplateSpec,
|
||||||
|
V1SeccompProfile,
|
||||||
V1SecretKeySelector,
|
V1SecretKeySelector,
|
||||||
|
V1SecurityContext,
|
||||||
)
|
)
|
||||||
|
|
||||||
from authentik import __version__, get_full_version
|
from authentik import __version__, get_full_version
|
||||||
@ -103,6 +107,11 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
image_pull_secrets=[
|
image_pull_secrets=[
|
||||||
V1ObjectReference(name=secret) for secret in image_pull_secrets
|
V1ObjectReference(name=secret) for secret in image_pull_secrets
|
||||||
],
|
],
|
||||||
|
security_context=V1PodSecurityContext(
|
||||||
|
seccomp_profile=V1SeccompProfile(
|
||||||
|
type="RuntimeDefault",
|
||||||
|
),
|
||||||
|
),
|
||||||
containers=[
|
containers=[
|
||||||
V1Container(
|
V1Container(
|
||||||
name=str(self.outpost.type),
|
name=str(self.outpost.type),
|
||||||
@ -146,6 +155,13 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
security_context=V1SecurityContext(
|
||||||
|
run_as_non_root=True,
|
||||||
|
allow_privilege_escalation=False,
|
||||||
|
capabilities=V1Capabilities(
|
||||||
|
drop=["ALL"],
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
20
authentik/outposts/migrations/0020_alter_outpost_type.py
Normal file
20
authentik/outposts/migrations/0020_alter_outpost_type.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 4.1.7 on 2023-03-20 10:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("authentik_outposts", "0019_alter_outpost_name_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="outpost",
|
||||||
|
name="type",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[("proxy", "Proxy"), ("ldap", "Ldap"), ("radius", "Radius")],
|
||||||
|
default="proxy",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -94,6 +94,7 @@ class OutpostType(models.TextChoices):
|
|||||||
|
|
||||||
PROXY = "proxy"
|
PROXY = "proxy"
|
||||||
LDAP = "ldap"
|
LDAP = "ldap"
|
||||||
|
RADIUS = "radius"
|
||||||
|
|
||||||
|
|
||||||
def default_outpost_config(host: Optional[str] = None):
|
def default_outpost_config(host: Optional[str] = None):
|
||||||
|
|||||||
@ -19,9 +19,9 @@ CELERY_BEAT_SCHEDULE = {
|
|||||||
"schedule": crontab(minute=fqdn_rand("outpost_token_ensurer"), hour="*/8"),
|
"schedule": crontab(minute=fqdn_rand("outpost_token_ensurer"), hour="*/8"),
|
||||||
"options": {"queue": "authentik_scheduled"},
|
"options": {"queue": "authentik_scheduled"},
|
||||||
},
|
},
|
||||||
"outpost_local_connection": {
|
"outpost_connection_discovery": {
|
||||||
"task": "authentik.outposts.tasks.outpost_local_connection",
|
"task": "authentik.outposts.tasks.outpost_connection_discovery",
|
||||||
"schedule": crontab(minute=fqdn_rand("outpost_local_connection"), hour="*/8"),
|
"schedule": crontab(minute=fqdn_rand("outpost_connection_discovery"), hour="*/8"),
|
||||||
"options": {"queue": "authentik_scheduled"},
|
"options": {"queue": "authentik_scheduled"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ from urllib.parse import urlparse
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
|
from channels.layers import get_channel_layer
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import DatabaseError, InternalError, ProgrammingError
|
from django.db import DatabaseError, InternalError, ProgrammingError
|
||||||
from django.db.models.base import Model
|
from django.db.models.base import Model
|
||||||
@ -42,7 +43,6 @@ from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesContro
|
|||||||
from authentik.providers.proxy.controllers.docker import ProxyDockerController
|
from authentik.providers.proxy.controllers.docker import ProxyDockerController
|
||||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
from authentik.root.messages.storage import closing_send
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s"
|
CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s"
|
||||||
@ -214,50 +214,58 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
|||||||
outpost_send_update(reverse)
|
outpost_send_update(reverse)
|
||||||
|
|
||||||
|
|
||||||
def outpost_send_update(model_instace: Model):
|
def outpost_send_update(model_instance: Model):
|
||||||
"""Send outpost update to all registered outposts, regardless to which authentik
|
"""Send outpost update to all registered outposts, regardless to which authentik
|
||||||
instance they are connected"""
|
instance they are connected"""
|
||||||
if isinstance(model_instace, OutpostModel):
|
channel_layer = get_channel_layer()
|
||||||
for outpost in model_instace.outpost_set.all():
|
if isinstance(model_instance, OutpostModel):
|
||||||
_outpost_single_update(outpost)
|
for outpost in model_instance.outpost_set.all():
|
||||||
elif isinstance(model_instace, Outpost):
|
_outpost_single_update(outpost, channel_layer)
|
||||||
_outpost_single_update(model_instace)
|
elif isinstance(model_instance, Outpost):
|
||||||
|
_outpost_single_update(model_instance, channel_layer)
|
||||||
|
|
||||||
|
|
||||||
def _outpost_single_update(outpost: Outpost):
|
def _outpost_single_update(outpost: Outpost, layer=None):
|
||||||
"""Update outpost instances connected to a single outpost"""
|
"""Update outpost instances connected to a single outpost"""
|
||||||
# Ensure token again, because this function is called when anything related to an
|
# Ensure token again, because this function is called when anything related to an
|
||||||
# OutpostModel is saved, so we can be sure permissions are right
|
# OutpostModel is saved, so we can be sure permissions are right
|
||||||
_ = outpost.token
|
_ = outpost.token
|
||||||
outpost.build_user_permissions(outpost.user)
|
outpost.build_user_permissions(outpost.user)
|
||||||
|
if not layer: # pragma: no cover
|
||||||
|
layer = get_channel_layer()
|
||||||
for state in OutpostState.for_outpost(outpost):
|
for state in OutpostState.for_outpost(outpost):
|
||||||
for channel in state.channel_ids:
|
for channel in state.channel_ids:
|
||||||
LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost)
|
LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost)
|
||||||
async_to_sync(closing_send)(channel, {"type": "event.update"})
|
async_to_sync(layer.send)(channel, {"type": "event.update"})
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task(
|
||||||
def outpost_local_connection():
|
base=MonitoredTask,
|
||||||
|
bind=True,
|
||||||
|
)
|
||||||
|
def outpost_connection_discovery(self: MonitoredTask):
|
||||||
"""Checks the local environment and create Service connections."""
|
"""Checks the local environment and create Service connections."""
|
||||||
|
status = TaskResult(TaskResultStatus.SUCCESSFUL)
|
||||||
if not CONFIG.y_bool("outposts.discover"):
|
if not CONFIG.y_bool("outposts.discover"):
|
||||||
LOGGER.info("Outpost integration discovery is disabled")
|
status.messages.append("Outpost integration discovery is disabled")
|
||||||
|
self.set_status(status)
|
||||||
return
|
return
|
||||||
# Explicitly check against token filename, as that's
|
# Explicitly check against token filename, as that's
|
||||||
# only present when the integration is enabled
|
# only present when the integration is enabled
|
||||||
if Path(SERVICE_TOKEN_FILENAME).exists():
|
if Path(SERVICE_TOKEN_FILENAME).exists():
|
||||||
LOGGER.info("Detected in-cluster Kubernetes Config")
|
status.messages.append("Detected in-cluster Kubernetes Config")
|
||||||
if not KubernetesServiceConnection.objects.filter(local=True).exists():
|
if not KubernetesServiceConnection.objects.filter(local=True).exists():
|
||||||
LOGGER.debug("Created Service Connection for in-cluster")
|
status.messages.append("Created Service Connection for in-cluster")
|
||||||
KubernetesServiceConnection.objects.create(
|
KubernetesServiceConnection.objects.create(
|
||||||
name="Local Kubernetes Cluster", local=True, kubeconfig={}
|
name="Local Kubernetes Cluster", local=True, kubeconfig={}
|
||||||
)
|
)
|
||||||
# For development, check for the existence of a kubeconfig file
|
# For development, check for the existence of a kubeconfig file
|
||||||
kubeconfig_path = Path(KUBE_CONFIG_DEFAULT_LOCATION).expanduser()
|
kubeconfig_path = Path(KUBE_CONFIG_DEFAULT_LOCATION).expanduser()
|
||||||
if kubeconfig_path.exists():
|
if kubeconfig_path.exists():
|
||||||
LOGGER.info("Detected kubeconfig")
|
status.messages.append("Detected kubeconfig")
|
||||||
kubeconfig_local_name = f"k8s-{gethostname()}"
|
kubeconfig_local_name = f"k8s-{gethostname()}"
|
||||||
if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists():
|
if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists():
|
||||||
LOGGER.debug("Creating kubeconfig Service Connection")
|
status.messages.append("Creating kubeconfig Service Connection")
|
||||||
with kubeconfig_path.open("r", encoding="utf8") as _kubeconfig:
|
with kubeconfig_path.open("r", encoding="utf8") as _kubeconfig:
|
||||||
KubernetesServiceConnection.objects.create(
|
KubernetesServiceConnection.objects.create(
|
||||||
name=kubeconfig_local_name,
|
name=kubeconfig_local_name,
|
||||||
@ -266,11 +274,12 @@ def outpost_local_connection():
|
|||||||
unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path
|
unix_socket_path = urlparse(DEFAULT_UNIX_SOCKET).path
|
||||||
socket = Path(unix_socket_path)
|
socket = Path(unix_socket_path)
|
||||||
if socket.exists() and access(socket, R_OK):
|
if socket.exists() and access(socket, R_OK):
|
||||||
LOGGER.info("Detected local docker socket")
|
status.messages.append("Detected local docker socket")
|
||||||
if len(DockerServiceConnection.objects.filter(local=True)) == 0:
|
if len(DockerServiceConnection.objects.filter(local=True)) == 0:
|
||||||
LOGGER.debug("Created Service Connection for docker")
|
status.messages.append("Created Service Connection for docker")
|
||||||
DockerServiceConnection.objects.create(
|
DockerServiceConnection.objects.create(
|
||||||
name="Local Docker connection",
|
name="Local Docker connection",
|
||||||
local=True,
|
local=True,
|
||||||
url=unix_socket_path,
|
url=unix_socket_path,
|
||||||
)
|
)
|
||||||
|
self.set_status(status)
|
||||||
|
|||||||
8
authentik/outposts/urls.py
Normal file
8
authentik/outposts/urls.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""Outpost Websocket URLS"""
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from authentik.outposts.channels import OutpostConsumer
|
||||||
|
|
||||||
|
websocket_urlpatterns = [
|
||||||
|
path("ws/outpost/<uuid:pk>/", OutpostConsumer.as_asgi()),
|
||||||
|
]
|
||||||
@ -9,8 +9,6 @@ from authentik.flows.planner import PLAN_CONTEXT_SSO
|
|||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
from authentik.policies.exceptions import PolicyException
|
from authentik.policies.exceptions import PolicyException
|
||||||
from authentik.policies.models import Policy, PolicyBinding
|
|
||||||
from authentik.policies.process import PolicyProcess
|
|
||||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -32,22 +30,11 @@ class PolicyEvaluator(BaseEvaluator):
|
|||||||
# update website/docs/expressions/_functions.md
|
# update website/docs/expressions/_functions.md
|
||||||
self._context["ak_message"] = self.expr_func_message
|
self._context["ak_message"] = self.expr_func_message
|
||||||
self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator
|
self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator
|
||||||
self._context["ak_call_policy"] = self.expr_func_call_policy
|
|
||||||
|
|
||||||
def expr_func_message(self, message: str):
|
def expr_func_message(self, message: str):
|
||||||
"""Wrapper to append to messages list, which is returned with PolicyResult"""
|
"""Wrapper to append to messages list, which is returned with PolicyResult"""
|
||||||
self._messages.append(message)
|
self._messages.append(message)
|
||||||
|
|
||||||
def expr_func_call_policy(self, name: str, **kwargs) -> PolicyResult:
|
|
||||||
"""Call policy by name, with current request"""
|
|
||||||
policy = Policy.objects.filter(name=name).select_subclasses().first()
|
|
||||||
if not policy:
|
|
||||||
raise ValueError(f"Policy '{name}' not found.")
|
|
||||||
req: PolicyRequest = self._context["request"]
|
|
||||||
req.context.update(kwargs)
|
|
||||||
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
|
|
||||||
return proc.profiling_wrapper()
|
|
||||||
|
|
||||||
def set_policy_request(self, request: PolicyRequest):
|
def set_policy_request(self, request: PolicyRequest):
|
||||||
"""Update context based on policy request (if http request is given, update that too)"""
|
"""Update context based on policy request (if http request is given, update that too)"""
|
||||||
# update website/docs/expressions/_objects.md
|
# update website/docs/expressions/_objects.md
|
||||||
@ -83,6 +70,7 @@ class PolicyEvaluator(BaseEvaluator):
|
|||||||
return PolicyResult(False, str(exc))
|
return PolicyResult(False, str(exc))
|
||||||
else:
|
else:
|
||||||
policy_result = PolicyResult(False, *self._messages)
|
policy_result = PolicyResult(False, *self._messages)
|
||||||
|
policy_result.raw_result = result
|
||||||
if result is None:
|
if result is None:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Expression policy returned None",
|
"Expression policy returned None",
|
||||||
|
|||||||
@ -54,6 +54,7 @@ class TestPasswordPolicyFlow(FlowTestCase):
|
|||||||
component="ak-stage-prompt",
|
component="ak-stage-prompt",
|
||||||
fields=[
|
fields=[
|
||||||
{
|
{
|
||||||
|
"choices": None,
|
||||||
"field_key": "password",
|
"field_key": "password",
|
||||||
"label": "PASSWORD_LABEL",
|
"label": "PASSWORD_LABEL",
|
||||||
"order": 0,
|
"order": 0,
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -69,10 +69,11 @@ class PolicyRequest:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PolicyResult:
|
class PolicyResult:
|
||||||
"""Small data-class to hold policy results"""
|
"""Result from evaluating a policy."""
|
||||||
|
|
||||||
passing: bool
|
passing: bool
|
||||||
messages: tuple[str, ...]
|
messages: tuple[str, ...]
|
||||||
|
raw_result: Any
|
||||||
|
|
||||||
source_binding: Optional["PolicyBinding"]
|
source_binding: Optional["PolicyBinding"]
|
||||||
source_results: Optional[list["PolicyResult"]]
|
source_results: Optional[list["PolicyResult"]]
|
||||||
@ -83,6 +84,7 @@ class PolicyResult:
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.passing = passing
|
self.passing = passing
|
||||||
self.messages = messages
|
self.messages = messages
|
||||||
|
self.raw_result = None
|
||||||
self.source_binding = None
|
self.source_binding = None
|
||||||
self.source_results = []
|
self.source_results = []
|
||||||
self.log_messages = []
|
self.log_messages = []
|
||||||
|
|||||||
@ -26,6 +26,7 @@ class LDAPProviderSerializer(ProviderSerializer):
|
|||||||
"search_mode",
|
"search_mode",
|
||||||
"bind_mode",
|
"bind_mode",
|
||||||
]
|
]
|
||||||
|
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||||
|
|
||||||
|
|
||||||
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
|
class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
|||||||
@ -39,6 +39,7 @@ class OAuth2ProviderSerializer(ProviderSerializer):
|
|||||||
"issuer_mode",
|
"issuer_mode",
|
||||||
"jwks_sources",
|
"jwks_sources",
|
||||||
]
|
]
|
||||||
|
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||||
|
|
||||||
|
|
||||||
class OAuth2ProviderSetupURLs(PassiveSerializer):
|
class OAuth2ProviderSetupURLs(PassiveSerializer):
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import authentik.providers.oauth2.models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -37,4 +39,14 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="oauth2provider",
|
||||||
|
name="client_secret",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
default=authentik.providers.oauth2.models.generate_client_secret,
|
||||||
|
max_length=255,
|
||||||
|
verbose_name="Client Secret",
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -27,6 +27,11 @@ from authentik.providers.oauth2.id_token import IDToken, SubModes
|
|||||||
from authentik.sources.oauth.models import OAuthSource
|
from authentik.sources.oauth.models import OAuthSource
|
||||||
|
|
||||||
|
|
||||||
|
def generate_client_secret() -> str:
|
||||||
|
"""Generate client secret with adequate length"""
|
||||||
|
return generate_id(128)
|
||||||
|
|
||||||
|
|
||||||
class ClientTypes(models.TextChoices):
|
class ClientTypes(models.TextChoices):
|
||||||
"""Confidential clients are capable of maintaining the confidentiality
|
"""Confidential clients are capable of maintaining the confidentiality
|
||||||
of their credentials. Public clients are incapable."""
|
of their credentials. Public clients are incapable."""
|
||||||
@ -132,7 +137,7 @@ class OAuth2Provider(Provider):
|
|||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
verbose_name=_("Client Secret"),
|
verbose_name=_("Client Secret"),
|
||||||
default=generate_key,
|
default=generate_client_secret,
|
||||||
)
|
)
|
||||||
redirect_uris = models.TextField(
|
redirect_uris = models.TextField(
|
||||||
default="",
|
default="",
|
||||||
|
|||||||
@ -7,7 +7,6 @@ from rest_framework.test import APITestCase
|
|||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
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.lib.generators import generate_id, generate_key
|
|
||||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
|
||||||
|
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ class TestAPI(APITestCase):
|
|||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris="http://testserver",
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from authentik.core.models import Application
|
|||||||
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.challenge import ChallengeTypes
|
from authentik.flows.challenge import ChallengeTypes
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.providers.oauth2.constants import TOKEN_TYPE
|
from authentik.providers.oauth2.constants import TOKEN_TYPE
|
||||||
from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError
|
from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError
|
||||||
@ -298,7 +298,6 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id="test",
|
client_id="test",
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris="http://localhost",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
@ -355,13 +354,67 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
delta=5,
|
delta=5,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_full_fragment_code(self):
|
||||||
|
"""Test full authorization"""
|
||||||
|
flow = create_test_flow()
|
||||||
|
provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
|
name=generate_id(),
|
||||||
|
client_id="test",
|
||||||
|
authorization_flow=flow,
|
||||||
|
redirect_uris="http://localhost",
|
||||||
|
signing_key=self.keypair,
|
||||||
|
)
|
||||||
|
Application.objects.create(name="app", slug="app", provider=provider)
|
||||||
|
state = generate_id()
|
||||||
|
user = create_test_admin_user()
|
||||||
|
self.client.force_login(user)
|
||||||
|
with patch(
|
||||||
|
"authentik.providers.oauth2.id_token.get_login_event",
|
||||||
|
MagicMock(
|
||||||
|
return_value=Event(
|
||||||
|
action=EventAction.LOGIN,
|
||||||
|
context={PLAN_CONTEXT_METHOD: "password"},
|
||||||
|
created=now(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
# Step 1, initiate params and get redirect to flow
|
||||||
|
self.client.get(
|
||||||
|
reverse("authentik_providers_oauth2:authorize"),
|
||||||
|
data={
|
||||||
|
"response_type": "code",
|
||||||
|
"response_mode": "fragment",
|
||||||
|
"client_id": "test",
|
||||||
|
"state": state,
|
||||||
|
"scope": "openid",
|
||||||
|
"redirect_uri": "http://localhost",
|
||||||
|
"nonce": generate_id(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response = self.client.get(
|
||||||
|
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||||
|
)
|
||||||
|
code: AuthorizationCode = AuthorizationCode.objects.filter(user=user).first()
|
||||||
|
self.assertJSONEqual(
|
||||||
|
response.content.decode(),
|
||||||
|
{
|
||||||
|
"component": "xak-flow-redirect",
|
||||||
|
"type": ChallengeTypes.REDIRECT.value,
|
||||||
|
"to": (f"http://localhost#code={code.code}" f"&state={state}"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
code.expires.timestamp() - now().timestamp(),
|
||||||
|
timedelta_from_string(provider.access_code_validity).total_seconds(),
|
||||||
|
delta=5,
|
||||||
|
)
|
||||||
|
|
||||||
def test_full_form_post_id_token(self):
|
def test_full_form_post_id_token(self):
|
||||||
"""Test full authorization (form_post response)"""
|
"""Test full authorization (form_post response)"""
|
||||||
flow = create_test_flow()
|
flow = create_test_flow()
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
client_id=generate_id(),
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris="http://localhost",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
@ -411,7 +464,6 @@ class TestAuthorize(OAuthTestCase):
|
|||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
client_id=generate_id(),
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=flow,
|
authorization_flow=flow,
|
||||||
redirect_uris="http://localhost",
|
redirect_uris="http://localhost",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
||||||
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
|
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
@ -21,8 +21,6 @@ class TesOAuth2Introspection(OAuthTestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris="",
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
|
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, RefreshToken
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
@ -20,8 +20,6 @@ class TesOAuth2Revoke(OAuthTestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris="",
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
|
|||||||
@ -38,8 +38,6 @@ class TestToken(OAuthTestCase):
|
|||||||
"""test request param"""
|
"""test request param"""
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://TestServer",
|
redirect_uris="http://TestServer",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
@ -67,8 +65,6 @@ class TestToken(OAuthTestCase):
|
|||||||
"""test request param"""
|
"""test request param"""
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris="http://testserver",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
@ -90,8 +86,6 @@ class TestToken(OAuthTestCase):
|
|||||||
"""test request param"""
|
"""test request param"""
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
@ -120,8 +114,6 @@ class TestToken(OAuthTestCase):
|
|||||||
"""test request param"""
|
"""test request param"""
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
@ -163,8 +155,6 @@ class TestToken(OAuthTestCase):
|
|||||||
"""test request param"""
|
"""test request param"""
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
@ -215,8 +205,6 @@ class TestToken(OAuthTestCase):
|
|||||||
"""test request param"""
|
"""test request param"""
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://local.invalid",
|
redirect_uris="http://local.invalid",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
@ -263,8 +251,6 @@ class TestToken(OAuthTestCase):
|
|||||||
"""test request param"""
|
"""test request param"""
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris="http://testserver",
|
||||||
signing_key=self.keypair,
|
signing_key=self.keypair,
|
||||||
|
|||||||
@ -8,7 +8,6 @@ from jwt import decode
|
|||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents
|
from authentik.core.models import USER_ATTRIBUTE_SA, Application, Group, Token, TokenIntents
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.providers.oauth2.constants import (
|
from authentik.providers.oauth2.constants import (
|
||||||
GRANT_TYPE_CLIENT_CREDENTIALS,
|
GRANT_TYPE_CLIENT_CREDENTIALS,
|
||||||
@ -31,8 +30,6 @@ class TestTokenClientCredentials(OAuthTestCase):
|
|||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris="http://testserver",
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from jwt import decode
|
|||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Application, Group
|
from authentik.core.models import Application, Group
|
||||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.policies.models import PolicyBinding
|
from authentik.policies.models import PolicyBinding
|
||||||
from authentik.providers.oauth2.constants import (
|
from authentik.providers.oauth2.constants import (
|
||||||
GRANT_TYPE_CLIENT_CREDENTIALS,
|
GRANT_TYPE_CLIENT_CREDENTIALS,
|
||||||
@ -39,7 +39,7 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase):
|
|||||||
slug=generate_id(),
|
slug=generate_id(),
|
||||||
provider_type="openidconnect",
|
provider_type="openidconnect",
|
||||||
consumer_key=generate_id(),
|
consumer_key=generate_id(),
|
||||||
consumer_secret=generate_key(),
|
consumer_secret=generate_id(),
|
||||||
authorization_url="http://foo",
|
authorization_url="http://foo",
|
||||||
access_token_url=f"http://{generate_id()}",
|
access_token_url=f"http://{generate_id()}",
|
||||||
profile_url="http://foo",
|
profile_url="http://foo",
|
||||||
@ -52,8 +52,6 @@ class TestTokenClientCredentialsJWTSource(OAuthTestCase):
|
|||||||
|
|
||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris="http://testserver",
|
||||||
signing_key=self.cert,
|
signing_key=self.cert,
|
||||||
|
|||||||
@ -7,7 +7,7 @@ from django.urls import reverse
|
|||||||
from authentik.blueprints.tests import apply_blueprint
|
from authentik.blueprints.tests import apply_blueprint
|
||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.lib.generators import generate_code_fixed_length, generate_id, generate_key
|
from authentik.lib.generators import generate_code_fixed_length, generate_id
|
||||||
from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE
|
from authentik.providers.oauth2.constants import GRANT_TYPE_DEVICE_CODE
|
||||||
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider, ScopeMapping
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
@ -22,8 +22,6 @@ class TestTokenDeviceCode(OAuthTestCase):
|
|||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
self.provider = OAuth2Provider.objects.create(
|
self.provider = OAuth2Provider.objects.create(
|
||||||
name="test",
|
name="test",
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="http://testserver",
|
redirect_uris="http://testserver",
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
|
|||||||
@ -9,7 +9,7 @@ from authentik.blueprints.tests import apply_blueprint
|
|||||||
from authentik.core.models import Application
|
from authentik.core.models import Application
|
||||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.generators import generate_id, generate_key
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, ScopeMapping
|
from authentik.providers.oauth2.models import AccessToken, IDToken, OAuth2Provider, ScopeMapping
|
||||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||||
|
|
||||||
@ -23,8 +23,6 @@ class TestUserinfo(OAuthTestCase):
|
|||||||
self.app = Application.objects.create(name=generate_id(), slug=generate_id())
|
self.app = Application.objects.create(name=generate_id(), slug=generate_id())
|
||||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||||
name=generate_id(),
|
name=generate_id(),
|
||||||
client_id=generate_id(),
|
|
||||||
client_secret=generate_key(),
|
|
||||||
authorization_flow=create_test_flow(),
|
authorization_flow=create_test_flow(),
|
||||||
redirect_uris="",
|
redirect_uris="",
|
||||||
signing_key=create_test_cert(),
|
signing_key=create_test_cert(),
|
||||||
|
|||||||
@ -514,7 +514,12 @@ class OAuthFulfillmentStage(StageView):
|
|||||||
return urlunsplit(uri)
|
return urlunsplit(uri)
|
||||||
|
|
||||||
if self.params.response_mode == ResponseMode.FRAGMENT:
|
if self.params.response_mode == ResponseMode.FRAGMENT:
|
||||||
query_fragment = self.create_implicit_response(code)
|
query_fragment = {}
|
||||||
|
if self.params.grant_type in [GrantTypes.AUTHORIZATION_CODE]:
|
||||||
|
query_fragment["code"] = code.code
|
||||||
|
query_fragment["state"] = [str(self.params.state) if self.params.state else ""]
|
||||||
|
else:
|
||||||
|
query_fragment = self.create_implicit_response(code)
|
||||||
|
|
||||||
uri = uri._replace(
|
uri = uri._replace(
|
||||||
fragment=uri.fragment + urlencode(query_fragment, doseq=True),
|
fragment=uri.fragment + urlencode(query_fragment, doseq=True),
|
||||||
|
|||||||
@ -95,6 +95,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
|||||||
"refresh_token_validity",
|
"refresh_token_validity",
|
||||||
"outpost_set",
|
"outpost_set",
|
||||||
]
|
]
|
||||||
|
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||||
|
|
||||||
|
|
||||||
class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
|||||||
0
authentik/providers/radius/__init__.py
Normal file
0
authentik/providers/radius/__init__.py
Normal file
65
authentik/providers/radius/api.py
Normal file
65
authentik/providers/radius/api.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""RadiusProvider API Views"""
|
||||||
|
from rest_framework.fields import CharField
|
||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.providers.radius.models import RadiusProvider
|
||||||
|
|
||||||
|
|
||||||
|
class RadiusProviderSerializer(ProviderSerializer):
|
||||||
|
"""RadiusProvider Serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RadiusProvider
|
||||||
|
fields = ProviderSerializer.Meta.fields + [
|
||||||
|
"client_networks",
|
||||||
|
# Shared secret is not a write-only field, as
|
||||||
|
# an admin might have to view it
|
||||||
|
"shared_secret",
|
||||||
|
]
|
||||||
|
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class RadiusProviderViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
"""RadiusProvider Viewset"""
|
||||||
|
|
||||||
|
queryset = RadiusProvider.objects.all()
|
||||||
|
serializer_class = RadiusProviderSerializer
|
||||||
|
ordering = ["name"]
|
||||||
|
search_fields = ["name", "client_networks"]
|
||||||
|
filterset_fields = {
|
||||||
|
"application": ["isnull"],
|
||||||
|
"name": ["iexact"],
|
||||||
|
"authorization_flow__slug": ["iexact"],
|
||||||
|
"client_networks": ["iexact"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RadiusOutpostConfigSerializer(ModelSerializer):
|
||||||
|
"""RadiusProvider Serializer"""
|
||||||
|
|
||||||
|
application_slug = CharField(source="application.slug")
|
||||||
|
auth_flow_slug = CharField(source="authorization_flow.slug")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = RadiusProvider
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"name",
|
||||||
|
"application_slug",
|
||||||
|
"auth_flow_slug",
|
||||||
|
"client_networks",
|
||||||
|
"shared_secret",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class RadiusOutpostConfigViewSet(ReadOnlyModelViewSet):
|
||||||
|
"""RadiusProvider Viewset"""
|
||||||
|
|
||||||
|
queryset = RadiusProvider.objects.filter(application__isnull=False)
|
||||||
|
serializer_class = RadiusOutpostConfigSerializer
|
||||||
|
ordering = ["name"]
|
||||||
|
search_fields = ["name"]
|
||||||
|
filterset_fields = ["name"]
|
||||||
10
authentik/providers/radius/apps.py
Normal file
10
authentik/providers/radius/apps.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"""authentik radius provider app config"""
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AuthentikProviderRadiusConfig(AppConfig):
|
||||||
|
"""authentik radius provider app config"""
|
||||||
|
|
||||||
|
name = "authentik.providers.radius"
|
||||||
|
label = "authentik_providers_radius"
|
||||||
|
verbose_name = "authentik Providers.Radius"
|
||||||
0
authentik/providers/radius/controllers/__init__.py
Normal file
0
authentik/providers/radius/controllers/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user