Compare commits
220 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
bdd5e16db1 | |||
d4672bfe79 | |||
abd9fab41a | |||
7c8bf42ef9 | |||
274b555912 | |||
916530f0d8 | |||
95efd47f65 | |||
90ecb1af7f | |||
d7fdca1b44 | |||
37346763dc | |||
c35fd2755f | |||
281e3a0518 | |||
6349cdad2f | |||
ef341dd405 | |||
198e5ce642 | |||
923fbac5b0 | |||
5f28c7ace7 | |||
d96c96006f | |||
3ddf2d6f85 | |||
ba6849f29c | |||
942170f902 | |||
248f993541 | |||
56d40bddd0 | |||
3a700a449a | |||
a20f552bcf | |||
32331a56eb | |||
d752b7e41c | |||
0b4223c6ca | |||
a3ec5c13f0 | |||
128b582dd6 | |||
e59ede5422 | |||
6d08ba2513 | |||
23444f4df0 | |||
3338f7a401 | |||
b126519275 | |||
71e68b498e | |||
fb267ee223 | |||
8e59b06611 | |||
a4b3519428 | |||
4895fc3bbb | |||
3daabd6fa8 | |||
9fccb14065 | |||
12efe94fd1 | |||
375ef27b9f | |||
9a7fa39de4 | |||
c779ad2e3b | |||
7e7ef289ba | |||
223d9ad414 | |||
948ea7b087 | |||
bf771f8b6c | |||
6dc8aa396c | |||
92a48f9dc6 | |||
d0ad9fcb1f | |||
539e6deca5 | |||
df4c8003b8 | |||
169e748a78 | |||
39b365c6ae | |||
9a79bab43d | |||
e229eda96e | |||
4448145aa9 | |||
3d042e708a | |||
2428d5f1c2 | |||
f1dc2b4d2a | |||
7dfbcdbb81 | |||
5fd4f56fa2 | |||
b9d5ba6b0a | |||
2a4cb07ba8 | |||
7939286176 | |||
46ef49b897 | |||
b923d85f6a | |||
2862b4ecfb | |||
094acc62f0 | |||
13d17dc729 | |||
5cf3a13ca8 | |||
d0898a3869 | |||
7158c9d2ea | |||
c5cf17b60b | |||
da58796768 | |||
d98499a3fa | |||
e5944567e8 | |||
d296c12d01 | |||
4c3a9e69f2 | |||
eb2540a3c8 | |||
bf9a3615d9 | |||
33fb22e3e7 | |||
f3ff398a44 | |||
533eb59a04 | |||
8ca29f6d49 | |||
0a33d38adf | |||
f7afb60c1f | |||
b9c605bf1a | |||
2983adc719 | |||
502393ee56 | |||
121bba1d9f | |||
3c1b70c355 | |||
27508dd1f0 | |||
6d962dbdf3 | |||
9194e6368a | |||
917fb7d626 | |||
3cf5794b96 | |||
631b0a1819 | |||
6662dcc4b0 | |||
95db54b819 | |||
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 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.8.3
|
||||
current_version = 2021.9.1-rc1
|
||||
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]
|
||||
|
||||
|
307
.github/workflows/ci-main.yml
vendored
Normal file
307
.github/workflows/ci-main.yml
vendored
Normal file
@ -0,0 +1,307 @@
|
||||
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: prepare variables
|
||||
id: ev
|
||||
run: |
|
||||
python ./scripts/gh_do_set_branch.py
|
||||
- name: checkout current code
|
||||
run: |
|
||||
set -x
|
||||
git fetch
|
||||
git checkout ${{ steps.ev.outputs.branchName }}
|
||||
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 }}-${{ steps.ev.outputs.sha }}
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
69
.github/workflows/ci-outpost.yml
vendored
Normal file
69
.github/workflows/ci-outpost.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
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: 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 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
|
||||
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.3,
|
||||
beryju/authentik:2021.9.1-rc1,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.8.3,
|
||||
ghcr.io/goauthentik/server:2021.9.1-rc1,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.8.3', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.9.1-rc1', '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.3,
|
||||
beryju/authentik-proxy:2021.9.1-rc1,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.8.3,
|
||||
ghcr.io/goauthentik/proxy:2021.9.1-rc1,
|
||||
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.3', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.9.1-rc1', '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.3,
|
||||
beryju/authentik-ldap:2021.9.1-rc1,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.8.3,
|
||||
ghcr.io/goauthentik/ldap:2021.9.1-rc1,
|
||||
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.3', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.9.1-rc1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
@ -157,9 +157,9 @@ 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
|
||||
@ -175,7 +175,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.8.3
|
||||
version: authentik@2021.9.1-rc1
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: '~/static/dist'
|
2
.github/workflows/web-api-publish.yml
vendored
2
.github/workflows/web-api-publish.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
# Setup .npmrc file to publish to npm
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version: '16'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
- name: Generate API Client
|
||||
run: make gen-web
|
||||
|
27
Dockerfile
27
Dockerfile
@ -18,22 +18,7 @@ COPY ./website /static/
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build-docs-only
|
||||
|
||||
# Stage 3: Generate API Client
|
||||
FROM openapitools/openapi-generator-cli as go-api-builder
|
||||
|
||||
COPY ./schema.yml /local/schema.yml
|
||||
|
||||
RUN docker-entrypoint.sh generate \
|
||||
--git-host goauthentik.io \
|
||||
--git-repo-id outpost \
|
||||
--git-user-id api \
|
||||
-i /local/schema.yml \
|
||||
-g go \
|
||||
-o /local/api \
|
||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true && \
|
||||
rm -f /local/api/go.mod /local/api/go.sum
|
||||
|
||||
# Stage 4: Build webui
|
||||
# Stage 3: Build webui
|
||||
FROM node as web-builder
|
||||
|
||||
COPY ./web /static/
|
||||
@ -41,8 +26,8 @@ COPY ./web /static/
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build
|
||||
|
||||
# Stage 5: Build go proxy
|
||||
FROM golang:1.17.0 AS builder
|
||||
# Stage 4: Build go proxy
|
||||
FROM golang:1.17.1 AS builder
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
@ -52,7 +37,6 @@ COPY --from=web-builder /static/dist/ /work/web/dist/
|
||||
COPY --from=web-builder /static/authentik/ /work/web/authentik/
|
||||
COPY --from=website-builder /static/help/ /work/website/help/
|
||||
|
||||
COPY --from=go-api-builder /local/api api
|
||||
COPY ./cmd /work/cmd
|
||||
COPY ./web/static.go /work/web/static.go
|
||||
COPY ./website/static.go /work/website/static.go
|
||||
@ -62,7 +46,7 @@ COPY ./go.sum /work/go.sum
|
||||
|
||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||
|
||||
# Stage 6: Run
|
||||
# Stage 5: Run
|
||||
FROM python:3.9-slim-buster
|
||||
|
||||
WORKDIR /
|
||||
@ -97,6 +81,7 @@ COPY --from=builder /work/authentik /authentik-proxy
|
||||
|
||||
USER authentik
|
||||
ENV TMPDIR /dev/shm/
|
||||
ENV PYTHONUBUFFERED 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
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" ]
|
||||
|
8
Makefile
8
Makefile
@ -7,8 +7,6 @@ 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:
|
||||
@ -61,13 +59,13 @@ 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
|
||||
gen: gen-build gen-clean gen-web
|
||||
|
||||
migrate:
|
||||
python -m lifecycle.migrate
|
||||
|
||||
run:
|
||||
go run -v cmd/server/main.go
|
||||
WORKERS=1 go run -v cmd/server/main.go
|
||||
|
264
Pipfile.lock
generated
264
Pipfile.lock
generated
@ -122,19 +122,19 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:542336dda9a728c250cf24aea6d87454136d9d6f3d8a84ec5a737a7edba3b932",
|
||||
"sha256:9bf2a281a6df9f8948d3d322d532d03a1039f57a049a1aa2b72b4a28c9627013"
|
||||
"sha256:63b9846c26e0905f4e9e39d6b59f152330c53a926d693439161c43dcf9779365",
|
||||
"sha256:a9232185d8e7e2fd2b166c0ebee5d7b1f787fdb3093f33bbf5aa932c08f0ccac"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.18.30"
|
||||
"version": "==1.18.42"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:26ab09126dd05c968fbbcb894a1d623355e6119ff6d4a2bf5d292e3ad7cdd628",
|
||||
"sha256:9b0b3dbc144178e2b803097abcc95712a03b8dde5a02e4335ac870bc6c129dd9"
|
||||
"sha256:0952d1200968365b440045efe8e45bbae38cf603fee12bcfc3d7b5f963cbfa18",
|
||||
"sha256:6de4fec4ee10987e4dea96f289553c2f45109fcaafcb74a5baee1221926e1306"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.21.30"
|
||||
"version": "==1.21.42"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -254,11 +254,11 @@
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
|
||||
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
||||
"sha256:7098e7e862f6370a2a8d1a6398cd359815c45d12626267652c3f13dec58e2367",
|
||||
"sha256:fa471a601dfea0f492e4f4fca035cd82155e65dc45c9b83bf4322dfab63755dd"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.4"
|
||||
"version": "==2.0.5"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
@ -359,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",
|
||||
@ -395,11 +395,11 @@
|
||||
},
|
||||
"django-otp": {
|
||||
"hashes": [
|
||||
"sha256:01b5888f0bde5125e139433aacb947e52d5c406fa56c9db43c3e8d75b5c323c4",
|
||||
"sha256:0d56dd2a7fbb6ee6e54557e036ca64add0bd3596f471794bad673b7637d5e935"
|
||||
"sha256:0c03a471db9e876f3671314bc9a65bd56a5c3c108ee0562c473701310bba4a77",
|
||||
"sha256:4c90cdaed683d736b0efafc034a3c6b410e1be2a53c24da287165b1f371d8776"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.6"
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"django-prometheus": {
|
||||
"hashes": [
|
||||
@ -443,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:5b1c27de127c86564be5a967a6fa195cfe161b552d98364282ae9e6ed3d75a85",
|
||||
"sha256:8588706c27f44adfbb3405bae9ef9cd6506f4b59d4cbd66c59780dce035602d9"
|
||||
"sha256:47ef6ec8ff48ac8aede6ec12450a55fee381cf84de969ef1724dcde5a93de6b8",
|
||||
"sha256:d746b936cb4cddec380ea95bf91de6a6721777dfc42e0eea53b83c61a625e94e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.18.0"
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"duo-client": {
|
||||
"hashes": [
|
||||
@ -490,11 +490,11 @@
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:c012c8be7c442c8309ca8fa0876fef33f5fd977c467be1e1c1c2f721e8ebd73c",
|
||||
"sha256:ea1af050b3e06eb73e4470f704d23007307bc0e87c13e015f6b90460f1407bd3"
|
||||
"sha256:7ae5eda089d393ca01658b550df24913cbbbdd34e9e6dedc1cea747485ae0c04",
|
||||
"sha256:bde03220ed56e4e147dec92339c90ce95159dce657e2cccd0ac1fe82f6a96284"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.0.1"
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@ -1108,11 +1108,11 @@
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||
"sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae",
|
||||
"sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.1"
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"structlog": {
|
||||
"hashes": [
|
||||
@ -1151,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": [
|
||||
@ -1256,58 +1256,50 @@
|
||||
},
|
||||
"websockets": {
|
||||
"hashes": [
|
||||
"sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc",
|
||||
"sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e",
|
||||
"sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135",
|
||||
"sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02",
|
||||
"sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3",
|
||||
"sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf",
|
||||
"sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b",
|
||||
"sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2",
|
||||
"sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af",
|
||||
"sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d",
|
||||
"sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880",
|
||||
"sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077",
|
||||
"sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f",
|
||||
"sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec",
|
||||
"sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25",
|
||||
"sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0",
|
||||
"sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe",
|
||||
"sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a",
|
||||
"sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb",
|
||||
"sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d",
|
||||
"sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857",
|
||||
"sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c",
|
||||
"sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0",
|
||||
"sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40",
|
||||
"sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4",
|
||||
"sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20",
|
||||
"sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314",
|
||||
"sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da",
|
||||
"sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58",
|
||||
"sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2",
|
||||
"sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd",
|
||||
"sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a",
|
||||
"sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd"
|
||||
"sha256:01db0ecd1a0ca6702d02a5ed40413e18b7d22f94afb3bbe0d323bac86c42c1c8",
|
||||
"sha256:085bb8a6e780d30eaa1ba48ac7f3a6707f925edea787cfb761ce5a39e77ac09b",
|
||||
"sha256:1ac35426fe3e7d3d0fac3d63c8965c76ed67a8fd713937be072bf0ce22808539",
|
||||
"sha256:1f6b814cff6aadc4288297cb3a248614829c6e4ff5556593c44a115e9dd49939",
|
||||
"sha256:2a43072e434c041a99f2e1eb9b692df0232a38c37c61d00e9f24db79474329e4",
|
||||
"sha256:5b2600e01c7ca6f840c42c747ffbe0254f319594ed108db847eb3d75f4aacb80",
|
||||
"sha256:62160772314920397f9d219147f958b33fa27a12c662d4455c9ccbba9a07e474",
|
||||
"sha256:706e200fc7f03bed99ad0574cd1ea8b0951477dd18cc978ccb190683c69dba76",
|
||||
"sha256:71358c7816e2762f3e4af3adf0040f268e219f5a38cb3487a9d0fc2e554fef6a",
|
||||
"sha256:7d2e12e4f901f1bc062dfdf91831712c4106ed18a9a4cdb65e2e5f502124ca37",
|
||||
"sha256:7f79f02c7f9a8320aff7d3321cd1c7e3a7dbc15d922ac996cca827301ee75238",
|
||||
"sha256:82b17524b1ce6ae7f7dd93e4d18e9b9474071e28b65dbf1dfe9b5767778db379",
|
||||
"sha256:82bd921885231f4a30d9bc550552495b3fc36b1235add6d374e7c65c3babd805",
|
||||
"sha256:8bbf8660c3f833ddc8b1afab90213f2e672a9ddac6eecb3cde968e6b2807c1c7",
|
||||
"sha256:9a4d889162bd48588e80950e07fa5e039eee9deb76a58092e8c3ece96d7ef537",
|
||||
"sha256:b4ade7569b6fd17912452f9c3757d96f8e4044016b6d22b3b8391e641ca50456",
|
||||
"sha256:b8176deb6be540a46695960a765a77c28ac8b2e3ef2ec95d50a4f5df901edb1c",
|
||||
"sha256:c4fc9a1d242317892590abe5b61a9127f1a61740477bfb121743f290b8054002",
|
||||
"sha256:c5880442f5fc268f1ef6d37b2c152c114deccca73f48e3a8c48004d2f16f4567",
|
||||
"sha256:cd8c6f2ec24aedace251017bc7a414525171d4e6578f914acab9349362def4da",
|
||||
"sha256:d67646ddd17a86117ae21c27005d83c1895c0cef5d7be548b7549646372f868a",
|
||||
"sha256:e42a1f1e03437b017af341e9bbfdc09252cd48ef32a8c3c3ead769eab3b17368",
|
||||
"sha256:eb282127e9c136f860c6068a4fba5756eb25e755baffb5940b6f1eae071928b2",
|
||||
"sha256:fe83b3ec9ef34063d86dfe1029160a85f24a5a94271036e5714a57acfdd089a1",
|
||||
"sha256:ff59c6bdb87b31f7e2d596f09353d5a38c8c8ff571b0e2238e8ee2d55ad68465"
|
||||
],
|
||||
"version": "==9.1"
|
||||
"version": "==10.0"
|
||||
},
|
||||
"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": [
|
||||
@ -1420,11 +1412,11 @@
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:b6c2d75cd7c2982d09e7d41d70213e863b3ba34d3bd4014e08f167cee966e99e",
|
||||
"sha256:ecc50f9b3803ebf8ea19aa2c6df5622d8a5c31456a53c741d3be044d96ff0948"
|
||||
"sha256:3b680ce0419b8a771aba6190139a3998d14b413852506d99aff8dc2bf65ee67c",
|
||||
"sha256:dc1e8b28427d6bbef6b8842b18765ab58f558c42bb80540bd7648c98412af25e"
|
||||
],
|
||||
"markers": "python_version ~= '3.6'",
|
||||
"version": "==2.7.2"
|
||||
"version": "==2.7.3"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@ -1467,11 +1459,11 @@
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
|
||||
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
||||
"sha256:7098e7e862f6370a2a8d1a6398cd359815c45d12626267652c3f13dec58e2367",
|
||||
"sha256:fa471a601dfea0f492e4f4fca035cd82155e65dc45c9b83bf4322dfab63755dd"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.4"
|
||||
"version": "==2.0.5"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
@ -1582,7 +1574,7 @@
|
||||
"sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899",
|
||||
"sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"
|
||||
],
|
||||
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
|
||||
"markers": "python_version < '4' and python_full_version >= '3.6.1'",
|
||||
"version": "==5.9.3"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
@ -1652,19 +1644,19 @@
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c",
|
||||
"sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"
|
||||
"sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f",
|
||||
"sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.2.0"
|
||||
"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": [
|
||||
@ -1707,11 +1699,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
|
||||
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
|
||||
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
|
||||
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.2.4"
|
||||
"version": "==6.2.5"
|
||||
},
|
||||
"pytest-django": {
|
||||
"hashes": [
|
||||
@ -1758,49 +1750,49 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:0696eb934dee723e3292056a2c046ddb1e4dd3887685783a9f4af638e85dee76",
|
||||
"sha256:105122fa63da98d8456d5026bc6ac5a1399fd82fa6bad22c6ea641b1572c9142",
|
||||
"sha256:116c277774f84266044e889501fe79cfd293a8b4336b7a5e89b9f20f1e5a9f21",
|
||||
"sha256:12eaf0bbe568bd62e6cade7937e0bf01a2a4cef49a82f4fd204401e78409e158",
|
||||
"sha256:1401cfa4320691cbd91191ec678735c727dee674d0997b0902a5a38ad482faf5",
|
||||
"sha256:19acdb8831a4e3b03b23369db43178d8fee1f17b99c83af6cd907886f76bd9d4",
|
||||
"sha256:208851a2f8dd31e468f0b5aa6c94433975bd67a107a4e7da3bdda947c9f85e25",
|
||||
"sha256:24d68499a27b2d93831fde4a9b84ea5b19e0ab141425fbc9ab1e5b4dad179df7",
|
||||
"sha256:2778c6cb379d804e429cc8e627392909e60db5152b42c695c37ae5757aae50ae",
|
||||
"sha256:2a0a5e323cf86760784ce2b91d8ab5ea09d0865d6ef4da0151e03d15d097b24e",
|
||||
"sha256:2d9cbe0c755ab8b6f583169c0783f7278fc6b195e423b09c5a8da6f858025e96",
|
||||
"sha256:2de1429e4eeab799c168a4f6e6eecdf30fcaa389bba4039cc8a065d6b7aad647",
|
||||
"sha256:32753eda8d413ce4f208cfe01dd61171a78068a6f5d5f38ccd751e00585cdf1d",
|
||||
"sha256:3ee8ad16a35c45a5bab098e39020ecb6fec3b0e700a9d88983d35cbabcee79c8",
|
||||
"sha256:4f03fc0a25122cdcbf39136510d4ea7627f732206892db522adf510bc03b8c67",
|
||||
"sha256:4f3e36086d6631ceaf468503f96a3be0d247caef0660c9452fb1b0c055783851",
|
||||
"sha256:503c1ba0920a46a1844363725215ef44d59fcac2bd2c03ae3c59aa9d08d29bd6",
|
||||
"sha256:507861cf3d97a86fbe26ea6cc04660ae028b9e4080b8290e28b99547b4e15d89",
|
||||
"sha256:56ae6e3cf0506ec0c40b466e31f41ee7a7149a2b505ae0ee50edd9043b423d27",
|
||||
"sha256:6530b7b9505123cdea40a2301225183ca65f389bc6129f0c225b9b41680268d8",
|
||||
"sha256:6729914dd73483cd1c8aaace3ac082436fc98b0072743ac136eaea0b3811d42f",
|
||||
"sha256:7406dd2e44c7cfb4680c0a45a03264381802c67890cf506c147288f04c67177d",
|
||||
"sha256:7684016b73938ca12d160d2907d141f06b7597bd17d854e32bb7588be01afa1d",
|
||||
"sha256:7db58ad61f3f6ea393aaf124d774ee0c58806320bc85c06dc9480f5c7219c250",
|
||||
"sha256:83946ca9278b304728b637bc8d8200ab1663a79de85e47724594917aeed0e892",
|
||||
"sha256:84057cfae5676f456b03970eb78b7e182fddc80c2daafd83465a3d6ca9ff8dbf",
|
||||
"sha256:862b6164e9a38b5c495be2c2854e75fd8af12c5be4c61dc9b42d255980d7e907",
|
||||
"sha256:8ddb4f9ce6bb388ecc97b4b3eb37e786f05d7d5815e8822e0d87a3dbd7100649",
|
||||
"sha256:92eb03f47427fea452ff6956d11f5d5a3f22a048c90a0f34fa223e6badab6c85",
|
||||
"sha256:a5f3bc727fea58f21d99c22e6d4fca652dc11dbc2a1e7cfc4838cd53b2e3691f",
|
||||
"sha256:a6180dbf5945b27e9420e1b58c3cacfc79ad5278bdad3ea35109f5680fbe16d1",
|
||||
"sha256:b158f673ae6a6523f13704f70aa7e4ce875f91e379bece4362c89db18db189d5",
|
||||
"sha256:cd45b4542134de63e7b9dd653e0a2d7d47ffed9615e3637c27ca5f6b78ea68bb",
|
||||
"sha256:d2404336fd16788ea757d4218a2580de60adb052d9888031e765320be8884309",
|
||||
"sha256:db888d4fb33a2fd54b57ac55d5015e51fa849f0d8592bd799b4e47f83bd04e00",
|
||||
"sha256:dde0ac721c7c5bfa5f9fc285e811274dec3c392f2c1225f7d07ca98a8187ca84",
|
||||
"sha256:de0d06ccbc06af5bf93bddec10f4f80275c5d74ea6d28b456931f3955f58bc8c",
|
||||
"sha256:e02dad60e3e8442eefd28095e99b2ac98f2b8667167493ac6a2f3aadb5d84a17",
|
||||
"sha256:e960fe211496333b2f7e36badf4c22a919d740386681f79139ee346b403d1ca1",
|
||||
"sha256:e9700c52749cb3e90c98efd72b730c97b7e4962992fca5fbcaf1363be8e3b849",
|
||||
"sha256:ee318974a1fdacba1701bc9e552e9015788d6345416364af6fa987424ff8df53"
|
||||
"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.27"
|
||||
"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.3"
|
||||
__version__ = "2021.9.1-rc1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
@ -6,12 +6,14 @@ from django.core.cache import cache
|
||||
from django.core.validators import URLValidator
|
||||
from packaging.version import parse
|
||||
from prometheus_client import Info
|
||||
from requests import RequestException, get
|
||||
from requests import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -36,12 +38,17 @@ def _set_prom_info():
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def update_latest_version(self: MonitoredTask):
|
||||
"""Update latest version info"""
|
||||
if CONFIG.y_bool("disable_update_check"):
|
||||
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
|
||||
self.set_status(TaskResult(TaskResultStatus.WARNING, messages=["Version check disabled."]))
|
||||
return
|
||||
try:
|
||||
response = get("https://api.github.com/repos/goauthentik/authentik/releases/latest")
|
||||
response = get_http_session().get(
|
||||
"https://version.goauthentik.io/version.json",
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
tag_name = data.get("tag_name")
|
||||
upstream_version = tag_name.split("/")[1]
|
||||
upstream_version = data.get("stable", {}).get("version")
|
||||
cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT)
|
||||
self.set_status(
|
||||
TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"])
|
||||
@ -58,7 +65,7 @@ def update_latest_version(self: MonitoredTask):
|
||||
).exists():
|
||||
return
|
||||
event_dict = {"new_version": upstream_version}
|
||||
if match := re.search(URL_FINDER, data.get("body", "")):
|
||||
if match := re.search(URL_FINDER, data.get("stable", {}).get("changelog", "")):
|
||||
event_dict["message"] = f"Changelog: {match.group()}"
|
||||
Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save()
|
||||
except (RequestException, IndexError) as exc:
|
||||
|
@ -1,81 +1,58 @@
|
||||
"""test admin tasks"""
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from requests.exceptions import RequestException
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockResponse:
|
||||
"""Mock class to emulate the methods of requests's Response we need"""
|
||||
|
||||
status_code: int
|
||||
response: str
|
||||
|
||||
def json(self) -> dict:
|
||||
"""Get json parsed response"""
|
||||
return json.loads(self.response)
|
||||
|
||||
def raise_for_status(self):
|
||||
"""raise RequestException if status code is 400 or more"""
|
||||
if self.status_code >= 400:
|
||||
raise RequestException
|
||||
|
||||
|
||||
REQUEST_MOCK_VALID = Mock(
|
||||
return_value=MockResponse(
|
||||
200,
|
||||
"""{
|
||||
"tag_name": "version/99999999.9999999",
|
||||
"body": "https://goauthentik.io/test"
|
||||
}""",
|
||||
)
|
||||
)
|
||||
|
||||
REQUEST_MOCK_INVALID = Mock(return_value=MockResponse(400, "{}"))
|
||||
RESPONSE_VALID = {
|
||||
"$schema": "https://version.goauthentik.io/schema.json",
|
||||
"stable": {
|
||||
"version": "99999999.9999999",
|
||||
"changelog": "See https://goauthentik.io/test",
|
||||
"reason": "bugfix",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestAdminTasks(TestCase):
|
||||
"""test admin tasks"""
|
||||
|
||||
@patch("authentik.admin.tasks.get", REQUEST_MOCK_VALID)
|
||||
def test_version_valid_response(self):
|
||||
"""Test Update checker with valid response"""
|
||||
update_latest_version.delay().get()
|
||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999")
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE,
|
||||
context__new_version="99999999.9999999",
|
||||
context__message="Changelog: https://goauthentik.io/test",
|
||||
).exists()
|
||||
)
|
||||
# test that a consecutive check doesn't create a duplicate event
|
||||
update_latest_version.delay().get()
|
||||
self.assertEqual(
|
||||
len(
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://version.goauthentik.io/version.json", json=RESPONSE_VALID)
|
||||
update_latest_version.delay().get()
|
||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999")
|
||||
self.assertTrue(
|
||||
Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE,
|
||||
context__new_version="99999999.9999999",
|
||||
context__message="Changelog: https://goauthentik.io/test",
|
||||
)
|
||||
),
|
||||
1,
|
||||
)
|
||||
).exists()
|
||||
)
|
||||
# test that a consecutive check doesn't create a duplicate event
|
||||
update_latest_version.delay().get()
|
||||
self.assertEqual(
|
||||
len(
|
||||
Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE,
|
||||
context__new_version="99999999.9999999",
|
||||
context__message="Changelog: https://goauthentik.io/test",
|
||||
)
|
||||
),
|
||||
1,
|
||||
)
|
||||
|
||||
@patch("authentik.admin.tasks.get", REQUEST_MOCK_INVALID)
|
||||
def test_version_error(self):
|
||||
"""Test Update checker with invalid response"""
|
||||
update_latest_version.delay().get()
|
||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
|
||||
self.assertFalse(
|
||||
Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
||||
).exists()
|
||||
)
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://version.goauthentik.io/version.json", status_code=400)
|
||||
update_latest_version.delay().get()
|
||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
|
||||
self.assertFalse(
|
||||
Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
||||
).exists()
|
||||
)
|
||||
|
@ -31,7 +31,7 @@ VALIDATION_ERROR = build_object_type(
|
||||
"non_field_errors": build_array_type(build_standard_type(OpenApiTypes.STR)),
|
||||
"code": build_standard_type(OpenApiTypes.STR),
|
||||
},
|
||||
required=["detail"],
|
||||
required=[],
|
||||
additionalProperties={},
|
||||
)
|
||||
|
||||
|
19
authentik/api/tasks.py
Normal file
19
authentik/api/tasks.py
Normal file
@ -0,0 +1,19 @@
|
||||
"""API tasks"""
|
||||
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
SENTRY_SESSION = get_http_session()
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def sentry_proxy(payload: str):
|
||||
"""Relay data to sentry"""
|
||||
SENTRY_SESSION.post(
|
||||
"https://sentry.beryju.org/api/8/envelope/",
|
||||
data=payload,
|
||||
headers={
|
||||
"Content-Type": "application/octet-stream",
|
||||
},
|
||||
timeout=10,
|
||||
)
|
@ -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)),
|
||||
]
|
||||
|
@ -1,38 +0,0 @@
|
||||
"""Sentry tunnel"""
|
||||
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 authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class SentryTunnelView(View):
|
||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
||||
|
||||
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
|
||||
if not CONFIG.y_bool("error_reporting.enabled", False):
|
||||
return HttpResponse(status=400)
|
||||
# Body is 2 json objects separated by \n
|
||||
full_body = request.body
|
||||
header = loads(full_body.splitlines()[0])
|
||||
# Check that the DSN is what we expect
|
||||
dsn = header.get("dsn", "")
|
||||
if dsn != settings.SENTRY_DSN:
|
||||
return HttpResponse(status=400)
|
||||
response = post(
|
||||
"https://sentry.beryju.org/api/8/envelope/",
|
||||
data=full_body,
|
||||
headers={"Content-Type": "application/octet-stream"},
|
||||
)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except RequestException:
|
||||
return HttpResponse(status=500)
|
||||
return HttpResponse(status=response.status_code)
|
60
authentik/api/v3/sentry.py
Normal file
60
authentik/api/v3/sentry.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Sentry tunnel"""
|
||||
from json import loads
|
||||
|
||||
from django.conf import settings
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
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.api.tasks import sentry_proxy
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
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
|
||||
if not CONFIG.y_bool("error_reporting.enabled", False):
|
||||
return HttpResponse(status=400)
|
||||
# Body is 2 json objects separated by \n
|
||||
full_body = request.body
|
||||
lines = full_body.splitlines()
|
||||
if len(lines) < 1:
|
||||
return HttpResponse(status=400)
|
||||
header = loads(lines[0])
|
||||
# Check that the DSN is what we expect
|
||||
dsn = header.get("dsn", "")
|
||||
if dsn != settings.SENTRY_DSN:
|
||||
return HttpResponse(status=400)
|
||||
sentry_proxy.delay(full_body.decode())
|
||||
return HttpResponse(status=204)
|
@ -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
|
||||
@ -24,6 +24,7 @@ from authentik.core.api.users import UserViewSet
|
||||
from authentik.crypto.api import CertificateKeyPairViewSet
|
||||
from authentik.events.api.event import EventViewSet
|
||||
from authentik.events.api.notification import NotificationViewSet
|
||||
from authentik.events.api.notification_mapping import NotificationWebhookMappingViewSet
|
||||
from authentik.events.api.notification_rule import NotificationRuleViewSet
|
||||
from authentik.events.api.notification_transport import NotificationTransportViewSet
|
||||
from authentik.flows.api.bindings import FlowStageBindingViewSet
|
||||
@ -159,6 +160,7 @@ router.register("propertymappings/all", PropertyMappingViewSet)
|
||||
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||
router.register("propertymappings/scope", ScopeMappingViewSet)
|
||||
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
||||
|
||||
router.register("authenticators/duo", DuoDeviceViewSet)
|
||||
router.register("authenticators/static", StaticDeviceViewSet)
|
||||
@ -225,7 +227,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"),
|
||||
]
|
||||
)
|
@ -67,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",
|
||||
|
@ -2,7 +2,7 @@
|
||||
from django.db.models.query import QuerySet
|
||||
from django_filters.filters import ModelMultipleChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from rest_framework.fields import BooleanField, CharField, JSONField
|
||||
from rest_framework.fields import CharField, JSONField
|
||||
from rest_framework.serializers import ListSerializer, ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||
@ -15,7 +15,6 @@ from authentik.core.models import Group, User
|
||||
class GroupMemberSerializer(ModelSerializer):
|
||||
"""Stripped down user serializer to show relevant users for groups"""
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
avatar = CharField(read_only=True)
|
||||
attributes = JSONField(validators=[is_dict], required=False)
|
||||
uid = CharField(read_only=True)
|
||||
@ -29,7 +28,6 @@ class GroupMemberSerializer(ModelSerializer):
|
||||
"name",
|
||||
"is_active",
|
||||
"last_login",
|
||||
"is_superuser",
|
||||
"email",
|
||||
"avatar",
|
||||
"attributes",
|
||||
@ -81,7 +79,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
|
||||
|
@ -23,7 +23,7 @@ from authentik.managed.api import ManagedSerializer
|
||||
class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||
"""Token Serializer"""
|
||||
|
||||
user_obj = 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."""
|
||||
|
@ -28,3 +28,7 @@ class PostUserEnrollmentStage(StageView):
|
||||
source=connection.source,
|
||||
).from_http(self.request)
|
||||
return self.executor.stage_ok()
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Wrapper for post requests"""
|
||||
return self.get(request)
|
||||
|
@ -8,15 +8,15 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}">
|
||||
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}">
|
||||
{% block head_before %}
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
||||
<script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||
<script src="{% static 'dist/poly.js' %}" type="module"></script>
|
||||
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
|
@ -4,7 +4,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/AdminInterface.js' %}?v={{ ak_version }}" type="module"></script>
|
||||
<script src="{% static 'dist/AdminInterface.js' %}" type="module"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
@ -10,7 +10,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script>
|
||||
<script src="{% static 'dist/FlowInterface.js' %}" type="module"></script>
|
||||
<style>
|
||||
.pf-c-background-image::before {
|
||||
--ak-flow-background: url("{{ flow.background_url }}");
|
||||
|
@ -4,7 +4,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_before %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
|
@ -78,9 +78,7 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
||||
@property
|
||||
def kid(self):
|
||||
"""Get Key ID used for JWKS"""
|
||||
return "{0}".format(
|
||||
md5(self.key_data.encode("utf-8")).hexdigest() if self.key_data else "" # nosec
|
||||
)
|
||||
return md5(self.key_data.encode("utf-8")).hexdigest() if self.key_data else "" # nosec
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Certificate-Key Pair {self.name}"
|
||||
|
@ -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)
|
||||
|
28
authentik/events/api/notification_mapping.py
Normal file
28
authentik/events/api/notification_mapping.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""NotificationWebhookMapping API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.events.models import NotificationWebhookMapping
|
||||
|
||||
|
||||
class NotificationWebhookMappingSerializer(ModelSerializer):
|
||||
"""NotificationWebhookMapping Serializer"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = NotificationWebhookMapping
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"expression",
|
||||
]
|
||||
|
||||
|
||||
class NotificationWebhookMappingViewSet(UsedByMixin, ModelViewSet):
|
||||
"""NotificationWebhookMapping Viewset"""
|
||||
|
||||
queryset = NotificationWebhookMapping.objects.all()
|
||||
serializer_class = NotificationWebhookMappingSerializer
|
||||
filterset_fields = ["name"]
|
||||
ordering = ["name"]
|
@ -38,6 +38,7 @@ class NotificationTransportSerializer(ModelSerializer):
|
||||
"mode",
|
||||
"mode_verbose",
|
||||
"webhook_url",
|
||||
"webhook_mapping",
|
||||
"send_once",
|
||||
]
|
||||
|
||||
|
@ -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
|
||||
|
46
authentik/events/migrations/0018_auto_20210911_2217.py
Normal file
46
authentik/events/migrations/0018_auto_20210911_2217.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-11 22:17
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0028_alter_token_intent"),
|
||||
("authentik_events", "0017_alter_event_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="NotificationWebhookMapping",
|
||||
fields=[
|
||||
(
|
||||
"propertymapping_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.propertymapping",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Notification Webhook Mapping",
|
||||
"verbose_name_plural": "Notification Webhook Mappings",
|
||||
},
|
||||
bases=("authentik_core.propertymapping",),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="notificationtransport",
|
||||
name="webhook_mapping",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="authentik_events.notificationwebhookmapping",
|
||||
),
|
||||
),
|
||||
]
|
@ -2,25 +2,25 @@
|
||||
from datetime import timedelta
|
||||
from inspect import getmodule, stack
|
||||
from smtplib import SMTPException
|
||||
from typing import Optional, Union
|
||||
from typing import TYPE_CHECKING, Optional, Type, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.http.request import QueryDict
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from prometheus_client import Gauge
|
||||
from requests import RequestException, post
|
||||
from requests import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
|
||||
from authentik.core.models import ExpiringModel, Group, User
|
||||
from authentik.core.models import ExpiringModel, Group, PropertyMapping, User
|
||||
from authentik.events.geo import GEOIP_READER
|
||||
from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.lib.utils.http import get_client_ip, get_http_session
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
@ -28,11 +28,8 @@ 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"],
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
|
||||
def default_event_duration():
|
||||
@ -143,8 +140,9 @@ class Event(ExpiringModel):
|
||||
`user` arguments optionally overrides user from requests."""
|
||||
if request:
|
||||
self.context["http_request"] = {
|
||||
"path": request.get_full_path(),
|
||||
"path": request.path,
|
||||
"method": request.method,
|
||||
"args": QueryDict(request.META.get("QUERY_STRING", "")),
|
||||
}
|
||||
if hasattr(request, "tenant"):
|
||||
tenant: Tenant = request.tenant
|
||||
@ -182,14 +180,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 +190,6 @@ class Event(ExpiringModel):
|
||||
user=self.user,
|
||||
)
|
||||
super().save(*args, **kwargs)
|
||||
self._set_prom_metrics()
|
||||
|
||||
@property
|
||||
def summary(self) -> str:
|
||||
@ -235,6 +224,9 @@ class NotificationTransport(models.Model):
|
||||
mode = models.TextField(choices=TransportMode.choices)
|
||||
|
||||
webhook_url = models.TextField(blank=True)
|
||||
webhook_mapping = models.ForeignKey(
|
||||
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
|
||||
)
|
||||
send_once = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
@ -254,15 +246,22 @@ class NotificationTransport(models.Model):
|
||||
|
||||
def send_webhook(self, notification: "Notification") -> list[str]:
|
||||
"""Send notification to generic webhook"""
|
||||
default_body = {
|
||||
"body": notification.body,
|
||||
"severity": notification.severity,
|
||||
"user_email": notification.user.email,
|
||||
"user_username": notification.user.username,
|
||||
}
|
||||
if self.webhook_mapping:
|
||||
default_body = self.webhook_mapping.evaluate(
|
||||
user=notification.user,
|
||||
request=None,
|
||||
notification=notification,
|
||||
)
|
||||
try:
|
||||
response = post(
|
||||
response = get_http_session().post(
|
||||
self.webhook_url,
|
||||
json={
|
||||
"body": notification.body,
|
||||
"severity": notification.severity,
|
||||
"user_email": notification.user.email,
|
||||
"user_username": notification.user.username,
|
||||
},
|
||||
json=default_body,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
@ -312,7 +311,7 @@ class NotificationTransport(models.Model):
|
||||
if notification.event:
|
||||
body["attachments"][0]["title"] = notification.event.action
|
||||
try:
|
||||
response = post(self.webhook_url, json=body)
|
||||
response = get_http_session().post(self.webhook_url, json=body)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
text = exc.response.text if exc.response else str(exc)
|
||||
@ -429,3 +428,25 @@ class NotificationRule(PolicyBindingModel):
|
||||
|
||||
verbose_name = _("Notification Rule")
|
||||
verbose_name_plural = _("Notification Rules")
|
||||
|
||||
|
||||
class NotificationWebhookMapping(PropertyMapping):
|
||||
"""Modify the schema and layout of the webhook being sent"""
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-property-mapping-notification-form"
|
||||
|
||||
@property
|
||||
def serializer(self) -> Type["Serializer"]:
|
||||
from authentik.events.api.notification_mapping import NotificationWebhookMappingSerializer
|
||||
|
||||
return NotificationWebhookMappingSerializer
|
||||
|
||||
def __str__(self):
|
||||
return f"Notification Webhook Mapping {self.name}"
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Notification Webhook Mapping")
|
||||
verbose_name_plural = _("Notification Webhook Mappings")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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():
|
||||
|
@ -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."
|
||||
|
31
authentik/flows/tests/test_stage_views.py
Normal file
31
authentik/flows/tests/test_stage_views.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""stage view tests"""
|
||||
from typing import Callable, Type
|
||||
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views import FlowExecutorView
|
||||
from authentik.lib.utils.reflection import all_subclasses
|
||||
|
||||
|
||||
class TestViews(TestCase):
|
||||
"""Generic model properties tests"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.factory = RequestFactory()
|
||||
self.exec = FlowExecutorView(request=self.factory.get("/"))
|
||||
|
||||
|
||||
def view_tester_factory(view_class: Type[StageView]) -> Callable:
|
||||
"""Test a form"""
|
||||
|
||||
def tester(self: TestViews):
|
||||
model_class = view_class(self.exec)
|
||||
self.assertIsNotNone(model_class.post)
|
||||
self.assertIsNotNone(model_class.get)
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
for view in all_subclasses(StageView):
|
||||
setattr(TestViews, f"test_view_{view.__name__}", view_tester_factory(view))
|
@ -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(
|
||||
|
@ -9,7 +9,9 @@ postgresql:
|
||||
web:
|
||||
listen: 0.0.0.0:9000
|
||||
listen_tls: 0.0.0.0:9443
|
||||
listen_metrics: 0.0.0.0:9300
|
||||
load_local_files: false
|
||||
outpost_port_offset: 0
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
@ -54,6 +56,7 @@ outposts:
|
||||
# %(build_hash)s: Build hash if you're running a beta version
|
||||
docker_image_base: "ghcr.io/goauthentik/%(type)s:%(version)s"
|
||||
|
||||
disable_update_check: false
|
||||
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
|
||||
geoip: "./GeoLite2-City.mmdb"
|
||||
|
||||
|
@ -4,13 +4,13 @@ from textwrap import indent
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
from django.core.exceptions import FieldError
|
||||
from requests import Session
|
||||
from rest_framework.serializers import ValidationError
|
||||
from sentry_sdk.hub import Hub
|
||||
from sentry_sdk.tracing import Span
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -35,7 +35,7 @@ class BaseEvaluator:
|
||||
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
||||
"ak_user_by": BaseEvaluator.expr_user_by,
|
||||
"ak_logger": get_logger(),
|
||||
"requests": Session(),
|
||||
"requests": get_http_session(),
|
||||
}
|
||||
self._context = {}
|
||||
self._filename = "BaseEvalautor"
|
||||
|
@ -1,9 +1,13 @@
|
||||
"""http helpers"""
|
||||
from os import environ
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.http import HttpRequest
|
||||
from requests.sessions import Session
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
|
||||
OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP"
|
||||
OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN" # nosec
|
||||
DEFAULT_IP = "255.255.255.255"
|
||||
@ -60,3 +64,16 @@ def get_client_ip(request: Optional[HttpRequest]) -> str:
|
||||
if override:
|
||||
return override
|
||||
return _get_client_ip_from_meta(request.META)
|
||||
|
||||
|
||||
def authentik_user_agent() -> str:
|
||||
"""Get a common user agent"""
|
||||
build = environ.get(ENV_GIT_HASH_KEY, "tagged")
|
||||
return f"authentik@{__version__} (build={build})"
|
||||
|
||||
|
||||
def get_http_session() -> Session:
|
||||
"""Get a requests session with common headers"""
|
||||
session = Session()
|
||||
session.headers["User-Agent"] = authentik_user_agent()
|
||||
return session
|
||||
|
@ -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",
|
||||
|
@ -3,8 +3,8 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
|
||||
|
||||
from authentik.outposts.controllers.base import FIELD_MANAGER, DeploymentPort
|
||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
|
||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsRecreate
|
||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -21,44 +21,13 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||
def reconcile(self, current: V1Service, reference: V1Service):
|
||||
super().reconcile(current, reference)
|
||||
if len(current.spec.ports) != len(reference.spec.ports):
|
||||
raise NeedsUpdate()
|
||||
raise NeedsRecreate()
|
||||
for port in reference.spec.ports:
|
||||
if port not in current.spec.ports:
|
||||
raise NeedsUpdate()
|
||||
|
||||
def get_embedded_reference_object(self) -> V1Service:
|
||||
"""Get Service for embedded outpost"""
|
||||
selector_labels = {
|
||||
"app.kubernetes.io/name": "authentik",
|
||||
"app.kubernetes.io/component": "server",
|
||||
}
|
||||
meta = self.get_object_meta(name=self.name)
|
||||
ports = []
|
||||
for port in [
|
||||
DeploymentPort(9000, "http", "tcp"),
|
||||
DeploymentPort(9443, "https", "tcp"),
|
||||
]:
|
||||
ports.append(
|
||||
V1ServicePort(
|
||||
name=port.name,
|
||||
port=port.port,
|
||||
protocol=port.protocol.upper(),
|
||||
target_port=port.inner_port or port.port,
|
||||
)
|
||||
)
|
||||
return V1Service(
|
||||
metadata=meta,
|
||||
spec=V1ServiceSpec(
|
||||
ports=ports,
|
||||
selector=selector_labels,
|
||||
type=self.controller.outpost.config.kubernetes_service_type,
|
||||
),
|
||||
)
|
||||
raise NeedsRecreate()
|
||||
|
||||
def get_reference_object(self) -> V1Service:
|
||||
"""Get deployment object for outpost"""
|
||||
if self.is_embedded:
|
||||
return self.get_embedded_reference_object()
|
||||
meta = self.get_object_meta(name=self.name)
|
||||
ports = []
|
||||
for port in self.controller.deployment_ports:
|
||||
@ -70,7 +39,13 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||
target_port=port.inner_port or port.port,
|
||||
)
|
||||
)
|
||||
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
|
||||
if self.is_embedded:
|
||||
selector_labels = {
|
||||
"app.kubernetes.io/name": "authentik",
|
||||
"app.kubernetes.io/component": "server",
|
||||
}
|
||||
else:
|
||||
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
|
||||
return V1Service(
|
||||
metadata=meta,
|
||||
spec=V1ServiceSpec(
|
||||
|
150
authentik/outposts/controllers/k8s/service_monitor.py
Normal file
150
authentik/outposts/controllers/k8s/service_monitor.py
Normal file
@ -0,0 +1,150 @@
|
||||
"""Kubernetes Prometheus ServiceMonitor Reconciler"""
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dacite import from_dict
|
||||
from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
|
||||
|
||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrometheusServiceMonitorSpecEndpoint:
|
||||
"""Prometheus ServiceMonitor endpoint spec"""
|
||||
|
||||
port: str
|
||||
path: str = field(default="/metrics")
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrometheusServiceMonitorSpecSelector:
|
||||
"""Prometheus ServiceMonitor selector spec"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
matchLabels: dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrometheusServiceMonitorSpec:
|
||||
"""Prometheus ServiceMonitor spec"""
|
||||
|
||||
endpoints: list[PrometheusServiceMonitorSpecEndpoint]
|
||||
# pylint: disable=invalid-name
|
||||
selector: PrometheusServiceMonitorSpecSelector
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrometheusServiceMonitorMetadata:
|
||||
"""Prometheus ServiceMonitor metadata"""
|
||||
|
||||
name: str
|
||||
namespace: str
|
||||
labels: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PrometheusServiceMonitor:
|
||||
"""Prometheus ServiceMonitor"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
apiVersion: str
|
||||
kind: str
|
||||
metadata: PrometheusServiceMonitorMetadata
|
||||
spec: PrometheusServiceMonitorSpec
|
||||
|
||||
|
||||
CRD_NAME = "servicemonitors.monitoring.coreos.com"
|
||||
CRD_GROUP = "monitoring.coreos.com"
|
||||
CRD_VERSION = "v1"
|
||||
CRD_PLURAL = "servicemonitors"
|
||||
|
||||
|
||||
class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusServiceMonitor]):
|
||||
"""Kubernetes Prometheus ServiceMonitor Reconciler"""
|
||||
|
||||
def __init__(self, controller: "KubernetesController") -> None:
|
||||
super().__init__(controller)
|
||||
self.api_ex = ApiextensionsV1Api(controller.client)
|
||||
self.api = CustomObjectsApi(controller.client)
|
||||
|
||||
@property
|
||||
def noop(self) -> bool:
|
||||
return (not self._crd_exists()) or (self.is_embedded)
|
||||
|
||||
def _crd_exists(self) -> bool:
|
||||
"""Check if the Prometheus ServiceMonitor exists"""
|
||||
return bool(
|
||||
len(
|
||||
self.api_ex.list_custom_resource_definition(
|
||||
field_selector=f"metadata.name={CRD_NAME}"
|
||||
).items
|
||||
)
|
||||
)
|
||||
|
||||
def get_reference_object(self) -> PrometheusServiceMonitor:
|
||||
"""Get service monitor object for outpost"""
|
||||
return PrometheusServiceMonitor(
|
||||
apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
|
||||
kind="ServiceMonitor",
|
||||
metadata=PrometheusServiceMonitorMetadata(
|
||||
name=self.name,
|
||||
namespace=self.namespace,
|
||||
labels=self.get_object_meta().labels,
|
||||
),
|
||||
spec=PrometheusServiceMonitorSpec(
|
||||
endpoints=[
|
||||
PrometheusServiceMonitorSpecEndpoint(
|
||||
port="http-metrics",
|
||||
)
|
||||
],
|
||||
selector=PrometheusServiceMonitorSpecSelector(
|
||||
matchLabels=self.get_object_meta(name=self.name).labels,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def create(self, reference: PrometheusServiceMonitor):
|
||||
return self.api.create_namespaced_custom_object(
|
||||
group=CRD_GROUP,
|
||||
version=CRD_VERSION,
|
||||
plural=CRD_PLURAL,
|
||||
namespace=self.namespace,
|
||||
body=asdict(reference),
|
||||
field_manager=FIELD_MANAGER,
|
||||
)
|
||||
|
||||
def delete(self, reference: PrometheusServiceMonitor):
|
||||
return self.api.delete_namespaced_custom_object(
|
||||
group=CRD_GROUP,
|
||||
version=CRD_VERSION,
|
||||
namespace=self.namespace,
|
||||
plural=CRD_PLURAL,
|
||||
name=self.name,
|
||||
)
|
||||
|
||||
def retrieve(self) -> PrometheusServiceMonitor:
|
||||
return from_dict(
|
||||
PrometheusServiceMonitor,
|
||||
self.api.get_namespaced_custom_object(
|
||||
group=CRD_GROUP,
|
||||
version=CRD_VERSION,
|
||||
namespace=self.namespace,
|
||||
plural=CRD_PLURAL,
|
||||
name=self.name,
|
||||
),
|
||||
)
|
||||
|
||||
def update(self, current: PrometheusServiceMonitor, reference: PrometheusServiceMonitor):
|
||||
return self.api.patch_namespaced_custom_object(
|
||||
group=CRD_GROUP,
|
||||
version=CRD_VERSION,
|
||||
namespace=self.namespace,
|
||||
plural=CRD_PLURAL,
|
||||
name=self.name,
|
||||
body=asdict(reference),
|
||||
field_manager=FIELD_MANAGER,
|
||||
)
|
@ -13,6 +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.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler
|
||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost, ServiceConnectionInvalid
|
||||
|
||||
|
||||
@ -32,8 +33,9 @@ class KubernetesController(BaseController):
|
||||
"secret": SecretReconciler,
|
||||
"deployment": DeploymentReconciler,
|
||||
"service": ServiceReconciler,
|
||||
"prometheus servicemonitor": PrometheusServiceMonitorReconciler,
|
||||
}
|
||||
self.reconcile_order = ["secret", "deployment", "service"]
|
||||
self.reconcile_order = ["secret", "deployment", "service", "prometheus servicemonitor"]
|
||||
|
||||
def up(self):
|
||||
try:
|
||||
|
@ -100,7 +100,7 @@ def outpost_controller(
|
||||
if from_cache:
|
||||
outpost: Outpost = cache.get(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
|
||||
else:
|
||||
outpost: Outpost = Outpost.objects.get(pk=outpost_pk)
|
||||
outpost: Outpost = Outpost.objects.filter(pk=outpost_pk).first()
|
||||
if not outpost:
|
||||
return
|
||||
self.set_uid(slugify(outpost.name))
|
||||
@ -148,10 +148,7 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
||||
return
|
||||
|
||||
if isinstance(instance, Outpost):
|
||||
LOGGER.debug("Ensuring token and permissions for outpost", instance=instance)
|
||||
_ = instance.token
|
||||
_ = instance.user
|
||||
LOGGER.debug("Trigger reconcile for outpost")
|
||||
LOGGER.debug("Trigger reconcile for outpost", instance=instance)
|
||||
outpost_controller.delay(instance.pk)
|
||||
|
||||
if isinstance(instance, (OutpostModel, Outpost)):
|
||||
|
@ -3,10 +3,10 @@ from hashlib import sha1
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from requests import get
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.policies.models import Policy, PolicyResult
|
||||
from authentik.policies.types import PolicyRequest
|
||||
|
||||
@ -49,7 +49,7 @@ class HaveIBeenPwendPolicy(Policy):
|
||||
|
||||
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
|
||||
url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}"
|
||||
result = get(url).text
|
||||
result = get_http_session().get(url).text
|
||||
final_count = 0
|
||||
for line in result.split("\r\n"):
|
||||
full_hash, count = line.split(":")
|
||||
|
@ -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"]
|
||||
|
||||
|
||||
|
@ -12,4 +12,5 @@ class LDAPDockerController(DockerController):
|
||||
self.deployment_ports = [
|
||||
DeploymentPort(389, "ldap", "tcp", 3389),
|
||||
DeploymentPort(636, "ldaps", "tcp", 6636),
|
||||
DeploymentPort(9300, "http-metrics", "tcp", 9300),
|
||||
]
|
||||
|
@ -12,4 +12,5 @@ class LDAPKubernetesController(KubernetesController):
|
||||
self.deployment_ports = [
|
||||
DeploymentPort(389, "ldap", "tcp", 3389),
|
||||
DeploymentPort(636, "ldaps", "tcp", 6636),
|
||||
DeploymentPort(9300, "http-metrics", "tcp", 9300),
|
||||
]
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""OAuth2Provider API Views"""
|
||||
from django_filters.filters import AllValuesMultipleFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||
@ -23,7 +25,7 @@ class ScopeMappingSerializer(PropertyMappingSerializer):
|
||||
class ScopeMappingFilter(FilterSet):
|
||||
"""Filter for ScopeMapping"""
|
||||
|
||||
managed = AllValuesMultipleFilter(field_name="managed")
|
||||
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
|
||||
|
||||
class Meta:
|
||||
model = ScopeMapping
|
||||
|
@ -151,12 +151,13 @@ class AuthorizeError(OAuth2Error):
|
||||
# http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
|
||||
hash_or_question = "#" if self.grant_type == GrantTypes.IMPLICIT else "?"
|
||||
|
||||
uri = "{0}{1}error={2}&error_description={3}".format(
|
||||
self.redirect_uri, hash_or_question, self.error, description
|
||||
uri = (
|
||||
f"{self.redirect_uri}{hash_or_question}error="
|
||||
f"{self.error}&error_description={description}"
|
||||
)
|
||||
|
||||
# Add state if present.
|
||||
uri = uri + ("&state={0}".format(self.state) if self.state else "")
|
||||
uri = uri + (f"&state={self.state}" if self.state else "")
|
||||
|
||||
return uri
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-08 15:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.lib.utils.time
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0016_alter_authorizationcode_nonce"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="oauth2provider",
|
||||
name="token_validity",
|
||||
field=models.TextField(
|
||||
default="days=30",
|
||||
help_text="Tokens not valid on or after current time + this value (Format: hours=1;minutes=2;seconds=3).",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
]
|
@ -182,7 +182,7 @@ class OAuth2Provider(Provider):
|
||||
),
|
||||
)
|
||||
token_validity = models.TextField(
|
||||
default="minutes=10",
|
||||
default="days=30",
|
||||
validators=[timedelta_string_validator],
|
||||
help_text=_(
|
||||
(
|
||||
|
@ -247,7 +247,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
"to": (
|
||||
f"http://localhost#access_token={token.access_token}"
|
||||
f"&id_token={provider.encode(token.id_token.to_dict())}&token_type=bearer"
|
||||
f"&expires_in=600&state={state}"
|
||||
f"&expires_in=60&state={state}"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
@ -141,7 +141,7 @@ class TestToken(OAuthTestCase):
|
||||
"access_token": new_token.access_token,
|
||||
"refresh_token": new_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": 600,
|
||||
"expires_in": 2592000,
|
||||
"id_token": provider.encode(
|
||||
new_token.id_token.to_dict(),
|
||||
),
|
||||
@ -190,7 +190,7 @@ class TestToken(OAuthTestCase):
|
||||
"access_token": new_token.access_token,
|
||||
"refresh_token": new_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": 600,
|
||||
"expires_in": 2592000,
|
||||
"id_token": provider.encode(
|
||||
new_token.id_token.to_dict(),
|
||||
),
|
||||
@ -236,7 +236,7 @@ class TestToken(OAuthTestCase):
|
||||
"access_token": new_token.access_token,
|
||||
"refresh_token": new_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
"expires_in": 600,
|
||||
"expires_in": 2592000,
|
||||
"id_token": provider.encode(
|
||||
new_token.id_token.to_dict(),
|
||||
),
|
||||
|
@ -367,7 +367,7 @@ class OAuthFulfillmentStage(StageView):
|
||||
|
||||
query_fragment["token_type"] = "bearer"
|
||||
query_fragment["expires_in"] = int(
|
||||
timedelta_from_string(self.provider.token_validity).total_seconds()
|
||||
timedelta_from_string(self.provider.access_code_validity).total_seconds()
|
||||
)
|
||||
query_fragment["state"] = self.params.state if self.params.state else ""
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""ProxyProvider API Views"""
|
||||
from typing import Any
|
||||
|
||||
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
@ -72,6 +72,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
||||
"mode",
|
||||
"redirect_uris",
|
||||
"cookie_domain",
|
||||
"token_validity",
|
||||
]
|
||||
|
||||
|
||||
@ -80,11 +81,27 @@ 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"]
|
||||
|
||||
|
||||
@extend_schema_serializer(deprecate_fields=["forward_auth_mode"])
|
||||
class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
"""Proxy provider serializer for outposts"""
|
||||
|
||||
|
@ -13,8 +13,9 @@ class ProxyDockerController(DockerController):
|
||||
def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
|
||||
super().__init__(outpost, connection)
|
||||
self.deployment_ports = [
|
||||
DeploymentPort(4180, "http", "tcp"),
|
||||
DeploymentPort(4443, "https", "tcp"),
|
||||
DeploymentPort(9000, "http", "tcp"),
|
||||
DeploymentPort(9300, "http-metrics", "tcp"),
|
||||
DeploymentPort(9443, "https", "tcp"),
|
||||
]
|
||||
|
||||
def _get_labels(self) -> dict[str, str]:
|
||||
@ -30,5 +31,5 @@ class ProxyDockerController(DockerController):
|
||||
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"
|
||||
labels[f"traefik.http.services.{traefik_name}-service.loadbalancer.server.port"] = "9000"
|
||||
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:
|
||||
|
@ -106,7 +106,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}:9000/akprox/auth/traefik",
|
||||
authResponseHeaders=[
|
||||
"Set-Cookie",
|
||||
"X-Auth-Username",
|
||||
|
@ -12,8 +12,9 @@ class ProxyKubernetesController(KubernetesController):
|
||||
def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
|
||||
super().__init__(outpost, connection)
|
||||
self.deployment_ports = [
|
||||
DeploymentPort(4180, "http", "tcp"),
|
||||
DeploymentPort(4443, "https", "tcp"),
|
||||
DeploymentPort(9000, "http", "tcp"),
|
||||
DeploymentPort(9300, "http-metrics", "tcp"),
|
||||
DeploymentPort(9443, "https", "tcp"),
|
||||
]
|
||||
self.reconcilers["ingress"] = IngressReconciler
|
||||
self.reconcilers["traefik middleware"] = TraefikMiddlewareReconciler
|
||||
|
23
authentik/providers/proxy/migrations/0014_proxy_v2.py
Normal file
23
authentik/providers/proxy/migrations/0014_proxy_v2.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.6 on 2021-09-09 11:24
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def migrate_defaults(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from authentik.providers.proxy.models import JWTAlgorithms, ProxyProvider
|
||||
|
||||
db_alias = schema_editor.connection.alias
|
||||
for provider in ProxyProvider.objects.using(db_alias).filter(jwt_alg=JWTAlgorithms.RS256):
|
||||
provider.set_oauth_defaults()
|
||||
provider.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_proxy", "0013_mode"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(migrate_defaults)]
|
@ -128,8 +128,8 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
def set_oauth_defaults(self):
|
||||
"""Ensure all OAuth2-related settings are correct"""
|
||||
self.client_type = ClientTypes.CONFIDENTIAL
|
||||
self.jwt_alg = JWTAlgorithms.RS256
|
||||
self.rsa_key = CertificateKeyPair.objects.exclude(key_data__iexact="").first()
|
||||
self.jwt_alg = JWTAlgorithms.HS256
|
||||
self.rsa_key = None
|
||||
scopes = ScopeMapping.objects.filter(
|
||||
scope_name__in=[
|
||||
SCOPE_OPENID,
|
||||
@ -139,12 +139,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
||||
]
|
||||
)
|
||||
self.property_mappings.set(scopes)
|
||||
self.redirect_uris = "\n".join(
|
||||
[
|
||||
_get_callback_url(self.external_host),
|
||||
_get_callback_url(self.internal_host),
|
||||
]
|
||||
)
|
||||
self.redirect_uris = _get_callback_url(self.external_host)
|
||||
|
||||
def __str__(self):
|
||||
return f"Proxy Provider {self.name}"
|
||||
|
@ -9,7 +9,12 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django_filters.filters import AllValuesMultipleFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from drf_spectacular.utils import (
|
||||
OpenApiParameter,
|
||||
OpenApiResponse,
|
||||
extend_schema,
|
||||
extend_schema_field,
|
||||
)
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, FileField, SerializerMethodField
|
||||
from rest_framework.parsers import MultiPartParser
|
||||
@ -185,7 +190,7 @@ class SAMLPropertyMappingSerializer(PropertyMappingSerializer):
|
||||
class SAMLPropertyMappingFilter(FilterSet):
|
||||
"""Filter for SAMLPropertyMapping"""
|
||||
|
||||
managed = AllValuesMultipleFilter(field_name="managed")
|
||||
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
|
||||
|
||||
class Meta:
|
||||
model = SAMLPropertyMapping
|
||||
|
@ -13,7 +13,6 @@ 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
|
||||
@ -24,16 +23,14 @@ 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),
|
||||
}
|
||||
)
|
||||
application = ASGILogger(
|
||||
guarantee_single_callable(
|
||||
SentryAsgiMiddleware(
|
||||
ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"websocket": URLRouter(websocket.websocket_urlpatterns),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -1,38 +0,0 @@
|
||||
"""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",
|
||||
}
|
||||
)
|
@ -19,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
|
||||
@ -43,23 +45,27 @@ 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
|
||||
return await self.app(scope, receive, send_hooked)
|
||||
|
||||
def _get_ip(self, scope: Scope) -> str:
|
||||
def _get_ip(self, headers: dict[bytes, bytes], scope: Scope) -> str:
|
||||
client_ip = None
|
||||
headers = dict(scope.get("headers", []))
|
||||
for header in ASGI_IP_HEADERS:
|
||||
if header in headers:
|
||||
client_ip = headers[header].decode()
|
||||
@ -68,9 +74,10 @@ 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)
|
||||
headers = dict(scope.get("headers", []))
|
||||
host = self._get_ip(headers, scope)
|
||||
query_string = ""
|
||||
if scope.get("query_string", b"") != b"":
|
||||
query_string = f"?{scope.get('query_string').decode()}"
|
||||
@ -79,8 +86,9 @@ 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,
|
||||
user_agent=headers.get(b"user-agent", b"").decode(),
|
||||
**kwargs,
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -150,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",
|
||||
@ -180,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",
|
||||
@ -189,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://"
|
||||
@ -357,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",
|
||||
}
|
||||
@ -372,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,6 +2,7 @@
|
||||
from django.conf import settings
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
from tests.e2e.utils import get_docker_tag
|
||||
|
||||
|
||||
class PytestTestRunner: # pragma: no cover
|
||||
@ -17,7 +18,7 @@ class PytestTestRunner: # pragma: no cover
|
||||
CONFIG.y_set("authentik.geoip", "tests/GeoLite2-City-Test.mmdb")
|
||||
CONFIG.y_set(
|
||||
"outposts.docker_image_base",
|
||||
"beryju.org/authentik/outpost-%(type)s:gh-master",
|
||||
f"beryju.org/authentik/outpost-%(type)s:{get_docker_tag()}",
|
||||
)
|
||||
|
||||
def run_tests(self, test_labels):
|
||||
|
@ -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"))
|
||||
|
@ -5,7 +5,8 @@ from django.http.response import Http404
|
||||
from django.utils.text import slugify
|
||||
from django_filters.filters import AllValuesMultipleFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.request import Request
|
||||
@ -122,7 +123,7 @@ class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
|
||||
class LDAPPropertyMappingFilter(FilterSet):
|
||||
"""Filter for LDAPPropertyMapping"""
|
||||
|
||||
managed = AllValuesMultipleFilter(field_name="managed")
|
||||
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
|
||||
|
||||
class Meta:
|
||||
model = LDAPPropertyMapping
|
||||
|
@ -8,8 +8,8 @@ from requests.exceptions import RequestException
|
||||
from requests.models import Response
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -27,10 +27,9 @@ class BaseOAuthClient:
|
||||
|
||||
def __init__(self, source: OAuthSource, request: HttpRequest, callback: Optional[str] = None):
|
||||
self.source = source
|
||||
self.session = Session()
|
||||
self.session = get_http_session()
|
||||
self.request = request
|
||||
self.callback = callback
|
||||
self.session.headers.update({"User-Agent": f"authentik {__version__}"})
|
||||
|
||||
def get_access_token(self, **request_kwargs) -> Optional[dict[str, Any]]:
|
||||
"Fetch access token from callback request."
|
||||
@ -43,7 +42,6 @@ class BaseOAuthClient:
|
||||
profile_url = self.source.profile_url
|
||||
try:
|
||||
response = self.do_request("get", profile_url, token=token)
|
||||
LOGGER.debug(response.text)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning("Unable to fetch user profile", exc=exc)
|
||||
|
@ -65,7 +65,6 @@ class OAuth2Client(BaseOAuthClient):
|
||||
data=args,
|
||||
headers=self._default_headers,
|
||||
)
|
||||
LOGGER.debug(response.text)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning("Unable to fetch access token", exc=exc)
|
||||
@ -114,4 +113,4 @@ class OAuth2Client(BaseOAuthClient):
|
||||
|
||||
@property
|
||||
def session_key(self):
|
||||
return "oauth-client-{0}-request-state".format(self.source.name)
|
||||
return f"oauth-client-{self.source.name}-request-state"
|
||||
|
@ -163,7 +163,7 @@ class AzureADOAuthSource(OAuthSource):
|
||||
verbose_name_plural = _("Azure AD OAuth Sources")
|
||||
|
||||
|
||||
class OpenIDOAuthSource(OAuthSource):
|
||||
class OpenIDConnectOAuthSource(OAuthSource):
|
||||
"""Login using a Generic OpenID-Connect compliant provider."""
|
||||
|
||||
class Meta:
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""Twitter Type tests"""
|
||||
from django.test import Client, TestCase
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
|
||||
@ -92,7 +92,6 @@ class TestTypeGitHub(TestCase):
|
||||
"""OAuth Source tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.source = OAuthSource.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""AzureAD OAuth2 Views"""
|
||||
from typing import Any, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from requests.exceptions import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
@ -36,7 +35,6 @@ class AzureADClient(OAuth2Client):
|
||||
profile_url,
|
||||
headers={"Authorization": f"{token['token_type']} {token['access_token']}"},
|
||||
)
|
||||
LOGGER.debug(response.text)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
LOGGER.warning("Unable to fetch user profile", exc=exc)
|
||||
@ -50,12 +48,6 @@ class AzureADOAuthCallback(OAuthCallback):
|
||||
|
||||
client_class = AzureADClient
|
||||
|
||||
def get_user_id(self, info: dict[str, Any]) -> Optional[str]:
|
||||
try:
|
||||
return str(UUID(info.get("id")).int)
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
def get_user_enroll_context(
|
||||
self,
|
||||
info: dict[str, Any],
|
||||
|
@ -38,5 +38,5 @@ class GoogleType(SourceType):
|
||||
slug = "google"
|
||||
|
||||
authorization_url = "https://accounts.google.com/o/oauth2/auth"
|
||||
access_token_url = "https://accounts.google.com/o/oauth2/token" # nosec
|
||||
access_token_url = "https://oauth2.googleapis.com/token" # nosec
|
||||
profile_url = "https://www.googleapis.com/oauth2/v1/userinfo"
|
||||
|
@ -2,12 +2,12 @@
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.http.response import Http404
|
||||
from requests import Session
|
||||
from requests.exceptions import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.sources.plex.models import PlexSource, PlexSourceConnection
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -24,7 +24,7 @@ class PlexAuth:
|
||||
def __init__(self, source: PlexSource, token: str):
|
||||
self._source = source
|
||||
self._token = token
|
||||
self._session = Session()
|
||||
self._session = get_http_session()
|
||||
self._session.headers.update(
|
||||
{"Accept": "application/json", "Content-Type": "application/json"}
|
||||
)
|
||||
|
@ -1,7 +1,8 @@
|
||||
"""AuthenticatorDuoStage API Views"""
|
||||
from django_filters.rest_framework.backends import DjangoFilterBackend
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework import mixins
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
@ -12,6 +13,7 @@ from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
|
||||
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
@ -71,6 +73,43 @@ class AuthenticatorDuoStageViewSet(UsedByMixin, ModelViewSet):
|
||||
return Response(status=204)
|
||||
return Response(status=420)
|
||||
|
||||
@permission_required(
|
||||
"", ["authentik_stages_authenticator_duo.add_duodevice", "authentik_core.view_user"]
|
||||
)
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="duo_user_id", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="username", type=OpenApiTypes.STR, location=OpenApiParameter.QUERY
|
||||
),
|
||||
],
|
||||
responses={
|
||||
204: OpenApiResponse(description="Enrollment successful"),
|
||||
400: OpenApiResponse(description="Device exists already"),
|
||||
},
|
||||
)
|
||||
@action(methods=["POST"], detail=True)
|
||||
# pylint: disable=invalid-name,unused-argument
|
||||
def import_devices(self, request: Request, pk: str) -> Response:
|
||||
"""Import duo devices into authentik"""
|
||||
stage: AuthenticatorDuoStage = self.get_object()
|
||||
users = get_objects_for_user(request.user, "authentik_core.view_user").filter(
|
||||
username=request.query_params.get("username", "")
|
||||
)
|
||||
if not users.exists():
|
||||
return Response(data={"non_field_errors": ["user does not exist"]}, status=400)
|
||||
devices = DuoDevice.objects.filter(
|
||||
duo_user_id=request.query_params.get("duo_user_id"), user=users.first(), stage=stage
|
||||
)
|
||||
if devices.exists():
|
||||
return Response(data={"non_field_errors": ["device exists already"]}, status=400)
|
||||
DuoDevice.objects.create(
|
||||
duo_user_id=request.query_params.get("duo_user_id"), user=users.first(), stage=stage
|
||||
)
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class DuoDeviceSerializer(ModelSerializer):
|
||||
"""Serializer for Duo authenticator devices"""
|
||||
|
@ -51,20 +51,30 @@ def get_webauthn_challenge(request: HttpRequest, device: WebAuthnDevice) -> dict
|
||||
# for the reasons outlined in the comment in webauthn_begin_activate.
|
||||
request.session["challenge"] = challenge.rstrip("=")
|
||||
|
||||
webauthn_user = WebAuthnUser(
|
||||
device.user.uid,
|
||||
device.user.username,
|
||||
device.user.name,
|
||||
device.user.avatar,
|
||||
device.credential_id,
|
||||
device.public_key,
|
||||
device.sign_count,
|
||||
device.rp_id,
|
||||
)
|
||||
assertion = {}
|
||||
user = device.user
|
||||
|
||||
webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge)
|
||||
# We want all the user's WebAuthn devices and merge their challenges
|
||||
for user_device in WebAuthnDevice.objects.filter(user=device.user).order_by("name"):
|
||||
webauthn_user = WebAuthnUser(
|
||||
user.uid,
|
||||
user.username,
|
||||
user.name,
|
||||
user.avatar,
|
||||
user_device.credential_id,
|
||||
user_device.public_key,
|
||||
user_device.sign_count,
|
||||
user_device.rp_id,
|
||||
)
|
||||
webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge)
|
||||
if assertion == {}:
|
||||
assertion = webauthn_assertion_options.assertion_dict
|
||||
else:
|
||||
assertion["allowCredentials"] += webauthn_assertion_options.assertion_dict.get(
|
||||
"allowCredentials"
|
||||
)
|
||||
|
||||
return webauthn_assertion_options.assertion_dict
|
||||
return assertion
|
||||
|
||||
|
||||
def validate_challenge_code(code: str, request: HttpRequest, user: User) -> str:
|
||||
|
@ -20,8 +20,6 @@ from authentik.stages.authenticator_validate.models import AuthenticatorValidate
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
PER_DEVICE_CLASSES = [DeviceClasses.WEBAUTHN]
|
||||
|
||||
|
||||
class AuthenticatorValidationChallenge(WithUserInfoChallenge):
|
||||
"""Authenticator challenge"""
|
||||
@ -91,9 +89,9 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
if device_class not in stage.device_classes:
|
||||
LOGGER.debug("device class not allowed", device_class=device_class)
|
||||
continue
|
||||
# Ensure only classes in PER_DEVICE_CLASSES are returned per device
|
||||
# otherwise only return a single challenge
|
||||
if device_class in seen_classes and device_class not in PER_DEVICE_CLASSES:
|
||||
# Ensure only one challenge per device class
|
||||
# WebAuthn does another device loop to find all webuahtn devices
|
||||
if device_class in seen_classes:
|
||||
continue
|
||||
if device_class not in seen_classes:
|
||||
seen_classes.append(device_class)
|
||||
|
@ -1,12 +1,12 @@
|
||||
"""Test validator stage"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls.base import reverse
|
||||
from django.utils.encoding import force_str
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
@ -26,7 +26,7 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
|
||||
|
||||
class AuthenticatorValidateStageTests(TestCase):
|
||||
class AuthenticatorValidateStageTests(APITestCase):
|
||||
"""Test validator stage"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
|
@ -68,7 +68,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
|
||||
webauthn_credential = webauthn_registration_response.verify()
|
||||
except RegistrationRejectedException as exc:
|
||||
LOGGER.warning("registration failed", exc=exc)
|
||||
raise ValidationError("Registration failed. Error: {}".format(exc))
|
||||
raise ValidationError(f"Registration failed. Error: {exc}")
|
||||
|
||||
# Step 17.
|
||||
#
|
||||
|
@ -1,11 +1,10 @@
|
||||
"""authentik captcha stage"""
|
||||
|
||||
from django.http.response import HttpResponse
|
||||
from requests import RequestException, post
|
||||
from requests import RequestException
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.flows.challenge import (
|
||||
Challenge,
|
||||
ChallengeResponse,
|
||||
@ -13,7 +12,7 @@ from authentik.flows.challenge import (
|
||||
WithUserInfoChallenge,
|
||||
)
|
||||
from authentik.flows.stage import ChallengeStageView
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.lib.utils.http import get_client_ip, get_http_session
|
||||
from authentik.stages.captcha.models import CaptchaStage
|
||||
|
||||
|
||||
@ -34,11 +33,10 @@ class CaptchaChallengeResponse(ChallengeResponse):
|
||||
"""Validate captcha token"""
|
||||
stage: CaptchaStage = self.stage.executor.current_stage
|
||||
try:
|
||||
response = post(
|
||||
response = get_http_session().post(
|
||||
"https://www.google.com/recaptcha/api/siteverify",
|
||||
headers={
|
||||
"Content-type": "application/x-www-form-urlencoded",
|
||||
"User-agent": f"authentik {__version__} ReCaptcha",
|
||||
},
|
||||
data={
|
||||
"secret": stage.private_key,
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""captcha tests"""
|
||||
from django.test import Client, TestCase
|
||||
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
|
||||
@ -16,13 +16,12 @@ RECAPTCHA_PUBLIC_KEY = "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI"
|
||||
RECAPTCHA_PRIVATE_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
|
||||
|
||||
|
||||
class TestCaptchaStage(TestCase):
|
||||
class TestCaptchaStage(APITestCase):
|
||||
"""Captcha tests"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_user(username="unittest", email="test@beryju.org")
|
||||
self.client = Client()
|
||||
|
||||
self.flow = Flow.objects.create(
|
||||
name="test-captcha",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user