Compare commits
261 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
fe5d22ce6c | |||
0e30b6ee55 | |||
6cbba45291 | |||
ba023a3bba | |||
6c805bcf32 | |||
bc7d5042df | |||
de3e1c3dbc | |||
3c6aac5435 | |||
eeb755ab7d | |||
70d0dd51a5 | |||
073dd8b560 | |||
b5d2924d46 | |||
597e279f34 | |||
fc28def83d | |||
f6efdfded4 | |||
91312496e0 | |||
b557b4337d | |||
bfde186aa0 | |||
2bd75dd1a9 | |||
27ab31a9b0 | |||
44a8b737d9 | |||
b939ee7a09 | |||
0bae550520 | |||
b5cc2f2bda | |||
9ad4cf1db9 | |||
9dbafaaea2 | |||
2db8b07578 | |||
7c1a7bfd9d | |||
b7ef076798 | |||
37c29a073e | |||
0c288ea64b | |||
2476475174 | |||
71913c8164 | |||
6ec8432217 | |||
7a12c0e4d1 | |||
23a7eba16b | |||
3ba84a8e8b | |||
75476217a0 | |||
7771c0b905 | |||
3378e82ec7 | |||
126e43dea4 | |||
f725009530 | |||
70d1e3a0cb | |||
e751ce1220 | |||
e09a27cf87 | |||
06fbf44724 | |||
200e409d91 | |||
5e5854e256 | |||
3df8bcfc9c | |||
e76c14f9e0 | |||
6b6748b1c7 | |||
d92d8e6dbb | |||
c2b9dc5c75 | |||
5c1d27de2b | |||
6ab9e7cd68 | |||
3ef56e9ec1 | |||
6d8d157772 | |||
cadd466eec | |||
3fea0c1e49 | |||
4c58201adc | |||
4fb4e72624 | |||
276d8fe5cf | |||
92ce5f0931 | |||
7fea20375f | |||
d4d4034d2c | |||
f0db408699 | |||
5e200655d9 | |||
d5d1f2a645 | |||
cc5cc43baa | |||
e512f085db | |||
f323c01bd8 | |||
f56cacb406 | |||
eaecd31e9f | |||
36989d82e1 | |||
50777d9022 | |||
a15571bd3e | |||
26fd66d831 | |||
0be873025a | |||
28ada49910 | |||
4fc8e61f8c | |||
7d26ea1a9c | |||
3a58dc62e1 | |||
71fe7bc827 | |||
933336c38b | |||
371feb9a31 | |||
95a2fd3c9e | |||
17cb76c334 | |||
88f0dfc8cc | |||
f82aada23b | |||
ecaee92634 | |||
89252ec47b | |||
f0f25ab291 | |||
e4d0fec15a | |||
6b10baf086 | |||
f148b5d341 | |||
1471ff8940 | |||
d9a6ec2ac0 | |||
5745ffa0a8 | |||
b26202db35 | |||
6318577a51 | |||
6a2cd45847 | |||
ef5cea2c01 | |||
69f4d54bae | |||
b1eec5a7d2 | |||
1b8271d767 | |||
3e9f5ec5ef | |||
63f57b6a77 | |||
a016f99450 | |||
adc18b2991 | |||
e37a326b95 | |||
048467e97d | |||
cc2cd6919f | |||
0c6e781e5b | |||
7294d8fca5 | |||
16ec5680b4 | |||
87920fb1d7 | |||
523b96a6d2 | |||
45731d8069 | |||
e872371970 | |||
08e8cf850a | |||
b1ed2154ac | |||
7ef2aa3eb9 | |||
160139813d | |||
582ad92c76 | |||
f61736e3d1 | |||
eb02c96281 | |||
8619552920 | |||
6237352e25 | |||
2d8b4f543b | |||
8542dc10ab | |||
c55b63337c | |||
12ddee3bb6 | |||
dc41d0af27 | |||
3323b50036 | |||
8acb15a7fd | |||
f601e04b38 | |||
f50529cb5b | |||
3f1b6f9ed4 | |||
f1ab0f4314 | |||
4d1129f385 | |||
03ac9c6e16 | |||
c0839924f1 | |||
91e3aa760a | |||
5c0681d57b | |||
c4f72c2bc1 | |||
e92f9836e3 | |||
3818dc834b | |||
cda011a049 | |||
897f6f3473 | |||
b70b44490b | |||
77a5a58cb9 | |||
f3b227434e | |||
2ae164df78 | |||
9b09793230 | |||
f8a401aeca | |||
ffbab2cd68 | |||
734e5fcab4 | |||
78578c6c9d | |||
0ccec96490 | |||
8022d0801d | |||
d79975c409 | |||
20d65035d5 | |||
8d6227377f | |||
4bc50e7f57 | |||
945e42c940 | |||
052bb28086 | |||
4a84b7e2d5 | |||
4d27694706 | |||
16cfa8cae2 | |||
1a20c8ffc1 | |||
d7ad5f6a16 | |||
5af9a3d3be | |||
dec34bc948 | |||
cff37caa57 | |||
cc6d5765f2 | |||
2ec1ff2ebb | |||
884c2bd0e9 | |||
2c938ec9dc | |||
9733caf3b7 | |||
494af0a430 | |||
10e50bc77f | |||
44bfbb9e49 | |||
5be152e12d | |||
b0efab6d6d | |||
f2725b88c8 | |||
24cc123029 | |||
d75c9997f6 | |||
0a20a30af3 | |||
c60ba91fee | |||
37927c9361 | |||
0a63441935 | |||
6b7a8b6ac7 | |||
cba255eaaa | |||
859cf2bd8f | |||
a2578ffaad | |||
888526a2a7 | |||
0d00b9cc0d | |||
27cc5d7138 | |||
b2f077645a | |||
2878597603 | |||
5face5410f | |||
1b8750e13b | |||
e27a6fdeeb | |||
a9af40f85c | |||
59f04963be | |||
033c9a3bd3 | |||
09e3d616e9 | |||
0b280c0a47 | |||
07a4f474f4 | |||
244dc671db | |||
4308136108 | |||
69a0153619 | |||
2655768f5a | |||
73c55b56a0 | |||
bcbdd6c26f | |||
00e9b91f56 | |||
4cf76fdcda | |||
c4832206fa | |||
d05562a388 | |||
f217d34a98 | |||
89f2967f69 | |||
9a6a3e66b8 | |||
2f4b18ebbd | |||
20572c728d | |||
aad753de68 | |||
a79a150a1f | |||
8b23e4701a | |||
a366d61891 | |||
9a13dfd63a | |||
32d80829e2 | |||
f6953296d8 | |||
e4790f9060 | |||
58712047e1 | |||
85915905dc | |||
52f2838f57 | |||
12e2f7b945 | |||
45d47f828a | |||
cf7eb88661 | |||
6a14ae7975 | |||
08f3294a1d | |||
ac47fc9295 | |||
1ff19e1467 | |||
439454a71b | |||
2a11964e1a | |||
507b8d43fb | |||
7efec281be | |||
9469f86f65 | |||
e998919097 | |||
450d69a1a4 | |||
b74681f22c | |||
f95a7c26e5 | |||
ffc9bd2cec | |||
bb7db0c828 | |||
aec3e08201 | |||
0651fbba06 | |||
bd9cd086a0 | |||
14fb0c3d61 | |||
c52afe5952 | |||
1d4b941a3b | |||
0344e5d9b3 | |||
d8e8cc062b |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.8.1-rc1
|
||||
current_version = 2021.8.5
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
@ -23,7 +23,7 @@ values =
|
||||
|
||||
[bumpversion:file:schema.yml]
|
||||
|
||||
[bumpversion:file:.github/workflows/release.yml]
|
||||
[bumpversion:file:.github/workflows/release-publish.yml]
|
||||
|
||||
[bumpversion:file:authentik/__init__.py]
|
||||
|
||||
|
302
.github/workflows/ci-main.yml
vendored
Normal file
302
.github/workflows/ci-main.yml
vendored
Normal file
@ -0,0 +1,302 @@
|
||||
name: authentik-ci-main
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
- version-*
|
||||
paths-ignore:
|
||||
- website
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
POSTGRES_DB: authentik
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
|
||||
jobs:
|
||||
lint-pylint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run pylint
|
||||
run: pipenv run pylint authentik tests lifecycle
|
||||
lint-black:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run black
|
||||
run: pipenv run black --check authentik tests lifecycle
|
||||
lint-isort:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run isort
|
||||
run: pipenv run isort --check authentik tests lifecycle
|
||||
lint-bandit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run bandit
|
||||
run: pipenv run bandit -r authentik tests lifecycle
|
||||
lint-pyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: prepare
|
||||
run: |
|
||||
scripts/ci_prepare.sh
|
||||
npm install -g pyright@1.1.136
|
||||
- name: run bandit
|
||||
run: pipenv run pyright e2e lifecycle
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run migrations
|
||||
run: pipenv run python -m lifecycle.migrate
|
||||
test-migrations-from-stable:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: checkout stable
|
||||
run: |
|
||||
# Copy current, latest config to local
|
||||
cp authentik/lib/default.yml local.env.yml
|
||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run migrations to stable
|
||||
run: pipenv run python -m lifecycle.migrate
|
||||
- name: checkout current code
|
||||
run: |
|
||||
set -x
|
||||
git checkout $GITHUB_REF
|
||||
pipenv sync --dev
|
||||
- name: migrate to latest
|
||||
run: pipenv run python -m lifecycle.migrate
|
||||
test-unittest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
- name: run unittest
|
||||
run: |
|
||||
pipenv run make test
|
||||
pipenv run coverage xml
|
||||
- name: run testspace
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
testspace [unittest]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v2
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
- name: Create k8s Kind Cluster
|
||||
uses: helm/kind-action@v1.2.0
|
||||
- name: run integration
|
||||
run: |
|
||||
pipenv run make test-integration
|
||||
pipenv run coverage xml
|
||||
- name: run testspace
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
testspace [integration]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v2
|
||||
test-e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- uses: testspace-com/setup-testspace@v1
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: |
|
||||
scripts/ci_prepare.sh
|
||||
docker-compose -f tests/e2e/ci.docker-compose.yml up -d
|
||||
- id: cache-web
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }}
|
||||
- name: prepare web ui
|
||||
if: steps.cache-web.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd web
|
||||
npm i
|
||||
npm run build
|
||||
- name: run e2e
|
||||
run: |
|
||||
pipenv run make test-e2e
|
||||
pipenv run coverage xml
|
||||
- name: run testspace
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
testspace [e2e]unittest.xml --link=codecov
|
||||
- if: ${{ always() }}
|
||||
uses: codecov/codecov-action@v2
|
||||
build:
|
||||
needs:
|
||||
- lint-pylint
|
||||
- lint-black
|
||||
- lint-isort
|
||||
- lint-bandit
|
||||
- lint-pyright
|
||||
- test-migrations
|
||||
- test-migrations-from-stable
|
||||
- test-unittest
|
||||
- test-integration
|
||||
- test-e2e
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: prepare variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||
run: |
|
||||
python ./scripts/gh_do_set_branch.py
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v1
|
||||
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
with:
|
||||
registry: beryju.org
|
||||
username: ${{ secrets.HARBOR_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_PASSWORD }}
|
||||
- name: Building Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
tags: |
|
||||
beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}
|
||||
beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
75
.github/workflows/ci-outpost.yml
vendored
Normal file
75
.github/workflows/ci-outpost.yml
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
name: authentik-ci-outpost
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
- version-*
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
lint-golint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.16.3'
|
||||
- name: Generate API
|
||||
run: |
|
||||
make gen-outpost
|
||||
- name: Run linter
|
||||
run: |
|
||||
# Create folder structure for go embeds
|
||||
mkdir -p web/dist
|
||||
mkdir -p website/help
|
||||
touch web/dist/test website/help/test
|
||||
docker run \
|
||||
--rm \
|
||||
-v $(pwd):/app \
|
||||
-w /app \
|
||||
golangci/golangci-lint:v1.39.0 \
|
||||
golangci-lint run -v --timeout 200s
|
||||
build:
|
||||
needs:
|
||||
- lint-golint
|
||||
strategy:
|
||||
matrix:
|
||||
type:
|
||||
- proxy
|
||||
- ldap
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: prepare variables
|
||||
id: ev
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||
run: |
|
||||
python ./scripts/gh_do_set_branch.py
|
||||
- name: Login to Container Registry
|
||||
uses: docker/login-action@v1
|
||||
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
with:
|
||||
registry: beryju.org
|
||||
username: ${{ secrets.HARBOR_USERNAME }}
|
||||
password: ${{ secrets.HARBOR_PASSWORD }}
|
||||
- name: Building Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||
tags: |
|
||||
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}
|
||||
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}
|
||||
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }}
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
89
.github/workflows/ci-web.yml
vendored
Normal file
89
.github/workflows/ci-web.yml
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
name: authentik-ci-web
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
- version-*
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
lint-eslint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: |
|
||||
cd web
|
||||
npm install
|
||||
- name: Generate API
|
||||
run: make gen-web
|
||||
- name: Eslint
|
||||
run: |
|
||||
cd web
|
||||
npm run lint
|
||||
lint-prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: |
|
||||
cd web
|
||||
npm install
|
||||
- name: Generate API
|
||||
run: make gen-web
|
||||
- name: prettier
|
||||
run: |
|
||||
cd web
|
||||
npm run prettier-check
|
||||
lint-lit-analyse:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: |
|
||||
cd web
|
||||
npm install
|
||||
- name: Generate API
|
||||
run: make gen-web
|
||||
- name: prettier
|
||||
run: |
|
||||
cd web
|
||||
npm run lit-analyse
|
||||
build:
|
||||
needs:
|
||||
- lint-eslint
|
||||
- lint-prettier
|
||||
- lint-lit-analyse
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: |
|
||||
cd web
|
||||
npm install
|
||||
- name: Generate API
|
||||
run: make gen-web
|
||||
- name: build
|
||||
run: |
|
||||
cd web
|
||||
npm run build
|
@ -33,14 +33,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.8.1-rc1,
|
||||
beryju/authentik:2021.8.5,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.8.1-rc1,
|
||||
ghcr.io/goauthentik/server:2021.8.5,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1-rc1', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.5', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
@ -75,14 +75,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-proxy:2021.8.1-rc1,
|
||||
beryju/authentik-proxy:2021.8.5,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.8.1-rc1,
|
||||
ghcr.io/goauthentik/proxy:2021.8.5,
|
||||
ghcr.io/goauthentik/proxy:latest
|
||||
file: proxy.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1-rc1', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.5', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-proxy:latest
|
||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||
@ -117,14 +117,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-ldap:2021.8.1-rc1,
|
||||
beryju/authentik-ldap:2021.8.5,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.8.1-rc1,
|
||||
ghcr.io/goauthentik/ldap:2021.8.5,
|
||||
ghcr.io/goauthentik/ldap:latest
|
||||
file: ldap.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1-rc1', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.5', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
@ -157,13 +157,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2.4.0
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12.x
|
||||
node-version: '16'
|
||||
- name: Build web api client and web ui
|
||||
run: |
|
||||
export NODE_ENV=production
|
||||
make gen-web
|
||||
cd web
|
||||
npm i
|
||||
npm run build
|
||||
@ -176,7 +175,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.8.1-rc1
|
||||
version: authentik@2021.8.5
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: '~/static/dist'
|
39
.github/workflows/web-api-publish.yml
vendored
Normal file
39
.github/workflows/web-api-publish.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
name: authentik-web-api-publish
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'schema.yml'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Generate API Client
|
||||
run: make gen-web
|
||||
- name: Publish package
|
||||
run: |
|
||||
cd web-api/
|
||||
npm i
|
||||
npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
||||
- name: Upgrade /web
|
||||
run: |
|
||||
cd web/
|
||||
export VERSION=`node -e 'console.log(require("../web-api/package.json").version)'`
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: update-web-api-client
|
||||
commit-message: "web: Update Web API Client version"
|
||||
title: "web: Update Web API Client version"
|
||||
delete-branch: true
|
||||
signoff: true
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -201,3 +201,4 @@ media/
|
||||
|
||||
.idea/
|
||||
/api/
|
||||
/web-api/
|
||||
|
22
.vscode/settings.json
vendored
Normal file
22
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"asgi",
|
||||
"authentik",
|
||||
"authn",
|
||||
"goauthentik",
|
||||
"jwks",
|
||||
"oidc",
|
||||
"openid",
|
||||
"plex",
|
||||
"saml",
|
||||
"totp",
|
||||
"webauthn"
|
||||
],
|
||||
"python.linting.pylintEnabled": true,
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
"todo-tree.tree.showBadges": true,
|
||||
"python.formatting.provider": "black",
|
||||
"files.associations": {
|
||||
"*.akflow": "json"
|
||||
}
|
||||
}
|
@ -11,8 +11,8 @@ The following is a set of guidelines for contributing to authentik and its compo
|
||||
[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
|
||||
|
||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||
* [Atom and Packages](#atom-and-packages)
|
||||
* [Atom Design Decisions](#design-decisions)
|
||||
* [The components](#the-components)
|
||||
* [authentik's structure](#authentiks-structure)
|
||||
|
||||
[How Can I Contribute?](#how-can-i-contribute)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
@ -22,14 +22,9 @@ The following is a set of guidelines for contributing to authentik and its compo
|
||||
|
||||
[Styleguides](#styleguides)
|
||||
* [Git Commit Messages](#git-commit-messages)
|
||||
* [JavaScript Styleguide](#javascript-styleguide)
|
||||
* [CoffeeScript Styleguide](#coffeescript-styleguide)
|
||||
* [Specs Styleguide](#specs-styleguide)
|
||||
* [Python Styleguide](#python-styleguide)
|
||||
* [Documentation Styleguide](#documentation-styleguide)
|
||||
|
||||
[Additional Notes](#additional-notes)
|
||||
* [Issue and Pull Request Labels](#issue-and-pull-request-labels)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Basically, don't be a dickhead. This is an open-source non-profit project, that is made in the free time of Volunteers. If there's something you dislike or think can be done better, tell us! We'd love to hear any suggestions for improvement.
|
||||
|
16
Dockerfile
16
Dockerfile
@ -18,17 +18,6 @@ COPY ./website /static/
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build-docs-only
|
||||
|
||||
# Stage 3: Build web API
|
||||
FROM openapitools/openapi-generator-cli as web-api-builder
|
||||
|
||||
COPY ./schema.yml /local/schema.yml
|
||||
|
||||
RUN docker-entrypoint.sh generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/web/api \
|
||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
||||
|
||||
# Stage 3: Generate API Client
|
||||
FROM openapitools/openapi-generator-cli as go-api-builder
|
||||
|
||||
@ -48,7 +37,6 @@ RUN docker-entrypoint.sh generate \
|
||||
FROM node as web-builder
|
||||
|
||||
COPY ./web /static/
|
||||
COPY --from=web-api-builder /local/web/api /static/api
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build
|
||||
@ -110,4 +98,6 @@ COPY --from=builder /work/authentik /authentik-proxy
|
||||
USER authentik
|
||||
ENV TMPDIR /dev/shm/
|
||||
ENV PYTHONUBUFFERED 1
|
||||
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
||||
ENV prometheus_multiproc_dir /dev/shm/
|
||||
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
|
||||
ENTRYPOINT [ "/lifecycle/ak" ]
|
||||
|
16
Makefile
16
Makefile
@ -2,12 +2,11 @@
|
||||
PWD = $(shell pwd)
|
||||
UID = $(shell id -u)
|
||||
GID = $(shell id -g)
|
||||
NPM_VERSION = $(shell python -m scripts.npm_version)
|
||||
|
||||
all: lint-fix lint test gen
|
||||
|
||||
test-integration:
|
||||
k3d cluster create || exit 0
|
||||
k3d kubeconfig write -o ~/.kube/config --overwrite
|
||||
coverage run manage.py test -v 3 tests/integration
|
||||
|
||||
test-e2e:
|
||||
@ -41,10 +40,13 @@ gen-web:
|
||||
openapitools/openapi-generator-cli generate \
|
||||
-i /local/schema.yml \
|
||||
-g typescript-fetch \
|
||||
-o /local/web/api \
|
||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
||||
# npm i runs tsc as part of the installation process
|
||||
cd web/api && npm i
|
||||
-o /local/web-api \
|
||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=@goauthentik/api,npmVersion=${NPM_VERSION}
|
||||
mkdir -p web/node_modules/@goauthentik/api
|
||||
python -m scripts.web_api_esm
|
||||
\cp -fv scripts/web_api_readme.md web-api/README.md
|
||||
cd web-api && npm i
|
||||
\cp -rfv web-api/* web/node_modules/@goauthentik/api
|
||||
|
||||
gen-outpost:
|
||||
docker run \
|
||||
@ -57,7 +59,7 @@ gen-outpost:
|
||||
-i /local/schema.yml \
|
||||
-g go \
|
||||
-o /local/api \
|
||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true
|
||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false
|
||||
rm -f api/go.mod api/go.sum
|
||||
|
||||
gen: gen-build gen-clean gen-web gen-outpost
|
||||
|
219
Pipfile.lock
generated
219
Pipfile.lock
generated
@ -122,19 +122,19 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:057196ac15de4de2221a24a3a0a41692414fa1dd697994d062ebd447163265e7",
|
||||
"sha256:852e776cea4287f74edcb45564f8345fb6b0168dde0fd5bf46668b94c3f21177"
|
||||
"sha256:4df1085f5c24504a1b1a6584947f27b67c26eda123f29d3cecce9b2fd683e09b",
|
||||
"sha256:a7fccb61d95230322dd812629455df14167307c569077fa89d297eae73605ffb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.18.25"
|
||||
"version": "==1.18.36"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:201e10d3b1b40d65b7c9214be7087d78ed65de00e7362bd1e020741301d09fbc",
|
||||
"sha256:b9820ee29d70059c9b0e2a69ec13ebf80f4a0bc85f47578f17e951438c506b2d"
|
||||
"sha256:5b9a7d30e44b8a0a2bbbde62ae01bf6c349017e836985a0248552b00bbce7fae",
|
||||
"sha256:e3e522fbe0bad1197aa7182451dc05f650310e77cf0a77749f6a5e82794c53de"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.21.25"
|
||||
"version": "==1.21.36"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -305,22 +305,25 @@
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d",
|
||||
"sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959",
|
||||
"sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6",
|
||||
"sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873",
|
||||
"sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2",
|
||||
"sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713",
|
||||
"sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1",
|
||||
"sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177",
|
||||
"sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250",
|
||||
"sha256:b01fd6f2737816cb1e08ed4807ae194404790eac7ad030b34f2ce72b332f5586",
|
||||
"sha256:bf40af59ca2465b24e54f671b2de2c59257ddc4f7e5706dbd6930e26823668d3",
|
||||
"sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca",
|
||||
"sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d",
|
||||
"sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"
|
||||
"sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e",
|
||||
"sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b",
|
||||
"sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7",
|
||||
"sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085",
|
||||
"sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc",
|
||||
"sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a",
|
||||
"sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498",
|
||||
"sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9",
|
||||
"sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c",
|
||||
"sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7",
|
||||
"sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb",
|
||||
"sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14",
|
||||
"sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af",
|
||||
"sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e",
|
||||
"sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5",
|
||||
"sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06",
|
||||
"sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7"
|
||||
],
|
||||
"version": "==3.4.7"
|
||||
"version": "==3.4.8"
|
||||
},
|
||||
"dacite": {
|
||||
"hashes": [
|
||||
@ -356,11 +359,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:7f92413529aa0e291f3be78ab19be31aefb1e1c9a52cd59e130f505f27a51f13",
|
||||
"sha256:f27f8544c9d4c383bbe007c57e3235918e258364577373d4920e9162837be022"
|
||||
"sha256:95b318319d6997bac3595517101ad9cc83fe5672ac498ba48d1a410f47afecd2",
|
||||
"sha256:e93c93565005b37ddebf2396b4dc4b6913c1838baa82efdfb79acedd5816c240"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.6"
|
||||
"version": "==3.2.7"
|
||||
},
|
||||
"django-dbbackup": {
|
||||
"git": "https://github.com/django-dbbackup/django-dbbackup.git",
|
||||
@ -440,19 +443,19 @@
|
||||
},
|
||||
"docker": {
|
||||
"hashes": [
|
||||
"sha256:3e8bc47534e0ca9331d72c32f2881bb13b93ded0bcdeab3c833fb7cf61c0a9a5",
|
||||
"sha256:fc961d622160e8021c10d1bcabc388c57d55fb1f917175afbe24af442e6879bd"
|
||||
"sha256:21ec4998e90dff7a7aaaa098ca8d839c7de412b89e6f6c30908372d58fecf663",
|
||||
"sha256:9b17f0723d83c1f3418d2aa17bf90b24dbe97deda06208dd4262fa30a6ee87eb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.0.0"
|
||||
"version": "==5.0.2"
|
||||
},
|
||||
"drf-spectacular": {
|
||||
"hashes": [
|
||||
"sha256:f080128c42183fcaed6b9e8e5afcd2e5cd68426b1f80bfc85938f25e62db7fe5",
|
||||
"sha256:fb19aa69fcfcd37b0c9dfb9989c0671e1bb47af332ca2171378c7f840263788c"
|
||||
"sha256:47ef6ec8ff48ac8aede6ec12450a55fee381cf84de969ef1724dcde5a93de6b8",
|
||||
"sha256:d746b936cb4cddec380ea95bf91de6a6721777dfc42e0eea53b83c61a625e94e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.17.3"
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"duo-client": {
|
||||
"hashes": [
|
||||
@ -487,11 +490,11 @@
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:c012c8be7c442c8309ca8fa0876fef33f5fd977c467be1e1c1c2f721e8ebd73c",
|
||||
"sha256:ea1af050b3e06eb73e4470f704d23007307bc0e87c13e015f6b90460f1407bd3"
|
||||
"sha256:104475dc4d57bbae49017aea16fffbb763204fa2d6a70f1f3cc79962c1a383a4",
|
||||
"sha256:cde472372e030e1e0bc64dac00fb53e6c095d7ab641f4281e2c995e85e205d8b"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.0.1"
|
||||
"version": "==2.0.2"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@ -810,11 +813,11 @@
|
||||
},
|
||||
"prompt-toolkit": {
|
||||
"hashes": [
|
||||
"sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f",
|
||||
"sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"
|
||||
"sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c",
|
||||
"sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==3.0.19"
|
||||
"markers": "python_full_version >= '3.6.2'",
|
||||
"version": "==3.0.20"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
@ -1148,11 +1151,11 @@
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
|
||||
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
|
||||
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
|
||||
"sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e",
|
||||
"sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7",
|
||||
"sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"
|
||||
],
|
||||
"version": "==3.10.0.0"
|
||||
"version": "==3.10.0.2"
|
||||
},
|
||||
"ua-parser": {
|
||||
"hashes": [
|
||||
@ -1291,20 +1294,20 @@
|
||||
},
|
||||
"xmlsec": {
|
||||
"hashes": [
|
||||
"sha256:23f209260b37bdc2fd96af837494c47dd1e67964f077442b63acd83c0f62e212",
|
||||
"sha256:4fb38ab0bf3e47cbae136119674a869e09d61c939b510350f369c8ac46087373",
|
||||
"sha256:705ab5b848afdf3a5c78b1322276054c885f44dc51601e14cb883a9c86cbe20f",
|
||||
"sha256:843d10bba4c480609da74ee11fff1ee0fc1c12821c656979f12a7a4ecb043e03",
|
||||
"sha256:86d54b93f8278e2f0c504d0744e39a483c1c7ce9993f2ca70184cc7770faa982",
|
||||
"sha256:8922fba55a060ee81de4a7f5efc593c5bf121047763aecf0eead02e061c9d2db",
|
||||
"sha256:c7b49d4fce83186b89f7ce6cec765245d36a70d0acc2f3ed0ba95c735b3667da",
|
||||
"sha256:cd2eaaff7f31784a07dd99ce81fa767313df3ba1834faa4143ee2c07000cac7a",
|
||||
"sha256:dea5bef9b5830c36ccb7a68a0d94d49eaea4d03fbbd04179652bf661b7e6e30f",
|
||||
"sha256:eadff662d89c80db409c69d82eb3e695e16d4a5e8ab56b5b22670a54e9c6ff20",
|
||||
"sha256:ee233d0bc27fb8f447ca2622b0de2ac2df45b8795f02ef263825912011fe4fe9"
|
||||
"sha256:135724cdce60e6bbd072fca6f09a21f72e2cecc59eebb4eed7740c316ecabc7b",
|
||||
"sha256:1b4377f6d37ad714ba95a227ef40fb54ba1b22ef5170ce04c330fe45ee6ad184",
|
||||
"sha256:2c86ac6ce570c9e04f04da0cd5e7d3db346e4b5b1d006311606368f17c756ef9",
|
||||
"sha256:4e5f565de311afa33aaee4724566e685f951afe301212b6cf82f98cf9d8a1749",
|
||||
"sha256:9a2b8a780093b0fe8cecae53a81a8cd9edd50c08980d374c5317c91f065042d9",
|
||||
"sha256:ce9c681adbc87b4f06c2b16725d9b2edbdbd508117dae4288b5faf78c1406038",
|
||||
"sha256:d22da4d3dcc559fb2e54e782f39c9ddad5f8d5b356f86a79bbb80b0a45115c97",
|
||||
"sha256:db3e18ca883c01bbe28c9f5197c66f676c9772cf2d85f667e6122fc4d0702225",
|
||||
"sha256:e4783f7814aa2a3e318385cce8ef87c82954b9a59535a48f67da4e2c21c08ce1",
|
||||
"sha256:f32e54065f0404ceff71388daa7fa7df10e1fb800051dfe302d63abb0acf0020",
|
||||
"sha256:f5d242b1a19a36078608f5d7f4d561c5ca55cac8061a323a071c06275267dc19"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.11"
|
||||
"version": "==1.3.12"
|
||||
},
|
||||
"yarl": {
|
||||
"hashes": [
|
||||
@ -1417,11 +1420,11 @@
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:3975a0bd5373bdce166e60c851cfcbaf21ee96de80ec518c1f4cb3e94c3fb334",
|
||||
"sha256:ab7f36e8a78b8e54a62028ba6beef7561db4cdb6f2a5009ecc44a6f42b5697ef"
|
||||
"sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c",
|
||||
"sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"
|
||||
],
|
||||
"markers": "python_version ~= '3.6'",
|
||||
"version": "==2.6.6"
|
||||
"version": "==2.7.3"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@ -1647,13 +1650,21 @@
|
||||
"markers": "python_version >= '2.6'",
|
||||
"version": "==5.6.0"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f",
|
||||
"sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
|
||||
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.13.1"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
@ -1665,11 +1676,11 @@
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594",
|
||||
"sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e"
|
||||
"sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1",
|
||||
"sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.9.6"
|
||||
"version": "==2.10.2"
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
@ -1696,11 +1707,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
|
||||
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
|
||||
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
|
||||
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.2.4"
|
||||
"version": "==6.2.5"
|
||||
},
|
||||
"pytest-django": {
|
||||
"hashes": [
|
||||
@ -1747,41 +1758,49 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:026beb631097a4a3def7299aa5825e05e057de3c6d72b139c37813bfa351274b",
|
||||
"sha256:14caacd1853e40103f59571f169704367e79fb78fac3d6d09ac84d9197cadd16",
|
||||
"sha256:16d9eaa8c7e91537516c20da37db975f09ac2e7772a0694b245076c6d68f85da",
|
||||
"sha256:18fdc51458abc0a974822333bd3a932d4e06ba2a3243e9a1da305668bd62ec6d",
|
||||
"sha256:28e8af338240b6f39713a34e337c3813047896ace09d51593d6907c66c0708ba",
|
||||
"sha256:3835de96524a7b6869a6c710b26c90e94558c31006e96ca3cf6af6751b27dca1",
|
||||
"sha256:3905c86cc4ab6d71635d6419a6f8d972cab7c634539bba6053c47354fd04452c",
|
||||
"sha256:3c09d88a07483231119f5017904db8f60ad67906efac3f1baa31b9b7f7cca281",
|
||||
"sha256:4551728b767f35f86b8e5ec19a363df87450c7376d7419c3cac5b9ceb4bce576",
|
||||
"sha256:459bbe342c5b2dec5c5223e7c363f291558bc27982ef39ffd6569e8c082bdc83",
|
||||
"sha256:4f421e3cdd3a273bace013751c345f4ebeef08f05e8c10757533ada360b51a39",
|
||||
"sha256:577737ec3d4c195c4aef01b757905779a9e9aee608fa1cf0aec16b5576c893d3",
|
||||
"sha256:57fece29f7cc55d882fe282d9de52f2f522bb85290555b49394102f3621751ee",
|
||||
"sha256:7976d410e42be9ae7458c1816a416218364e06e162b82e42f7060737e711d9ce",
|
||||
"sha256:85f568892422a0e96235eb8ea6c5a41c8ccbf55576a2260c0160800dbd7c4f20",
|
||||
"sha256:8764a78c5464ac6bde91a8c87dd718c27c1cabb7ed2b4beaf36d3e8e390567f9",
|
||||
"sha256:8935937dad2c9b369c3d932b0edbc52a62647c2afb2fafc0c280f14a8bf56a6a",
|
||||
"sha256:8fe58d9f6e3d1abf690174fd75800fda9bdc23d2a287e77758dc0e8567e38ce6",
|
||||
"sha256:937b20955806381e08e54bd9d71f83276d1f883264808521b70b33d98e4dec5d",
|
||||
"sha256:9569da9e78f0947b249370cb8fadf1015a193c359e7e442ac9ecc585d937f08d",
|
||||
"sha256:a3b73390511edd2db2d34ff09aa0b2c08be974c71b4c0505b4a048d5dc128c2b",
|
||||
"sha256:a4eddbe2a715b2dd3849afbdeacf1cc283160b24e09baf64fa5675f51940419d",
|
||||
"sha256:a5c6dbe09aff091adfa8c7cfc1a0e83fdb8021ddb2c183512775a14f1435fe16",
|
||||
"sha256:b63e3571b24a7959017573b6455e05b675050bbbea69408f35f3cb984ec54363",
|
||||
"sha256:bb350eb1060591d8e89d6bac4713d41006cd4d479f5e11db334a48ff8999512f",
|
||||
"sha256:bf6d987edd4a44dd2fa2723fca2790f9442ae4de2c8438e53fcb1befdf5d823a",
|
||||
"sha256:bfa6a679410b394600eafd16336b2ce8de43e9b13f7fb9247d84ef5ad2b45e91",
|
||||
"sha256:c856ec9b42e5af4fe2d8e75970fcc3a2c15925cbcc6e7a9bcb44583b10b95e80",
|
||||
"sha256:cea56288eeda8b7511d507bbe7790d89ae7049daa5f51ae31a35ae3c05408531",
|
||||
"sha256:ea212df6e5d3f60341aef46401d32fcfded85593af1d82b8b4a7a68cd67fdd6b",
|
||||
"sha256:f35567470ee6dbfb946f069ed5f5615b40edcbb5f1e6e1d3d2b114468d505fc6",
|
||||
"sha256:fbc20975eee093efa2071de80df7f972b7b35e560b213aafabcec7c0bd00bd8c",
|
||||
"sha256:ff4a8ad9638b7ca52313d8732f37ecd5fd3c8e3aff10a8ccb93176fd5b3812f6"
|
||||
"sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468",
|
||||
"sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354",
|
||||
"sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308",
|
||||
"sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d",
|
||||
"sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc",
|
||||
"sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8",
|
||||
"sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797",
|
||||
"sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2",
|
||||
"sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13",
|
||||
"sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d",
|
||||
"sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a",
|
||||
"sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0",
|
||||
"sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73",
|
||||
"sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1",
|
||||
"sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed",
|
||||
"sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a",
|
||||
"sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b",
|
||||
"sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f",
|
||||
"sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256",
|
||||
"sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb",
|
||||
"sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2",
|
||||
"sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983",
|
||||
"sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb",
|
||||
"sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645",
|
||||
"sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8",
|
||||
"sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a",
|
||||
"sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906",
|
||||
"sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f",
|
||||
"sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c",
|
||||
"sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892",
|
||||
"sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0",
|
||||
"sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e",
|
||||
"sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e",
|
||||
"sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed",
|
||||
"sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c",
|
||||
"sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374",
|
||||
"sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd",
|
||||
"sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791",
|
||||
"sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a",
|
||||
"sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1",
|
||||
"sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"
|
||||
],
|
||||
"version": "==2021.8.3"
|
||||
"version": "==2021.8.28"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
|
11
README.md
11
README.md
@ -4,14 +4,15 @@
|
||||
|
||||
---
|
||||
|
||||
[](https://discord.gg/jg33eMhnj6)
|
||||
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6)
|
||||
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6)
|
||||
[](https://discord.gg/jg33eMhnj6)
|
||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml)
|
||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
|
||||
[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
|
||||
[](https://codecov.io/gh/goauthentik/authentik)
|
||||
[](https://goauthentik.testspace.com/)
|
||||

|
||||

|
||||

|
||||
[Transifex](https://www.transifex.com/beryjuorg/authentik/)
|
||||
[](https://www.transifex.com/beryjuorg/authentik/)
|
||||
|
||||
## What is authentik?
|
||||
|
||||
|
@ -6,9 +6,8 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ------------------ |
|
||||
| 2021.5.x | :white_check_mark: |
|
||||
| 2021.6.x | :white_check_mark: |
|
||||
| 2021.7.x | :white_check_mark: |
|
||||
| 2021.8.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.8.1-rc1"
|
||||
__version__ = "2021.8.5"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
@ -34,6 +34,7 @@ class RuntimeDict(TypedDict):
|
||||
class SystemSerializer(PassiveSerializer):
|
||||
"""Get system information."""
|
||||
|
||||
env = SerializerMethodField()
|
||||
http_headers = SerializerMethodField()
|
||||
http_host = SerializerMethodField()
|
||||
http_is_secure = SerializerMethodField()
|
||||
@ -42,6 +43,10 @@ class SystemSerializer(PassiveSerializer):
|
||||
server_time = SerializerMethodField()
|
||||
embedded_outpost_host = SerializerMethodField()
|
||||
|
||||
def get_env(self, request: Request) -> dict[str, str]:
|
||||
"""Get Environment"""
|
||||
return os.environ.copy()
|
||||
|
||||
def get_http_headers(self, request: Request) -> dict[str, str]:
|
||||
"""Get HTTP Request headers"""
|
||||
headers = {}
|
||||
|
@ -33,7 +33,7 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
|
||||
raise AuthenticationFailed("Malformed header")
|
||||
# Accept credentials with username and without
|
||||
if ":" in auth_credentials:
|
||||
_, password = auth_credentials.split(":")
|
||||
_, _, password = auth_credentials.partition(":")
|
||||
else:
|
||||
password = auth_credentials
|
||||
if password == "": # nosec
|
||||
|
@ -1,8 +1,10 @@
|
||||
"""authentik api urls"""
|
||||
from django.urls import include, path
|
||||
|
||||
from authentik.api.v2.urls import urlpatterns as v2_urls
|
||||
from authentik.api.v3.urls import urlpatterns as v3_urls
|
||||
|
||||
urlpatterns = [
|
||||
path("v2beta/", include(v2_urls)),
|
||||
# Remove in 2022.1
|
||||
path("v2beta/", include(v3_urls)),
|
||||
path("v3/", include(v3_urls)),
|
||||
]
|
||||
|
@ -4,16 +4,44 @@ from json import loads
|
||||
from django.conf import settings
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
from django.views.generic.base import View
|
||||
from requests import post
|
||||
from requests.exceptions import RequestException
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.parsers import BaseParser
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class SentryTunnelView(View):
|
||||
class PlainTextParser(BaseParser):
|
||||
"""Plain text parser."""
|
||||
|
||||
media_type = "text/plain"
|
||||
|
||||
def parse(self, stream, media_type=None, parser_context=None) -> str:
|
||||
"""Simply return a string representing the body of the request."""
|
||||
return stream.read()
|
||||
|
||||
|
||||
class CsrfExemptSessionAuthentication(SessionAuthentication):
|
||||
"""CSRF-exempt Session authentication"""
|
||||
|
||||
def enforce_csrf(self, request: Request):
|
||||
return # To not perform the csrf check previously happening
|
||||
|
||||
|
||||
class SentryTunnelView(APIView):
|
||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
||||
|
||||
serializer_class = None
|
||||
parser_classes = [PlainTextParser]
|
||||
throttle_classes = [AnonRateThrottle]
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = [CsrfExemptSessionAuthentication]
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
||||
# Only allow usage of this endpoint when error reporting is enabled
|
@ -1,6 +1,6 @@
|
||||
"""api v2 urls"""
|
||||
"""api v3 urls"""
|
||||
from django.urls import path
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.cache import cache_page
|
||||
from drf_spectacular.views import SpectacularAPIView
|
||||
from rest_framework import routers
|
||||
|
||||
@ -10,8 +10,8 @@ from authentik.admin.api.system import SystemView
|
||||
from authentik.admin.api.tasks import TaskViewSet
|
||||
from authentik.admin.api.version import VersionView
|
||||
from authentik.admin.api.workers import WorkerView
|
||||
from authentik.api.v2.config import ConfigView
|
||||
from authentik.api.v2.sentry import SentryTunnelView
|
||||
from authentik.api.v3.config import ConfigView
|
||||
from authentik.api.v3.sentry import SentryTunnelView
|
||||
from authentik.api.views import APIBrowserView
|
||||
from authentik.core.api.applications import ApplicationViewSet
|
||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||
@ -225,7 +225,7 @@ urlpatterns = (
|
||||
FlowExecutorView.as_view(),
|
||||
name="flow-executor",
|
||||
),
|
||||
path("sentry/", csrf_exempt(SentryTunnelView.as_view()), name="sentry"),
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("sentry/", SentryTunnelView.as_view(), name="sentry"),
|
||||
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
|
||||
]
|
||||
)
|
@ -4,14 +4,9 @@ from django.db.models import QuerySet
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
extend_schema,
|
||||
inline_serializer,
|
||||
)
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, CharField, FileField, ReadOnlyField
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -24,6 +19,7 @@ from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import FilePathSerializer, FileUploadSerializer
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.events.models import EventAction
|
||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||
@ -71,7 +67,7 @@ class ApplicationSerializer(ModelSerializer):
|
||||
class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Application Viewset"""
|
||||
|
||||
queryset = Application.objects.all()
|
||||
queryset = Application.objects.all().prefetch_related("provider")
|
||||
serializer_class = ApplicationSerializer
|
||||
search_fields = [
|
||||
"name",
|
||||
@ -122,7 +118,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
# If the current user is superuser, they can set `for_user`
|
||||
for_user = request.user
|
||||
if request.user.is_superuser and "for_user" in request.query_params:
|
||||
for_user = get_object_or_404(User, pk=request.query_params.get("for_user"))
|
||||
try:
|
||||
for_user = get_object_or_404(User, pk=request.query_params.get("for_user"))
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest("for_user must be numerical")
|
||||
engine = PolicyEngine(application, for_user, request)
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
@ -177,13 +176,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
@permission_required("authentik_core.change_application")
|
||||
@extend_schema(
|
||||
request={
|
||||
"multipart/form-data": inline_serializer(
|
||||
"SetIcon",
|
||||
fields={
|
||||
"file": FileField(required=False),
|
||||
"clear": BooleanField(default=False),
|
||||
},
|
||||
)
|
||||
"multipart/form-data": FileUploadSerializer,
|
||||
},
|
||||
responses={
|
||||
200: OpenApiResponse(description="Success"),
|
||||
@ -215,7 +208,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@permission_required("authentik_core.change_application")
|
||||
@extend_schema(
|
||||
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
|
||||
request=FilePathSerializer,
|
||||
responses={
|
||||
200: OpenApiResponse(description="Success"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
|
@ -81,7 +81,7 @@ class GroupFilter(FilterSet):
|
||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Group Viewset"""
|
||||
|
||||
queryset = Group.objects.all()
|
||||
queryset = Group.objects.all().select_related("parent").prefetch_related("users")
|
||||
serializer_class = GroupSerializer
|
||||
search_fields = ["name", "is_superuser"]
|
||||
filterset_class = GroupFilter
|
||||
|
@ -1,7 +1,10 @@
|
||||
"""Tokens API Viewset"""
|
||||
from typing import Any
|
||||
|
||||
from django.http.response import Http404
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -20,7 +23,16 @@ from authentik.managed.api import ManagedSerializer
|
||||
class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
"""Token Serializer"""
|
||||
|
||||
user = UserSerializer(required=False)
|
||||
user_obj = UserSerializer(required=False, source="user")
|
||||
|
||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||
"""Ensure only API or App password tokens are created."""
|
||||
request: Request = self.context["request"]
|
||||
attrs.setdefault("user", request.user)
|
||||
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
||||
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
||||
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
||||
return attrs
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -31,11 +43,14 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
"identifier",
|
||||
"intent",
|
||||
"user",
|
||||
"user_obj",
|
||||
"description",
|
||||
"expires",
|
||||
"expiring",
|
||||
]
|
||||
depth = 2
|
||||
extra_kwargs = {
|
||||
"user": {"required": False},
|
||||
}
|
||||
|
||||
|
||||
class TokenViewSerializer(PassiveSerializer):
|
||||
@ -69,7 +84,6 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
def perform_create(self, serializer: TokenSerializer):
|
||||
serializer.save(
|
||||
user=self.request.user,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
|
||||
)
|
||||
|
||||
|
@ -3,13 +3,20 @@ from json import loads
|
||||
from typing import Optional
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.transaction import atomic
|
||||
from django.db.utils import IntegrityError
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import gettext as _
|
||||
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_field
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
extend_schema,
|
||||
extend_schema_field,
|
||||
inline_serializer,
|
||||
)
|
||||
from guardian.shortcuts import get_anonymous_user, get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||
@ -34,7 +41,14 @@ from authentik.core.api.groups import GroupSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
||||
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
|
||||
from authentik.core.models import Group, Token, TokenIntents, User
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_SA,
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
Group,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
)
|
||||
from authentik.events.models import EventAction
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
@ -220,6 +234,51 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
return link, token
|
||||
|
||||
@permission_required(None, ["authentik_core.add_user", "authentik_core.add_token"])
|
||||
@extend_schema(
|
||||
request=inline_serializer(
|
||||
"UserServiceAccountSerializer",
|
||||
{
|
||||
"name": CharField(required=True),
|
||||
"create_group": BooleanField(default=False),
|
||||
},
|
||||
),
|
||||
responses={
|
||||
200: inline_serializer(
|
||||
"UserServiceAccountResponse",
|
||||
{
|
||||
"username": CharField(required=True),
|
||||
"token": CharField(required=True),
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
@action(detail=False, methods=["POST"], pagination_class=None, filter_backends=[])
|
||||
def service_account(self, request: Request) -> Response:
|
||||
"""Create a new user account that is marked as a service account"""
|
||||
username = request.data.get("name")
|
||||
create_group = request.data.get("create_group", False)
|
||||
with atomic():
|
||||
try:
|
||||
user = User.objects.create(
|
||||
username=username,
|
||||
name=username,
|
||||
attributes={USER_ATTRIBUTE_SA: True, USER_ATTRIBUTE_TOKEN_EXPIRING: False},
|
||||
)
|
||||
if create_group:
|
||||
group = Group.objects.create(
|
||||
name=username,
|
||||
)
|
||||
group.users.add(user)
|
||||
token = Token.objects.create(
|
||||
identifier=f"service-account-{username}-password",
|
||||
intent=TokenIntents.INTENT_APP_PASSWORD,
|
||||
user=user,
|
||||
)
|
||||
return Response({"username": user.username, "token": token.key})
|
||||
except (IntegrityError) as exc:
|
||||
return Response(data={"non_field_errors": [str(exc)]}, status=400)
|
||||
|
||||
@extend_schema(responses={200: SessionUserSerializer(many=False)})
|
||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||
# pylint: disable=invalid-name
|
||||
@ -251,7 +310,9 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
# since it caches the full object
|
||||
if SESSION_IMPERSONATE_USER in request.session:
|
||||
request.session[SESSION_IMPERSONATE_USER] = new_user
|
||||
return self.me(request)
|
||||
serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data})
|
||||
serializer.is_valid()
|
||||
return Response(serializer.data)
|
||||
|
||||
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
||||
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||
|
@ -2,7 +2,7 @@
|
||||
from typing import Any
|
||||
|
||||
from django.db.models import Model
|
||||
from rest_framework.fields import CharField, IntegerField
|
||||
from rest_framework.fields import BooleanField, CharField, FileField, IntegerField
|
||||
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
|
||||
|
||||
|
||||
@ -22,8 +22,18 @@ class PassiveSerializer(Serializer):
|
||||
def update(self, instance: Model, validated_data: dict) -> Model: # pragma: no cover
|
||||
return Model()
|
||||
|
||||
class Meta:
|
||||
model = Model
|
||||
|
||||
class FileUploadSerializer(PassiveSerializer):
|
||||
"""Serializer to upload file"""
|
||||
|
||||
file = FileField(required=False)
|
||||
clear = BooleanField(default=False)
|
||||
|
||||
|
||||
class FilePathSerializer(PassiveSerializer):
|
||||
"""Serializer to upload file"""
|
||||
|
||||
url = CharField()
|
||||
|
||||
|
||||
class MetaNameSerializer(PassiveSerializer):
|
||||
|
59
authentik/core/auth.py
Normal file
59
authentik/core/auth.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""Authenticate with tokens"""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.http.request import HttpRequest
|
||||
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.events.utils import cleanse_dict, sanitize_dict
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
|
||||
|
||||
class InbuiltBackend(ModelBackend):
|
||||
"""Inbuilt backend"""
|
||||
|
||||
def authenticate(
|
||||
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
||||
) -> Optional[User]:
|
||||
user = super().authenticate(request, username=username, password=password, **kwargs)
|
||||
if not user:
|
||||
return None
|
||||
self.set_method("password", request)
|
||||
return user
|
||||
|
||||
def set_method(self, method: str, request: Optional[HttpRequest], **kwargs):
|
||||
"""Set method data on current flow, if possbiel"""
|
||||
if not request:
|
||||
return
|
||||
# Since we can't directly pass other variables to signals, and we want to log the method
|
||||
# and the token used, we assume we're running in a flow and set a variable in the context
|
||||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD] = method
|
||||
flow_plan.context[PLAN_CONTEXT_METHOD_ARGS] = cleanse_dict(sanitize_dict(kwargs))
|
||||
request.session[SESSION_KEY_PLAN] = flow_plan
|
||||
|
||||
|
||||
class TokenBackend(InbuiltBackend):
|
||||
"""Authenticate with token"""
|
||||
|
||||
def authenticate(
|
||||
self, request: HttpRequest, username: Optional[str], password: Optional[str], **kwargs: Any
|
||||
) -> Optional[User]:
|
||||
try:
|
||||
user = User._default_manager.get_by_natural_key(username)
|
||||
except User.DoesNotExist:
|
||||
# Run the default password hasher once to reduce the timing
|
||||
# difference between an existing and a nonexistent user (#20760).
|
||||
User().set_password(password)
|
||||
return None
|
||||
tokens = Token.filter_not_expired(
|
||||
user=user, key=password, intent=TokenIntents.INTENT_APP_PASSWORD
|
||||
)
|
||||
if not tokens.exists():
|
||||
return None
|
||||
token = tokens.first()
|
||||
self.set_method("password", request, token=token)
|
||||
return token.user
|
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
26
authentik/core/migrations/0028_alter_token_intent.py
Normal file
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.6 on 2021-08-23 14:35
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0027_bootstrap_token"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="token",
|
||||
name="intent",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("verification", "Intent Verification"),
|
||||
("api", "Intent Api"),
|
||||
("recovery", "Intent Recovery"),
|
||||
("app_password", "Intent App Password"),
|
||||
],
|
||||
default="verification",
|
||||
),
|
||||
),
|
||||
]
|
@ -28,6 +28,7 @@ from authentik.core.signals import password_changed
|
||||
from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.managed.models import ManagedModel
|
||||
@ -54,7 +55,9 @@ def default_token_duration():
|
||||
|
||||
def default_token_key():
|
||||
"""Default token key"""
|
||||
return uuid4().hex
|
||||
# We use generate_id since the chars in the key should be easy
|
||||
# to use in Emails (for verification) and URLs (for recovery)
|
||||
return generate_id(128)
|
||||
|
||||
|
||||
class Group(models.Model):
|
||||
@ -408,6 +411,9 @@ class TokenIntents(models.TextChoices):
|
||||
# Recovery use for the recovery app
|
||||
INTENT_RECOVERY = "recovery"
|
||||
|
||||
# App-specific passwords
|
||||
INTENT_APP_PASSWORD = "app_password" # nosec
|
||||
|
||||
|
||||
class Token(ManagedModel, ExpiringModel):
|
||||
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
|
||||
|
@ -25,7 +25,7 @@ from authentik.flows.planner import (
|
||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
@ -189,7 +189,7 @@ class SourceFlowManager:
|
||||
kwargs.update(
|
||||
{
|
||||
# Since we authenticate the user by their token, they have no backend set
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_DJANGO,
|
||||
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
|
||||
PLAN_CONTEXT_SSO: True,
|
||||
PLAN_CONTEXT_SOURCE: self.source,
|
||||
PLAN_CONTEXT_REDIRECT: final_redirect,
|
||||
|
@ -1,16 +1,13 @@
|
||||
"""Test Source flow_manager"""
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http.request import HttpRequest
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import SourceUserMatchingModes, User
|
||||
from authentik.core.sources.flow_manager import Action
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.providers.oauth2.generators import generate_client_id
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.views.callback import OAuthSourceFlowManager
|
||||
|
||||
@ -22,24 +19,12 @@ class TestSourceFlowManager(TestCase):
|
||||
super().setUp()
|
||||
self.source = OAuthSource.objects.create(name="test")
|
||||
self.factory = RequestFactory()
|
||||
self.identifier = generate_client_id()
|
||||
|
||||
def get_request(self, user: User) -> HttpRequest:
|
||||
"""Helper to create a get request with session and message middleware"""
|
||||
request = self.factory.get("/")
|
||||
request.user = user
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
middleware = MessageMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
return request
|
||||
self.identifier = generate_id()
|
||||
|
||||
def test_unauthenticated_enroll(self):
|
||||
"""Test un-authenticated user enrolling"""
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.ENROLL)
|
||||
@ -52,7 +37,7 @@ class TestSourceFlowManager(TestCase):
|
||||
)
|
||||
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.AUTH)
|
||||
@ -65,7 +50,7 @@ class TestSourceFlowManager(TestCase):
|
||||
)
|
||||
user = User.objects.create(username="foo", email="foo@bar.baz")
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(user), self.identifier, {}
|
||||
self.source, get_request("/", user=user), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.LINK)
|
||||
@ -78,7 +63,7 @@ class TestSourceFlowManager(TestCase):
|
||||
|
||||
# Without email, deny
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.DENY)
|
||||
@ -86,7 +71,7 @@ class TestSourceFlowManager(TestCase):
|
||||
# With email
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"email": "foo@bar.baz"},
|
||||
)
|
||||
@ -101,7 +86,7 @@ class TestSourceFlowManager(TestCase):
|
||||
|
||||
# Without username, deny
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
self.source, get_request("/", user=AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.DENY)
|
||||
@ -109,7 +94,7 @@ class TestSourceFlowManager(TestCase):
|
||||
# With username
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
@ -125,7 +110,7 @@ class TestSourceFlowManager(TestCase):
|
||||
# With non-existent username, enroll
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{
|
||||
"username": "bar",
|
||||
@ -137,7 +122,7 @@ class TestSourceFlowManager(TestCase):
|
||||
# With username
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
@ -151,7 +136,7 @@ class TestSourceFlowManager(TestCase):
|
||||
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
get_request("/", user=AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
|
@ -27,6 +27,14 @@ class TestTokenAPI(APITestCase):
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, True)
|
||||
|
||||
def test_token_create_invalid(self):
|
||||
"""Test token creation endpoint (invalid data)"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-list"),
|
||||
{"identifier": "test-token", "intent": TokenIntents.INTENT_RECOVERY},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_token_create_non_expiring(self):
|
||||
"""Test token creation endpoint"""
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = False
|
||||
|
40
authentik/core/tests/test_token_auth.py
Normal file
40
authentik/core/tests/test_token_auth.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""Test token auth"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.auth import TokenBackend
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.lib.tests.utils import get_request
|
||||
|
||||
|
||||
class TestTokenAuth(TestCase):
|
||||
"""Test token auth"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create(username="test-user")
|
||||
self.token = Token.objects.create(
|
||||
expiring=False, user=self.user, intent=TokenIntents.INTENT_APP_PASSWORD
|
||||
)
|
||||
# To test with session we need to create a request and pass it through all middlewares
|
||||
self.request = get_request("/")
|
||||
self.request.session[SESSION_KEY_PLAN] = FlowPlan("test")
|
||||
|
||||
def test_token_auth(self):
|
||||
"""Test auth with token"""
|
||||
self.assertEqual(
|
||||
TokenBackend().authenticate(self.request, "test-user", self.token.key), self.user
|
||||
)
|
||||
|
||||
def test_token_auth_none(self):
|
||||
"""Test auth with token (non-existent user)"""
|
||||
self.assertIsNone(
|
||||
TokenBackend().authenticate(self.request, "test-user-foo", self.token.key), self.user
|
||||
)
|
||||
|
||||
def test_token_auth_invalid(self):
|
||||
"""Test auth with token (invalid token)"""
|
||||
self.assertIsNone(
|
||||
TokenBackend().authenticate(self.request, "test-user", self.token.key + "foo"),
|
||||
self.user,
|
||||
)
|
@ -105,3 +105,39 @@ class TestUsersAPI(APITestCase):
|
||||
+ f"?email_stage={stage.pk}"
|
||||
)
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
def test_service_account(self):
|
||||
"""Service account creation"""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(reverse("authentik_api:user-service-account"))
|
||||
self.assertEqual(response.status_code, 400)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-service-account"),
|
||||
data={
|
||||
"name": "test-sa",
|
||||
"create_group": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||
|
||||
def test_service_account_invalid(self):
|
||||
"""Service account creation (twice with same name, expect error)"""
|
||||
self.client.force_login(self.admin)
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-service-account"),
|
||||
data={
|
||||
"name": "test-sa",
|
||||
"create_group": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(User.objects.filter(username="test-sa").exists())
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:user-service-account"),
|
||||
data={
|
||||
"name": "test-sa",
|
||||
"create_group": True,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
@ -10,7 +10,7 @@ from authentik.crypto.api import CertificateKeyPairSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ class TestCrypto(TestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
client_secret=generate_client_secret(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://localhost",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
|
@ -1,8 +1,13 @@
|
||||
"""Notification API Views"""
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
|
||||
@ -53,3 +58,18 @@ class NotificationViewSet(
|
||||
]
|
||||
permission_classes = [OwnerPermissions]
|
||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses={
|
||||
204: OpenApiResponse(description="Marked tasks as read successfully."),
|
||||
},
|
||||
)
|
||||
@action(detail=False, methods=["post"])
|
||||
def mark_all_seen(self, request: Request) -> Response:
|
||||
"""Mark all the user's notifications as seen"""
|
||||
notifications = Notification.objects.filter(user=request.user)
|
||||
for notification in notifications:
|
||||
notification.seen = True
|
||||
Notification.objects.bulk_update(notifications, ["seen"])
|
||||
return Response({}, status=204)
|
||||
|
@ -1,10 +1,7 @@
|
||||
"""authentik events app"""
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db import ProgrammingError
|
||||
from django.utils.timezone import now
|
||||
|
||||
|
||||
class AuthentikEventsConfig(AppConfig):
|
||||
@ -16,12 +13,3 @@ class AuthentikEventsConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
import_module("authentik.events.signals")
|
||||
try:
|
||||
from authentik.events.models import Event
|
||||
|
||||
date_from = now() - timedelta(days=1)
|
||||
|
||||
for event in Event.objects.filter(created__gte=date_from):
|
||||
event._set_prom_metrics()
|
||||
except ProgrammingError:
|
||||
pass
|
||||
|
@ -10,7 +10,6 @@ from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from prometheus_client import Gauge
|
||||
from requests import RequestException, post
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -28,11 +27,6 @@ from authentik.tenants.models import Tenant
|
||||
from authentik.tenants.utils import DEFAULT_TENANT
|
||||
|
||||
LOGGER = get_logger("authentik.events")
|
||||
GAUGE_EVENTS = Gauge(
|
||||
"authentik_events",
|
||||
"Events in authentik",
|
||||
["action", "user_username", "app", "client_ip"],
|
||||
)
|
||||
|
||||
|
||||
def default_event_duration():
|
||||
@ -182,14 +176,6 @@ class Event(ExpiringModel):
|
||||
return
|
||||
self.context["geo"] = city
|
||||
|
||||
def _set_prom_metrics(self):
|
||||
GAUGE_EVENTS.labels(
|
||||
action=self.action,
|
||||
user_username=self.user.get("username"),
|
||||
app=self.app,
|
||||
client_ip=self.client_ip,
|
||||
).set(self.created.timestamp())
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
LOGGER.debug(
|
||||
@ -200,7 +186,6 @@ class Event(ExpiringModel):
|
||||
user=self.user,
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
self._set_prom_metrics()
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
|
@ -11,6 +11,7 @@ from django.core.cache import cache
|
||||
from prometheus_client import Gauge
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
|
||||
GAUGE_TASKS = Gauge(
|
||||
"authentik_system_tasks",
|
||||
@ -174,9 +175,7 @@ class MonitoredTask(Task):
|
||||
).save(self.result_timeout_hours)
|
||||
Event.new(
|
||||
EventAction.SYSTEM_TASK_EXCEPTION,
|
||||
message=(
|
||||
f"Task {self.__name__} encountered an error: " "\n".join(self._result.messages)
|
||||
),
|
||||
message=(f"Task {self.__name__} encountered an error: {exception_to_string(exc)}"),
|
||||
).save()
|
||||
return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
|
||||
|
||||
|
@ -15,6 +15,7 @@ from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
||||
from authentik.flows.views import SESSION_KEY_PLAN
|
||||
from authentik.stages.invitation.models import Invitation
|
||||
from authentik.stages.invitation.signals import invitation_used
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
|
||||
from authentik.stages.user_write.signals import user_write
|
||||
|
||||
|
||||
@ -46,7 +47,13 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
|
||||
flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
|
||||
if PLAN_CONTEXT_SOURCE in flow_plan.context:
|
||||
# Login request came from an external source, save it in the context
|
||||
thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
||||
thread.kwargs[PLAN_CONTEXT_SOURCE] = flow_plan.context[PLAN_CONTEXT_SOURCE]
|
||||
if PLAN_CONTEXT_METHOD in flow_plan.context:
|
||||
thread.kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD]
|
||||
# Save the login method used
|
||||
thread.kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(
|
||||
PLAN_CONTEXT_METHOD_ARGS, {}
|
||||
)
|
||||
thread.user = user
|
||||
thread.run()
|
||||
|
||||
|
@ -4,15 +4,15 @@ from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.models import Event, EventAction, Notification, NotificationSeverity
|
||||
|
||||
|
||||
class TestEventsAPI(APITestCase):
|
||||
"""Test Event API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
user = User.objects.get(username="akadmin")
|
||||
self.client.force_login(user)
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_top_n(self):
|
||||
"""Test top_per_user"""
|
||||
@ -30,3 +30,14 @@ class TestEventsAPI(APITestCase):
|
||||
reverse("authentik_api:event-actions"),
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_notifications(self):
|
||||
"""Test notifications"""
|
||||
notification = Notification.objects.create(
|
||||
user=self.user, severity=NotificationSeverity.ALERT, body="", seen=False
|
||||
)
|
||||
self.client.post(
|
||||
reverse("authentik_api:notification-mark-all-seen"),
|
||||
)
|
||||
notification.refresh_from_db()
|
||||
self.assertTrue(notification.seen)
|
||||
|
@ -7,10 +7,10 @@ from django.http.response import HttpResponseBadRequest, JsonResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import BooleanField, FileField, ReadOnlyField
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -20,7 +20,12 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import CacheSerializer, LinkSerializer
|
||||
from authentik.core.api.utils import (
|
||||
CacheSerializer,
|
||||
FilePathSerializer,
|
||||
FileUploadSerializer,
|
||||
LinkSerializer,
|
||||
)
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||
@ -147,7 +152,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
||||
],
|
||||
)
|
||||
@extend_schema(
|
||||
request={"multipart/form-data": inline_serializer("SetIcon", fields={"file": FileField()})},
|
||||
request={"multipart/form-data": FileUploadSerializer},
|
||||
responses={
|
||||
204: OpenApiResponse(description="Successfully imported flow"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
@ -259,13 +264,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
||||
@permission_required("authentik_flows.change_flow")
|
||||
@extend_schema(
|
||||
request={
|
||||
"multipart/form-data": inline_serializer(
|
||||
"SetIcon",
|
||||
fields={
|
||||
"file": FileField(required=False),
|
||||
"clear": BooleanField(default=False),
|
||||
},
|
||||
)
|
||||
"multipart/form-data": FileUploadSerializer,
|
||||
},
|
||||
responses={
|
||||
200: OpenApiResponse(description="Success"),
|
||||
@ -301,7 +300,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
@permission_required("authentik_core.change_application")
|
||||
@extend_schema(
|
||||
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
|
||||
request=FilePathSerializer,
|
||||
responses={
|
||||
200: OpenApiResponse(description="Success"),
|
||||
400: OpenApiResponse(description="Bad request"),
|
||||
|
@ -11,7 +11,7 @@ class Command(BaseCommand): # pragma: no cover
|
||||
def handle(self, *args, **options):
|
||||
"""Apply all flows in order, abort when one fails to import"""
|
||||
for flow_path in options.get("flows", []):
|
||||
with open(flow_path, "r") as flow_file:
|
||||
with open(flow_path, "r", encoding="utf8") as flow_file:
|
||||
importer = FlowImporter(flow_file.read())
|
||||
valid = importer.validate()
|
||||
if not valid:
|
||||
|
@ -31,6 +31,7 @@ class FlowPlanProcess(PROCESS_CLASS): # pragma: no cover
|
||||
self.request = RequestFactory().get("/")
|
||||
|
||||
def run(self):
|
||||
"""Execute 1000 flow plans"""
|
||||
print(f"Proc {self.index} Running")
|
||||
|
||||
def test_inner():
|
||||
|
@ -6,7 +6,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.stages.identification.models import UserFields
|
||||
from authentik.stages.password import BACKEND_DJANGO, BACKEND_LDAP
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||
|
||||
|
||||
def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
@ -26,7 +26,7 @@ def create_default_authentication_flow(apps: Apps, schema_editor: BaseDatabaseSc
|
||||
|
||||
password_stage, _ = PasswordStage.objects.using(db_alias).update_or_create(
|
||||
name="default-authentication-password",
|
||||
defaults={"backends": [BACKEND_DJANGO, BACKEND_LDAP]},
|
||||
defaults={"backends": [BACKEND_INBUILT, BACKEND_LDAP, BACKEND_APP_PASSWORD]},
|
||||
)
|
||||
|
||||
login_stage, _ = UserLoginStage.objects.using(db_alias).update_or_create(
|
||||
|
@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.2.6 on 2021-08-30 14:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0023_alter_flow_background"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="flow",
|
||||
name="compatibility_mode",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Enable compatibility mode, increases compatibility with password managers on mobile devices.",
|
||||
),
|
||||
),
|
||||
]
|
@ -125,7 +125,7 @@ class Flow(SerializerModel, PolicyBindingModel):
|
||||
)
|
||||
|
||||
compatibility_mode = models.BooleanField(
|
||||
default=True,
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Enable compatibility mode, increases compatibility with "
|
||||
"password managers on mobile devices."
|
||||
|
@ -3,7 +3,6 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
@ -13,6 +12,7 @@ from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableExce
|
||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.policies.types import PolicyResult
|
||||
@ -24,11 +24,6 @@ CACHE_MOCK = Mock(wraps=cache)
|
||||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||
|
||||
|
||||
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||
"""Dummy get_response for SessionMiddleware"""
|
||||
return None
|
||||
|
||||
|
||||
class TestFlowPlanner(TestCase):
|
||||
"""Test planner logic"""
|
||||
|
||||
|
@ -7,9 +7,9 @@ from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.transfer.common import DataclassEncoder
|
||||
from authentik.flows.transfer.exporter import FlowExporter
|
||||
from authentik.flows.transfer.importer import FlowImporter, transaction_rollback
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.policies.expression.models import ExpressionPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
from authentik.providers.oauth2.generators import generate_client_id
|
||||
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
from authentik.stages.user_login.models import UserLoginStage
|
||||
|
||||
@ -31,15 +31,15 @@ class TestFlowTransfer(TransactionTestCase):
|
||||
|
||||
def test_export_validate_import(self):
|
||||
"""Test export and validate it"""
|
||||
flow_slug = generate_client_id()
|
||||
flow_slug = generate_id()
|
||||
with transaction_rollback():
|
||||
login_stage = UserLoginStage.objects.create(name=generate_client_id())
|
||||
login_stage = UserLoginStage.objects.create(name=generate_id())
|
||||
|
||||
flow = Flow.objects.create(
|
||||
slug=flow_slug,
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
name=generate_client_id(),
|
||||
title=generate_client_id(),
|
||||
name=generate_id(),
|
||||
title=generate_id(),
|
||||
)
|
||||
FlowStageBinding.objects.update_or_create(
|
||||
target=flow,
|
||||
@ -60,18 +60,18 @@ class TestFlowTransfer(TransactionTestCase):
|
||||
|
||||
def test_export_validate_import_policies(self):
|
||||
"""Test export and validate it"""
|
||||
flow_slug = generate_client_id()
|
||||
stage_name = generate_client_id()
|
||||
flow_slug = generate_id()
|
||||
stage_name = generate_id()
|
||||
with transaction_rollback():
|
||||
flow_policy = ExpressionPolicy.objects.create(
|
||||
name=generate_client_id(),
|
||||
name=generate_id(),
|
||||
expression="return True",
|
||||
)
|
||||
flow = Flow.objects.create(
|
||||
slug=flow_slug,
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
name=generate_client_id(),
|
||||
title=generate_client_id(),
|
||||
name=generate_id(),
|
||||
title=generate_id(),
|
||||
)
|
||||
PolicyBinding.objects.create(policy=flow_policy, target=flow, order=0)
|
||||
|
||||
@ -111,15 +111,15 @@ class TestFlowTransfer(TransactionTestCase):
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name=generate_client_id())
|
||||
first_stage = PromptStage.objects.create(name=generate_id())
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.save()
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name=generate_client_id(),
|
||||
slug=generate_client_id(),
|
||||
name=generate_id(),
|
||||
slug=generate_id(),
|
||||
designation=FlowDesignation.ENROLLMENT,
|
||||
title=generate_client_id(),
|
||||
title=generate_id(),
|
||||
)
|
||||
|
||||
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
|
||||
|
@ -16,7 +16,7 @@ def pbflow_tester(file_name: str) -> Callable:
|
||||
"""This is used instead of subTest for better visibility"""
|
||||
|
||||
def tester(self: TestTransferDocs):
|
||||
with open(file_name, "r") as flow_json:
|
||||
with open(file_name, "r", encoding="utf8") as flow_json:
|
||||
importer = FlowImporter(flow_json.read())
|
||||
self.assertTrue(importer.validate())
|
||||
self.assertTrue(importer.apply())
|
||||
|
@ -2,10 +2,10 @@
|
||||
from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
@ -37,7 +37,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse):
|
||||
TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response)
|
||||
|
||||
|
||||
class TestFlowExecutor(TestCase):
|
||||
class TestFlowExecutor(APITestCase):
|
||||
"""Test views logic"""
|
||||
|
||||
def setUp(self):
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""flow views tests"""
|
||||
from django.test import Client, TestCase
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
@ -10,9 +10,6 @@ from authentik.flows.views import SESSION_KEY_PLAN
|
||||
class TestHelperView(TestCase):
|
||||
"""Test helper views logic"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
def test_default_view(self):
|
||||
"""Test that ToDefaultFlow returns the expected URL"""
|
||||
flow = Flow.objects.filter(
|
||||
|
@ -25,6 +25,8 @@ def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
||||
"component",
|
||||
"flow_set",
|
||||
"promptstage_set",
|
||||
"policybindingmodel_ptr_id",
|
||||
"export_url",
|
||||
)
|
||||
for to_remove_name in to_remove:
|
||||
if to_remove_name in data:
|
||||
|
@ -79,7 +79,7 @@ class ConfigLoader:
|
||||
value = os.getenv(url.netloc, url.query)
|
||||
if url.scheme == "file":
|
||||
try:
|
||||
with open(url.path, "r") as _file:
|
||||
with open(url.path, "r", encoding="utf8") as _file:
|
||||
value = _file.read()
|
||||
except OSError:
|
||||
self._log("error", f"Failed to read config value from {url.path}")
|
||||
@ -89,7 +89,7 @@ class ConfigLoader:
|
||||
def update_from_file(self, path: str):
|
||||
"""Update config from file contents"""
|
||||
try:
|
||||
with open(path) as file:
|
||||
with open(path, encoding="utf8") as file:
|
||||
try:
|
||||
self.update(self.__config, yaml.safe_load(file))
|
||||
self._log("debug", "Loaded config", file=path)
|
||||
|
18
authentik/lib/generators.py
Normal file
18
authentik/lib/generators.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""ID/Secret Generators"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
|
||||
def generate_id(length=40):
|
||||
"""Generate a random client ID"""
|
||||
rand = SystemRandom()
|
||||
return "".join(rand.choice(string.ascii_letters + string.digits) for x in range(length))
|
||||
|
||||
|
||||
def generate_key(length=128):
|
||||
"""Generate a suitable client secret"""
|
||||
rand = SystemRandom()
|
||||
return "".join(
|
||||
rand.choice(string.ascii_letters + string.digits + string.punctuation)
|
||||
for x in range(length)
|
||||
)
|
27
authentik/lib/tests/utils.py
Normal file
27
authentik/lib/tests/utils.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Test utils"""
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http import HttpRequest
|
||||
from django.test.client import RequestFactory
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
|
||||
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||
"""Dummy get_response for SessionMiddleware"""
|
||||
return None
|
||||
|
||||
|
||||
def get_request(*args, user=None, **kwargs):
|
||||
"""Get a request with usable session"""
|
||||
request = RequestFactory().get(*args, **kwargs)
|
||||
if user:
|
||||
request.user = user
|
||||
else:
|
||||
request.user = get_anonymous_user()
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
middleware = MessageMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
return request
|
@ -15,7 +15,7 @@ from authentik.core.channels import AuthJsonConsumer
|
||||
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
|
||||
|
||||
GAUGE_OUTPOSTS_CONNECTED = Gauge(
|
||||
"authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid"]
|
||||
"authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid", "expected"]
|
||||
)
|
||||
GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
|
||||
"authentik_outposts_last_update",
|
||||
@ -76,6 +76,7 @@ class OutpostConsumer(AuthJsonConsumer):
|
||||
GAUGE_OUTPOSTS_CONNECTED.labels(
|
||||
outpost=self.outpost.name,
|
||||
uid=self.last_uid,
|
||||
expected=self.outpost.config.kubernetes_replicas,
|
||||
).dec()
|
||||
LOGGER.debug(
|
||||
"removed outpost instance from cache",
|
||||
@ -100,6 +101,7 @@ class OutpostConsumer(AuthJsonConsumer):
|
||||
GAUGE_OUTPOSTS_CONNECTED.labels(
|
||||
outpost=self.outpost.name,
|
||||
uid=self.last_uid,
|
||||
expected=self.outpost.config.kubernetes_replicas,
|
||||
).inc()
|
||||
LOGGER.debug(
|
||||
"added outpost instace to cache",
|
||||
|
@ -29,7 +29,9 @@ class DockerController(BaseController):
|
||||
raise ControllerException from exc
|
||||
|
||||
def _get_labels(self) -> dict[str, str]:
|
||||
return {}
|
||||
return {
|
||||
"io.goauthentik.outpost-uuid": self.outpost.pk.hex,
|
||||
}
|
||||
|
||||
def _get_env(self) -> dict[str, str]:
|
||||
return {
|
||||
@ -49,6 +51,17 @@ class DockerController(BaseController):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _comp_labels(self, container: Container) -> bool:
|
||||
"""Check if container's labels is equal to what we would set. Return true if container needs
|
||||
to be rebuilt."""
|
||||
should_be = self._get_labels()
|
||||
for key, expected_value in should_be.items():
|
||||
if key not in container.labels:
|
||||
return True
|
||||
if container.labels[key] != expected_value:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _comp_ports(self, container: Container) -> bool:
|
||||
"""Check that the container has the correct ports exposed. Return true if container needs
|
||||
to be rebuilt."""
|
||||
@ -92,9 +105,11 @@ class DockerController(BaseController):
|
||||
"environment": self._get_env(),
|
||||
"labels": self._get_labels(),
|
||||
"restart_policy": {"Name": "unless-stopped"},
|
||||
"network": self.outpost.config.docker_network,
|
||||
}
|
||||
if settings.TEST:
|
||||
del container_args["ports"]
|
||||
del container_args["network"]
|
||||
container_args["network_mode"] = "host"
|
||||
return (
|
||||
self.client.containers.create(**container_args),
|
||||
@ -102,9 +117,11 @@ class DockerController(BaseController):
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def up(self):
|
||||
def up(self, depth=1):
|
||||
if self.outpost.managed == MANAGED_OUTPOST:
|
||||
return None
|
||||
if depth >= 10:
|
||||
raise ControllerException("Giving up since we exceeded recursion limit.")
|
||||
try:
|
||||
container, has_been_created = self._get_container()
|
||||
if has_been_created:
|
||||
@ -120,17 +137,22 @@ class DockerController(BaseController):
|
||||
should=self.get_container_image(),
|
||||
)
|
||||
self.down()
|
||||
return self.up()
|
||||
return self.up(depth + 1)
|
||||
# Check container's ports
|
||||
if self._comp_ports(container):
|
||||
self.logger.info("Container has mis-matched ports, re-creating...")
|
||||
self.down()
|
||||
return self.up()
|
||||
return self.up(depth + 1)
|
||||
# Check that container values match our values
|
||||
if self._comp_env(container):
|
||||
self.logger.info("Container has outdated config, re-creating...")
|
||||
self.down()
|
||||
return self.up()
|
||||
return self.up(depth + 1)
|
||||
# Check that container values match our values
|
||||
if self._comp_labels(container):
|
||||
self.logger.info("Container has outdated labels, re-creating...")
|
||||
self.down()
|
||||
return self.up(depth + 1)
|
||||
if (
|
||||
container.attrs.get("HostConfig", {})
|
||||
.get("RestartPolicy", {})
|
||||
@ -140,7 +162,7 @@ class DockerController(BaseController):
|
||||
):
|
||||
self.logger.info("Container has mis-matched restart policy, re-creating...")
|
||||
self.down()
|
||||
return self.up()
|
||||
return self.up(depth + 1)
|
||||
# Check that container is healthy
|
||||
if (
|
||||
container.status == "running"
|
||||
|
@ -3,10 +3,11 @@ from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
|
||||
from django.utils.text import slugify
|
||||
from kubernetes.client import V1ObjectMeta
|
||||
from kubernetes.client.exceptions import ApiException, OpenApiException
|
||||
from kubernetes.client.models.v1_deployment import V1Deployment
|
||||
from kubernetes.client.models.v1_pod import V1Pod
|
||||
from kubernetes.client.rest import ApiException
|
||||
from structlog.stdlib import get_logger
|
||||
from urllib3.exceptions import HTTPError
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
@ -72,8 +73,9 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
try:
|
||||
try:
|
||||
current = self.retrieve()
|
||||
except ApiException as exc:
|
||||
if exc.status == 404:
|
||||
except (OpenApiException, HTTPError) as exc:
|
||||
# pylint: disable=no-member
|
||||
if isinstance(exc, ApiException) and exc.status == 404:
|
||||
self.logger.debug("Failed to get current, triggering recreate")
|
||||
raise NeedsRecreate from exc
|
||||
self.logger.debug("Other unhandled error", exc=exc)
|
||||
@ -104,8 +106,9 @@ class KubernetesObjectReconciler(Generic[T]):
|
||||
current = self.retrieve()
|
||||
self.delete(current)
|
||||
self.logger.debug("Removing")
|
||||
except ApiException as exc:
|
||||
if exc.status == 404:
|
||||
except (OpenApiException, HTTPError) as exc:
|
||||
# pylint: disable=no-member
|
||||
if isinstance(exc, ApiException) and exc.status == 404:
|
||||
self.logger.debug("Failed to get current, assuming non-existant")
|
||||
return
|
||||
self.logger.debug("Other unhandled error", exc=exc)
|
||||
|
@ -1,11 +1,13 @@
|
||||
"""k8s utils"""
|
||||
from pathlib import Path
|
||||
|
||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
|
||||
|
||||
|
||||
def get_namespace() -> str:
|
||||
"""Get the namespace if we're running in a pod, otherwise default to default"""
|
||||
path = Path("/var/run/secrets/kubernetes.io/serviceaccount/namespace")
|
||||
path = Path(SERVICE_TOKEN_FILENAME.replace("token", "namespace"))
|
||||
if path.exists():
|
||||
with open(path, "r") as _namespace_file:
|
||||
with open(path, "r", encoding="utf8") as _namespace_file:
|
||||
return _namespace_file.read()
|
||||
return "default"
|
||||
|
@ -3,8 +3,9 @@ from io import StringIO
|
||||
from typing import Type
|
||||
|
||||
from kubernetes.client.api_client import ApiClient
|
||||
from kubernetes.client.exceptions import ApiException
|
||||
from kubernetes.client.exceptions import OpenApiException
|
||||
from structlog.testing import capture_logs
|
||||
from urllib3.exceptions import HTTPError
|
||||
from yaml import dump_all
|
||||
|
||||
from authentik.outposts.controllers.base import BaseController, ControllerException
|
||||
@ -12,7 +13,7 @@ from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
from authentik.outposts.controllers.k8s.secret import SecretReconciler
|
||||
from authentik.outposts.controllers.k8s.service import ServiceReconciler
|
||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost
|
||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost, ServiceConnectionInvalid
|
||||
|
||||
|
||||
class KubernetesController(BaseController):
|
||||
@ -40,7 +41,7 @@ class KubernetesController(BaseController):
|
||||
reconciler = self.reconcilers[reconcile_key](self)
|
||||
reconciler.up()
|
||||
|
||||
except ApiException as exc:
|
||||
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
|
||||
raise ControllerException(str(exc)) from exc
|
||||
|
||||
def up_with_logs(self) -> list[str]:
|
||||
@ -55,7 +56,7 @@ class KubernetesController(BaseController):
|
||||
reconciler.up()
|
||||
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
||||
return all_logs
|
||||
except ApiException as exc:
|
||||
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
|
||||
raise ControllerException(str(exc)) from exc
|
||||
|
||||
def down(self):
|
||||
@ -65,7 +66,7 @@ class KubernetesController(BaseController):
|
||||
self.logger.debug("Tearing down object", name=reconcile_key)
|
||||
reconciler.down()
|
||||
|
||||
except ApiException as exc:
|
||||
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
|
||||
raise ControllerException(str(exc)) from exc
|
||||
|
||||
def down_with_logs(self) -> list[str]:
|
||||
@ -80,7 +81,7 @@ class KubernetesController(BaseController):
|
||||
reconciler.down()
|
||||
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
||||
return all_logs
|
||||
except ApiException as exc:
|
||||
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
|
||||
raise ControllerException(str(exc)) from exc
|
||||
|
||||
def get_static_deployment(self) -> str:
|
||||
|
@ -25,7 +25,7 @@ class DockerInlineTLS:
|
||||
def write_file(self, name: str, contents: str) -> str:
|
||||
"""Wrapper for mkstemp that uses fdopen"""
|
||||
path = Path(gettempdir(), name)
|
||||
with open(path, "w") as _file:
|
||||
with open(path, "w", encoding="utf8") as _file:
|
||||
_file.write(contents)
|
||||
return str(path)
|
||||
|
||||
|
0
authentik/outposts/management/__init__.py
Normal file
0
authentik/outposts/management/__init__.py
Normal file
0
authentik/outposts/management/commands/__init__.py
Normal file
0
authentik/outposts/management/commands/__init__.py
Normal file
15
authentik/outposts/management/commands/repair_permissions.py
Normal file
15
authentik/outposts/management/commands/repair_permissions.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Repair missing permissions"""
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.management import create_permissions
|
||||
from django.core.management.base import BaseCommand, no_translations
|
||||
|
||||
|
||||
class Command(BaseCommand): # pragma: no cover
|
||||
"""Repair missing permissions"""
|
||||
|
||||
@no_translations
|
||||
def handle(self, *args, **options):
|
||||
"""Check permissions for all apps"""
|
||||
for app in apps.get_app_configs():
|
||||
self.stdout.write(f"Checking app {app.name} ({app.label})\n")
|
||||
create_permissions(app, verbosity=0)
|
@ -37,9 +37,11 @@ from authentik.core.models import (
|
||||
User,
|
||||
)
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import InheritanceForeignKey
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.managed.models import ManagedModel
|
||||
from authentik.outposts.controllers.k8s.utils import get_namespace
|
||||
from authentik.outposts.docker_tls import DockerInlineTLS
|
||||
@ -54,6 +56,7 @@ class ServiceConnectionInvalid(SentryIgnoredException):
|
||||
|
||||
|
||||
@dataclass
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class OutpostConfig:
|
||||
"""Configuration an outpost uses to configure it self"""
|
||||
|
||||
@ -65,8 +68,10 @@ class OutpostConfig:
|
||||
log_level: str = CONFIG.y("log_level")
|
||||
error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
|
||||
error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer")
|
||||
|
||||
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
||||
|
||||
docker_network: Optional[str] = field(default=None)
|
||||
|
||||
kubernetes_replicas: int = field(default=1)
|
||||
kubernetes_namespace: str = field(default_factory=get_namespace)
|
||||
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
||||
@ -358,7 +363,24 @@ class Outpost(ManagedModel):
|
||||
code_name = (
|
||||
f"{model_or_perm._meta.app_label}." f"view_{model_or_perm._meta.model_name}"
|
||||
)
|
||||
assign_perm(code_name, user, model_or_perm)
|
||||
try:
|
||||
assign_perm(code_name, user, model_or_perm)
|
||||
except (Permission.DoesNotExist, AttributeError) as exc:
|
||||
LOGGER.warning(
|
||||
"permission doesn't exist",
|
||||
code_name=code_name,
|
||||
user=user,
|
||||
model=model_or_perm,
|
||||
)
|
||||
Event.new(
|
||||
action=EventAction.SYSTEM_EXCEPTION,
|
||||
message=(
|
||||
"While setting the permissions for the service-account, a "
|
||||
"permission was not found: Check "
|
||||
"https://goauthentik.io/docs/troubleshooting/missing_permission"
|
||||
)
|
||||
+ exception_to_string(exc),
|
||||
).set_user(user).save()
|
||||
else:
|
||||
app_label, perm = model_or_perm.split(".")
|
||||
permission = Permission.objects.filter(
|
||||
|
@ -227,7 +227,7 @@ def outpost_local_connection():
|
||||
kubeconfig_local_name = f"k8s-{gethostname()}"
|
||||
if not KubernetesServiceConnection.objects.filter(name=kubeconfig_local_name).exists():
|
||||
LOGGER.debug("Creating kubeconfig Service Connection")
|
||||
with open(kubeconfig_path, "r") as _kubeconfig:
|
||||
with open(kubeconfig_path, "r", encoding="utf8") as _kubeconfig:
|
||||
KubernetesServiceConnection.objects.create(
|
||||
name=kubeconfig_local_name,
|
||||
kubeconfig=yaml.safe_load(_kubeconfig),
|
||||
|
@ -2,9 +2,9 @@
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.policies.hibp.models import HaveIBeenPwendPolicy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
|
||||
|
||||
class TestHIBPPolicy(TestCase):
|
||||
@ -37,7 +37,7 @@ class TestHIBPPolicy(TestCase):
|
||||
name="test_true",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = generate_client_secret()
|
||||
request.context["password"] = generate_key()
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertTrue(result.passing)
|
||||
self.assertEqual(result.messages, tuple())
|
||||
|
@ -8,8 +8,11 @@ from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.policies.models import Policy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
LOGGER = get_logger()
|
||||
RE_LOWER = re.compile("[a-z]")
|
||||
RE_UPPER = re.compile("[A-Z]")
|
||||
|
||||
|
||||
class PasswordPolicy(Policy):
|
||||
@ -38,31 +41,42 @@ class PasswordPolicy(Policy):
|
||||
return "ak-policy-password-form"
|
||||
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
if self.password_field not in request.context:
|
||||
if (
|
||||
self.password_field not in request.context
|
||||
and self.password_field not in request.context.get(PLAN_CONTEXT_PROMPT, {})
|
||||
):
|
||||
LOGGER.warning(
|
||||
"Password field not set in Policy Request",
|
||||
field=self.password_field,
|
||||
fields=request.context.keys(),
|
||||
prompt_fields=request.context.get(PLAN_CONTEXT_PROMPT, {}).keys(),
|
||||
)
|
||||
return PolicyResult(False, _("Password not set in context"))
|
||||
password = request.context[self.password_field]
|
||||
|
||||
filter_regex = []
|
||||
if self.amount_lowercase > 0:
|
||||
filter_regex.append(r"[a-z]{%d,}" % self.amount_lowercase)
|
||||
if self.amount_uppercase > 0:
|
||||
filter_regex.append(r"[A-Z]{%d,}" % self.amount_uppercase)
|
||||
if self.password_field in request.context:
|
||||
password = request.context[self.password_field]
|
||||
else:
|
||||
password = request.context[PLAN_CONTEXT_PROMPT][self.password_field]
|
||||
|
||||
if len(password) < self.length_min:
|
||||
LOGGER.debug("password failed", reason="length")
|
||||
return PolicyResult(False, self.error_message)
|
||||
|
||||
if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase:
|
||||
LOGGER.debug("password failed", reason="amount_lowercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_uppercase > 0 and len(RE_UPPER.findall(password)) < self.amount_lowercase:
|
||||
LOGGER.debug("password failed", reason="amount_uppercase")
|
||||
return PolicyResult(False, self.error_message)
|
||||
if self.amount_symbols > 0:
|
||||
filter_regex.append(r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols))
|
||||
full_regex = "|".join(filter_regex)
|
||||
LOGGER.debug("Built regex", regexp=full_regex)
|
||||
result = bool(re.compile(full_regex).match(password))
|
||||
count = 0
|
||||
for symbol in self.symbol_charset:
|
||||
count += password.count(symbol)
|
||||
if count < self.amount_symbols:
|
||||
LOGGER.debug("password failed", reason="amount_symbols")
|
||||
return PolicyResult(False, self.error_message)
|
||||
|
||||
result = result and len(password) >= self.length_min
|
||||
|
||||
if not result:
|
||||
return PolicyResult(result, self.error_message)
|
||||
return PolicyResult(result)
|
||||
return PolicyResult(True)
|
||||
|
||||
class Meta:
|
||||
|
||||
|
@ -1,57 +0,0 @@
|
||||
"""Password Policy tests"""
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.policies.password.models import PasswordPolicy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class TestPasswordPolicy(TestCase):
|
||||
"""Test Password Policy"""
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test without password"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
name="test_invalid",
|
||||
amount_uppercase=1,
|
||||
amount_lowercase=2,
|
||||
amount_symbols=3,
|
||||
length_min=24,
|
||||
error_message="test message",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages[0], "Password not set in context")
|
||||
|
||||
def test_false(self):
|
||||
"""Failing password case"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
name="test_false",
|
||||
amount_uppercase=1,
|
||||
amount_lowercase=2,
|
||||
amount_symbols=3,
|
||||
length_min=24,
|
||||
error_message="test message",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "test"
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages, ("test message",))
|
||||
|
||||
def test_true(self):
|
||||
"""Positive password case"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
name="test_true",
|
||||
amount_uppercase=1,
|
||||
amount_lowercase=2,
|
||||
amount_symbols=3,
|
||||
length_min=3,
|
||||
error_message="test message",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "Test()!"
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertTrue(result.passing)
|
||||
self.assertEqual(result.messages, tuple())
|
0
authentik/policies/password/tests/__init__.py
Normal file
0
authentik/policies/password/tests/__init__.py
Normal file
80
authentik/policies/password/tests/test_flows.py
Normal file
80
authentik/policies/password/tests/test_flows.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""Password flow tests"""
|
||||
from django.urls.base import reverse
|
||||
from django.utils.encoding import force_str
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.policies.password.models import PasswordPolicy
|
||||
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
|
||||
|
||||
class TestPasswordPolicyFlow(APITestCase):
|
||||
"""Test Password Policy"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create(username="unittest", email="test@beryju.org")
|
||||
|
||||
self.flow = Flow.objects.create(
|
||||
name="test-prompt",
|
||||
slug="test-prompt",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
password_prompt = Prompt.objects.create(
|
||||
field_key="password",
|
||||
label="PASSWORD_LABEL",
|
||||
type=FieldTypes.PASSWORD,
|
||||
required=True,
|
||||
placeholder="PASSWORD_PLACEHOLDER",
|
||||
)
|
||||
|
||||
self.policy = PasswordPolicy.objects.create(
|
||||
name="test_true",
|
||||
amount_uppercase=1,
|
||||
amount_lowercase=2,
|
||||
amount_symbols=3,
|
||||
length_min=3,
|
||||
error_message="test message",
|
||||
)
|
||||
stage = PromptStage.objects.create(name="prompt-stage")
|
||||
stage.validation_policies.set([self.policy])
|
||||
stage.fields.set(
|
||||
[
|
||||
password_prompt,
|
||||
]
|
||||
)
|
||||
FlowStageBinding.objects.create(target=self.flow, stage=stage, order=2)
|
||||
|
||||
def test_prompt_data(self):
|
||||
"""Test policy attached to a prompt stage"""
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}),
|
||||
{"password": "akadmin"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{
|
||||
"component": "ak-stage-prompt",
|
||||
"fields": [
|
||||
{
|
||||
"field_key": "password",
|
||||
"label": "PASSWORD_LABEL",
|
||||
"order": 0,
|
||||
"placeholder": "PASSWORD_PLACEHOLDER",
|
||||
"required": True,
|
||||
"type": "password",
|
||||
}
|
||||
],
|
||||
"flow_info": {
|
||||
"background": self.flow.background_url,
|
||||
"cancel_url": reverse("authentik_flows:cancel"),
|
||||
"title": "",
|
||||
},
|
||||
"response_errors": {
|
||||
"non_field_errors": [{"code": "invalid", "string": self.policy.error_message}]
|
||||
},
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
},
|
||||
)
|
68
authentik/policies/password/tests/test_policy.py
Normal file
68
authentik/policies/password/tests/test_policy.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""Password Policy tests"""
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.policies.password.models import PasswordPolicy
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class TestPasswordPolicy(TestCase):
|
||||
"""Test Password Policy"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.policy = PasswordPolicy.objects.create(
|
||||
name="test_false",
|
||||
amount_uppercase=1,
|
||||
amount_lowercase=2,
|
||||
amount_symbols=3,
|
||||
length_min=24,
|
||||
error_message="test message",
|
||||
)
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test without password"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages[0], "Password not set in context")
|
||||
|
||||
def test_failed_length(self):
|
||||
"""Password too short"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "test"
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages, ("test message",))
|
||||
|
||||
def test_failed_lowercase(self):
|
||||
"""not enough lowercase"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe"
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages, ("test message",))
|
||||
|
||||
def test_failed_uppercase(self):
|
||||
"""not enough uppercase"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "tttttttttttttttttttttttE"
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages, ("test message",))
|
||||
|
||||
def test_failed_symbols(self):
|
||||
"""not enough uppercase"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "TETETETETETETETETETETETETe!!!"
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages, ("test message",))
|
||||
|
||||
def test_true(self):
|
||||
"""Positive password case"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = generate_key() + "ee!!!"
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertTrue(result.passing)
|
||||
self.assertEqual(result.messages, tuple())
|
@ -29,7 +29,19 @@ class LDAPProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
queryset = LDAPProvider.objects.all()
|
||||
serializer_class = LDAPProviderSerializer
|
||||
filterset_fields = "__all__"
|
||||
filterset_fields = {
|
||||
"application": ["isnull"],
|
||||
"name": ["iexact"],
|
||||
"authorization_flow__slug": ["iexact"],
|
||||
"base_dn": ["iexact"],
|
||||
"search_group__group_uuid": ["iexact"],
|
||||
"search_group__name": ["iexact"],
|
||||
"certificate__kp_uuid": ["iexact"],
|
||||
"certificate__name": ["iexact"],
|
||||
"tls_server_name": ["iexact"],
|
||||
"uid_start_number": ["iexact"],
|
||||
"gid_start_number": ["iexact"],
|
||||
}
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import ReadOnlyField
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -49,12 +49,12 @@ class OAuth2ProviderSerializer(ProviderSerializer):
|
||||
class OAuth2ProviderSetupURLs(PassiveSerializer):
|
||||
"""OAuth2 Provider Metadata serializer"""
|
||||
|
||||
issuer = ReadOnlyField()
|
||||
authorize = ReadOnlyField()
|
||||
token = ReadOnlyField()
|
||||
user_info = ReadOnlyField()
|
||||
provider_info = ReadOnlyField()
|
||||
logout = ReadOnlyField()
|
||||
issuer = CharField(read_only=True)
|
||||
authorize = CharField(read_only=True)
|
||||
token = CharField(read_only=True)
|
||||
user_info = CharField(read_only=True)
|
||||
provider_info = CharField(read_only=True)
|
||||
logout = CharField(read_only=True)
|
||||
|
||||
|
||||
class OAuth2ProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -1,15 +0,0 @@
|
||||
"""OAuth2 Client ID/Secret Generators"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
|
||||
|
||||
def generate_client_id():
|
||||
"""Generate a random client ID"""
|
||||
rand = SystemRandom()
|
||||
return "".join(rand.choice(string.ascii_letters + string.digits) for x in range(40))
|
||||
|
||||
|
||||
def generate_client_secret():
|
||||
"""Generate a suitable client secret"""
|
||||
rand = SystemRandom()
|
||||
return "".join(rand.choice(string.ascii_letters + string.digits) for x in range(128))
|
@ -7,8 +7,8 @@ from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import authentik.core.models
|
||||
import authentik.lib.generators
|
||||
import authentik.lib.utils.time
|
||||
import authentik.providers.oauth2.generators
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -55,7 +55,7 @@ class Migration(migrations.Migration):
|
||||
(
|
||||
"client_id",
|
||||
models.CharField(
|
||||
default=authentik.providers.oauth2.generators.generate_client_id,
|
||||
default=authentik.lib.generators.generate_id,
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name="Client ID",
|
||||
@ -65,7 +65,7 @@ class Migration(migrations.Migration):
|
||||
"client_secret",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default=authentik.providers.oauth2.generators.generate_client_secret,
|
||||
default=authentik.lib.generators.generate_key,
|
||||
max_length=255,
|
||||
verbose_name="Client Secret",
|
||||
),
|
||||
|
@ -22,10 +22,10 @@ from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.utils import get_user
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
|
||||
from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config
|
||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
|
||||
from authentik.providers.oauth2.generators import generate_client_id, generate_client_secret
|
||||
|
||||
|
||||
class ClientTypes(models.TextChoices):
|
||||
@ -138,13 +138,13 @@ class OAuth2Provider(Provider):
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name=_("Client ID"),
|
||||
default=generate_client_id,
|
||||
default=generate_id,
|
||||
)
|
||||
client_secret = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
verbose_name=_("Client Secret"),
|
||||
default=generate_client_secret,
|
||||
default=generate_key,
|
||||
)
|
||||
jwt_alg = models.CharField(
|
||||
max_length=10,
|
||||
|
@ -7,8 +7,8 @@ from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.providers.oauth2.errors import AuthorizeError, ClientIdError, RedirectUriError
|
||||
from authentik.providers.oauth2.generators import generate_client_id, generate_client_secret
|
||||
from authentik.providers.oauth2.models import (
|
||||
AuthorizationCode,
|
||||
GrantTypes,
|
||||
@ -183,7 +183,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
redirect_uris="foo://localhost",
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_client_id()
|
||||
state = generate_id()
|
||||
user = User.objects.get(username="akadmin")
|
||||
self.client.force_login(user)
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
@ -215,13 +215,13 @@ class TestAuthorize(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
client_secret=generate_client_secret(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_client_id()
|
||||
state = generate_id()
|
||||
user = User.objects.get(username="akadmin")
|
||||
self.client.force_login(user)
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
|
@ -9,12 +9,12 @@ from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
GRANT_TYPE_REFRESH_TOKEN,
|
||||
)
|
||||
from authentik.providers.oauth2.errors import TokenError
|
||||
from authentik.providers.oauth2.generators import generate_client_id, generate_client_secret
|
||||
from authentik.providers.oauth2.models import AuthorizationCode, OAuth2Provider, RefreshToken
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
from authentik.providers.oauth2.views.token import TokenParams
|
||||
@ -32,8 +32,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -53,14 +53,14 @@ class TestToken(OAuthTestCase):
|
||||
params = TokenParams.parse(request, provider, provider.client_id, provider.client_secret)
|
||||
self.assertEqual(params.provider, provider)
|
||||
with self.assertRaises(TokenError):
|
||||
TokenParams.parse(request, provider, provider.client_id, generate_client_secret())
|
||||
TokenParams.parse(request, provider, provider.client_id, generate_key())
|
||||
|
||||
def test_request_auth_code_invalid(self):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -82,8 +82,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -93,7 +93,7 @@ class TestToken(OAuthTestCase):
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
refresh_token=generate_id(),
|
||||
)
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
@ -111,8 +111,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -153,8 +153,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -167,7 +167,7 @@ class TestToken(OAuthTestCase):
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
refresh_token=generate_id(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
@ -202,8 +202,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -213,7 +213,7 @@ class TestToken(OAuthTestCase):
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
refresh_token=generate_id(),
|
||||
)
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
@ -247,8 +247,8 @@ class TestToken(OAuthTestCase):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -261,7 +261,7 @@ class TestToken(OAuthTestCase):
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
refresh_token=generate_id(),
|
||||
)
|
||||
# Create initial refresh token
|
||||
response = self.client.post(
|
||||
|
@ -9,8 +9,8 @@ from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.oauth2.generators import generate_client_id, generate_client_secret
|
||||
from authentik.providers.oauth2.models import IDToken, OAuth2Provider, RefreshToken, ScopeMapping
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
|
||||
@ -24,8 +24,8 @@ class TestUserinfo(OAuthTestCase):
|
||||
self.app = Application.objects.create(name="test", slug="test")
|
||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
@ -38,8 +38,8 @@ class TestUserinfo(OAuthTestCase):
|
||||
self.token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=self.provider,
|
||||
user=self.user,
|
||||
access_token=generate_client_id(),
|
||||
refresh_token=generate_client_id(),
|
||||
access_token=generate_id(),
|
||||
refresh_token=generate_id(),
|
||||
_scope="openid user profile",
|
||||
_id_token=json.dumps(
|
||||
asdict(
|
||||
|
@ -103,8 +103,8 @@ def extract_client_auth(request: HttpRequest) -> tuple[str, str]:
|
||||
if re.compile(r"^Basic\s{1}.+$").match(auth_header):
|
||||
b64_user_pass = auth_header.split()[1]
|
||||
try:
|
||||
user_pass = b64decode(b64_user_pass).decode("utf-8").split(":")
|
||||
client_id, client_secret = user_pass
|
||||
user_pass = b64decode(b64_user_pass).decode("utf-8").partition(":")
|
||||
client_id, _, client_secret = user_pass
|
||||
except (ValueError, Error):
|
||||
client_id = client_secret = "" # nosec
|
||||
else:
|
||||
|
@ -80,7 +80,24 @@ class ProxyProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
queryset = ProxyProvider.objects.all()
|
||||
serializer_class = ProxyProviderSerializer
|
||||
filterset_fields = "__all__"
|
||||
filterset_fields = {
|
||||
"application": ["isnull"],
|
||||
"name": ["iexact"],
|
||||
"authorization_flow__slug": ["iexact"],
|
||||
"property_mappings": ["iexact"],
|
||||
"internal_host": ["iexact"],
|
||||
"external_host": ["iexact"],
|
||||
"internal_host_ssl_validation": ["iexact"],
|
||||
"certificate__kp_uuid": ["iexact"],
|
||||
"certificate__name": ["iexact"],
|
||||
"skip_path_regex": ["iexact"],
|
||||
"basic_auth_enabled": ["iexact"],
|
||||
"basic_auth_password_attribute": ["iexact"],
|
||||
"basic_auth_user_attribute": ["iexact"],
|
||||
"mode": ["iexact"],
|
||||
"redirect_uris": ["iexact"],
|
||||
"cookie_domain": ["iexact"],
|
||||
}
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
|
@ -22,13 +22,13 @@ class ProxyDockerController(DockerController):
|
||||
for proxy_provider in ProxyProvider.objects.filter(outpost__in=[self.outpost]):
|
||||
proxy_provider: ProxyProvider
|
||||
external_host_name = urlparse(proxy_provider.external_host)
|
||||
hosts.append(f"`{external_host_name}`")
|
||||
hosts.append(f"`{external_host_name.netloc}`")
|
||||
traefik_name = f"ak-outpost-{self.outpost.pk.hex}"
|
||||
return {
|
||||
"traefik.enable": "true",
|
||||
f"traefik.http.routers.{traefik_name}-router.rule": f"Host({','.join(hosts)})",
|
||||
f"traefik.http.routers.{traefik_name}-router.tls": "true",
|
||||
f"traefik.http.routers.{traefik_name}-router.service": f"{traefik_name}-service",
|
||||
f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path": "/",
|
||||
f"traefik.http.services.{traefik_name}-service.loadbalancer.server.port": "4180",
|
||||
}
|
||||
labels = super()._get_labels()
|
||||
labels["traefik.enable"] = "true"
|
||||
labels[f"traefik.http.routers.{traefik_name}-router.rule"] = f"Host({','.join(hosts)})"
|
||||
labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true"
|
||||
labels[f"traefik.http.routers.{traefik_name}-router.service"] = f"{traefik_name}-service"
|
||||
labels[f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path"] = "/"
|
||||
labels[f"traefik.http.services.{traefik_name}-service.loadbalancer.server.port"] = "4180"
|
||||
return labels
|
||||
|
@ -61,9 +61,10 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
|
||||
have_hosts.sort()
|
||||
|
||||
have_hosts_tls = []
|
||||
for tls_config in current.spec.tls:
|
||||
if tls_config and tls_config.hosts:
|
||||
have_hosts_tls += tls_config.hosts
|
||||
if current.spec.tls:
|
||||
for tls_config in current.spec.tls:
|
||||
if tls_config and tls_config.hosts:
|
||||
have_hosts_tls += tls_config.hosts
|
||||
have_hosts_tls.sort()
|
||||
|
||||
if have_hosts != expected_hosts:
|
||||
|
@ -96,6 +96,7 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
|
||||
|
||||
def get_reference_object(self) -> TraefikMiddleware:
|
||||
"""Get deployment object for outpost"""
|
||||
port = 9000 if self.is_embedded else 4180
|
||||
return TraefikMiddleware(
|
||||
apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
|
||||
kind="Middleware",
|
||||
@ -106,7 +107,7 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
|
||||
),
|
||||
spec=TraefikMiddlewareSpec(
|
||||
forwardAuth=TraefikMiddlewareSpecForwardAuth(
|
||||
address=f"http://{self.name}.{self.namespace}:4180/akprox/auth?traefik",
|
||||
address=f"http://{self.name}.{self.namespace}:{port}/akprox/auth?traefik",
|
||||
authResponseHeaders=[
|
||||
"Set-Cookie",
|
||||
"X-Auth-Username",
|
||||
|
@ -11,7 +11,7 @@ from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, FileField, ReadOnlyField, SerializerMethodField
|
||||
from rest_framework.fields import CharField, FileField, SerializerMethodField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
@ -70,8 +70,8 @@ class SAMLProviderSerializer(ProviderSerializer):
|
||||
class SAMLMetadataSerializer(PassiveSerializer):
|
||||
"""SAML Provider Metadata serializer"""
|
||||
|
||||
metadata = ReadOnlyField()
|
||||
download_url = ReadOnlyField(required=False)
|
||||
metadata = CharField(read_only=True)
|
||||
download_url = CharField(read_only=True, required=False)
|
||||
|
||||
|
||||
class SAMLProviderImportSerializer(PassiveSerializer):
|
||||
|
@ -1,16 +1,14 @@
|
||||
"""Test AuthN Request generator and parser"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http.request import QueryDict
|
||||
from django.test import RequestFactory, TestCase
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
@ -99,11 +97,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_signed_valid(self):
|
||||
"""Test generated AuthNRequest with valid signature"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -117,12 +111,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_request_full_signed(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -145,12 +134,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_request_id_invalid(self):
|
||||
"""Test generated AuthNRequest with invalid request ID"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -179,11 +163,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_signed_valid_detached(self):
|
||||
"""Test generated AuthNRequest with valid signature (detached)"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -243,12 +223,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_request_attributes(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = User.objects.get(username="akadmin")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/", user=User.objects.get(username="akadmin"))
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -264,12 +239,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
def test_request_attributes_invalid(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = User.objects.get(username="akadmin")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/", user=User.objects.get(username="akadmin"))
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -1,18 +1,16 @@
|
||||
"""Test Requests and Responses against schema"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import RequestFactory, TestCase
|
||||
from guardian.utils import get_anonymous_user
|
||||
from lxml import etree # nosec
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.tests.utils import get_request
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||
from authentik.providers.saml.tests.test_auth_n_request import dummy_get_response
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.request import RequestProcessor
|
||||
|
||||
@ -43,11 +41,7 @@ class TestSchema(TestCase):
|
||||
|
||||
def test_request_schema(self):
|
||||
"""Test generated AuthNRequest against Schema"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
@ -60,12 +54,7 @@ class TestSchema(TestCase):
|
||||
|
||||
def test_response_schema(self):
|
||||
"""Test generated AuthNRequest against Schema"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
http_request = get_request("/")
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.stages.password import BACKEND_DJANGO
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
|
||||
|
||||
class UseTokenView(View):
|
||||
@ -19,7 +19,7 @@ class UseTokenView(View):
|
||||
if not tokens.exists():
|
||||
raise Http404
|
||||
token = tokens.first()
|
||||
login(request, token.user, backend=BACKEND_DJANGO)
|
||||
login(request, token.user, backend=BACKEND_INBUILT)
|
||||
token.delete()
|
||||
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||
return redirect("authentik_core:if-admin")
|
||||
|
0
authentik/root/asgi/__init__.py
Normal file
0
authentik/root/asgi/__init__.py
Normal file
40
authentik/root/asgi/app.py
Normal file
40
authentik/root/asgi/app.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""
|
||||
ASGI config for authentik project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
import django
|
||||
from asgiref.compatibility import guarantee_single_callable
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from defusedxml import defuse_stdlib
|
||||
from django.core.asgi import get_asgi_application
|
||||
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
||||
|
||||
from authentik.root.asgi.error_handler import ASGIErrorHandler
|
||||
from authentik.root.asgi.logger import ASGILogger
|
||||
|
||||
# DJANGO_SETTINGS_MODULE is set in gunicorn.conf.py
|
||||
|
||||
defuse_stdlib()
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from authentik.root import websocket # noqa # isort:skip
|
||||
|
||||
application = ASGIErrorHandler(
|
||||
ASGILogger(
|
||||
guarantee_single_callable(
|
||||
SentryAsgiMiddleware(
|
||||
ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"websocket": URLRouter(websocket.websocket_urlpatterns),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
38
authentik/root/asgi/error_handler.py
Normal file
38
authentik/root/asgi/error_handler.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""ASGI Error handler"""
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.root.asgi.types import ASGIApp, Receive, Scope, Send
|
||||
|
||||
LOGGER = get_logger("authentik.asgi")
|
||||
|
||||
|
||||
class ASGIErrorHandler:
|
||||
"""ASGI Error handler"""
|
||||
|
||||
app: ASGIApp
|
||||
|
||||
def __init__(self, app: ASGIApp):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
try:
|
||||
return await self.app(scope, receive, send)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
LOGGER.warning("Fatal ASGI exception", exc=exc)
|
||||
return await self.error_handler(scope, send)
|
||||
|
||||
async def error_handler(self, scope: Scope, send: Send) -> None:
|
||||
"""Return a generic error message"""
|
||||
if scope.get("scheme", "http") == "http":
|
||||
return await send(
|
||||
{
|
||||
"type": "http.request",
|
||||
"body": b"Internal server error",
|
||||
"more_body": False,
|
||||
}
|
||||
)
|
||||
return await send(
|
||||
{
|
||||
"type": "websocket.close",
|
||||
}
|
||||
)
|
@ -1,41 +1,10 @@
|
||||
"""
|
||||
ASGI config for authentik project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
import typing
|
||||
"""ASGI Logger"""
|
||||
from time import time
|
||||
|
||||
import django
|
||||
from asgiref.compatibility import guarantee_single_callable
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from defusedxml import defuse_stdlib
|
||||
from django.core.asgi import get_asgi_application
|
||||
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.middleware import RESPONSE_HEADER_ID
|
||||
|
||||
# DJANGO_SETTINGS_MODULE is set in gunicorn.conf.py
|
||||
|
||||
defuse_stdlib()
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from authentik.root import websocket # noqa # isort:skip
|
||||
|
||||
|
||||
# See https://github.com/encode/starlette/blob/master/starlette/types.py
|
||||
Scope = typing.MutableMapping[str, typing.Any]
|
||||
Message = typing.MutableMapping[str, typing.Any]
|
||||
|
||||
Receive = typing.Callable[[], typing.Awaitable[Message]]
|
||||
Send = typing.Callable[[Message], typing.Awaitable[None]]
|
||||
|
||||
ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
|
||||
from authentik.root.asgi.types import ASGIApp, Message, Receive, Scope, Send
|
||||
|
||||
ASGI_IP_HEADERS = (
|
||||
b"x-forwarded-for",
|
||||
@ -50,22 +19,24 @@ class ASGILogger:
|
||||
|
||||
app: ASGIApp
|
||||
|
||||
status_code: int
|
||||
start: float
|
||||
|
||||
def __init__(self, app: ASGIApp):
|
||||
self.app = app
|
||||
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
content_length = 0
|
||||
status_code = 0
|
||||
request_id = ""
|
||||
location = ""
|
||||
start = time()
|
||||
|
||||
async def send_hooked(message: Message) -> None:
|
||||
"""Hooked send method, which records status code and content-length, and for the final
|
||||
requests logs it"""
|
||||
|
||||
headers = dict(message.get("headers", []))
|
||||
if "status" in message:
|
||||
self.status_code = message["status"]
|
||||
nonlocal status_code
|
||||
status_code = message["status"]
|
||||
|
||||
if b"Content-Length" in headers:
|
||||
nonlocal content_length
|
||||
@ -74,19 +45,24 @@ class ASGILogger:
|
||||
if message["type"] == "http.response.start":
|
||||
response_headers = dict(message["headers"])
|
||||
nonlocal request_id
|
||||
nonlocal location
|
||||
request_id = response_headers.get(RESPONSE_HEADER_ID.encode(), b"").decode()
|
||||
location = response_headers.get(b"Location", b"").decode()
|
||||
|
||||
if message["type"] == "http.response.body" and not message.get("more_body", True):
|
||||
runtime = int((time() - self.start) * 1000)
|
||||
self.log(scope, runtime, content_length, request_id=request_id)
|
||||
nonlocal start
|
||||
runtime = int((time() - start) * 1000)
|
||||
kwargs = {"request_id": request_id}
|
||||
if location != "":
|
||||
kwargs["location"] = location
|
||||
self.log(scope, runtime, content_length, status_code, **kwargs)
|
||||
await send(message)
|
||||
|
||||
self.start = time()
|
||||
if scope["type"] == "lifespan":
|
||||
# https://code.djangoproject.com/ticket/31508
|
||||
# https://github.com/encode/uvicorn/issues/266
|
||||
return
|
||||
await self.app(scope, receive, send_hooked)
|
||||
return await self.app(scope, receive, send_hooked)
|
||||
|
||||
def _get_ip(self, scope: Scope) -> str:
|
||||
client_ip = None
|
||||
@ -99,7 +75,7 @@ class ASGILogger:
|
||||
# Check if header has multiple values, and use the first one
|
||||
return client_ip.split(", ")[0]
|
||||
|
||||
def log(self, scope: Scope, content_length: int, runtime: float, **kwargs):
|
||||
def log(self, scope: Scope, content_length: int, runtime: float, status_code: int, **kwargs):
|
||||
"""Outpot access logs in a structured format"""
|
||||
host = self._get_ip(scope)
|
||||
query_string = ""
|
||||
@ -110,22 +86,8 @@ class ASGILogger:
|
||||
host=host,
|
||||
method=scope.get("method", ""),
|
||||
scheme=scope.get("scheme", ""),
|
||||
status=self.status_code,
|
||||
status=status_code,
|
||||
size=content_length / 1000 if content_length > 0 else 0,
|
||||
runtime=runtime,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
application = ASGILogger(
|
||||
guarantee_single_callable(
|
||||
SentryAsgiMiddleware(
|
||||
ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"websocket": URLRouter(websocket.websocket_urlpatterns),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
11
authentik/root/asgi/types.py
Normal file
11
authentik/root/asgi/types.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""ASGI Types"""
|
||||
import typing
|
||||
|
||||
# See https://github.com/encode/starlette/blob/master/starlette/types.py
|
||||
Scope = typing.MutableMapping[str, typing.Any]
|
||||
Message = typing.MutableMapping[str, typing.Any]
|
||||
|
||||
Receive = typing.Callable[[], typing.Awaitable[Message]]
|
||||
Send = typing.Callable[[Message], typing.Awaitable[None]]
|
||||
|
||||
ASGIApp = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]]
|
@ -24,7 +24,7 @@ class SessionMiddleware(UpstreamSessionMiddleware):
|
||||
# Since go does not consider localhost with http a secure origin
|
||||
# we can't set the secure flag.
|
||||
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
||||
if user_agent.startswith("authentik-outpost@"):
|
||||
if user_agent.startswith("authentik-outpost@") or "safari" in user_agent.lower():
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
@ -32,6 +32,7 @@ from authentik.core.middleware import structlog_add_request_id
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.logging import add_process_id
|
||||
from authentik.lib.sentry import before_send
|
||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||
|
||||
|
||||
def j_print(event: str, log_level: str = "info", **kwargs):
|
||||
@ -74,6 +75,9 @@ SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}"
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
BACKEND_INBUILT,
|
||||
BACKEND_APP_PASSWORD,
|
||||
BACKEND_LDAP,
|
||||
"guardian.backends.ObjectPermissionBackend",
|
||||
]
|
||||
|
||||
@ -146,9 +150,20 @@ SPECTACULAR_SETTINGS = {
|
||||
"DESCRIPTION": "Making authentication simple.",
|
||||
"VERSION": __version__,
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
"SCHEMA_PATH_PREFIX": "/api/v([0-9]+(beta)?)",
|
||||
"SCHEMA_PATH_PREFIX_TRIM": True,
|
||||
"SERVERS": [
|
||||
{
|
||||
"url": "/api/v3/",
|
||||
},
|
||||
{
|
||||
"url": "/api/v2beta/",
|
||||
},
|
||||
],
|
||||
"CONTACT": {
|
||||
"email": "hello@beryju.org",
|
||||
},
|
||||
"AUTHENTICATION_WHITELIST": ["authentik.api.authentication.TokenAuthentication"],
|
||||
"LICENSE": {
|
||||
"name": "GNU GPLv3",
|
||||
"url": "https://github.com/goauthentik/authentik/blob/master/LICENSE",
|
||||
@ -176,6 +191,9 @@ REST_FRAMEWORK = {
|
||||
"rest_framework.filters.OrderingFilter",
|
||||
"rest_framework.filters.SearchFilter",
|
||||
],
|
||||
"DEFAULT_PARSER_CLASSES": [
|
||||
"rest_framework.parsers.JSONParser",
|
||||
],
|
||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.DjangoObjectPermissions",),
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||
"authentik.api.authentication.TokenAuthentication",
|
||||
@ -185,6 +203,7 @@ REST_FRAMEWORK = {
|
||||
"rest_framework.renderers.JSONRenderer",
|
||||
],
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
"TEST_REQUEST_DEFAULT_FORMAT": "json",
|
||||
}
|
||||
|
||||
REDIS_PROTOCOL_PREFIX = "redis://"
|
||||
@ -251,7 +270,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
ASGI_APPLICATION = "authentik.root.asgi.application"
|
||||
ASGI_APPLICATION = "authentik.root.asgi.app.application"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
@ -353,7 +372,7 @@ CELERY_RESULT_BACKEND = (
|
||||
# Database backup
|
||||
DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage"
|
||||
DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"}
|
||||
DBBACKUP_FILENAME_TEMPLATE = "authentik-backup-{datetime}.sql"
|
||||
DBBACKUP_FILENAME_TEMPLATE = f"authentik-backup-{__version__}-{{datetime}}.sql"
|
||||
DBBACKUP_CONNECTOR_MAPPING = {
|
||||
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
|
||||
}
|
||||
@ -368,6 +387,7 @@ if CONFIG.y("postgresql.s3_backup"):
|
||||
"default_acl": "private",
|
||||
"endpoint_url": CONFIG.y("postgresql.s3_backup.host"),
|
||||
"location": CONFIG.y("postgresql.s3_backup.location", ""),
|
||||
"verify": not CONFIG.y_bool("postgresql.s3_backup.insecure_skip_verify", False),
|
||||
}
|
||||
j_print(
|
||||
"Database backup to S3 is configured",
|
||||
|
@ -2,17 +2,13 @@
|
||||
from base64 import b64encode
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import Client, TestCase
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class TestRoot(TestCase):
|
||||
"""Test root application"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client = Client()
|
||||
|
||||
def test_monitoring_error(self):
|
||||
"""Test monitoring without any credentials"""
|
||||
response = self.client.get(reverse("metrics"))
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user