Compare commits
360 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
| 632e3cf7dc | |||
| e7144649d5 | |||
| dd8909c9b2 | |||
| e6818c1f6a | |||
| 10c4e3c717 | |||
| b8425867c8 | |||
| a05da8cdbf | |||
| c3aeefa653 | |||
| 62c840df21 | |||
| 45d1db8880 | |||
| b34f30f1dd | |||
| 7a54e84eb4 | |||
| 917eef96fb | |||
| 9a393848b2 | |||
| a6abeb50c6 | |||
| 39acb044fb | |||
| 7d2f622f4b | |||
| a2b38caf64 | |||
| 1193b9fd22 | |||
| e3a5ef1907 | |||
| e597bb4542 | |||
| c31df2b3f9 | |||
| 3f2637cffa | |||
| 3b6d9bec0a | |||
| b184210610 | |||
| d2010808ee | |||
| f5b185dd06 | |||
| ae161c1ba9 | |||
| 109283b189 | |||
| 235d283def | |||
| 96a86b3298 | |||
| db9ea8603c | |||
| 8b7f698c7b | |||
| 813c13ce45 | |||
| 629a0e1a4d | |||
| d1e2c018a3 | |||
| 1e86844823 | |||
| b58875d4c7 | |||
| 03e0eecb1d | |||
| 7aa61d86e4 | |||
| 0e6a799e6d | |||
| bc6afdf94f | |||
| 80364b04a9 | |||
| 0948e0ee1c | |||
| 5c54de66fc | |||
| 937edc73bc | |||
| 2c0d8d8943 | |||
| 059ccdd592 | |||
| 0ec0d3f1aa | |||
| 0a0eee138a | |||
| 3ed4c38101 | |||
| de8cf65503 | |||
| 121b36f35f | |||
| 363aed2a47 | |||
| ef994e0084 | |||
| e1ef196283 | |||
| f81ffd54f3 | |||
| f9bfae9190 | |||
| 0d686465a4 | |||
| e13b4a561f | |||
| f6417f95e5 | |||
| 9c6bf5f4ae | |||
| d2d7acb50e | |||
| c7681dde32 | |||
| 8cf9661e08 | |||
| 2dbd76cf90 | |||
| 28d39f4d80 | |||
| 760428aa18 | |||
| 49bbac7441 | |||
| 0b8cfd437b | |||
| b69aaf9417 | |||
| 758d1bdfd4 | |||
| ab501ca971 | |||
| 9657741a3d | |||
| 29b7368f42 | |||
| 75724b6f8d | |||
| 7c9f821bfd | |||
| 5b9e6bed6c | |||
| 6113d7d768 | |||
| 0e3602d7eb | |||
| 2b94e9a687 | |||
| 6ed7d842e4 | |||
| 8794c840cf | |||
| 9c9c00755a | |||
| 6703c0a5d1 | |||
| 060f19ce06 | |||
| b2d2e7cbc8 | |||
| 91fd792f88 | |||
| 2d9cd28221 | |||
| aa64cf898f | |||
| 27d109c1fe | |||
| 1b4a14f3ee | |||
| 9835785864 | |||
| d785998c5a | |||
| 8ba9553220 | |||
| 6eb132c48b | |||
| b523cd064b | |||
| 355b832cc3 | |||
| 8f5af464a2 | |||
| fb70769358 | |||
| ad06778c34 | |||
| bcb4451fb7 | |||
| 110d558572 | |||
| e32d4f0095 | |||
| 0e413acd61 | |||
| d3397c349f | |||
| fb18a10e61 | |||
| 9bb0d04aeb | |||
| 666cf77b04 | |||
| 90ca1b8e5a | |||
| f1e95b8816 | |||
| dad8547212 | |||
| a957e1fc45 | |||
| 39e3f02503 | |||
| 2b999e922c | |||
| 4224134a19 | |||
| eda260dddd | |||
| 8a1dd521e1 | |||
| 1c5e91de1d | |||
| 4b1744fad0 | |||
| f17b83010d | |||
| 12ddf9e73c | |||
| 0b3b300333 | |||
| 23f1a19765 | |||
| b27e998615 | |||
| 2b928146a8 | |||
| a94b0504b7 | |||
| 4fcbfa7709 | |||
| 986e01db20 | |||
| 9092d1189b | |||
| 605ed94ba2 | |||
| 4cbeeb9a0c | |||
| 993dee6aad | |||
| c663deb659 | |||
| 61621e7d60 | |||
| 0ee9b07172 | |||
| 431ba6b4ef | |||
| 146818793e | |||
| 0ce663bce4 | |||
| 923ba4fb42 | |||
| bb6eed0db1 | |||
| d1bd8f333b | |||
| 2ac9f5426d | |||
| 8d1fd48003 | |||
| 241cb01ec6 | |||
| 65b4139997 | |||
| 1431be8c44 | |||
| 049fceeeee | |||
| e6638afa3c | |||
| 465898c7d0 | |||
| c363b1cfde | |||
| b30ffd1318 | |||
| fe0d3a64c8 | |||
| ae9f1c1063 | |||
| ea63d384fd | |||
| c28d75754d | |||
| 518b691e00 | |||
| cd845be45d | |||
| a813d8e05e | |||
| 75f850f4d2 | |||
| c84265c6f0 | |||
| a477ea29cd | |||
| f6aa85e340 | |||
| 0aeedb3ad8 | |||
| 4b29f238b5 | |||
| 34157db06a | |||
| 84b9e66a97 | |||
| e831e4fb94 | |||
| 956922820b | |||
| b0fac9c9f1 | |||
| f4db09cd59 | |||
| 047030f901 | |||
| 638e8d741f | |||
| 425b87a6d0 | |||
| e7dc763612 | |||
| a80cc94da9 | |||
| 547dd3cb7a | |||
| 95739a934c | |||
| d12e24017e | |||
| e4a0345231 | |||
| 078633c2af | |||
| 4b8b800648 | |||
| 6f9ed001a1 | |||
| e4095dfffe | |||
| d5341c2284 | |||
| 357bd65028 | |||
| 867fb0dac0 | |||
| 2666aa2c73 | |||
| f0e9bafa35 | |||
| 0d739f5c1a | |||
| e08077c73a | |||
| 7cf8a31057 | |||
| c43049a981 | |||
| 1a9ace6f9d | |||
| b8d86bc482 | |||
| f7044e41c6 | |||
| fa59fec17a | |||
| e29afa289e | |||
| 4d4193a586 | |||
| 59343ff441 | |||
| cab564152d | |||
| 97b814ab33 | |||
| 88516ba2ca | |||
| f069cfb643 | |||
| 4ce3c2341c | |||
| 77e42d60cb | |||
| cacb919c6f | |||
| 2a3b049b01 | |||
| e4a5e86c93 | |||
| 3a51bcd890 | |||
| c28f68400d | |||
| 5d50fc281a | |||
| 9f7d1466e9 | |||
| c815d24806 | |||
| d1200a7e40 | |||
| edd4f9ceae | |||
| 1cfe81887b | |||
| bb5e0ebab1 | |||
| dfda76d896 | |||
| 8fc5114ce4 | |||
| e7b4363d21 | |||
| 53905d1a89 | |||
| 0ad1392632 | |||
| 6db1c914ee | |||
| 00324f922d | |||
| 8a24ddad28 | |||
| 0f85fe3c29 | |||
| 1f05eaa420 | |||
| 84e126a32c | |||
| 9ae69866bd | |||
| 56576a7f44 | |||
| 7f0295ba53 | |||
| 5553b3ff36 | |||
| 6f969525fe | |||
| bac12246fb | |||
| b53ef6e529 | |||
| 39c62afb93 | |||
| c98bdbacc5 | |||
| 1e8d45dc15 | |||
| 202b057ce9 | |||
| d5d8641b37 | |||
| 9dd37689e3 | |||
| cc0832f487 | |||
| b515bf7d2e | |||
| 34fbf3941b | |||
| e73606b54d | |||
| 0a413fe21a | |||
| d1b9f1e6b8 | |||
| e5a6e128e4 | |||
| 9295d1ed0b | |||
| 5d479a6c8f | |||
| 4a773b2b4f | |||
| 8003d67844 | |||
| 58baf97e2d | |||
| 51783c1cbb | |||
| 94290c7e36 | |||
| 123ff7ad1f | |||
| 8f3e863cce | |||
| 3d6c459349 | |||
| 6a583bae49 | |||
| 78e5879d9a | |||
| fdcac2a9ed | |||
| e81715caef | |||
| ab2b13938e | |||
| 5c97a3aef3 | |||
| e6963c543d | |||
| 9ca15983a2 | |||
| 99ef94b7aa | |||
| 133bedafba | |||
| c3faa61ed9 | |||
| da74304221 | |||
| ed6659a46d | |||
| 0abb1f94a4 | |||
| c7e299e0bf | |||
| 8a6590bac8 | |||
| ed717dcfa2 | |||
| b6df42f580 | |||
| 2ea85bd0c4 | |||
| 68fa8105e1 | |||
| 79db0ce4c1 | |||
| 5e23b11764 | |||
| c4e029ffe2 | |||
| 61b5b36192 | |||
| c6cc1b1728 | |||
| 77dd652160 | |||
| 1144944adb | |||
| 7751be284e | |||
| 74382c6287 | |||
| 011babbbd9 | |||
| 3c01a1dd7b | |||
| 6e832be2de | |||
| 46017f2f86 | |||
| da50eb0369 | |||
| b996e3cee7 | |||
| 12735cc14c | |||
| 4d36699b78 | |||
| 8110d2861b | |||
| 1cc60f572d | |||
| 90151a13ae | |||
| f958aa6930 | |||
| 13fbac30a2 | |||
| 4f4cdf16f1 | |||
| 7d75599627 | |||
| 924a13e832 | |||
| ae83c35dfd | |||
| e9102f4e28 | |||
| 9b8c1cbea5 | |||
| 6424bf98da | |||
| 74fb0f9e2a | |||
| 4380f37a77 | |||
| 17fccd44e6 | |||
| 217a8b5610 | |||
| 2cef220a3e | |||
| 5a8c66d325 | |||
| 8de13d3f67 | |||
| 5c22bedbaf | |||
| 8a0f993f0b | |||
| abcf515a69 | |||
| 894f704c27 | |||
| 7798292aa8 | |||
| 3005ca17bd | |||
| 909461e533 | |||
| df838a4023 | |||
| 0f86b62dd3 | |||
| a40c3aeb68 | |||
| 4080738ded | |||
| 4a89be3048 | |||
| e587c53e18 | |||
| 023b97aa69 | |||
| 51365dba74 | |||
| 0d3705685e | |||
| 738e4d5c74 | |||
| b14b9cb0dd | |||
| 2a21ebf7b0 | |||
| 5bc1301043 | |||
| e0e4bf6972 | |||
| 337677ad12 | |||
| 3712d5aee2 | |||
| dd82d55725 | |||
| 8d766efecb | |||
| 9ac3b29418 | |||
| 5000c5b061 | |||
| b362d2af03 | |||
| bcd42fce13 | |||
| 6deddd038f | |||
| 3b47cb64da | |||
| cf5e70c759 | |||
| 20bc38a54b | |||
| 672a4ab1f4 | |||
| 47dd667261 | |||
| d1ac69789b | |||
| 08abf81c6d | |||
| 76bd987e6f | |||
| 5374352411 | |||
| 08eff4cc5d | |||
| c87a9f9489 | |||
| 8f6d700aa8 | |||
| c6843b026c | |||
| 3769c33ef0 | |||
| 8982afaf44 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.10.2
|
||||
current_version = 2021.12.1-rc1
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
|
||||
129
.github/workflows/ci-main.yml
vendored
129
.github/workflows/ci-main.yml
vendored
@ -18,79 +18,16 @@ env:
|
||||
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-v2-${{ 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-v2-${{ 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-v2-${{ 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-v2-${{ 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:
|
||||
lint:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- pylint
|
||||
- black
|
||||
- isort
|
||||
- bandit
|
||||
- pyright
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -100,12 +37,17 @@ jobs:
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
- name: prepare
|
||||
run: |
|
||||
scripts/ci_prepare.sh
|
||||
npm install -g pyright@1.1.136
|
||||
- name: run bandit
|
||||
run: pipenv run pyright e2e lifecycle
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
run: scripts/ci_prepare.sh
|
||||
- name: run pylint
|
||||
run: pipenv run make ci-${{ matrix.job }}
|
||||
test-migrations:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@ -114,7 +56,7 @@ jobs:
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
@ -138,7 +80,7 @@ jobs:
|
||||
run: |
|
||||
python ./scripts/gh_env.py
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
@ -147,6 +89,8 @@ jobs:
|
||||
# Copy current, latest config to local
|
||||
cp authentik/lib/default.yml local.env.yml
|
||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||
git checkout $GITHUB_HEAD_REF -- .github
|
||||
git checkout $GITHUB_HEAD_REF -- scripts
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||
@ -160,7 +104,7 @@ jobs:
|
||||
run: |
|
||||
set -x
|
||||
git fetch
|
||||
git checkout ${{ steps.ev.outputs.branchName }}
|
||||
git checkout $GITHUB_HEAD_REF
|
||||
pipenv sync --dev
|
||||
- name: prepare
|
||||
env:
|
||||
@ -176,7 +120,7 @@ jobs:
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
@ -205,7 +149,7 @@ jobs:
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
@ -244,7 +188,7 @@ jobs:
|
||||
with:
|
||||
domain: ${{github.repository_owner}}
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
@ -255,7 +199,7 @@ jobs:
|
||||
scripts/ci_prepare.sh
|
||||
docker-compose -f tests/e2e/docker-compose.yml up -d
|
||||
- id: cache-web
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: web/dist
|
||||
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }}
|
||||
@ -277,19 +221,23 @@ jobs:
|
||||
uses: codecov/codecov-action@v2
|
||||
build:
|
||||
needs:
|
||||
- lint-pylint
|
||||
- lint-black
|
||||
- lint-isort
|
||||
- lint-bandit
|
||||
- lint-pyright
|
||||
- lint
|
||||
- test-migrations
|
||||
- test-migrations-from-stable
|
||||
- test-unittest
|
||||
- test-integration
|
||||
- test-e2e
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
arch:
|
||||
- 'linux/amd64'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: prepare variables
|
||||
@ -314,3 +262,4 @@ jobs:
|
||||
ghcr.io/goauthentik/dev-server:gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }}
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
platforms: ${{ matrix.arch }}
|
||||
|
||||
7
.github/workflows/ci-outpost.yml
vendored
7
.github/workflows/ci-outpost.yml
vendored
@ -31,16 +31,22 @@ jobs:
|
||||
golangci/golangci-lint:v1.39.0 \
|
||||
golangci-lint run -v --timeout 200s
|
||||
build:
|
||||
timeout-minutes: 120
|
||||
needs:
|
||||
- lint-golint
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
type:
|
||||
- proxy
|
||||
- ldap
|
||||
arch:
|
||||
- 'linux/amd64'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: prepare variables
|
||||
@ -67,3 +73,4 @@ jobs:
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
build-args: |
|
||||
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||
platforms: ${{ matrix.arch }}
|
||||
|
||||
85
.github/workflows/release-publish.yml
vendored
85
.github/workflows/release-publish.yml
vendored
@ -30,14 +30,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.10.2,
|
||||
beryju/authentik:2021.12.1-rc1,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.10.2,
|
||||
ghcr.io/goauthentik/server:2021.12.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.10.2', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
@ -45,8 +45,14 @@ jobs:
|
||||
docker pull ghcr.io/goauthentik/server:latest
|
||||
docker tag ghcr.io/goauthentik/server:latest ghcr.io/goauthentik/server:stable
|
||||
docker push ghcr.io/goauthentik/server:stable
|
||||
build-proxy:
|
||||
build-outpost:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
type:
|
||||
- proxy
|
||||
- ldap
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
@ -72,68 +78,25 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-proxy:2021.10.2,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.10.2,
|
||||
ghcr.io/goauthentik/proxy:latest
|
||||
file: proxy.Dockerfile
|
||||
beryju/authentik-${{ matrix.type }}:2021.12.1-rc1,
|
||||
beryju/authentik-${{ matrix.type }}:latest,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:2021.12.1-rc1,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:latest
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.10.2', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.12.1-rc1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-proxy:latest
|
||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||
docker push beryju/authentik-proxy:stable
|
||||
docker pull ghcr.io/goauthentik/proxy:latest
|
||||
docker tag ghcr.io/goauthentik/proxy:latest ghcr.io/goauthentik/proxy:stable
|
||||
docker push ghcr.io/goauthentik/proxy:stable
|
||||
build-ldap:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "^1.15"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1.2.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
- name: Docker Login Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Building Docker Image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-ldap:2021.10.2,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.10.2,
|
||||
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.10.2', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
docker push beryju/authentik-ldap:stable
|
||||
docker pull ghcr.io/goauthentik/ldap:latest
|
||||
docker tag ghcr.io/goauthentik/ldap:latest ghcr.io/goauthentik/ldap:stable
|
||||
docker push ghcr.io/goauthentik/ldap:stable
|
||||
docker pull beryju/authentik-${{ matrix.type }}:latest
|
||||
docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
|
||||
docker push beryju/authentik-${{ matrix.type }}:stable
|
||||
docker pull ghcr.io/goauthentik/${{ matrix.type }}:latest
|
||||
docker tag ghcr.io/goauthentik/${{ matrix.type }}:latest ghcr.io/goauthentik/${{ matrix.type }}:stable
|
||||
docker push ghcr.io/goauthentik/${{ matrix.type }}:stable
|
||||
test-release:
|
||||
needs:
|
||||
- build-server
|
||||
- build-proxy
|
||||
- build-ldap
|
||||
- build-outpost
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -170,7 +133,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.10.2
|
||||
version: authentik@2021.12.1-rc1
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: '~/static/dist'
|
||||
|
||||
1
.github/workflows/release-tag.yml
vendored
1
.github/workflows/release-tag.yml
vendored
@ -15,6 +15,7 @@ jobs:
|
||||
run: |
|
||||
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
|
||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
|
||||
docker buildx install
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t testing:latest \
|
||||
|
||||
14
.github/workflows/translation-compile.yml
vendored
14
.github/workflows/translation-compile.yml
vendored
@ -4,6 +4,9 @@ on:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '/locale/'
|
||||
pull_request:
|
||||
paths:
|
||||
- '/locale/'
|
||||
schedule:
|
||||
- cron: "0 */2 * * *"
|
||||
workflow_dispatch:
|
||||
@ -22,7 +25,7 @@ jobs:
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- id: cache-pipenv
|
||||
uses: actions/cache@v2.1.6
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs
|
||||
key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||
@ -37,10 +40,19 @@ jobs:
|
||||
run: pipenv run ./manage.py compilemessages
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: compile-backend-translation
|
||||
commit-message: "core: compile backend translations"
|
||||
title: "core: compile backend translations"
|
||||
body: "core: compile backend translations"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
- name: Enable Pull Request Automerge
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
uses: peter-evans/enable-pull-request-automerge@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
|
||||
9
.github/workflows/web-api-publish.yml
vendored
9
.github/workflows/web-api-publish.yml
vendored
@ -30,10 +30,19 @@ jobs:
|
||||
npm i @goauthentik/api@$VERSION
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: update-web-api-client
|
||||
commit-message: "web: Update Web API Client version"
|
||||
title: "web: Update Web API Client version"
|
||||
body: "web: Update Web API Client version"
|
||||
delete-branch: true
|
||||
signoff: true
|
||||
- name: Enable Pull Request Automerge
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
uses: peter-evans/enable-pull-request-automerge@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -66,7 +66,9 @@ coverage.xml
|
||||
unittest.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
# Have to include binary mo files as they are annoying to compile at build time
|
||||
# since a full postgres and redis instance are required
|
||||
# *.mo
|
||||
|
||||
# Django stuff:
|
||||
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -10,7 +10,8 @@
|
||||
"plex",
|
||||
"saml",
|
||||
"totp",
|
||||
"webauthn"
|
||||
"webauthn",
|
||||
"traefik"
|
||||
],
|
||||
"python.linting.pylintEnabled": true,
|
||||
"todo-tree.tree.showCountsInTree": true,
|
||||
|
||||
38
Dockerfile
38
Dockerfile
@ -1,5 +1,5 @@
|
||||
# Stage 1: Lock python dependencies
|
||||
FROM docker.io/python:3.9-bullseye as locker
|
||||
FROM docker.io/python:3.9-slim-bullseye as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
@ -11,31 +11,32 @@ RUN pip install pipenv && \
|
||||
pipenv lock -r --dev-only > requirements-dev.txt
|
||||
|
||||
# Stage 2: Build website
|
||||
FROM docker.io/node:16 as website-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/node:16 as website-builder
|
||||
|
||||
COPY ./website /static/
|
||||
COPY ./website /work/website/
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build-docs-only
|
||||
RUN cd /work/website && npm i && npm run build-docs-only
|
||||
|
||||
# Stage 3: Build webui
|
||||
FROM docker.io/node:16 as web-builder
|
||||
FROM --platform=${BUILDPLATFORM} docker.io/node:16 as web-builder
|
||||
|
||||
COPY ./web /static/
|
||||
COPY ./web /work/web/
|
||||
COPY ./website /work/website/
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build
|
||||
RUN cd /work/web && npm i && npm run build
|
||||
|
||||
# Stage 4: Build go proxy
|
||||
FROM docker.io/golang:1.17.2-bullseye AS builder
|
||||
FROM docker.io/golang:1.17.3-bullseye AS builder
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
COPY --from=web-builder /static/robots.txt /work/web/robots.txt
|
||||
COPY --from=web-builder /static/security.txt /work/web/security.txt
|
||||
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=web-builder /work/web/robots.txt /work/web/robots.txt
|
||||
COPY --from=web-builder /work/web/security.txt /work/web/security.txt
|
||||
COPY --from=web-builder /work/web/dist/ /work/web/dist/
|
||||
COPY --from=web-builder /work/web/authentik/ /work/web/authentik/
|
||||
COPY --from=website-builder /work/website/help/ /work/website/help/
|
||||
|
||||
COPY ./cmd /work/cmd
|
||||
COPY ./web/static.go /work/web/static.go
|
||||
@ -47,7 +48,7 @@ COPY ./go.sum /work/go.sum
|
||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||
|
||||
# Stage 5: Run
|
||||
FROM docker.io/python:3.9-bullseye
|
||||
FROM docker.io/python:3.9-slim-bullseye
|
||||
|
||||
WORKDIR /
|
||||
COPY --from=locker /app/requirements.txt /
|
||||
@ -57,11 +58,10 @@ ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends curl ca-certificates gnupg git runit && \
|
||||
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
||||
echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends libpq-dev postgresql-client build-essential libxmlsec1-dev pkg-config libmaxminddb0 && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
curl ca-certificates gnupg git runit libpq-dev \
|
||||
postgresql-client build-essential libxmlsec1-dev \
|
||||
pkg-config libmaxminddb0 && \
|
||||
pip install -r /requirements.txt --no-cache-dir && \
|
||||
apt-get remove --purge -y build-essential git && \
|
||||
apt-get autoremove --purge -y && \
|
||||
|
||||
53
Makefile
53
Makefile
@ -7,13 +7,13 @@ NPM_VERSION = $(shell python -m scripts.npm_version)
|
||||
all: lint-fix lint test gen
|
||||
|
||||
test-integration:
|
||||
coverage run manage.py test -v 3 tests/integration
|
||||
coverage run manage.py test tests/integration
|
||||
|
||||
test-e2e:
|
||||
coverage run manage.py test --failfast -v 3 tests/e2e
|
||||
coverage run manage.py test tests/e2e
|
||||
|
||||
test:
|
||||
coverage run manage.py test -v 3 authentik
|
||||
coverage run manage.py test authentik
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
@ -33,9 +33,10 @@ lint:
|
||||
bandit -r authentik tests lifecycle -x node_modules
|
||||
pylint authentik tests lifecycle
|
||||
|
||||
i18n-extract:
|
||||
i18n-extract: i18n-extract-core web-extract
|
||||
|
||||
i18n-extract-core:
|
||||
./manage.py makemessages --ignore web --ignore internal --ignore web --ignore web-api --ignore website -l en
|
||||
cd web && npm run extract
|
||||
|
||||
gen-build:
|
||||
./manage.py spectacular --file schema.yml
|
||||
@ -60,18 +61,20 @@ gen-web:
|
||||
\cp -rfv web-api/* web/node_modules/@goauthentik/api
|
||||
|
||||
gen-outpost:
|
||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
|
||||
mkdir -p templates
|
||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache
|
||||
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache
|
||||
docker run \
|
||||
--rm -v ${PWD}:/local \
|
||||
--user ${UID}:${GID} \
|
||||
openapitools/openapi-generator-cli 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,disallowAdditionalPropertiesIfNotPresent=false
|
||||
rm -f api/go.mod api/go.sum
|
||||
-c /local/config.yaml
|
||||
go mod edit -replace goauthentik.io/api=./api
|
||||
rm -rf config.yaml ./templates/
|
||||
|
||||
gen: gen-build gen-clean gen-web
|
||||
|
||||
@ -80,3 +83,33 @@ migrate:
|
||||
|
||||
run:
|
||||
go run -v cmd/server/main.go
|
||||
|
||||
web: web-lint-fix web-lint web-extract
|
||||
|
||||
web-lint-fix:
|
||||
cd web && npm run prettier
|
||||
|
||||
web-lint:
|
||||
cd web && npm run lint
|
||||
cd web && npm run lit-analyse
|
||||
|
||||
web-extract:
|
||||
cd web && npm run extract
|
||||
|
||||
# These targets are use by GitHub actions to allow usage of matrix
|
||||
# which makes the YAML File a lot smaller
|
||||
|
||||
ci-pylint:
|
||||
pylint authentik tests lifecycle
|
||||
|
||||
ci-black:
|
||||
black --check authentik tests lifecycle
|
||||
|
||||
ci-isort:
|
||||
isort --check authentik tests lifecycle
|
||||
|
||||
ci-bandit:
|
||||
bandit -r authentik tests lifecycle
|
||||
|
||||
ci-pyright:
|
||||
pyright e2e lifecycle
|
||||
|
||||
16
Pipfile
16
Pipfile
@ -8,7 +8,10 @@ boto3 = "*"
|
||||
celery = "*"
|
||||
channels = "*"
|
||||
channels-redis = "*"
|
||||
codespell = "*"
|
||||
colorama = "*"
|
||||
dacite = "*"
|
||||
deepmerge = "*"
|
||||
defusedxml = "*"
|
||||
django = "*"
|
||||
django-dbbackup = { git = 'https://github.com/django-dbbackup/django-dbbackup.git', ref = '9d1909c30a3271c8c9c8450add30d6e0b996e145' }
|
||||
@ -23,6 +26,7 @@ djangorestframework = "*"
|
||||
djangorestframework-guardian = "*"
|
||||
docker = "*"
|
||||
drf-spectacular = "*"
|
||||
duo-client = "*"
|
||||
facebook-sdk = "*"
|
||||
geoip2 = "*"
|
||||
gunicorn = "*"
|
||||
@ -40,19 +44,15 @@ service_identity = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
twisted = "==21.7.0"
|
||||
ua-parser = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
uvicorn = {extras = ["standard"],version = "*"}
|
||||
webauthn = "*"
|
||||
xmlsec = "*"
|
||||
duo-client = "*"
|
||||
ua-parser = "*"
|
||||
deepmerge = "*"
|
||||
colorama = "*"
|
||||
codespell = "*"
|
||||
|
||||
[dev-packages]
|
||||
bandit = "*"
|
||||
black = "==21.9b0"
|
||||
black = "==21.11b1"
|
||||
bump2version = "*"
|
||||
colorama = "*"
|
||||
coverage = {extras = ["toml"],version = "*"}
|
||||
@ -60,5 +60,7 @@ pylint = "*"
|
||||
pylint-django = "*"
|
||||
pytest = "*"
|
||||
pytest-django = "*"
|
||||
selenium = "*"
|
||||
pytest-randomly = "*"
|
||||
requests-mock = "*"
|
||||
selenium = "*"
|
||||
importlib-metadata = "*"
|
||||
|
||||
901
Pipfile.lock
generated
901
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,8 @@
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ------------------ |
|
||||
| 2021.8.x | :white_check_mark: |
|
||||
| 2021.9.x | :white_check_mark: |
|
||||
| 2021.10.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.10.2"
|
||||
__version__ = "2021.12.1-rc1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
@ -86,7 +86,7 @@ class SystemSerializer(PassiveSerializer):
|
||||
def get_embedded_outpost_host(self, request: Request) -> str:
|
||||
"""Get the FQDN configured on the embedded outpost"""
|
||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||
if not outposts.exists():
|
||||
if not outposts.exists(): # pragma: no cover
|
||||
return ""
|
||||
return outposts.first().config.authentik_host
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ class TaskSerializer(PassiveSerializer):
|
||||
are pickled in cache. In that case, just delete the info"""
|
||||
try:
|
||||
return super().to_representation(instance)
|
||||
except AttributeError:
|
||||
except AttributeError: # pragma: no cover
|
||||
if isinstance(self.instance, list):
|
||||
for inst in self.instance:
|
||||
inst.delete()
|
||||
|
||||
@ -23,6 +23,6 @@ class WorkerView(APIView):
|
||||
"""Get currently connected worker count."""
|
||||
count = len(CELERY_APP.control.ping(timeout=0.5))
|
||||
# In debug we run with `CELERY_TASK_ALWAYS_EAGER`, so tasks are ran on the main process
|
||||
if settings.DEBUG:
|
||||
if settings.DEBUG: # pragma: no cover
|
||||
count += 1
|
||||
return Response({"count": count})
|
||||
|
||||
@ -27,6 +27,7 @@ VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
|
||||
# Chop of the first ^ because we want to search the entire string
|
||||
URL_FINDER = URLValidator.regex.pattern[1:]
|
||||
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
||||
LOCAL_VERSION = parse(__version__)
|
||||
|
||||
|
||||
def _set_prom_info():
|
||||
@ -48,7 +49,7 @@ def clear_update_notifications():
|
||||
if "new_version" not in notification.event.context:
|
||||
continue
|
||||
notification_version = notification.event.context["new_version"]
|
||||
if notification_version == __version__:
|
||||
if LOCAL_VERSION >= parse(notification_version):
|
||||
notification.delete()
|
||||
|
||||
|
||||
@ -74,8 +75,7 @@ def update_latest_version(self: MonitoredTask):
|
||||
_set_prom_info()
|
||||
# Check if upstream version is newer than what we're running,
|
||||
# and if no event exists yet, create one.
|
||||
local_version = parse(__version__)
|
||||
if local_version < parse(upstream_version):
|
||||
if LOCAL_VERSION < parse(upstream_version):
|
||||
# Event has already been created, don't create duplicate
|
||||
if Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE,
|
||||
|
||||
@ -8,6 +8,7 @@ from authentik import __version__
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.tasks import clean_expired_models
|
||||
from authentik.events.monitored_tasks import TaskResultStatus
|
||||
from authentik.managed.tasks import managed_reconcile
|
||||
|
||||
|
||||
class TestAdminAPI(TestCase):
|
||||
@ -94,5 +95,7 @@ class TestAdminAPI(TestCase):
|
||||
|
||||
def test_system(self):
|
||||
"""Test system API"""
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
managed_reconcile() # pylint: disable=no-value-for-parameter
|
||||
response = self.client.get(reverse("authentik_api:admin_system"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@ -3,8 +3,13 @@ from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||
from authentik.admin.tasks import (
|
||||
VERSION_CACHE_KEY,
|
||||
clear_update_notifications,
|
||||
update_latest_version,
|
||||
)
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
RESPONSE_VALID = {
|
||||
"$schema": "https://version.goauthentik.io/schema.json",
|
||||
@ -56,3 +61,23 @@ class TestAdminTasks(TestCase):
|
||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_version_disabled(self):
|
||||
"""Test Update checker while its disabled"""
|
||||
with CONFIG.patch("disable_update_check", True):
|
||||
update_latest_version.delay().get()
|
||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
|
||||
|
||||
def test_clear_update_notifications(self):
|
||||
"""Test clear of previous notification"""
|
||||
Event.objects.create(
|
||||
action=EventAction.UPDATE_AVAILABLE, context={"new_version": "99999999.9999999.9999999"}
|
||||
)
|
||||
Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={"new_version": "1.1.1"})
|
||||
Event.objects.create(action=EventAction.UPDATE_AVAILABLE, context={})
|
||||
clear_update_notifications()
|
||||
self.assertFalse(
|
||||
Event.objects.filter(
|
||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="1.1"
|
||||
).exists()
|
||||
)
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
"""Throttling classes"""
|
||||
from typing import Type
|
||||
|
||||
from django.views import View
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.throttling import ScopedRateThrottle
|
||||
|
||||
|
||||
class SessionThrottle(ScopedRateThrottle):
|
||||
"""Throttle based on session key"""
|
||||
|
||||
def allow_request(self, request: Request, view):
|
||||
if request._request.user.is_superuser:
|
||||
return True
|
||||
return super().allow_request(request, view)
|
||||
|
||||
def get_cache_key(self, request: Request, view: Type[View]) -> str:
|
||||
return f"authentik-throttle-session-{request._request.session.session_key}"
|
||||
@ -4,7 +4,7 @@ from django.urls import include, path
|
||||
from authentik.api.v3.urls import urlpatterns as v3_urls
|
||||
|
||||
urlpatterns = [
|
||||
# Remove in 2022.1
|
||||
# TODO: Remove in 2022.1
|
||||
path("v2beta/", include(v3_urls)),
|
||||
path("v3/", include(v3_urls)),
|
||||
]
|
||||
|
||||
@ -5,7 +5,14 @@ from django.conf import settings
|
||||
from django.db import models
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||
from rest_framework.fields import BooleanField, CharField, ChoiceField, IntegerField, ListField
|
||||
from rest_framework.fields import (
|
||||
BooleanField,
|
||||
CharField,
|
||||
ChoiceField,
|
||||
FloatField,
|
||||
IntegerField,
|
||||
ListField,
|
||||
)
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
@ -24,13 +31,19 @@ class Capabilities(models.TextChoices):
|
||||
CAN_BACKUP = "can_backup"
|
||||
|
||||
|
||||
class ErrorReportingConfigSerializer(PassiveSerializer):
|
||||
"""Config for error reporting"""
|
||||
|
||||
enabled = BooleanField(read_only=True)
|
||||
environment = CharField(read_only=True)
|
||||
send_pii = BooleanField(read_only=True)
|
||||
traces_sample_rate = FloatField(read_only=True)
|
||||
|
||||
|
||||
class ConfigSerializer(PassiveSerializer):
|
||||
"""Serialize authentik Config into DRF Object"""
|
||||
|
||||
error_reporting_enabled = BooleanField(read_only=True)
|
||||
error_reporting_environment = CharField(read_only=True)
|
||||
error_reporting_send_pii = BooleanField(read_only=True)
|
||||
|
||||
error_reporting = ErrorReportingConfigSerializer(required=True)
|
||||
capabilities = ListField(child=ChoiceField(choices=Capabilities.choices))
|
||||
|
||||
cache_timeout = IntegerField(required=True)
|
||||
@ -66,9 +79,12 @@ class ConfigView(APIView):
|
||||
"""Retrieve public configuration options"""
|
||||
config = ConfigSerializer(
|
||||
{
|
||||
"error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
|
||||
"error_reporting_environment": CONFIG.y("error_reporting.environment"),
|
||||
"error_reporting_send_pii": CONFIG.y("error_reporting.send_pii"),
|
||||
"error_reporting": {
|
||||
"enabled": CONFIG.y("error_reporting.enabled"),
|
||||
"environment": CONFIG.y("error_reporting.environment"),
|
||||
"send_pii": CONFIG.y("error_reporting.send_pii"),
|
||||
"traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)),
|
||||
},
|
||||
"capabilities": self.get_capabilities(),
|
||||
"cache_timeout": int(CONFIG.y("redis.cache_timeout")),
|
||||
"cache_timeout_flows": int(CONFIG.y("redis.cache_timeout_flows")),
|
||||
|
||||
@ -42,6 +42,7 @@ class GroupSerializer(ModelSerializer):
|
||||
users_obj = ListSerializer(
|
||||
child=GroupMemberSerializer(), read_only=True, source="users", required=False
|
||||
)
|
||||
parent_name = CharField(source="parent.name", read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -51,6 +52,7 @@ class GroupSerializer(ModelSerializer):
|
||||
"name",
|
||||
"is_superuser",
|
||||
"parent",
|
||||
"parent_name",
|
||||
"users",
|
||||
"attributes",
|
||||
"users_obj",
|
||||
|
||||
@ -56,6 +56,7 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
||||
"component",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -43,6 +43,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"assigned_application_name",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -48,6 +48,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"component",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
"policy_engine_mode",
|
||||
"user_matching_mode",
|
||||
]
|
||||
|
||||
@ -55,6 +55,7 @@ from authentik.core.models import (
|
||||
User,
|
||||
)
|
||||
from authentik.events.models import EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
@ -125,7 +126,9 @@ class UserSelfSerializer(ModelSerializer):
|
||||
|
||||
def validate_email(self, email: str):
|
||||
"""Check if the user is allowed to change their email"""
|
||||
if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_EMAIL, True):
|
||||
if self.instance.group_attributes().get(
|
||||
USER_ATTRIBUTE_CHANGE_EMAIL, CONFIG.y_bool("default_user_change_email", True)
|
||||
):
|
||||
return email
|
||||
if email != self.instance.email:
|
||||
raise ValidationError("Not allowed to change email.")
|
||||
@ -133,7 +136,9 @@ class UserSelfSerializer(ModelSerializer):
|
||||
|
||||
def validate_username(self, username: str):
|
||||
"""Check if the user is allowed to change their username"""
|
||||
if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_USERNAME, True):
|
||||
if self.instance.group_attributes().get(
|
||||
USER_ATTRIBUTE_CHANGE_USERNAME, CONFIG.y_bool("default_user_change_username", True)
|
||||
):
|
||||
return username
|
||||
if username != self.instance.username:
|
||||
raise ValidationError("Not allowed to change username.")
|
||||
|
||||
@ -41,6 +41,7 @@ class MetaNameSerializer(PassiveSerializer):
|
||||
|
||||
verbose_name = SerializerMethodField()
|
||||
verbose_name_plural = SerializerMethodField()
|
||||
meta_model_name = SerializerMethodField()
|
||||
|
||||
def get_verbose_name(self, obj: Model) -> str:
|
||||
"""Return object's verbose_name"""
|
||||
@ -50,6 +51,10 @@ class MetaNameSerializer(PassiveSerializer):
|
||||
"""Return object's plural verbose_name"""
|
||||
return obj._meta.verbose_name_plural
|
||||
|
||||
def get_meta_model_name(self, obj: Model) -> str:
|
||||
"""Return internal model name"""
|
||||
return f"{obj._meta.app_label}.{obj._meta.model_name}"
|
||||
|
||||
|
||||
class TypeCreateSerializer(PassiveSerializer):
|
||||
"""Types of an object that can be created"""
|
||||
|
||||
@ -55,5 +55,5 @@ class TokenBackend(InbuiltBackend):
|
||||
if not tokens.exists():
|
||||
return None
|
||||
token = tokens.first()
|
||||
self.set_method("password", request, token=token)
|
||||
self.set_method("token", request, token=token)
|
||||
return token.user
|
||||
|
||||
@ -12,7 +12,6 @@ LOCAL = local()
|
||||
RESPONSE_HEADER_ID = "X-authentik-id"
|
||||
KEY_AUTH_VIA = "auth_via"
|
||||
KEY_USER = "user"
|
||||
INTERNAL_HEADER_PREFIX = "X-authentik-internal-"
|
||||
|
||||
|
||||
class ImpersonateMiddleware:
|
||||
@ -53,9 +52,9 @@ class RequestIDMiddleware:
|
||||
}
|
||||
response = self.get_response(request)
|
||||
response[RESPONSE_HEADER_ID] = request.request_id
|
||||
if auth_via := LOCAL.authentik.get(KEY_AUTH_VIA, None):
|
||||
response[INTERNAL_HEADER_PREFIX + KEY_AUTH_VIA] = auth_via
|
||||
response[INTERNAL_HEADER_PREFIX + KEY_USER] = request.user.username
|
||||
setattr(response, "ak_context", {})
|
||||
response.ak_context.update(LOCAL.authentik)
|
||||
response.ak_context[KEY_USER] = request.user.username
|
||||
for key in list(LOCAL.authentik.keys()):
|
||||
del LOCAL.authentik[key]
|
||||
return response
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import uuid
|
||||
from os import environ
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.apps.registry import Apps
|
||||
from django.conf import settings
|
||||
@ -12,6 +11,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
from django.db.models import Count
|
||||
|
||||
import authentik.core.models
|
||||
import authentik.lib.models
|
||||
|
||||
|
||||
def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
@ -161,7 +161,7 @@ class Migration(migrations.Migration):
|
||||
model_name="application",
|
||||
name="meta_launch_url",
|
||||
field=models.TextField(
|
||||
blank=True, default="", validators=[django.core.validators.URLValidator()]
|
||||
blank=True, default="", validators=[authentik.lib.models.DomainlessURLValidator()]
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
# Generated by Django 3.2.3 on 2021-06-02 21:51
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.lib.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -17,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
validators=[django.core.validators.URLValidator()],
|
||||
validators=[authentik.lib.models.DomainlessURLValidator()],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@ -9,7 +9,6 @@ from deepmerge import always_merger
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
from django.db.models import Q, QuerySet, options
|
||||
from django.http import HttpRequest
|
||||
@ -29,7 +28,7 @@ from authentik.core.types import UILoginButton, UserSettingSerializer
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
|
||||
from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.managed.models import ManagedModel
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
@ -81,6 +80,27 @@ class Group(models.Model):
|
||||
)
|
||||
attributes = models.JSONField(default=dict, blank=True)
|
||||
|
||||
def is_member(self, user: "User") -> bool:
|
||||
"""Recursively check if `user` is member of us, or any parent."""
|
||||
query = """
|
||||
WITH RECURSIVE parents AS (
|
||||
SELECT authentik_core_group.*, 0 AS relative_depth
|
||||
FROM authentik_core_group
|
||||
WHERE authentik_core_group.group_uuid = %s
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT authentik_core_group.*, parents.relative_depth - 1
|
||||
FROM authentik_core_group,parents
|
||||
WHERE authentik_core_group.parent_id = parents.group_uuid
|
||||
)
|
||||
SELECT group_uuid
|
||||
FROM parents
|
||||
GROUP BY group_uuid;
|
||||
"""
|
||||
groups = Group.objects.raw(query, [self.group_uuid])
|
||||
return user.ak_groups.filter(pk__in=[group.pk for group in groups]).exists()
|
||||
|
||||
def __str__(self):
|
||||
return f"Group {self.name}"
|
||||
|
||||
@ -153,7 +173,7 @@ class User(GuardianUserMixin, AbstractUser):
|
||||
if mode == "none":
|
||||
return DEFAULT_AVATAR
|
||||
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
||||
mail_hash = md5(self.email.encode("utf-8")).hexdigest() # nosec
|
||||
mail_hash = md5(self.email.lower().encode("utf-8")).hexdigest() # nosec
|
||||
if mode == "gravatar":
|
||||
parameters = [
|
||||
("s", "158"),
|
||||
@ -225,7 +245,7 @@ class Application(PolicyBindingModel):
|
||||
)
|
||||
|
||||
meta_launch_url = models.TextField(
|
||||
default="", blank=True, validators=[validators.URLValidator()]
|
||||
default="", blank=True, validators=[DomainlessURLValidator()]
|
||||
)
|
||||
# For template applications, this can be set to /static/authentik/applications/*
|
||||
meta_icon = models.FileField(
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/AdminInterface.js' %}" type="module"></script>
|
||||
<script src="{% static 'dist/admin/AdminInterface.js' %}" type="module"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{% static 'dist/FlowInterface.js' %}" type="module"></script>
|
||||
<script src="{% static 'dist/flow/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 %}
|
||||
<script src="{% static 'dist/UserInterface.js' %}" type="module"></script>
|
||||
<script src="{% static 'dist/user/UserInterface.js' %}" type="module"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@ -3,7 +3,8 @@ from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.models import PolicyBinding
|
||||
|
||||
@ -12,7 +13,7 @@ class TestApplicationsAPI(APITestCase):
|
||||
"""Test applications API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.allowed = Application.objects.create(name="allowed", slug="allowed")
|
||||
self.denied = Application.objects.create(name="denied", slug="denied")
|
||||
PolicyBinding.objects.create(
|
||||
|
||||
@ -6,6 +6,7 @@ from django.utils.encoding import force_str
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestAuthenticatedSessionsAPI(APITestCase):
|
||||
@ -13,7 +14,7 @@ class TestAuthenticatedSessionsAPI(APITestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.other_user = User.objects.create(username="normal-user")
|
||||
|
||||
def test_list(self):
|
||||
|
||||
40
authentik/core/tests/test_groups.py
Normal file
40
authentik/core/tests/test_groups.py
Normal file
@ -0,0 +1,40 @@
|
||||
"""group tests"""
|
||||
from django.test.testcases import TestCase
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
|
||||
|
||||
class TestGroups(TestCase):
|
||||
"""Test group membership"""
|
||||
|
||||
def test_group_membership_simple(self):
|
||||
"""Test simple membership"""
|
||||
user = User.objects.create(username="user")
|
||||
user2 = User.objects.create(username="user2")
|
||||
group = Group.objects.create(name="group")
|
||||
group.users.add(user)
|
||||
self.assertTrue(group.is_member(user))
|
||||
self.assertFalse(group.is_member(user2))
|
||||
|
||||
def test_group_membership_parent(self):
|
||||
"""Test parent membership"""
|
||||
user = User.objects.create(username="user")
|
||||
user2 = User.objects.create(username="user2")
|
||||
first = Group.objects.create(name="first")
|
||||
second = Group.objects.create(name="second", parent=first)
|
||||
second.users.add(user)
|
||||
self.assertTrue(first.is_member(user))
|
||||
self.assertFalse(first.is_member(user2))
|
||||
|
||||
def test_group_membership_parent_extra(self):
|
||||
"""Test parent membership"""
|
||||
user = User.objects.create(username="user")
|
||||
user2 = User.objects.create(username="user2")
|
||||
first = Group.objects.create(name="first")
|
||||
second = Group.objects.create(name="second", parent=first)
|
||||
third = Group.objects.create(name="third", parent=second)
|
||||
second.users.add(user)
|
||||
self.assertTrue(first.is_member(user))
|
||||
self.assertFalse(first.is_member(user2))
|
||||
self.assertFalse(third.is_member(user))
|
||||
self.assertFalse(third.is_member(user2))
|
||||
@ -5,6 +5,7 @@ from django.test.testcases import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestImpersonation(TestCase):
|
||||
@ -13,14 +14,14 @@ class TestImpersonation(TestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.other_user = User.objects.create(username="to-impersonate")
|
||||
self.akadmin = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
|
||||
def test_impersonate_simple(self):
|
||||
"""test simple impersonation and un-impersonation"""
|
||||
# test with an inactive user to ensure that still works
|
||||
self.other_user.is_active = False
|
||||
self.other_user.save()
|
||||
self.client.force_login(self.akadmin)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.client.get(
|
||||
reverse(
|
||||
@ -32,13 +33,13 @@ class TestImpersonation(TestCase):
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
response_body = loads(response.content.decode())
|
||||
self.assertEqual(response_body["user"]["username"], self.other_user.username)
|
||||
self.assertEqual(response_body["original"]["username"], self.akadmin.username)
|
||||
self.assertEqual(response_body["original"]["username"], self.user.username)
|
||||
|
||||
self.client.get(reverse("authentik_core:impersonate-end"))
|
||||
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
response_body = loads(response.content.decode())
|
||||
self.assertEqual(response_body["user"]["username"], self.akadmin.username)
|
||||
self.assertEqual(response_body["user"]["username"], self.user.username)
|
||||
self.assertNotIn("original", response_body)
|
||||
|
||||
def test_impersonate_denied(self):
|
||||
@ -46,7 +47,7 @@ class TestImpersonation(TestCase):
|
||||
self.client.force_login(self.other_user)
|
||||
|
||||
self.client.get(
|
||||
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.akadmin.pk})
|
||||
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
|
||||
)
|
||||
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
|
||||
@ -49,7 +49,7 @@ def provider_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||
|
||||
def tester(self: TestModels):
|
||||
model_class = None
|
||||
if test_model._meta.abstract:
|
||||
if test_model._meta.abstract: # pragma: no cover
|
||||
model_class = test_model.__bases__[0]()
|
||||
else:
|
||||
model_class = test_model()
|
||||
@ -59,6 +59,6 @@ def provider_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||
|
||||
|
||||
for model in all_subclasses(Source):
|
||||
setattr(TestModels, f"test_model_{model.__name__}", source_tester_factory(model))
|
||||
setattr(TestModels, f"test_source_{model.__name__}", source_tester_factory(model))
|
||||
for model in all_subclasses(Provider):
|
||||
setattr(TestModels, f"test_model_{model.__name__}", provider_tester_factory(model))
|
||||
setattr(TestModels, f"test_provider_{model.__name__}", provider_tester_factory(model))
|
||||
|
||||
@ -6,7 +6,8 @@ from rest_framework.serializers import ValidationError
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.api.propertymappings import PropertyMappingSerializer
|
||||
from authentik.core.models import PropertyMapping, User
|
||||
from authentik.core.models import PropertyMapping
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestPropertyMappingAPI(APITestCase):
|
||||
@ -17,7 +18,7 @@ class TestPropertyMappingAPI(APITestCase):
|
||||
self.mapping = PropertyMapping.objects.create(
|
||||
name="dummy", expression="""return {'foo': 'bar'}"""
|
||||
)
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_test_call(self):
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import PropertyMapping, User
|
||||
from authentik.core.models import PropertyMapping
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestProvidersAPI(APITestCase):
|
||||
@ -13,7 +14,7 @@ class TestProvidersAPI(APITestCase):
|
||||
self.mapping = PropertyMapping.objects.create(
|
||||
name="dummy", expression="""return {'foo': 'bar'}"""
|
||||
)
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_types(self):
|
||||
|
||||
@ -8,6 +8,7 @@ from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents, User
|
||||
from authentik.core.tasks import clean_expired_models
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestTokenAPI(APITestCase):
|
||||
@ -16,7 +17,7 @@ class TestTokenAPI(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = User.objects.create(username="testuser")
|
||||
self.admin = User.objects.get(username="akadmin")
|
||||
self.admin = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_token_create(self):
|
||||
|
||||
@ -3,7 +3,8 @@ from django.urls.base import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, User
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_tenant
|
||||
from authentik.flows.models import FlowDesignation
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
@ -12,7 +13,7 @@ class TestUsersAPI(APITestCase):
|
||||
"""Test Users API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.admin = User.objects.get(username="akadmin")
|
||||
self.admin = create_test_admin_user()
|
||||
self.user = User.objects.create(username="test-user")
|
||||
|
||||
def test_update_self(self):
|
||||
@ -69,10 +70,8 @@ class TestUsersAPI(APITestCase):
|
||||
|
||||
def test_recovery(self):
|
||||
"""Test user recovery link (no recovery flow set)"""
|
||||
flow = Flow.objects.create(
|
||||
name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY
|
||||
)
|
||||
tenant: Tenant = Tenant.objects.first()
|
||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_recovery = flow
|
||||
tenant.save()
|
||||
self.client.force_login(self.admin)
|
||||
@ -99,10 +98,8 @@ class TestUsersAPI(APITestCase):
|
||||
"""Test user recovery link (no email stage)"""
|
||||
self.user.email = "foo@bar.baz"
|
||||
self.user.save()
|
||||
flow = Flow.objects.create(
|
||||
name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY
|
||||
)
|
||||
tenant: Tenant = Tenant.objects.first()
|
||||
flow = create_test_flow(designation=FlowDesignation.RECOVERY)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_recovery = flow
|
||||
tenant.save()
|
||||
self.client.force_login(self.admin)
|
||||
@ -115,10 +112,8 @@ class TestUsersAPI(APITestCase):
|
||||
"""Test user recovery link"""
|
||||
self.user.email = "foo@bar.baz"
|
||||
self.user.save()
|
||||
flow = Flow.objects.create(
|
||||
name="test", title="test", slug="test", designation=FlowDesignation.RECOVERY
|
||||
)
|
||||
tenant: Tenant = Tenant.objects.first()
|
||||
flow = create_test_flow(FlowDesignation.RECOVERY)
|
||||
tenant: Tenant = create_test_tenant()
|
||||
tenant.flow_recovery = flow
|
||||
tenant.save()
|
||||
|
||||
|
||||
57
authentik/core/tests/utils.py
Normal file
57
authentik/core/tests/utils.py
Normal file
@ -0,0 +1,57 @@
|
||||
"""Test Utils"""
|
||||
from typing import Optional
|
||||
|
||||
from django.utils.text import slugify
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
def create_test_flow(designation: FlowDesignation = FlowDesignation.STAGE_CONFIGURATION) -> Flow:
|
||||
"""Generate a flow that can be used for testing"""
|
||||
uid = generate_id(10)
|
||||
return Flow.objects.create(
|
||||
name=uid,
|
||||
title=uid,
|
||||
slug=slugify(uid),
|
||||
designation=designation,
|
||||
)
|
||||
|
||||
|
||||
def create_test_admin_user(name: Optional[str] = None) -> User:
|
||||
"""Generate a test-admin user"""
|
||||
uid = generate_id(20) if not name else name
|
||||
group = Group.objects.create(name=uid, is_superuser=True)
|
||||
user: User = User.objects.create(
|
||||
username=uid,
|
||||
name=uid,
|
||||
email=f"{uid}@goauthentik.io",
|
||||
)
|
||||
user.set_password(uid)
|
||||
user.save()
|
||||
group.users.add(user)
|
||||
return user
|
||||
|
||||
|
||||
def create_test_tenant() -> Tenant:
|
||||
"""Generate a test tenant, removing all other tenants to make sure this one
|
||||
matches."""
|
||||
uid = generate_id(20)
|
||||
Tenant.objects.all().delete()
|
||||
return Tenant.objects.create(domain=uid, default=True)
|
||||
|
||||
|
||||
def create_test_cert() -> CertificateKeyPair:
|
||||
"""Generate a certificate for testing"""
|
||||
CertificateKeyPair.objects.filter(name="goauthentik.io").delete()
|
||||
builder = CertificateBuilder()
|
||||
builder.common_name = "goauthentik.io"
|
||||
builder.build(
|
||||
subject_alt_names=["goauthentik.io"],
|
||||
validity_days=360,
|
||||
)
|
||||
return builder.save()
|
||||
@ -55,7 +55,7 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
|
||||
@property
|
||||
def private_key(self) -> Optional[RSAPrivateKey]:
|
||||
"""Get python cryptography PrivateKey instance"""
|
||||
if not self._private_key and self._private_key != "":
|
||||
if not self._private_key and self.key_data != "":
|
||||
try:
|
||||
self._private_key = load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in self.key_data.split("\n")])),
|
||||
|
||||
@ -1,25 +1,33 @@
|
||||
"""Crypto tests"""
|
||||
import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.api.used_by import DeleteAction
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.crypto.api import CertificateKeyPairSerializer
|
||||
from authentik.crypto.builder import CertificateBuilder
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
|
||||
|
||||
class TestCrypto(TestCase):
|
||||
class TestCrypto(APITestCase):
|
||||
"""Test Crypto validation"""
|
||||
|
||||
def test_model_private(self):
|
||||
"""Test model private key"""
|
||||
cert = CertificateKeyPair.objects.create(
|
||||
name="test",
|
||||
certificate_data="foo",
|
||||
key_data="foo",
|
||||
)
|
||||
self.assertIsNone(cert.private_key)
|
||||
|
||||
def test_serializer(self):
|
||||
"""Test API Validation"""
|
||||
keypair = CertificateKeyPair.objects.first()
|
||||
keypair = create_test_cert()
|
||||
self.assertTrue(
|
||||
CertificateKeyPairSerializer(
|
||||
data={
|
||||
@ -54,10 +62,38 @@ class TestCrypto(TestCase):
|
||||
self.assertEqual(instance.name, "test-cert")
|
||||
self.assertEqual((instance.certificate.not_valid_after - now).days, 2)
|
||||
|
||||
def test_builder_api(self):
|
||||
"""Test Builder (via API)"""
|
||||
self.client.force_login(create_test_admin_user())
|
||||
self.client.post(
|
||||
reverse("authentik_api:certificatekeypair-generate"),
|
||||
data={"common_name": "foo", "subject_alt_name": "bar,baz", "validity_days": 3},
|
||||
)
|
||||
self.assertTrue(CertificateKeyPair.objects.filter(name="foo").exists())
|
||||
|
||||
def test_builder_api_invalid(self):
|
||||
"""Test Builder (via API) (invalid)"""
|
||||
self.client.force_login(create_test_admin_user())
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:certificatekeypair-generate"),
|
||||
data={},
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_list(self):
|
||||
"""Test API List"""
|
||||
self.client.force_login(create_test_admin_user())
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-list",
|
||||
)
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
def test_certificate_download(self):
|
||||
"""Test certificate export (download)"""
|
||||
self.client.force_login(User.objects.get(username="akadmin"))
|
||||
keypair = CertificateKeyPair.objects.first()
|
||||
self.client.force_login(create_test_admin_user())
|
||||
keypair = create_test_cert()
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-view-certificate",
|
||||
@ -77,8 +113,8 @@ class TestCrypto(TestCase):
|
||||
|
||||
def test_private_key_download(self):
|
||||
"""Test private_key export (download)"""
|
||||
self.client.force_login(User.objects.get(username="akadmin"))
|
||||
keypair = CertificateKeyPair.objects.first()
|
||||
self.client.force_login(create_test_admin_user())
|
||||
keypair = create_test_cert()
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_api:certificatekeypair-view-private-key",
|
||||
@ -98,15 +134,15 @@ class TestCrypto(TestCase):
|
||||
|
||||
def test_used_by(self):
|
||||
"""Test used_by endpoint"""
|
||||
self.client.force_login(User.objects.get(username="akadmin"))
|
||||
keypair = CertificateKeyPair.objects.first()
|
||||
self.client.force_login(create_test_admin_user())
|
||||
keypair = create_test_cert()
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://localhost",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=keypair,
|
||||
)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
|
||||
@ -7,16 +7,25 @@ from django.core.exceptions import SuspiciousOperation
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django_otp.plugins.otp_static.models import StaticToken
|
||||
from guardian.models import UserObjectPermission
|
||||
|
||||
from authentik.core.middleware import LOCAL
|
||||
from authentik.core.models import User
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
from authentik.events.models import Event, EventAction, Notification
|
||||
from authentik.events.signals import EventNewThread
|
||||
from authentik.events.utils import model_to_dict
|
||||
from authentik.lib.sentry import before_send
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
|
||||
IGNORED_MODELS = (
|
||||
Event,
|
||||
Notification,
|
||||
UserObjectPermission,
|
||||
AuthenticatedSession,
|
||||
StaticToken,
|
||||
)
|
||||
|
||||
|
||||
class AuditMiddleware:
|
||||
"""Register handlers for duration of request-response that log creation/update/deletion
|
||||
@ -82,7 +91,7 @@ class AuditMiddleware:
|
||||
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
||||
):
|
||||
"""Signal handler for all object's post_save"""
|
||||
if isinstance(instance, (Event, Notification, UserObjectPermission)):
|
||||
if isinstance(instance, IGNORED_MODELS):
|
||||
return
|
||||
|
||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||
@ -92,7 +101,7 @@ class AuditMiddleware:
|
||||
# pylint: disable=unused-argument
|
||||
def pre_delete_handler(user: User, request: HttpRequest, sender, instance: Model, **_):
|
||||
"""Signal handler for all object's pre_delete"""
|
||||
if isinstance(instance, (Event, Notification, UserObjectPermission)): # pragma: no cover
|
||||
if isinstance(instance, IGNORED_MODELS): # pragma: no cover
|
||||
return
|
||||
|
||||
EventNewThread(
|
||||
|
||||
@ -4,7 +4,6 @@ import uuid
|
||||
from datetime import timedelta
|
||||
from typing import Iterable
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.apps.registry import Apps
|
||||
from django.conf import settings
|
||||
@ -12,6 +11,7 @@ from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
import authentik.events.models
|
||||
import authentik.lib.models
|
||||
from authentik.events.models import EventAction, NotificationSeverity, TransportMode
|
||||
|
||||
|
||||
@ -826,6 +826,8 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="notificationtransport",
|
||||
name="webhook_url",
|
||||
field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]),
|
||||
field=models.TextField(
|
||||
blank=True, validators=[authentik.lib.models.DomainlessURLValidator()]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
# Generated by Django 3.2.7 on 2021-10-04 15:31
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.lib.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
@ -14,6 +15,8 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name="notificationtransport",
|
||||
name="webhook_url",
|
||||
field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]),
|
||||
field=models.TextField(
|
||||
blank=True, validators=[authentik.lib.models.DomainlessURLValidator()]
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@ -6,7 +6,6 @@ from typing import TYPE_CHECKING, Optional, Type, Union
|
||||
from uuid import uuid4
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.validators import URLValidator
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.http.request import QueryDict
|
||||
@ -20,6 +19,7 @@ from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION
|
||||
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.models import DomainlessURLValidator
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.http import get_client_ip, get_http_session
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
@ -224,7 +224,7 @@ class NotificationTransport(models.Model):
|
||||
name = models.TextField(unique=True)
|
||||
mode = models.TextField(choices=TransportMode.choices)
|
||||
|
||||
webhook_url = models.TextField(blank=True, validators=[URLValidator()])
|
||||
webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()])
|
||||
webhook_mapping = models.ForeignKey(
|
||||
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
|
||||
)
|
||||
|
||||
@ -3,14 +3,14 @@ from threading import Thread
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.http import HttpRequest
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.signals import password_changed
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.tasks import event_notification_handler
|
||||
from authentik.events.tasks import event_notification_handler, gdpr_cleanup
|
||||
from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.stages.invitation.models import Invitation
|
||||
@ -108,3 +108,10 @@ def on_password_changed(sender, user: User, password: str, **_):
|
||||
def event_post_save_notification(sender, instance: Event, **_):
|
||||
"""Start task to check if any policies trigger an notification on this event"""
|
||||
event_notification_handler.delay(instance.event_uuid.hex)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=User)
|
||||
# pylint: disable=unused-argument
|
||||
def event_user_pre_delete_cleanup(sender, instance: User, **_):
|
||||
"""If gdpr_compliance is enabled, remove all the user's events"""
|
||||
gdpr_cleanup.delay(instance.pk)
|
||||
|
||||
@ -106,3 +106,11 @@ def notification_transport(self: MonitoredTask, notification_pk: int, transport_
|
||||
except NotificationTransportError as exc:
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
raise exc
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def gdpr_cleanup(user_pk: int):
|
||||
"""cleanup events from gdpr_compliance"""
|
||||
events = Event.objects.filter(user__pk=user_pk)
|
||||
LOGGER.debug("GDPR cleanup, removing events from user", events=events.count())
|
||||
events.delete()
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.events.models import (
|
||||
Event,
|
||||
EventAction,
|
||||
@ -17,7 +17,7 @@ class TestEventsAPI(APITestCase):
|
||||
"""Test Event API"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_top_n(self):
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
|
||||
@ -12,7 +13,7 @@ class TestEventsMiddleware(APITestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_create(self):
|
||||
|
||||
@ -41,6 +41,7 @@ class StageSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"component",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
"flow_set",
|
||||
]
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ from django.test import RequestFactory
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
|
||||
@ -68,7 +68,7 @@ class Command(BaseCommand): # pragma: no cover
|
||||
def benchmark_flows(self, proc_count):
|
||||
"""Get full recovery link"""
|
||||
flow = Flow.objects.get(slug="default-authentication-flow")
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
manager = Manager()
|
||||
return_dict = manager.dict()
|
||||
|
||||
|
||||
@ -149,7 +149,7 @@ class ChallengeStageView(StageView):
|
||||
)
|
||||
challenge_response.initial_data["response_errors"] = full_errors
|
||||
if not challenge_response.is_valid():
|
||||
LOGGER.warning(
|
||||
LOGGER.error(
|
||||
"f(ch): invalid challenge response",
|
||||
binding=self.executor.current_binding,
|
||||
errors=challenge_response.errors,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.flows.api.stages import StageSerializer, StageViewSet
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
@ -47,7 +47,7 @@ class TestFlowsAPI(APITestCase):
|
||||
|
||||
def test_api_diagram(self):
|
||||
"""Test flow diagram."""
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
flow = Flow.objects.create(
|
||||
@ -77,7 +77,7 @@ class TestFlowsAPI(APITestCase):
|
||||
|
||||
def test_api_diagram_no_stages(self):
|
||||
"""Test flow diagram with no stages."""
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
flow = Flow.objects.create(
|
||||
@ -93,7 +93,7 @@ class TestFlowsAPI(APITestCase):
|
||||
|
||||
def test_types(self):
|
||||
"""Test Stage's types endpoint"""
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
|
||||
response = self.client.get(
|
||||
|
||||
@ -6,7 +6,7 @@ from django.test.client import RequestFactory
|
||||
from django.urls.base import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, InvalidResponseAction
|
||||
from authentik.stages.dummy.models import DummyStage
|
||||
@ -18,7 +18,7 @@ class TestFlowInspector(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.request_factory = RequestFactory()
|
||||
self.admin = User.objects.get(username="akadmin")
|
||||
self.admin = create_test_admin_user()
|
||||
self.client.force_login(self.admin)
|
||||
|
||||
def test(self):
|
||||
@ -77,7 +77,7 @@ class TestFlowInspector(APITestCase):
|
||||
|
||||
self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
{"uid_field": "akadmin"},
|
||||
{"uid_field": self.admin.username},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
@ -89,5 +89,5 @@ class TestFlowInspector(APITestCase):
|
||||
self.assertEqual(content["plans"][0]["current_stage"]["stage_obj"]["name"], "ident")
|
||||
self.assertEqual(content["current_plan"]["current_stage"]["stage_obj"]["name"], "dummy2")
|
||||
self.assertEqual(
|
||||
content["current_plan"]["plan_context"]["pending_user"]["username"], "akadmin"
|
||||
content["current_plan"]["plan_context"]["pending_user"]["username"], self.admin.username
|
||||
)
|
||||
|
||||
@ -17,13 +17,13 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||
|
||||
def tester(self: TestModels):
|
||||
model_class = None
|
||||
if test_model._meta.abstract:
|
||||
if test_model._meta.abstract: # pragma: no cover
|
||||
model_class = test_model.__bases__[0]()
|
||||
else:
|
||||
model_class = test_model()
|
||||
self.assertTrue(issubclass(model_class.type, StageView))
|
||||
self.assertIsNotNone(test_model.component)
|
||||
_ = test_model.ui_user_settings
|
||||
_ = model_class.ui_user_settings
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
@ -12,9 +13,8 @@ class TestHelperView(TestCase):
|
||||
|
||||
def test_default_view(self):
|
||||
"""Test that ToDefaultFlow returns the expected URL"""
|
||||
flow = Flow.objects.filter(
|
||||
designation=FlowDesignation.INVALIDATION,
|
||||
).first()
|
||||
Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete()
|
||||
flow = create_test_flow(FlowDesignation.INVALIDATION)
|
||||
response = self.client.get(
|
||||
reverse("authentik_flows:default-invalidation"),
|
||||
)
|
||||
@ -24,9 +24,8 @@ class TestHelperView(TestCase):
|
||||
|
||||
def test_default_view_invalid_plan(self):
|
||||
"""Test that ToDefaultFlow returns the expected URL (with an invalid plan)"""
|
||||
flow = Flow.objects.filter(
|
||||
designation=FlowDesignation.INVALIDATION,
|
||||
).first()
|
||||
Flow.objects.filter(designation=FlowDesignation.INVALIDATION).delete()
|
||||
flow = create_test_flow(FlowDesignation.INVALIDATION)
|
||||
plan = FlowPlan(flow_pk=flow.pk.hex + "aa")
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
|
||||
@ -3,7 +3,9 @@ import os
|
||||
from collections.abc import Mapping
|
||||
from contextlib import contextmanager
|
||||
from glob import glob
|
||||
from json import dumps
|
||||
from json import dumps, loads
|
||||
from json.decoder import JSONDecodeError
|
||||
from sys import argv, stderr
|
||||
from time import time
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
@ -59,7 +61,7 @@ class ConfigLoader:
|
||||
"timestamp": time(),
|
||||
}
|
||||
output.update(kwargs)
|
||||
print(dumps(output))
|
||||
print(dumps(output), file=stderr)
|
||||
|
||||
def update(self, root: dict[str, Any], updatee: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Recursively update dictionary"""
|
||||
@ -81,8 +83,8 @@ class ConfigLoader:
|
||||
try:
|
||||
with open(url.path, "r", encoding="utf8") as _file:
|
||||
value = _file.read()
|
||||
except OSError:
|
||||
self._log("error", f"Failed to read config value from {url.path}")
|
||||
except OSError as exc:
|
||||
self._log("error", f"Failed to read config value from {url.path}: {exc}")
|
||||
value = url.query
|
||||
return value
|
||||
|
||||
@ -123,6 +125,11 @@ class ConfigLoader:
|
||||
if dot_part not in current_obj:
|
||||
current_obj[dot_part] = {}
|
||||
current_obj = current_obj[dot_part]
|
||||
# Check if the value is json, and try to load it
|
||||
try:
|
||||
value = loads(value)
|
||||
except JSONDecodeError:
|
||||
pass
|
||||
current_obj[dot_parts[-1]] = value
|
||||
idx += 1
|
||||
if idx > 0:
|
||||
@ -174,3 +181,9 @@ class ConfigLoader:
|
||||
|
||||
|
||||
CONFIG = ConfigLoader()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(argv) < 2:
|
||||
print(dumps(CONFIG.raw, indent=4))
|
||||
else:
|
||||
print(CONFIG.y(argv[1]))
|
||||
|
||||
@ -64,16 +64,21 @@ outposts:
|
||||
# %(type)s: Outpost type; proxy, ldap, etc
|
||||
# %(version)s: Current version; 2021.4.1
|
||||
# %(build_hash)s: Build hash if you're running a beta version
|
||||
container_image_base: env://AUTHENTIK_OUTPOSTS__DOCKER_IMAGE_BASE?goauthentik.io/%(type)s:%(version)s
|
||||
container_image_base: goauthentik.io/%(type)s:%(version)s
|
||||
|
||||
cookie_domain: null
|
||||
disable_update_check: false
|
||||
disable_startup_analytics: false
|
||||
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
|
||||
geoip: "./GeoLite2-City.mmdb"
|
||||
|
||||
# Can't currently be configured via environment variables, only yaml
|
||||
footer_links:
|
||||
- name: Documentation
|
||||
href: https://goauthentik.io/docs/?utm_source=authentik
|
||||
- name: authentik Website
|
||||
href: https://goauthentik.io/?utm_source=authentik
|
||||
|
||||
default_user_change_email: true
|
||||
default_user_change_username: true
|
||||
|
||||
gdpr_compliance: true
|
||||
|
||||
@ -66,3 +66,11 @@ class DomainlessURLValidator(URLValidator):
|
||||
r"\Z",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
self.schemes = ["http", "https", "blank"] + list(self.schemes)
|
||||
|
||||
def __call__(self, value):
|
||||
# Check if the scheme is valid.
|
||||
scheme = value.split("://")[0].lower()
|
||||
if scheme not in self.schemes:
|
||||
value = "default" + value
|
||||
return super().__call__(value)
|
||||
|
||||
@ -8,11 +8,13 @@ from botocore.exceptions import BotoCoreError
|
||||
from celery.exceptions import CeleryError
|
||||
from channels.middleware import BaseMiddleware
|
||||
from channels_redis.core import ChannelFull
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError
|
||||
from django.db import InternalError, OperationalError, ProgrammingError
|
||||
from django.http.response import Http404
|
||||
from django_redis.exceptions import ConnectionInterrupted
|
||||
from docker.errors import DockerException
|
||||
from h11 import LocalProtocolError
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
from redis.exceptions import ConnectionError as RedisConnectionError
|
||||
from redis.exceptions import RedisError, ResponseError
|
||||
@ -72,6 +74,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
|
||||
# websocket errors
|
||||
ChannelFull,
|
||||
WebSocketException,
|
||||
LocalProtocolError,
|
||||
# rest_framework error
|
||||
APIException,
|
||||
# celery errors
|
||||
@ -90,6 +93,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
|
||||
# End-user errors
|
||||
Http404,
|
||||
)
|
||||
exc_value = None
|
||||
if "exc_info" in hint:
|
||||
_, exc_value, _ = hint["exc_info"]
|
||||
if isinstance(exc_value, ignored_classes):
|
||||
@ -103,6 +107,10 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
|
||||
"asyncio",
|
||||
"multiprocessing",
|
||||
"django_redis",
|
||||
"django.security.DisallowedHost",
|
||||
]:
|
||||
return None
|
||||
LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None))
|
||||
if settings.DEBUG:
|
||||
return None
|
||||
return event
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"""Test Evaluator base functions"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
|
||||
|
||||
@ -19,12 +19,11 @@ class TestEvaluator(TestCase):
|
||||
|
||||
def test_user_by(self):
|
||||
"""Test expr_user_by"""
|
||||
self.assertIsNotNone(BaseEvaluator.expr_user_by(username="akadmin"))
|
||||
user = create_test_admin_user()
|
||||
self.assertIsNotNone(BaseEvaluator.expr_user_by(username=user.username))
|
||||
self.assertIsNone(BaseEvaluator.expr_user_by(username="bar"))
|
||||
self.assertIsNone(BaseEvaluator.expr_user_by(foo="bar"))
|
||||
|
||||
def test_is_group_member(self):
|
||||
"""Test expr_is_group_member"""
|
||||
self.assertFalse(
|
||||
BaseEvaluator.expr_is_group_member(User.objects.get(username="akadmin"), name="test")
|
||||
)
|
||||
self.assertFalse(BaseEvaluator.expr_is_group_member(create_test_admin_user(), name="test"))
|
||||
|
||||
@ -1,17 +1,24 @@
|
||||
"""Test HTTP Helpers"""
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_CAN_OVERRIDE_IP, Token, TokenIntents, User
|
||||
from authentik.core.models import USER_ATTRIBUTE_CAN_OVERRIDE_IP, Token, TokenIntents
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.lib.utils.http import OUTPOST_REMOTE_IP_HEADER, OUTPOST_TOKEN_HEADER, get_client_ip
|
||||
from authentik.lib.views import bad_request_message
|
||||
|
||||
|
||||
class TestHTTP(TestCase):
|
||||
"""Test HTTP Helpers"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_bad_request_message(self):
|
||||
"""test bad_request_message"""
|
||||
request = self.factory.get("/")
|
||||
self.assertEqual(bad_request_message(request, "foo").status_code, 400)
|
||||
|
||||
def test_normal(self):
|
||||
"""Test normal request"""
|
||||
request = self.factory.get("/")
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
"""authentik lib reflection utilities"""
|
||||
import os
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from django.conf import settings
|
||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
def all_subclasses(cls, sort=True):
|
||||
@ -42,3 +47,16 @@ def get_apps():
|
||||
for _app in apps.get_app_configs():
|
||||
if _app.name.startswith("authentik"):
|
||||
yield _app
|
||||
|
||||
|
||||
def get_env() -> str:
|
||||
"""Get environment in which authentik is currently running"""
|
||||
if SERVICE_HOST_ENV_NAME in os.environ:
|
||||
return "kubernetes"
|
||||
if "CI" in os.environ:
|
||||
return "ci"
|
||||
if Path("/tmp/authentik-mode").exists(): # nosec
|
||||
return "compose"
|
||||
if CONFIG.y_bool("debug"):
|
||||
return "dev"
|
||||
return "custom"
|
||||
|
||||
@ -20,5 +20,5 @@ def managed_reconcile(self: MonitoredTask):
|
||||
self.set_status(
|
||||
TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated managed models."])
|
||||
)
|
||||
except DatabaseError as exc:
|
||||
except DatabaseError as exc: # pragma: no cover
|
||||
self.set_status(TaskResult(TaskResultStatus.WARNING, [str(exc)]))
|
||||
|
||||
13
authentik/managed/tests.py
Normal file
13
authentik/managed/tests.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""managed tests"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.managed.tasks import managed_reconcile
|
||||
|
||||
|
||||
class TestManaged(TestCase):
|
||||
"""managed tests"""
|
||||
|
||||
def test_reconcile(self):
|
||||
"""Test reconcile"""
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
managed_reconcile() # pylint: disable=no-value-for-parameter
|
||||
@ -46,6 +46,7 @@ class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
|
||||
"component",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ class OutpostConsumer(AuthJsonConsumer):
|
||||
self.send_json(asdict(response))
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def event_update(self, event):
|
||||
def event_update(self, event): # pragma: no cover
|
||||
"""Event handler which is called by post_save signals, Send update instruction"""
|
||||
self.send_json(
|
||||
asdict(WebsocketMessage(instruction=WebsocketMessageInstruction.TRIGGER_UPDATE))
|
||||
|
||||
@ -67,8 +67,6 @@ class OutpostConfig:
|
||||
authentik_host_browser: str = ""
|
||||
|
||||
log_level: str = CONFIG.y("log_level")
|
||||
error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
|
||||
error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer")
|
||||
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
||||
|
||||
docker_network: Optional[str] = field(default=None)
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import PropertyMapping, User
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.core.models import PropertyMapping
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.outposts.api.outposts import OutpostSerializer
|
||||
from authentik.outposts.models import OutpostType, default_outpost_config
|
||||
from authentik.providers.ldap.models import LDAPProvider
|
||||
@ -18,7 +18,7 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
|
||||
self.mapping = PropertyMapping.objects.create(
|
||||
name="dummy", expression="""return {'foo': 'bar'}"""
|
||||
)
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_outpost_validaton(self):
|
||||
@ -30,7 +30,7 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
|
||||
"config": default_outpost_config(),
|
||||
"providers": [
|
||||
ProxyProvider.objects.create(
|
||||
name="test", authorization_flow=Flow.objects.first()
|
||||
name="test", authorization_flow=create_test_flow()
|
||||
).pk
|
||||
],
|
||||
}
|
||||
@ -43,7 +43,7 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
|
||||
"config": default_outpost_config(),
|
||||
"providers": [
|
||||
LDAPProvider.objects.create(
|
||||
name="test", authorization_flow=Flow.objects.first()
|
||||
name="test", authorization_flow=create_test_flow()
|
||||
).pk
|
||||
],
|
||||
}
|
||||
@ -60,9 +60,7 @@ class TestOutpostServiceConnectionsAPI(APITestCase):
|
||||
|
||||
def test_outpost_config(self):
|
||||
"""Test Outpost's config field"""
|
||||
provider = ProxyProvider.objects.create(
|
||||
name="test", authorization_flow=Flow.objects.first()
|
||||
)
|
||||
provider = ProxyProvider.objects.create(name="test", authorization_flow=create_test_flow())
|
||||
invalid = OutpostSerializer(data={"name": "foo", "providers": [provider.pk], "config": ""})
|
||||
self.assertFalse(invalid.is_valid())
|
||||
self.assertIn("config", invalid.errors)
|
||||
|
||||
15
authentik/outposts/tests/test_commands.py
Normal file
15
authentik/outposts/tests/test_commands.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""management command tests"""
|
||||
from io import StringIO
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestManagementCommands(TestCase):
|
||||
"""management command tests"""
|
||||
|
||||
def test_repair_permissions(self):
|
||||
"""Test repair_permissions"""
|
||||
out = StringIO()
|
||||
call_command("repair_permissions", stdout=out)
|
||||
self.assertNotEqual(out.getvalue(), "")
|
||||
@ -4,8 +4,7 @@ from django.contrib.auth.management import create_permissions
|
||||
from django.test import TestCase
|
||||
from guardian.models import UserObjectPermission
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.outposts.models import Outpost, OutpostType
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
|
||||
@ -23,7 +22,7 @@ class OutpostTests(TestCase):
|
||||
name="test",
|
||||
internal_host="http://localhost",
|
||||
external_host="http://localhost",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
)
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
@ -45,7 +44,7 @@ class OutpostTests(TestCase):
|
||||
self.assertEqual(permissions[1].object_pk, str(provider.pk))
|
||||
|
||||
# Provider requires a certificate-key-pair, user should have permissions for it
|
||||
keypair = CertificateKeyPair.objects.first()
|
||||
keypair = create_test_cert()
|
||||
provider.certificate = keypair
|
||||
provider.save()
|
||||
permissions = UserObjectPermission.objects.filter(user=outpost.user).order_by(
|
||||
|
||||
97
authentik/outposts/tests/test_ws.py
Normal file
97
authentik/outposts/tests/test_ws.py
Normal file
@ -0,0 +1,97 @@
|
||||
"""Websocket tests"""
|
||||
from dataclasses import asdict
|
||||
|
||||
from channels.routing import URLRouter
|
||||
from channels.testing import WebsocketCommunicator
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.outposts.channels import WebsocketMessage, WebsocketMessageInstruction
|
||||
from authentik.outposts.models import Outpost, OutpostType
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from authentik.root import websocket
|
||||
|
||||
|
||||
class TestOutpostWS(TransactionTestCase):
|
||||
"""Websocket tests"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.provider: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="test",
|
||||
internal_host="http://localhost",
|
||||
external_host="http://localhost",
|
||||
authorization_flow=Flow.objects.create(
|
||||
name="foo", slug="foo", designation=FlowDesignation.AUTHORIZATION
|
||||
),
|
||||
)
|
||||
self.outpost: Outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
type=OutpostType.PROXY,
|
||||
)
|
||||
self.outpost.providers.add(self.provider)
|
||||
self.token = self.outpost.token.key
|
||||
|
||||
async def test_auth(self):
|
||||
"""Test auth without token"""
|
||||
communicator = WebsocketCommunicator(
|
||||
URLRouter(websocket.websocket_urlpatterns), f"/ws/outpost/{self.outpost.pk}/"
|
||||
)
|
||||
connected, _ = await communicator.connect()
|
||||
self.assertFalse(connected)
|
||||
|
||||
async def test_auth_valid(self):
|
||||
"""Test auth with token"""
|
||||
communicator = WebsocketCommunicator(
|
||||
URLRouter(websocket.websocket_urlpatterns),
|
||||
f"/ws/outpost/{self.outpost.pk}/",
|
||||
{b"authorization": f"Bearer {self.token}".encode()},
|
||||
)
|
||||
connected, _ = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
|
||||
async def test_send(self):
|
||||
"""Test sending of Hello"""
|
||||
communicator = WebsocketCommunicator(
|
||||
URLRouter(websocket.websocket_urlpatterns),
|
||||
f"/ws/outpost/{self.outpost.pk}/",
|
||||
{b"authorization": f"Bearer {self.token}".encode()},
|
||||
)
|
||||
connected, _ = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
await communicator.send_json_to(
|
||||
asdict(
|
||||
WebsocketMessage(
|
||||
instruction=WebsocketMessageInstruction.HELLO,
|
||||
args={
|
||||
"version": __version__,
|
||||
"buildHash": "foo",
|
||||
"uuid": "123",
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
response = await communicator.receive_json_from()
|
||||
self.assertEqual(
|
||||
response, asdict(WebsocketMessage(instruction=WebsocketMessageInstruction.ACK, args={}))
|
||||
)
|
||||
await communicator.disconnect()
|
||||
|
||||
async def test_send_ack(self):
|
||||
"""Test sending of ACK"""
|
||||
communicator = WebsocketCommunicator(
|
||||
URLRouter(websocket.websocket_urlpatterns),
|
||||
f"/ws/outpost/{self.outpost.pk}/",
|
||||
{b"authorization": f"Bearer {self.token}".encode()},
|
||||
)
|
||||
connected, _ = await communicator.connect()
|
||||
self.assertTrue(connected)
|
||||
await communicator.send_json_to(
|
||||
asdict(
|
||||
WebsocketMessage(
|
||||
instruction=WebsocketMessageInstruction.ACK,
|
||||
args={},
|
||||
)
|
||||
)
|
||||
)
|
||||
await communicator.disconnect()
|
||||
@ -66,6 +66,7 @@ class PolicySerializer(ModelSerializer, MetaNameSerializer):
|
||||
"component",
|
||||
"verbose_name",
|
||||
"verbose_name_plural",
|
||||
"meta_model_name",
|
||||
"bound_to",
|
||||
]
|
||||
depth = 3
|
||||
|
||||
@ -50,7 +50,7 @@ class PolicyEvaluator(BaseEvaluator):
|
||||
if device_class == device_type:
|
||||
return True
|
||||
return False
|
||||
return len(user_devices) > 0
|
||||
return len(list(user_devices)) > 0
|
||||
|
||||
def set_policy_request(self, request: PolicyRequest):
|
||||
"""Update context based on policy request (if http request is given, update that too)"""
|
||||
|
||||
@ -26,7 +26,7 @@ class TestHIBPPolicy(TestCase):
|
||||
name="test_false",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "password"
|
||||
request.context["password"] = "password" # nosec
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertTrue(result.messages[0].startswith("Password exists on "))
|
||||
|
||||
@ -65,14 +65,14 @@ class PolicyBinding(SerializerModel):
|
||||
# This is quite an ugly hack to prevent pylint from trying
|
||||
# to resolve authentik_core.models.Group
|
||||
# as python import path
|
||||
"authentik_core." + "Group",
|
||||
"authentik_core.Group",
|
||||
on_delete=models.CASCADE,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
user = models.ForeignKey(
|
||||
"authentik_core." + "User",
|
||||
"authentik_core.User",
|
||||
on_delete=models.CASCADE,
|
||||
default=None,
|
||||
null=True,
|
||||
@ -96,7 +96,7 @@ class PolicyBinding(SerializerModel):
|
||||
self.policy: Policy
|
||||
return self.policy.passes(request)
|
||||
if self.group:
|
||||
return PolicyResult(self.group.users.filter(pk=request.user.pk).exists())
|
||||
return PolicyResult(self.group.is_member(request.user))
|
||||
if self.user:
|
||||
return PolicyResult(request.user == self.user)
|
||||
return PolicyResult(False)
|
||||
|
||||
@ -30,7 +30,7 @@ class TestPasswordPolicy(TestCase):
|
||||
def test_failed_length(self):
|
||||
"""Password too short"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "test"
|
||||
request.context["password"] = "test" # nosec
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages, ("test message",))
|
||||
@ -38,7 +38,7 @@ class TestPasswordPolicy(TestCase):
|
||||
def test_failed_lowercase(self):
|
||||
"""not enough lowercase"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe"
|
||||
request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe" # nosec
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages, ("test message",))
|
||||
@ -46,7 +46,7 @@ class TestPasswordPolicy(TestCase):
|
||||
def test_failed_uppercase(self):
|
||||
"""not enough uppercase"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "tttttttttttttttttttttttE"
|
||||
request.context["password"] = "tttttttttttttttttttttttE" # nosec
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages, ("test message",))
|
||||
@ -54,7 +54,7 @@ class TestPasswordPolicy(TestCase):
|
||||
def test_failed_symbols(self):
|
||||
"""not enough uppercase"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = "TETETETETETETETETETETETETe!!!"
|
||||
request.context["password"] = "TETETETETETETETETETETETETe!!!" # nosec
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages, ("test message",))
|
||||
@ -62,7 +62,7 @@ class TestPasswordPolicy(TestCase):
|
||||
def test_true(self):
|
||||
"""Positive password case"""
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
request.context["password"] = generate_key() + "ee!!!"
|
||||
request.context["password"] = generate_key() + "ee!!!" # nosec
|
||||
result: PolicyResult = self.policy.passes(request)
|
||||
self.assertTrue(result.passing)
|
||||
self.assertEqual(result.messages, tuple())
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@ class TestBindingsAPI(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.pbm = PolicyBindingModel.objects.create()
|
||||
self.group = Group.objects.first()
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.group = self.user.ak_groups.first()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_valid_binding(self):
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ class TestPoliciesAPI(APITestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.policy = DummyPolicy.objects.create(name="dummy", result=True)
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_test_call(self):
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""LDAPProvider API Views"""
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.fields import CharField, ListField
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||
|
||||
@ -11,6 +11,8 @@ from authentik.providers.ldap.models import LDAPProvider
|
||||
class LDAPProviderSerializer(ProviderSerializer):
|
||||
"""LDAPProvider Serializer"""
|
||||
|
||||
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
||||
|
||||
class Meta:
|
||||
|
||||
model = LDAPProvider
|
||||
@ -21,6 +23,8 @@ class LDAPProviderSerializer(ProviderSerializer):
|
||||
"tls_server_name",
|
||||
"uid_start_number",
|
||||
"gid_start_number",
|
||||
"outpost_set",
|
||||
"search_mode",
|
||||
]
|
||||
|
||||
|
||||
@ -65,6 +69,7 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
|
||||
"tls_server_name",
|
||||
"uid_start_number",
|
||||
"gid_start_number",
|
||||
"search_mode",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
# Generated by Django 3.2.8 on 2021-11-05 09:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
replaces = [
|
||||
("authentik_providers_ldap", "0001_initial"),
|
||||
("authentik_providers_ldap", "0002_ldapprovider_search_group"),
|
||||
("authentik_providers_ldap", "0003_auto_20210713_1138"),
|
||||
("authentik_providers_ldap", "0004_auto_20210713_2115"),
|
||||
("authentik_providers_ldap", "0005_ldapprovider_search_mode"),
|
||||
]
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0019_source_managed"),
|
||||
("authentik_crypto", "0002_create_self_signed_kp"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LDAPProvider",
|
||||
fields=[
|
||||
(
|
||||
"provider_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.provider",
|
||||
),
|
||||
),
|
||||
(
|
||||
"base_dn",
|
||||
models.TextField(
|
||||
default="DC=ldap,DC=goauthentik,DC=io",
|
||||
help_text="DN under which objects are accessible.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"search_group",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
help_text="Users in this group can do search queries. If not set, every user can execute search queries.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="authentik_core.group",
|
||||
),
|
||||
),
|
||||
(
|
||||
"certificate",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="authentik_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
("tls_server_name", models.TextField(blank=True, default="")),
|
||||
(
|
||||
"gid_start_number",
|
||||
models.IntegerField(
|
||||
default=4000,
|
||||
help_text="The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
|
||||
),
|
||||
),
|
||||
(
|
||||
"uid_start_number",
|
||||
models.IntegerField(
|
||||
default=2000,
|
||||
help_text="The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
|
||||
),
|
||||
),
|
||||
(
|
||||
"search_mode",
|
||||
models.TextField(
|
||||
choices=[("direct", "Direct"), ("cached", "Cached")], default="direct"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "LDAP Provider",
|
||||
"verbose_name_plural": "LDAP Providers",
|
||||
},
|
||||
bases=("authentik_core.provider", models.Model),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.8 on 2021-11-05 09:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_ldap", "0004_auto_20210713_2115"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="ldapprovider",
|
||||
name="search_mode",
|
||||
field=models.TextField(
|
||||
choices=[("direct", "Direct"), ("cached", "Cached")], default="direct"
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -10,6 +10,13 @@ from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.outposts.models import OutpostModel
|
||||
|
||||
|
||||
class SearchModes(models.TextChoices):
|
||||
"""Search modes"""
|
||||
|
||||
DIRECT = "direct"
|
||||
CACHED = "cached"
|
||||
|
||||
|
||||
class LDAPProvider(OutpostModel, Provider):
|
||||
"""Allow applications to authenticate against authentik's users using LDAP."""
|
||||
|
||||
@ -59,6 +66,8 @@ class LDAPProvider(OutpostModel, Provider):
|
||||
),
|
||||
)
|
||||
|
||||
search_mode = models.TextField(default=SearchModes.DIRECT, choices=SearchModes.choices)
|
||||
|
||||
@property
|
||||
def launch_url(self) -> Optional[str]:
|
||||
"""LDAP never has a launch URL"""
|
||||
|
||||
@ -2,8 +2,7 @@
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.providers.oauth2.models import JWTAlgorithms
|
||||
|
||||
|
||||
@ -12,7 +11,7 @@ class TestOAuth2ProviderAPI(APITestCase):
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_validate(self):
|
||||
@ -24,9 +23,7 @@ class TestOAuth2ProviderAPI(APITestCase):
|
||||
data={
|
||||
"name": "test",
|
||||
"jwt_alg": str(JWTAlgorithms.RS256),
|
||||
"authorization_flow": Flow.objects.filter(designation=FlowDesignation.AUTHORIZATION)
|
||||
.first()
|
||||
.pk,
|
||||
"authorization_flow": create_test_flow().pk,
|
||||
},
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
|
||||
@ -3,8 +3,8 @@ from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.flows.challenge import ChallengeTypes
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
@ -43,7 +43,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
with self.assertRaises(AuthorizeError):
|
||||
@ -63,7 +63,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
@ -85,7 +85,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get("/", data={"response_type": "code", "client_id": "test"})
|
||||
@ -105,7 +105,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
request = self.factory.get(
|
||||
@ -184,7 +184,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_id()
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
self.client.get(
|
||||
@ -218,11 +218,11 @@ class TestAuthorize(OAuthTestCase):
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=flow,
|
||||
redirect_uris="http://localhost",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
Application.objects.create(name="app", slug="app", provider=provider)
|
||||
state = generate_id()
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
self.client.force_login(user)
|
||||
# Step 1, initiate params and get redirect to flow
|
||||
self.client.get(
|
||||
|
||||
@ -6,8 +6,7 @@ from django.urls.base import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.core.tests.utils import create_test_cert, create_test_flow
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
|
||||
@ -24,9 +23,9 @@ class TestJWKS(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||
response = self.client.get(
|
||||
@ -40,7 +39,7 @@ class TestJWKS(OAuthTestCase):
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||
|
||||
@ -5,10 +5,9 @@ from django.test import RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
@ -34,12 +33,12 @@ class TestToken(OAuthTestCase):
|
||||
name="test",
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
code = AuthorizationCode.objects.create(code="foobar", provider=provider, user=user)
|
||||
request = self.factory.post(
|
||||
"/",
|
||||
@ -61,9 +60,9 @@ class TestToken(OAuthTestCase):
|
||||
name="test",
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
request = self.factory.post(
|
||||
@ -84,12 +83,12 @@ class TestToken(OAuthTestCase):
|
||||
name="test",
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
@ -113,15 +112,15 @@ class TestToken(OAuthTestCase):
|
||||
name="test",
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
self.app.save()
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
code = AuthorizationCode.objects.create(
|
||||
code="foobar", provider=provider, user=user, is_open_id=True
|
||||
)
|
||||
@ -155,15 +154,15 @@ class TestToken(OAuthTestCase):
|
||||
name="test",
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
self.app.save()
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
@ -204,12 +203,12 @@ class TestToken(OAuthTestCase):
|
||||
name="test",
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
@ -249,15 +248,15 @@ class TestToken(OAuthTestCase):
|
||||
name="test",
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
self.app.save()
|
||||
header = b64encode(f"{provider.client_id}:{provider.client_secret}".encode()).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
user = create_test_admin_user()
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
|
||||
@ -5,10 +5,9 @@ from dataclasses import asdict
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.core.models import Application
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_cert, create_test_flow
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.oauth2.models import IDToken, OAuth2Provider, RefreshToken, ScopeMapping
|
||||
@ -26,15 +25,15 @@ class TestUserinfo(OAuthTestCase):
|
||||
name="test",
|
||||
client_id=generate_id(),
|
||||
client_secret=generate_key(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
authorization_flow=create_test_flow(),
|
||||
redirect_uris="",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
rsa_key=create_test_cert(),
|
||||
)
|
||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = self.provider
|
||||
self.app.save()
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.user = create_test_admin_user()
|
||||
self.token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=self.provider,
|
||||
user=self.user,
|
||||
@ -57,12 +56,12 @@ class TestUserinfo(OAuthTestCase):
|
||||
self.assertJSONEqual(
|
||||
force_str(res.content),
|
||||
{
|
||||
"name": "authentik Default Admin",
|
||||
"given_name": "authentik Default Admin",
|
||||
"name": self.user.name,
|
||||
"given_name": self.user.name,
|
||||
"family_name": "",
|
||||
"preferred_username": "akadmin",
|
||||
"nickname": "akadmin",
|
||||
"groups": ["authentik Admins"],
|
||||
"preferred_username": self.user.name,
|
||||
"nickname": self.user.name,
|
||||
"groups": [group.name for group in self.user.ak_groups.all()],
|
||||
"sub": "bar",
|
||||
},
|
||||
)
|
||||
@ -80,12 +79,12 @@ class TestUserinfo(OAuthTestCase):
|
||||
self.assertJSONEqual(
|
||||
force_str(res.content),
|
||||
{
|
||||
"name": "authentik Default Admin",
|
||||
"given_name": "authentik Default Admin",
|
||||
"name": self.user.name,
|
||||
"given_name": self.user.name,
|
||||
"family_name": "",
|
||||
"preferred_username": "akadmin",
|
||||
"nickname": "akadmin",
|
||||
"groups": ["authentik Admins"],
|
||||
"preferred_username": self.user.name,
|
||||
"nickname": self.user.name,
|
||||
"groups": [group.name for group in self.user.ak_groups.all()],
|
||||
"sub": "bar",
|
||||
},
|
||||
)
|
||||
|
||||
@ -369,7 +369,7 @@ class OAuthFulfillmentStage(StageView):
|
||||
if self.params.grant_type == GrantTypes.HYBRID:
|
||||
query_fragment["code"] = code.code
|
||||
|
||||
query_fragment["token_type"] = "bearer"
|
||||
query_fragment["token_type"] = "bearer" # nosec
|
||||
query_fragment["expires_in"] = int(
|
||||
timedelta_from_string(self.provider.access_code_validity).total_seconds()
|
||||
)
|
||||
|
||||
@ -11,6 +11,7 @@ from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.models import ScopeMapping
|
||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
|
||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||
|
||||
@ -36,6 +37,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
||||
"""ProxyProvider Serializer"""
|
||||
|
||||
redirect_uris = CharField(read_only=True)
|
||||
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
||||
|
||||
def validate(self, attrs) -> dict[Any, str]:
|
||||
"""Check that internal_host is set when mode is Proxy"""
|
||||
@ -74,6 +76,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
||||
"redirect_uris",
|
||||
"cookie_domain",
|
||||
"token_validity",
|
||||
"outpost_set",
|
||||
]
|
||||
|
||||
|
||||
@ -108,6 +111,7 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
|
||||
oidc_configuration = SerializerMethodField()
|
||||
token_validity = SerializerMethodField()
|
||||
scopes_to_request = SerializerMethodField()
|
||||
|
||||
@extend_schema_field(OpenIDConnectConfigurationSerializer)
|
||||
def get_oidc_configuration(self, obj: ProxyProvider):
|
||||
@ -118,6 +122,14 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
"""Get token validity as second count"""
|
||||
return timedelta_from_string(obj.token_validity).total_seconds()
|
||||
|
||||
def get_scopes_to_request(self, obj: ProxyProvider) -> list[str]:
|
||||
"""Get all the scope names the outpost should request,
|
||||
including custom-defined ones"""
|
||||
scope_names = set(
|
||||
ScopeMapping.objects.filter(provider__in=[obj]).values_list("scope_name", flat=True)
|
||||
)
|
||||
return list(scope_names)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ProxyProvider
|
||||
@ -139,6 +151,7 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
"mode",
|
||||
"cookie_domain",
|
||||
"token_validity",
|
||||
"scopes_to_request",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -110,14 +110,14 @@ class Migration(migrations.Migration):
|
||||
"require_signing",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Require Requests to be signed by an X509 Certificate. Must match the Certificate selected in `Singing Keypair`.",
|
||||
help_text="Require Requests to be signed by an X509 Certificate. Must match the Certificate selected in `Signing Keypair`.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"signing_kp",
|
||||
models.ForeignKey(
|
||||
default=None,
|
||||
help_text="Singing is enabled upon selection of a Key Pair.",
|
||||
help_text="Signing is enabled upon selection of a Key Pair.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="authentik_crypto.CertificateKeyPair",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user