Compare commits
486 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b730dec54 | |||
| 2aacb311bc | |||
| 40055ef01b | |||
| 75608dce5c | |||
| b0f7083879 | |||
| 62bf79ce32 | |||
| 7a16c9cb14 | |||
| d29d161ac6 | |||
| 653631ac77 | |||
| cde303e780 | |||
| aa359a032c | |||
| 6491065aab | |||
| 79eec5a3a0 | |||
| cd5e091937 | |||
| 7ed8952803 | |||
| c1f302fb7c | |||
| cb31e52d0e | |||
| 782764ac73 | |||
| d0c56325ef | |||
| 73d57d6f82 | |||
| 2716a26887 | |||
| 0452537e8b | |||
| d1a1bfbbc5 | |||
| a69fcbca9a | |||
| 1ac4dacc3b | |||
| bcf7e162a4 | |||
| f44956bd61 | |||
| cb37e5c10e | |||
| 73bb778d62 | |||
| b612a82e16 | |||
| 83991c743e | |||
| 09f43ca43b | |||
| 1c91835a26 | |||
| c032914092 | |||
| 3634bf4629 | |||
| 45f99fbaf0 | |||
| e31a3307b5 | |||
| d28fcca344 | |||
| c296e1214c | |||
| d676cf6e3f | |||
| 39d87841d0 | |||
| fcd879034c | |||
| b285814e24 | |||
| 1a6ea72c09 | |||
| c251b87f8c | |||
| 21a9aa229a | |||
| 5f6565ee27 | |||
| afad55a357 | |||
| f25d76fa43 | |||
| 10b45d954e | |||
| 339eaf37f2 | |||
| f723fdd551 | |||
| 8359f0bfb3 | |||
| ee610a906a | |||
| 828eeb5ebb | |||
| c9c177d8f9 | |||
| c19afa4f16 | |||
| 941bc61b31 | |||
| 282b364606 | |||
| ad4bc4083d | |||
| ebe282eb1a | |||
| 830c26ca25 | |||
| ed3b4a3d4a | |||
| 975c4ddc04 | |||
| 7e2896298a | |||
| cba9cf8361 | |||
| bf12580f64 | |||
| 75ef4ce596 | |||
| c2f3ce11b0 | |||
| 3c256fecc6 | |||
| 0285b84133 | |||
| 99a371a02c | |||
| c7e6eb8896 | |||
| 674bd9e05c | |||
| b79901df87 | |||
| b248f450dd | |||
| 05db9e5c40 | |||
| 234a5e2b66 | |||
| aea1736f70 | |||
| 9f4a4449f5 | |||
| b6b55e2336 | |||
| 8f2805e05b | |||
| 4f3583cd7e | |||
| 617e90dca3 | |||
| f7408626a8 | |||
| 4dcb15af46 | |||
| 89beb7a9f7 | |||
| 28eeb4798e | |||
| 79b92e764e | |||
| 919336a519 | |||
| 27e04589c1 | |||
| ba44fbdac8 | |||
| 0e093a8917 | |||
| d0bfb99859 | |||
| 93bdea3769 | |||
| e681654af7 | |||
| cab7593dca | |||
| cf92f9aefc | |||
| 8d72b3498d | |||
| 42ab858c50 | |||
| a1abae9ab1 | |||
| 8f36b49061 | |||
| 64b4e851ce | |||
| 40a62ac1e5 | |||
| 5df60e4d87 | |||
| 50ebc8522d | |||
| eddca478dc | |||
| 99a7fca08e | |||
| a7e3602908 | |||
| 74169860cf | |||
| 52bb774f73 | |||
| f26fcaf825 | |||
| b8e92e2f11 | |||
| 08adfc94d6 | |||
| 236fafb735 | |||
| 5ad9ddee3c | |||
| 24d220ff49 | |||
| 3364c195b7 | |||
| 50aa87d141 | |||
| 72b375023d | |||
| 77ba186818 | |||
| 2fe6de0505 | |||
| bf9e969b53 | |||
| 184f119b16 | |||
| ebc06f1abe | |||
| 0f8880ab0a | |||
| ee56da5092 | |||
| 2152004502 | |||
| 45d0b80d02 | |||
| 96065eb942 | |||
| ac944fee8b | |||
| 1d0e5fc353 | |||
| 1f97420207 | |||
| ae07f13a87 | |||
| 0aec504170 | |||
| 3b4c9bcc57 | |||
| 5182a6741e | |||
| da7635ae5c | |||
| a92a0fb60a | |||
| cb10c1753b | |||
| ae654bd4c8 | |||
| 28192655ec | |||
| 9582294eb8 | |||
| 0172430d7d | |||
| 1454b65933 | |||
| 432a7792e2 | |||
| 54069618b4 | |||
| 81feb313df | |||
| e6b275add3 | |||
| 27016a5527 | |||
| 4c29d517f0 | |||
| 180d27cc37 | |||
| 5a8b356dc7 | |||
| 3195640776 | |||
| f463296d47 | |||
| adf4b23c01 | |||
| d900a2b6a9 | |||
| 95a2fddfa8 | |||
| 8f7d21b692 | |||
| 3f84abec2f | |||
| b5c857aff4 | |||
| f8dee09107 | |||
| 84a800583c | |||
| 88de94f014 | |||
| 25549ec339 | |||
| fe4923bff6 | |||
| bb1a0b6bd2 | |||
| 879b5ead71 | |||
| 1670ec9167 | |||
| ac52667327 | |||
| 0d7c5c2108 | |||
| 73e3d19384 | |||
| f6e0f0282d | |||
| 3f42067a8f | |||
| ed6f5b98df | |||
| dd290e264c | |||
| c85484fc00 | |||
| 663dffd8be | |||
| c15d0c3d17 | |||
| bf09a54f35 | |||
| 930dd51663 | |||
| 12a523c7aa | |||
| ea9a6d57dd | |||
| 91958e1232 | |||
| 8925afb089 | |||
| ccafe7be4f | |||
| 8279690a8f | |||
| 763d3ae76a | |||
| b775e7f4d3 | |||
| 3d8d93ece5 | |||
| 06af306e8a | |||
| 9257f3c919 | |||
| 2fe7f4cf04 | |||
| 04399bc8bb | |||
| fcbcfbc3c0 | |||
| 3e4ce62dfe | |||
| d8292151e6 | |||
| 3d01a59b34 | |||
| 5df15c4105 | |||
| 75d695105d | |||
| 28189bdddf | |||
| f6885c7cf8 | |||
| 2c43f0824e | |||
| 13e2eea72f | |||
| 9441be1ee2 | |||
| d7ab2a362a | |||
| e920be3a72 | |||
| f771383c4b | |||
| 65c75f085a | |||
| 17503365f7 | |||
| ebf9f0ca63 | |||
| ae26d2756f | |||
| 124071f9be | |||
| 471f7d9c62 | |||
| a6a6b3bd06 | |||
| 48ad3dccda | |||
| 341c58a722 | |||
| 9b04f2da48 | |||
| f7a296544f | |||
| 78641a57ad | |||
| a77ff5ffec | |||
| bdd5e16db1 | |||
| d4672bfe79 | |||
| abd9fab41a | |||
| 7c8bf42ef9 | |||
| 274b555912 | |||
| 916530f0d8 | |||
| 95efd47f65 | |||
| 90ecb1af7f | |||
| d7fdca1b44 | |||
| 37346763dc | |||
| c35fd2755f | |||
| 281e3a0518 | |||
| 6349cdad2f | |||
| ef341dd405 | |||
| 198e5ce642 | |||
| 923fbac5b0 | |||
| 5f28c7ace7 | |||
| d96c96006f | |||
| 3ddf2d6f85 | |||
| ba6849f29c | |||
| 942170f902 | |||
| 248f993541 | |||
| 56d40bddd0 | |||
| 3a700a449a | |||
| a20f552bcf | |||
| 32331a56eb | |||
| d752b7e41c | |||
| 0b4223c6ca | |||
| a3ec5c13f0 | |||
| 128b582dd6 | |||
| e59ede5422 | |||
| 6d08ba2513 | |||
| 23444f4df0 | |||
| 3338f7a401 | |||
| b126519275 | |||
| 71e68b498e | |||
| fb267ee223 | |||
| 8e59b06611 | |||
| a4b3519428 | |||
| 4895fc3bbb | |||
| 3daabd6fa8 | |||
| 9fccb14065 | |||
| 12efe94fd1 | |||
| 375ef27b9f | |||
| 9a7fa39de4 | |||
| c779ad2e3b | |||
| 7e7ef289ba | |||
| 223d9ad414 | |||
| 948ea7b087 | |||
| bf771f8b6c | |||
| 6dc8aa396c | |||
| 92a48f9dc6 | |||
| d0ad9fcb1f | |||
| 539e6deca5 | |||
| df4c8003b8 | |||
| 169e748a78 | |||
| 39b365c6ae | |||
| 9a79bab43d | |||
| e229eda96e | |||
| 4448145aa9 | |||
| 3d042e708a | |||
| 2428d5f1c2 | |||
| f1dc2b4d2a | |||
| 7dfbcdbb81 | |||
| 5fd4f56fa2 | |||
| b9d5ba6b0a | |||
| 2a4cb07ba8 | |||
| 7939286176 | |||
| 46ef49b897 | |||
| b923d85f6a | |||
| 2862b4ecfb | |||
| 094acc62f0 | |||
| 13d17dc729 | |||
| 5cf3a13ca8 | |||
| d0898a3869 | |||
| 7158c9d2ea | |||
| c5cf17b60b | |||
| da58796768 | |||
| d98499a3fa | |||
| e5944567e8 | |||
| d296c12d01 | |||
| 4c3a9e69f2 | |||
| eb2540a3c8 | |||
| bf9a3615d9 | |||
| 33fb22e3e7 | |||
| f3ff398a44 | |||
| 533eb59a04 | |||
| 8ca29f6d49 | |||
| 0a33d38adf | |||
| f7afb60c1f | |||
| b9c605bf1a | |||
| 2983adc719 | |||
| 502393ee56 | |||
| 121bba1d9f | |||
| 3c1b70c355 | |||
| 27508dd1f0 | |||
| 6d962dbdf3 | |||
| 9194e6368a | |||
| 917fb7d626 | |||
| 3cf5794b96 | |||
| 631b0a1819 | |||
| 6662dcc4b0 | |||
| 95db54b819 | |||
| bc7d5042df | |||
| de3e1c3dbc | |||
| 3c6aac5435 | |||
| eeb755ab7d | |||
| 70d0dd51a5 | |||
| 073dd8b560 | |||
| b5d2924d46 | |||
| 597e279f34 | |||
| fc28def83d | |||
| f6efdfded4 | |||
| 91312496e0 | |||
| b557b4337d | |||
| bfde186aa0 | |||
| 2bd75dd1a9 | |||
| 27ab31a9b0 | |||
| 44a8b737d9 | |||
| b939ee7a09 | |||
| 0bae550520 | |||
| b5cc2f2bda | |||
| 9ad4cf1db9 | |||
| 9dbafaaea2 | |||
| 2db8b07578 | |||
| 7c1a7bfd9d | |||
| b7ef076798 | |||
| 37c29a073e | |||
| 0c288ea64b | |||
| 2476475174 | |||
| 71913c8164 | |||
| 6ec8432217 | |||
| 7a12c0e4d1 | |||
| 23a7eba16b | |||
| 3ba84a8e8b | |||
| 75476217a0 | |||
| 7771c0b905 | |||
| 3378e82ec7 | |||
| 126e43dea4 | |||
| f725009530 | |||
| 70d1e3a0cb | |||
| e751ce1220 | |||
| e09a27cf87 | |||
| 06fbf44724 | |||
| 200e409d91 | |||
| 5e5854e256 | |||
| 3df8bcfc9c | |||
| e76c14f9e0 | |||
| 6b6748b1c7 | |||
| d92d8e6dbb | |||
| c2b9dc5c75 | |||
| 5c1d27de2b | |||
| 6ab9e7cd68 | |||
| 3ef56e9ec1 | |||
| 6d8d157772 | |||
| cadd466eec | |||
| 3fea0c1e49 | |||
| 4c58201adc | |||
| 4fb4e72624 | |||
| 276d8fe5cf | |||
| 92ce5f0931 | |||
| 7fea20375f | |||
| d4d4034d2c | |||
| f0db408699 | |||
| 5e200655d9 | |||
| d5d1f2a645 | |||
| cc5cc43baa | |||
| e512f085db | |||
| f323c01bd8 | |||
| f56cacb406 | |||
| eaecd31e9f | |||
| 36989d82e1 | |||
| 50777d9022 | |||
| a15571bd3e | |||
| 26fd66d831 | |||
| 0be873025a | |||
| 28ada49910 | |||
| 4fc8e61f8c | |||
| 7d26ea1a9c | |||
| 3a58dc62e1 | |||
| 71fe7bc827 | |||
| 933336c38b | |||
| 371feb9a31 | |||
| 95a2fd3c9e | |||
| 17cb76c334 | |||
| 88f0dfc8cc | |||
| f82aada23b | |||
| ecaee92634 | |||
| 89252ec47b | |||
| f0f25ab291 | |||
| e4d0fec15a | |||
| 6b10baf086 | |||
| f148b5d341 | |||
| 1471ff8940 | |||
| d9a6ec2ac0 | |||
| 5745ffa0a8 | |||
| b26202db35 | |||
| 6318577a51 | |||
| 6a2cd45847 | |||
| ef5cea2c01 | |||
| 69f4d54bae | |||
| b1eec5a7d2 | |||
| 1b8271d767 | |||
| 3e9f5ec5ef | |||
| 63f57b6a77 | |||
| a016f99450 | |||
| adc18b2991 | |||
| e37a326b95 | |||
| 048467e97d | |||
| cc2cd6919f | |||
| 0c6e781e5b | |||
| 7294d8fca5 | |||
| 16ec5680b4 | |||
| 87920fb1d7 | |||
| 523b96a6d2 | |||
| 45731d8069 | |||
| e872371970 | |||
| 08e8cf850a | |||
| b1ed2154ac | |||
| 7ef2aa3eb9 | |||
| 160139813d | |||
| 582ad92c76 | |||
| f61736e3d1 | |||
| eb02c96281 | |||
| 8619552920 | |||
| 6237352e25 | |||
| 2d8b4f543b | |||
| 8542dc10ab | |||
| c55b63337c | |||
| 12ddee3bb6 | |||
| dc41d0af27 | |||
| 3323b50036 | |||
| 8acb15a7fd | |||
| f601e04b38 | |||
| f50529cb5b | |||
| 3f1b6f9ed4 | |||
| f1ab0f4314 | |||
| 4d1129f385 | |||
| 03ac9c6e16 | |||
| c0839924f1 | |||
| 91e3aa760a | |||
| 5c0681d57b | |||
| c4f72c2bc1 | |||
| e92f9836e3 | |||
| 3818dc834b | |||
| cda011a049 | |||
| 897f6f3473 | |||
| b70b44490b | |||
| 77a5a58cb9 | |||
| f3b227434e | |||
| 2ae164df78 | |||
| 9b09793230 | |||
| f8a401aeca | |||
| ffbab2cd68 | |||
| 734e5fcab4 | |||
| 78578c6c9d | |||
| 0ccec96490 | |||
| 8022d0801d | |||
| d79975c409 | |||
| 20d65035d5 | |||
| 8d6227377f | |||
| 4bc50e7f57 | |||
| 945e42c940 | |||
| 052bb28086 | |||
| 4a84b7e2d5 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2021.8.1-rc2
|
current_version = 2021.9.6
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||||
@ -23,7 +23,7 @@ values =
|
|||||||
|
|
||||||
[bumpversion:file:schema.yml]
|
[bumpversion:file:schema.yml]
|
||||||
|
|
||||||
[bumpversion:file:.github/workflows/release.yml]
|
[bumpversion:file:.github/workflows/release-publish.yml]
|
||||||
|
|
||||||
[bumpversion:file:authentik/__init__.py]
|
[bumpversion:file:authentik/__init__.py]
|
||||||
|
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -27,7 +27,7 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
Output of docker-compose logs or kubectl logs respectively
|
Output of docker-compose logs or kubectl logs respectively
|
||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
- authentik version: [e.g. 0.10.0-stable]
|
- authentik version: [e.g. 2021.8.5]
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@ -20,7 +20,7 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
Output of docker-compose logs or kubectl logs respectively
|
Output of docker-compose logs or kubectl logs respectively
|
||||||
|
|
||||||
**Version and Deployment (please complete the following information):**
|
**Version and Deployment (please complete the following information):**
|
||||||
- authentik version: [e.g. 0.10.0-stable]
|
- authentik version: [e.g. 2021.8.5]
|
||||||
- Deployment: [e.g. docker-compose, helm]
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|||||||
1
.github/codespell-words.txt
vendored
Normal file
1
.github/codespell-words.txt
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
keypair
|
||||||
309
.github/workflows/ci-main.yml
vendored
Normal file
309
.github/workflows/ci-main.yml
vendored
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
name: authentik-ci-main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next
|
||||||
|
- version-*
|
||||||
|
paths-ignore:
|
||||||
|
- website
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: authentik
|
||||||
|
POSTGRES_USER: authentik
|
||||||
|
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-pylint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
# - id: cache-pipenv
|
||||||
|
# uses: actions/cache@v2.1.6
|
||||||
|
# with:
|
||||||
|
# path: ~/.local/share/virtualenvs
|
||||||
|
# key: ${{ runner.os }}-pipenv-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:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- name: prepare
|
||||||
|
run: |
|
||||||
|
scripts/ci_prepare.sh
|
||||||
|
npm install -g pyright@1.1.136
|
||||||
|
- name: run bandit
|
||||||
|
run: pipenv run pyright e2e lifecycle
|
||||||
|
test-migrations:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
# - id: cache-pipenv
|
||||||
|
# uses: actions/cache@v2.1.6
|
||||||
|
# with:
|
||||||
|
# path: ~/.local/share/virtualenvs
|
||||||
|
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
|
- name: prepare
|
||||||
|
# env:
|
||||||
|
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
run: scripts/ci_prepare.sh
|
||||||
|
- name: run migrations
|
||||||
|
run: pipenv run python -m lifecycle.migrate
|
||||||
|
test-migrations-from-stable:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- name: checkout stable
|
||||||
|
run: |
|
||||||
|
# Copy current, latest config to local
|
||||||
|
cp authentik/lib/default.yml local.env.yml
|
||||||
|
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||||
|
# - id: cache-pipenv
|
||||||
|
# uses: actions/cache@v2.1.6
|
||||||
|
# with:
|
||||||
|
# path: ~/.local/share/virtualenvs
|
||||||
|
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
|
- name: prepare
|
||||||
|
# env:
|
||||||
|
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
run: scripts/ci_prepare.sh
|
||||||
|
- name: run migrations to stable
|
||||||
|
run: pipenv run python -m lifecycle.migrate
|
||||||
|
- name: prepare variables
|
||||||
|
id: ev
|
||||||
|
run: |
|
||||||
|
python ./scripts/gh_do_set_branch.py
|
||||||
|
- name: checkout current code
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
git fetch
|
||||||
|
git checkout ${{ steps.ev.outputs.branchName }}
|
||||||
|
pipenv sync --dev
|
||||||
|
- name: migrate to latest
|
||||||
|
run: pipenv run python -m lifecycle.migrate
|
||||||
|
test-unittest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
# - id: cache-pipenv
|
||||||
|
# uses: actions/cache@v2.1.6
|
||||||
|
# with:
|
||||||
|
# path: ~/.local/share/virtualenvs
|
||||||
|
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
|
- name: prepare
|
||||||
|
# env:
|
||||||
|
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
run: scripts/ci_prepare.sh
|
||||||
|
- uses: testspace-com/setup-testspace@v1
|
||||||
|
with:
|
||||||
|
domain: ${{github.repository_owner}}
|
||||||
|
- name: run unittest
|
||||||
|
run: |
|
||||||
|
pipenv run make test
|
||||||
|
pipenv run coverage xml
|
||||||
|
- name: run testspace
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
testspace [unittest]unittest.xml --link=codecov
|
||||||
|
- if: ${{ always() }}
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
test-integration:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
# - id: cache-pipenv
|
||||||
|
# uses: actions/cache@v2.1.6
|
||||||
|
# with:
|
||||||
|
# path: ~/.local/share/virtualenvs
|
||||||
|
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
|
- name: prepare
|
||||||
|
# env:
|
||||||
|
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
run: scripts/ci_prepare.sh
|
||||||
|
- uses: testspace-com/setup-testspace@v1
|
||||||
|
with:
|
||||||
|
domain: ${{github.repository_owner}}
|
||||||
|
- name: Create k8s Kind Cluster
|
||||||
|
uses: helm/kind-action@v1.2.0
|
||||||
|
- name: run integration
|
||||||
|
run: |
|
||||||
|
pipenv run make test-integration
|
||||||
|
pipenv run coverage xml
|
||||||
|
- name: run testspace
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
testspace [integration]unittest.xml --link=codecov
|
||||||
|
- if: ${{ always() }}
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
test-e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.9'
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- uses: testspace-com/setup-testspace@v1
|
||||||
|
with:
|
||||||
|
domain: ${{github.repository_owner}}
|
||||||
|
# - id: cache-pipenv
|
||||||
|
# uses: actions/cache@v2.1.6
|
||||||
|
# with:
|
||||||
|
# path: ~/.local/share/virtualenvs
|
||||||
|
# key: ${{ runner.os }}-pipenv-v2-${{ hashFiles('**/Pipfile.lock') }}
|
||||||
|
- name: prepare
|
||||||
|
# env:
|
||||||
|
# INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
run: |
|
||||||
|
scripts/ci_prepare.sh
|
||||||
|
docker-compose -f tests/e2e/ci.docker-compose.yml up -d
|
||||||
|
- id: cache-web
|
||||||
|
uses: actions/cache@v2.1.6
|
||||||
|
with:
|
||||||
|
path: web/dist
|
||||||
|
key: ${{ runner.os }}-web-${{ hashFiles('web/package-lock.json', 'web/**') }}
|
||||||
|
- name: prepare web ui
|
||||||
|
if: steps.cache-web.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm i
|
||||||
|
npm run build
|
||||||
|
- name: run e2e
|
||||||
|
run: |
|
||||||
|
pipenv run make test-e2e
|
||||||
|
pipenv run coverage xml
|
||||||
|
- name: run testspace
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
testspace [e2e]unittest.xml --link=codecov
|
||||||
|
- if: ${{ always() }}
|
||||||
|
uses: codecov/codecov-action@v2
|
||||||
|
build:
|
||||||
|
needs:
|
||||||
|
- lint-pylint
|
||||||
|
- lint-black
|
||||||
|
- lint-isort
|
||||||
|
- lint-bandit
|
||||||
|
- lint-pyright
|
||||||
|
- test-migrations
|
||||||
|
- test-migrations-from-stable
|
||||||
|
- test-unittest
|
||||||
|
- test-integration
|
||||||
|
- test-e2e
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: prepare variables
|
||||||
|
id: ev
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||||
|
run: |
|
||||||
|
python ./scripts/gh_do_set_branch.py
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
|
with:
|
||||||
|
registry: beryju.org
|
||||||
|
username: ${{ secrets.HARBOR_USERNAME }}
|
||||||
|
password: ${{ secrets.HARBOR_PASSWORD }}
|
||||||
|
- name: Building Docker Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
|
tags: |
|
||||||
|
beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}
|
||||||
|
beryju.org/authentik/server:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.sha }}
|
||||||
|
build-args: |
|
||||||
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
69
.github/workflows/ci-outpost.yml
vendored
Normal file
69
.github/workflows/ci-outpost.yml
vendored
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
name: authentik-ci-outpost
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next
|
||||||
|
- version-*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-golint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: '^1.16.3'
|
||||||
|
- name: Run linter
|
||||||
|
run: |
|
||||||
|
# Create folder structure for go embeds
|
||||||
|
mkdir -p web/dist
|
||||||
|
mkdir -p website/help
|
||||||
|
touch web/dist/test website/help/test
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v $(pwd):/app \
|
||||||
|
-w /app \
|
||||||
|
golangci/golangci-lint:v1.39.0 \
|
||||||
|
golangci-lint run -v --timeout 200s
|
||||||
|
build:
|
||||||
|
needs:
|
||||||
|
- lint-golint
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
type:
|
||||||
|
- proxy
|
||||||
|
- ldap
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: prepare variables
|
||||||
|
id: ev
|
||||||
|
env:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||||
|
run: |
|
||||||
|
python ./scripts/gh_do_set_branch.py
|
||||||
|
- name: Login to Container Registry
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
if: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
|
with:
|
||||||
|
registry: beryju.org
|
||||||
|
username: ${{ secrets.HARBOR_USERNAME }}
|
||||||
|
password: ${{ secrets.HARBOR_PASSWORD }}
|
||||||
|
- name: Building Docker Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
push: ${{ steps.ev.outputs.shouldBuild == 'true' }}
|
||||||
|
tags: |
|
||||||
|
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}
|
||||||
|
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.branchName }}-${{ steps.ev.outputs.timestamp }}
|
||||||
|
beryju.org/authentik/outpost-${{ matrix.type }}:gh-${{ steps.ev.outputs.sha }}
|
||||||
|
file: ${{ matrix.type }}.Dockerfile
|
||||||
|
build-args: |
|
||||||
|
GIT_BUILD_HASH=${{ steps.ev.outputs.sha }}
|
||||||
89
.github/workflows/ci-web.yml
vendored
Normal file
89
.github/workflows/ci-web.yml
vendored
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
name: authentik-ci-web
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- next
|
||||||
|
- version-*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-eslint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-web
|
||||||
|
- name: Eslint
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm run lint
|
||||||
|
lint-prettier:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-web
|
||||||
|
- name: prettier
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm run prettier-check
|
||||||
|
lint-lit-analyse:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-web
|
||||||
|
- name: lit-analyse
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm run lit-analyse
|
||||||
|
build:
|
||||||
|
needs:
|
||||||
|
- lint-eslint
|
||||||
|
- lint-prettier
|
||||||
|
- lint-lit-analyse
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: web/package-lock.json
|
||||||
|
- run: |
|
||||||
|
cd web
|
||||||
|
npm install
|
||||||
|
- name: Generate API
|
||||||
|
run: make gen-web
|
||||||
|
- name: build
|
||||||
|
run: |
|
||||||
|
cd web
|
||||||
|
npm run build
|
||||||
@ -33,14 +33,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik:2021.8.1-rc2,
|
beryju/authentik:2021.9.6,
|
||||||
beryju/authentik:latest,
|
beryju/authentik:latest,
|
||||||
ghcr.io/goauthentik/server:2021.8.1-rc2,
|
ghcr.io/goauthentik/server:2021.9.6,
|
||||||
ghcr.io/goauthentik/server:latest
|
ghcr.io/goauthentik/server:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
context: .
|
context: .
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1-rc2', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.9.6', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik:latest
|
docker pull beryju/authentik:latest
|
||||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||||
@ -75,14 +75,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-proxy:2021.8.1-rc2,
|
beryju/authentik-proxy:2021.9.6,
|
||||||
beryju/authentik-proxy:latest,
|
beryju/authentik-proxy:latest,
|
||||||
ghcr.io/goauthentik/proxy:2021.8.1-rc2,
|
ghcr.io/goauthentik/proxy:2021.9.6,
|
||||||
ghcr.io/goauthentik/proxy:latest
|
ghcr.io/goauthentik/proxy:latest
|
||||||
file: proxy.Dockerfile
|
file: proxy.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1-rc2', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.9.6', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-proxy:latest
|
docker pull beryju/authentik-proxy:latest
|
||||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||||
@ -117,14 +117,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-ldap:2021.8.1-rc2,
|
beryju/authentik-ldap:2021.9.6,
|
||||||
beryju/authentik-ldap:latest,
|
beryju/authentik-ldap:latest,
|
||||||
ghcr.io/goauthentik/ldap:2021.8.1-rc2,
|
ghcr.io/goauthentik/ldap:2021.9.6,
|
||||||
ghcr.io/goauthentik/ldap:latest
|
ghcr.io/goauthentik/ldap:latest
|
||||||
file: ldap.Dockerfile
|
file: ldap.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.8.1-rc2', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.9.6', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-ldap:latest
|
docker pull beryju/authentik-ldap:latest
|
||||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||||
@ -157,9 +157,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Setup Node.js environment
|
- name: Setup Node.js environment
|
||||||
uses: actions/setup-node@v2.4.0
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: '16'
|
||||||
- name: Build web api client and web ui
|
- name: Build web api client and web ui
|
||||||
run: |
|
run: |
|
||||||
export NODE_ENV=production
|
export NODE_ENV=production
|
||||||
@ -175,7 +175,7 @@ jobs:
|
|||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
version: authentik@2021.8.1-rc2
|
version: authentik@2021.9.6
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
sourcemaps: './web/dist'
|
sourcemaps: './web/dist'
|
||||||
url_prefix: '~/static/dist'
|
url_prefix: '~/static/dist'
|
||||||
@ -27,7 +27,7 @@ jobs:
|
|||||||
docker-compose run -u root server test
|
docker-compose run -u root server test
|
||||||
- name: Extract version number
|
- name: Extract version number
|
||||||
id: get_version
|
id: get_version
|
||||||
uses: actions/github-script@v4.1
|
uses: actions/github-script@v5
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
4
.github/workflows/web-api-publish.yml
vendored
4
.github/workflows/web-api-publish.yml
vendored
@ -1,7 +1,7 @@
|
|||||||
name: authentik-web-api-publish
|
name: authentik-web-api-publish
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ master, version* ]
|
branches: [ master ]
|
||||||
paths:
|
paths:
|
||||||
- 'schema.yml'
|
- 'schema.yml'
|
||||||
jobs:
|
jobs:
|
||||||
@ -12,7 +12,7 @@ jobs:
|
|||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '16'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
- name: Generate API Client
|
- name: Generate API Client
|
||||||
run: make gen-web
|
run: make gen-web
|
||||||
|
|||||||
@ -11,8 +11,8 @@ The following is a set of guidelines for contributing to authentik and its compo
|
|||||||
[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
|
[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
|
||||||
|
|
||||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||||
* [Atom and Packages](#atom-and-packages)
|
* [The components](#the-components)
|
||||||
* [Atom Design Decisions](#design-decisions)
|
* [authentik's structure](#authentiks-structure)
|
||||||
|
|
||||||
[How Can I Contribute?](#how-can-i-contribute)
|
[How Can I Contribute?](#how-can-i-contribute)
|
||||||
* [Reporting Bugs](#reporting-bugs)
|
* [Reporting Bugs](#reporting-bugs)
|
||||||
@ -22,14 +22,9 @@ The following is a set of guidelines for contributing to authentik and its compo
|
|||||||
|
|
||||||
[Styleguides](#styleguides)
|
[Styleguides](#styleguides)
|
||||||
* [Git Commit Messages](#git-commit-messages)
|
* [Git Commit Messages](#git-commit-messages)
|
||||||
* [JavaScript Styleguide](#javascript-styleguide)
|
* [Python Styleguide](#python-styleguide)
|
||||||
* [CoffeeScript Styleguide](#coffeescript-styleguide)
|
|
||||||
* [Specs Styleguide](#specs-styleguide)
|
|
||||||
* [Documentation Styleguide](#documentation-styleguide)
|
* [Documentation Styleguide](#documentation-styleguide)
|
||||||
|
|
||||||
[Additional Notes](#additional-notes)
|
|
||||||
* [Issue and Pull Request Labels](#issue-and-pull-request-labels)
|
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
Basically, don't be a dickhead. This is an open-source non-profit project, that is made in the free time of Volunteers. If there's something you dislike or think can be done better, tell us! We'd love to hear any suggestions for improvement.
|
Basically, don't be a dickhead. This is an open-source non-profit project, that is made in the free time of Volunteers. If there's something you dislike or think can be done better, tell us! We'd love to hear any suggestions for improvement.
|
||||||
@ -122,7 +117,7 @@ This section guides you through submitting a bug report for authentik. Following
|
|||||||
|
|
||||||
Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
|
Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
|
||||||
|
|
||||||
This form will have the full stack trace of the error that ocurred and shouldn't contain any sensitive data.
|
This form will have the full stack trace of the error that occurred and shouldn't contain any sensitive data.
|
||||||
|
|
||||||
### Suggesting Enhancements
|
### Suggesting Enhancements
|
||||||
|
|
||||||
|
|||||||
39
Dockerfile
39
Dockerfile
@ -1,5 +1,5 @@
|
|||||||
# Stage 1: Lock python dependencies
|
# Stage 1: Lock python dependencies
|
||||||
FROM python:3.9-slim-buster as locker
|
FROM docker.io/python:3.9-slim-buster as locker
|
||||||
|
|
||||||
COPY ./Pipfile /app/
|
COPY ./Pipfile /app/
|
||||||
COPY ./Pipfile.lock /app/
|
COPY ./Pipfile.lock /app/
|
||||||
@ -11,38 +11,23 @@ RUN pip install pipenv && \
|
|||||||
pipenv lock -r --dev-only > requirements-dev.txt
|
pipenv lock -r --dev-only > requirements-dev.txt
|
||||||
|
|
||||||
# Stage 2: Build website
|
# Stage 2: Build website
|
||||||
FROM node as website-builder
|
FROM docker.io/node as website-builder
|
||||||
|
|
||||||
COPY ./website /static/
|
COPY ./website /static/
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN cd /static && npm i && npm run build-docs-only
|
RUN cd /static && npm i && npm run build-docs-only
|
||||||
|
|
||||||
# Stage 3: Generate API Client
|
# Stage 3: Build webui
|
||||||
FROM openapitools/openapi-generator-cli as go-api-builder
|
FROM docker.io/node as web-builder
|
||||||
|
|
||||||
COPY ./schema.yml /local/schema.yml
|
|
||||||
|
|
||||||
RUN docker-entrypoint.sh generate \
|
|
||||||
--git-host goauthentik.io \
|
|
||||||
--git-repo-id outpost \
|
|
||||||
--git-user-id api \
|
|
||||||
-i /local/schema.yml \
|
|
||||||
-g go \
|
|
||||||
-o /local/api \
|
|
||||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true && \
|
|
||||||
rm -f /local/api/go.mod /local/api/go.sum
|
|
||||||
|
|
||||||
# Stage 4: Build webui
|
|
||||||
FROM node as web-builder
|
|
||||||
|
|
||||||
COPY ./web /static/
|
COPY ./web /static/
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN cd /static && npm i && npm run build
|
RUN cd /static && npm i && npm run build
|
||||||
|
|
||||||
# Stage 5: Build go proxy
|
# Stage 4: Build go proxy
|
||||||
FROM golang:1.17.0 AS builder
|
FROM docker.io/golang:1.17.1 AS builder
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
@ -52,7 +37,6 @@ COPY --from=web-builder /static/dist/ /work/web/dist/
|
|||||||
COPY --from=web-builder /static/authentik/ /work/web/authentik/
|
COPY --from=web-builder /static/authentik/ /work/web/authentik/
|
||||||
COPY --from=website-builder /static/help/ /work/website/help/
|
COPY --from=website-builder /static/help/ /work/website/help/
|
||||||
|
|
||||||
COPY --from=go-api-builder /local/api api
|
|
||||||
COPY ./cmd /work/cmd
|
COPY ./cmd /work/cmd
|
||||||
COPY ./web/static.go /work/web/static.go
|
COPY ./web/static.go /work/web/static.go
|
||||||
COPY ./website/static.go /work/website/static.go
|
COPY ./website/static.go /work/website/static.go
|
||||||
@ -62,8 +46,8 @@ COPY ./go.sum /work/go.sum
|
|||||||
|
|
||||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||||
|
|
||||||
# Stage 6: Run
|
# Stage 5: Run
|
||||||
FROM python:3.9-slim-buster
|
FROM docker.io/python:3.9-slim-buster
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY --from=locker /app/requirements.txt /
|
COPY --from=locker /app/requirements.txt /
|
||||||
@ -96,7 +80,12 @@ COPY ./lifecycle/ /lifecycle
|
|||||||
COPY --from=builder /work/authentik /authentik-proxy
|
COPY --from=builder /work/authentik /authentik-proxy
|
||||||
|
|
||||||
USER authentik
|
USER authentik
|
||||||
|
|
||||||
ENV TMPDIR /dev/shm/
|
ENV TMPDIR /dev/shm/
|
||||||
ENV PYTHONUBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
ENV prometheus_multiproc_dir /dev/shm/
|
||||||
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
|
ENV PATH "/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/lifecycle"
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=30s --start-period=60s --retries=3 CMD [ "/lifecycle/ak healthcheck" ]
|
||||||
|
|
||||||
ENTRYPOINT [ "/lifecycle/ak" ]
|
ENTRYPOINT [ "/lifecycle/ak" ]
|
||||||
|
|||||||
9
Makefile
9
Makefile
@ -7,8 +7,6 @@ NPM_VERSION = $(shell python -m scripts.npm_version)
|
|||||||
all: lint-fix lint test gen
|
all: lint-fix lint test gen
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
k3d cluster create || exit 0
|
|
||||||
k3d kubeconfig write -o ~/.kube/config --overwrite
|
|
||||||
coverage run manage.py test -v 3 tests/integration
|
coverage run manage.py test -v 3 tests/integration
|
||||||
|
|
||||||
test-e2e:
|
test-e2e:
|
||||||
@ -22,6 +20,7 @@ test:
|
|||||||
lint-fix:
|
lint-fix:
|
||||||
isort authentik tests lifecycle
|
isort authentik tests lifecycle
|
||||||
black authentik tests lifecycle
|
black authentik tests lifecycle
|
||||||
|
codespell -I .github/codespell-words.txt -S 'web/src/locales/**' -w authentik internal cmd web/src website/src
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
pyright authentik tests lifecycle
|
pyright authentik tests lifecycle
|
||||||
@ -61,13 +60,13 @@ gen-outpost:
|
|||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g go \
|
-g go \
|
||||||
-o /local/api \
|
-o /local/api \
|
||||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true
|
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false
|
||||||
rm -f api/go.mod api/go.sum
|
rm -f api/go.mod api/go.sum
|
||||||
|
|
||||||
gen: gen-build gen-clean gen-web gen-outpost
|
gen: gen-build gen-clean gen-web
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
python -m lifecycle.migrate
|
python -m lifecycle.migrate
|
||||||
|
|
||||||
run:
|
run:
|
||||||
go run -v cmd/server/main.go
|
WORKERS=1 go run -v cmd/server/main.go
|
||||||
|
|||||||
4
Pipfile
4
Pipfile
@ -48,9 +48,7 @@ duo-client = "*"
|
|||||||
ua-parser = "*"
|
ua-parser = "*"
|
||||||
deepmerge = "*"
|
deepmerge = "*"
|
||||||
colorama = "*"
|
colorama = "*"
|
||||||
|
codespell = "*"
|
||||||
[requires]
|
|
||||||
python_version = "3.9"
|
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
bandit = "*"
|
bandit = "*"
|
||||||
|
|||||||
456
Pipfile.lock
generated
456
Pipfile.lock
generated
@ -1,12 +1,10 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "f0befa9b3dacc1c3363b9442fa7a43f6be2c46a8fcb80a994230d288a384e54d"
|
"sha256": "babb6061c555f8f239f00210b2a0356763bdaaca2f3d704cf3444891b84db84d"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {},
|
||||||
"python_version": "3.9"
|
|
||||||
},
|
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"name": "pypi",
|
"name": "pypi",
|
||||||
@ -122,27 +120,27 @@
|
|||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7209b79833bdf13753aa24f76bf533890ffed2cc4fe1fe08619d223c209bbd11",
|
"sha256:7b45b224442c479de4bc6e6e9cb0557b642fc7a77edc8702e393ccaa2e0aa128",
|
||||||
"sha256:f46c93d09acd4d4bfc6b9522ed852fecbdc508e0365f29ddfb3c146aae784b4e"
|
"sha256:c388da7dc1a596755f39de990a72e05cee558d098e81de63de55bd9598cc5134"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.18.27"
|
"version": "==1.18.48"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8c99abd7093ab11ce8d09c68732aeeb6065a53d2fe371568452e99291817fff5",
|
"sha256:17a10dd33334e7e3aaa4e12f66317284f96bb53267e20bc877a187c442681772",
|
||||||
"sha256:b9e2c90bad164d111c229102f58f995c28576e719dd116b446965e1b786f8fa5"
|
"sha256:2089f9fa36a59d8c02435c49d58ccc7b3ceb9c0c054ea4f71631c3c3a1c5245e"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==1.21.27"
|
"version": "==1.21.51"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001",
|
"sha256:0a3d3556c2c3befdbba2f93b78792c199c66201c999e97947ea0b7437758246b",
|
||||||
"sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff"
|
"sha256:6a6fa6802188ab7e77bab2db001d676e854499552b0037d999d5b9f211db5250"
|
||||||
],
|
],
|
||||||
"markers": "python_version ~= '3.5'",
|
"markers": "python_version ~= '3.5'",
|
||||||
"version": "==4.2.2"
|
"version": "==4.2.3"
|
||||||
},
|
},
|
||||||
"cbor2": {
|
"cbor2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -254,11 +252,11 @@
|
|||||||
},
|
},
|
||||||
"charset-normalizer": {
|
"charset-normalizer": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
|
"sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6",
|
||||||
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
"sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3'",
|
"markers": "python_version >= '3'",
|
||||||
"version": "==2.0.4"
|
"version": "==2.0.6"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -270,9 +268,11 @@
|
|||||||
},
|
},
|
||||||
"click-didyoumean": {
|
"click-didyoumean": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"
|
"sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
|
||||||
|
"sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
|
||||||
],
|
],
|
||||||
"version": "==0.0.3"
|
"markers": "python_version < '4' and python_full_version >= '3.6.2'",
|
||||||
|
"version": "==0.3.0"
|
||||||
},
|
},
|
||||||
"click-plugins": {
|
"click-plugins": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -288,6 +288,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.2.0"
|
"version": "==0.2.0"
|
||||||
},
|
},
|
||||||
|
"codespell": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:19d3fe5644fef3425777e66f225a8c82d39059dcfe9edb3349a8a2cf48383ee5",
|
||||||
|
"sha256:b864c7d917316316ac24272ee992d7937c3519be4569209c5b60035ac5d569b5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.1.0"
|
||||||
|
},
|
||||||
"colorama": {
|
"colorama": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
|
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
|
||||||
@ -305,22 +313,28 @@
|
|||||||
},
|
},
|
||||||
"cryptography": {
|
"cryptography": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d",
|
"sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6",
|
||||||
"sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959",
|
"sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6",
|
||||||
"sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6",
|
"sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c",
|
||||||
"sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873",
|
"sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999",
|
||||||
"sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2",
|
"sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e",
|
||||||
"sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713",
|
"sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992",
|
||||||
"sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1",
|
"sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d",
|
||||||
"sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177",
|
"sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588",
|
||||||
"sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250",
|
"sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa",
|
||||||
"sha256:b01fd6f2737816cb1e08ed4807ae194404790eac7ad030b34f2ce72b332f5586",
|
"sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d",
|
||||||
"sha256:bf40af59ca2465b24e54f671b2de2c59257ddc4f7e5706dbd6930e26823668d3",
|
"sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd",
|
||||||
"sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca",
|
"sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d",
|
||||||
"sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d",
|
"sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953",
|
||||||
"sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9"
|
"sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2",
|
||||||
|
"sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8",
|
||||||
|
"sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6",
|
||||||
|
"sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9",
|
||||||
|
"sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6",
|
||||||
|
"sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad",
|
||||||
|
"sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"
|
||||||
],
|
],
|
||||||
"version": "==3.4.7"
|
"version": "==35.0.0"
|
||||||
},
|
},
|
||||||
"dacite": {
|
"dacite": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -356,11 +370,11 @@
|
|||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7f92413529aa0e291f3be78ab19be31aefb1e1c9a52cd59e130f505f27a51f13",
|
"sha256:95b318319d6997bac3595517101ad9cc83fe5672ac498ba48d1a410f47afecd2",
|
||||||
"sha256:f27f8544c9d4c383bbe007c57e3235918e258364577373d4920e9162837be022"
|
"sha256:e93c93565005b37ddebf2396b4dc4b6913c1838baa82efdfb79acedd5816c240"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.2.6"
|
"version": "==3.2.7"
|
||||||
},
|
},
|
||||||
"django-dbbackup": {
|
"django-dbbackup": {
|
||||||
"git": "https://github.com/django-dbbackup/django-dbbackup.git",
|
"git": "https://github.com/django-dbbackup/django-dbbackup.git",
|
||||||
@ -368,11 +382,11 @@
|
|||||||
},
|
},
|
||||||
"django-filter": {
|
"django-filter": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06",
|
"sha256:632a251fa8f1aadb4b8cceff932bb52fe2f826dd7dfe7f3eac40e5c463d6836e",
|
||||||
"sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"
|
"sha256:f4a6737a30104c98d2e2a5fb93043f36dd7978e0c7ddc92f5998e85433ea5063"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.4.0"
|
"version": "==21.1"
|
||||||
},
|
},
|
||||||
"django-guardian": {
|
"django-guardian": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -392,11 +406,11 @@
|
|||||||
},
|
},
|
||||||
"django-otp": {
|
"django-otp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:01b5888f0bde5125e139433aacb947e52d5c406fa56c9db43c3e8d75b5c323c4",
|
"sha256:0c03a471db9e876f3671314bc9a65bd56a5c3c108ee0562c473701310bba4a77",
|
||||||
"sha256:0d56dd2a7fbb6ee6e54557e036ca64add0bd3596f471794bad673b7637d5e935"
|
"sha256:4c90cdaed683d736b0efafc034a3c6b410e1be2a53c24da287165b1f371d8776"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.0.6"
|
"version": "==1.1.1"
|
||||||
},
|
},
|
||||||
"django-prometheus": {
|
"django-prometheus": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -440,19 +454,19 @@
|
|||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:3e8bc47534e0ca9331d72c32f2881bb13b93ded0bcdeab3c833fb7cf61c0a9a5",
|
"sha256:21ec4998e90dff7a7aaaa098ca8d839c7de412b89e6f6c30908372d58fecf663",
|
||||||
"sha256:fc961d622160e8021c10d1bcabc388c57d55fb1f917175afbe24af442e6879bd"
|
"sha256:9b17f0723d83c1f3418d2aa17bf90b24dbe97deda06208dd4262fa30a6ee87eb"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.0.0"
|
"version": "==5.0.2"
|
||||||
},
|
},
|
||||||
"drf-spectacular": {
|
"drf-spectacular": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:f080128c42183fcaed6b9e8e5afcd2e5cd68426b1f80bfc85938f25e62db7fe5",
|
"sha256:65df818226477cdfa629947ea52bc0cc13eb40550b192eeccec64a6b782651fd",
|
||||||
"sha256:fb19aa69fcfcd37b0c9dfb9989c0671e1bb47af332ca2171378c7f840263788c"
|
"sha256:f71205da3645d770545abeaf48e8a15afd6ee9a76e57c03df4592e51be1059bf"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.17.3"
|
"version": "==0.19.0"
|
||||||
},
|
},
|
||||||
"duo-client": {
|
"duo-client": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -479,19 +493,19 @@
|
|||||||
},
|
},
|
||||||
"geoip2": {
|
"geoip2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:906a1dbf15a179a1af3522970e8420ab15bb3e0afc526942cc179e12146d9c1d",
|
"sha256:f150bed3190d543712a17467208388d31bd8ddb49b2226fba53db8aaedb8ba89",
|
||||||
"sha256:b97b44031fdc463e84eb1316b4f19edd978cb1d78703465fcb1e36dc5a822ba6"
|
"sha256:f9172cdfb2a5f9225ace5e30dd7426413ad28798a5f474cd1538780686bd6a87"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.2.0"
|
"version": "==4.4.0"
|
||||||
},
|
},
|
||||||
"google-auth": {
|
"google-auth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c012c8be7c442c8309ca8fa0876fef33f5fd977c467be1e1c1c2f721e8ebd73c",
|
"sha256:2a92b485afed5292946b324e91fcbe03db277ee4cb64c998c6cfa66d4af01dee",
|
||||||
"sha256:ea1af050b3e06eb73e4470f704d23007307bc0e87c13e015f6b90460f1407bd3"
|
"sha256:6dc8173abd50f25b6e62fc5b42802c96fc7cd9deb9bfeeb10a79f5606225cdf4"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.0.1"
|
"version": "==2.2.1"
|
||||||
},
|
},
|
||||||
"gunicorn": {
|
"gunicorn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -615,10 +629,10 @@
|
|||||||
},
|
},
|
||||||
"jsonschema": {
|
"jsonschema": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163",
|
"sha256:bc51325b929171791c42ebc1c70b9713eb134d3bb8ebd5474c8b659b15be6d86",
|
||||||
"sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"
|
"sha256:c773028c649441ab980015b5b622f4cd5134cf563daaf0235ca4b73cc3734f20"
|
||||||
],
|
],
|
||||||
"version": "==3.2.0"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"kombu": {
|
"kombu": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -703,10 +717,10 @@
|
|||||||
},
|
},
|
||||||
"maxminddb": {
|
"maxminddb": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
"sha256:e37707ec4fab115804670e0fb7aedb4b57075a8b6f80052bdc648d3c005184e5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.0.3"
|
"version": "==2.2.0"
|
||||||
},
|
},
|
||||||
"msgpack": {
|
"msgpack": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -897,39 +911,39 @@
|
|||||||
},
|
},
|
||||||
"pycryptodome": {
|
"pycryptodome": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:09c1555a3fa450e7eaca41ea11cd00afe7c91fef52353488e65663777d8524e0",
|
"sha256:04e14c732c3693d2830839feed5129286ce47ffa8bfe90e4ae042c773e51c677",
|
||||||
"sha256:12222a5edc9ca4a29de15fbd5339099c4c26c56e13c2ceddf0b920794f26165d",
|
"sha256:11d3164fb49fdee000fde05baecce103c0c698168ef1a18d9c7429dd66f0f5bb",
|
||||||
"sha256:1723ebee5561628ce96748501cdaa7afaa67329d753933296321f0be55358dce",
|
"sha256:217dcc0c92503f7dd4b3d3b7d974331a4419f97f555c99a845c3b366fed7056b",
|
||||||
"sha256:1c5e1ca507de2ad93474be5cfe2bfa76b7cf039a1a32fc196f40935944871a06",
|
"sha256:24c1b7705d19d8ae3e7255431efd2e526006855df62620118dd7b5374c6372f6",
|
||||||
"sha256:2603c98ae04aac675fefcf71a6c87dc4bb74a75e9071ae3923bbc91a59f08d35",
|
"sha256:309529d2526f3fb47102aeef376b3459110a6af7efb162e860b32e3a17a46f06",
|
||||||
"sha256:2dea65df54349cdfa43d6b2e8edb83f5f8d6861e5cf7b1fbc3e34c5694c85e27",
|
"sha256:3a153658d97258ca20bf18f7fe31c09cc7c558b6f8974a6ec74e19f6c634bd64",
|
||||||
"sha256:31c1df17b3dc5f39600a4057d7db53ac372f492c955b9b75dd439f5d8b460129",
|
"sha256:3f9fb499e267039262569d08658132c9cd8b136bf1d8c56b72f70ed05551e526",
|
||||||
"sha256:38661348ecb71476037f1e1f553159b80d256c00f6c0b00502acac891f7116d9",
|
"sha256:3faa6ebd35c61718f3f8862569c1f38450c24f3ededb213e1a64806f02f584bc",
|
||||||
"sha256:3e2e3a06580c5f190df843cdb90ea28d61099cf4924334d5297a995de68e4673",
|
"sha256:40083b0d7f277452c7f2dd4841801f058cc12a74c219ee4110d65774c6a58bef",
|
||||||
"sha256:3f840c49d38986f6e17dbc0673d37947c88bc9d2d9dba1c01b979b36f8447db1",
|
"sha256:49e54f2245befb0193848c8c8031d8d1358ed4af5a1ae8d0a3ba669a5cdd3a72",
|
||||||
"sha256:501ab36aae360e31d0ec370cf5ce8ace6cb4112060d099b993bc02b36ac83fb6",
|
"sha256:4e8fc4c48365ce8a542fe48bf1360da05bb2851df12f64fc94d751705e7cdbe7",
|
||||||
"sha256:60386d1d4cfaad299803b45a5bc2089696eaf6cdd56f9fc17479a6f89595cfc8",
|
"sha256:54d4e4d45f349d8c4e2f31c2734637ff62a844af391b833f789da88e43a8f338",
|
||||||
"sha256:6260e24d41149268122dd39d4ebd5941e9d107f49463f7e071fd397e29923b0c",
|
"sha256:66301e4c42dee43ee2da256625d3fe81ef98cc9924c2bd535008cc3ad8ded77b",
|
||||||
"sha256:6bbf7fee7b7948b29d7e71fcacf48bac0c57fb41332007061a933f2d996f9713",
|
"sha256:6b45fcace5a5d9c57ba87cf804b161adc62aa826295ce7f7acbcbdc0df74ed37",
|
||||||
"sha256:6d2df5223b12437e644ce0a3be7809471ffa71de44ccd28b02180401982594a6",
|
"sha256:7efec2418e9746ec48e264eea431f8e422d931f71c57b1c96ee202b117f58fa9",
|
||||||
"sha256:758949ca62690b1540dfb24ad773c6da9cd0e425189e83e39c038bbd52b8e438",
|
"sha256:851e6d4930b160417235955322db44adbdb19589918670d63f4acd5d92959ac0",
|
||||||
"sha256:77997519d8eb8a4adcd9a47b9cec18f9b323e296986528186c0e9a7a15d6a07e",
|
"sha256:8e82524e7c354033508891405574d12e612cc4fdd3b55d2c238fc1a3e300b606",
|
||||||
"sha256:7fd519b89585abf57bf47d90166903ec7b43af4fe23c92273ea09e6336af5c07",
|
"sha256:8ec154ec445412df31acf0096e7f715e30e167c8f2318b8f5b1ab7c28f4c82f7",
|
||||||
"sha256:98213ac2b18dc1969a47bc65a79a8fca02a414249d0c8635abb081c7f38c91b6",
|
"sha256:91ba4215a1f37d0f371fe43bc88c5ff49c274849f3868321c889313787de7672",
|
||||||
"sha256:99b2f3fc51d308286071d0953f92055504a6ffe829a832a9fc7a04318a7683dd",
|
"sha256:97e7df67a4da2e3f60612bbfd6c3f243a63a15d8f4797dd275e1d7b44a65cb12",
|
||||||
"sha256:9b6f711b25e01931f1c61ce0115245a23cdc8b80bf8539ac0363bdcf27d649b6",
|
"sha256:9a2312440057bf29b9582f72f14d79692044e63bfbc4b4bbea8559355f44f3dd",
|
||||||
"sha256:a3105a0eb63eacf98c2ecb0eb4aa03f77f40fbac2bdde22020bb8a536b226bb8",
|
"sha256:a7471646d8cd1a58bb696d667dcb3853e5c9b341b68dcf3c3cc0893d0f98ca5f",
|
||||||
"sha256:a8eb8b6ea09ec1c2535bf39914377bc8abcab2c7d30fa9225eb4fe412024e427",
|
"sha256:ac3012c36633564b2b5539bb7c6d9175f31d2ce74844e9abe654c428f02d0fd8",
|
||||||
"sha256:a92d5c414e8ee1249e850789052608f582416e82422502dc0ac8c577808a9067",
|
"sha256:b1daf251395af7336ddde6a0015ba5e632c18fe646ba930ef87402537358e3b4",
|
||||||
"sha256:d3d6958d53ad307df5e8469cc44474a75393a434addf20ecd451f38a72fe29b8",
|
"sha256:b217b4525e60e1af552d62bec01b4685095436d4de5ecde0f05d75b2f95ba6d4",
|
||||||
"sha256:e0a4d5933a88a2c98bbe19c0c722f5483dc628d7a38338ac2cb64a7dbd34064b",
|
"sha256:c61ea053bd5d4c12a063d7e704fbe1c45abb5d2510dab55bd95d166ba661604f",
|
||||||
"sha256:e3bf558c6aeb49afa9f0c06cee7fb5947ee5a1ff3bd794b653d39926b49077fa",
|
"sha256:c6469d1453f5864e3321a172b0aa671b938d753cbf2376b99fa2ab8841539bb8",
|
||||||
"sha256:e61e363d9a5d7916f3a4ce984a929514c0df3daf3b1b2eb5e6edbb131ee771cf",
|
"sha256:cefe6b267b8e5c3c72e11adec35a9c7285b62e8ea141b63e87055e9a9e5f2f8c",
|
||||||
"sha256:f977cdf725b20f6b8229b0c87acb98c7717e742ef9f46b113985303ae12a99da",
|
"sha256:d713dc0910e5ded07852a05e9b75f1dd9d3a31895eebee0668f612779b2a748c",
|
||||||
"sha256:fc7489a50323a0df02378bc2fff86eb69d94cc5639914346c736be981c6a02e7"
|
"sha256:db15fa07d2a4c00beeb5e9acdfdbc1c79f9ccfbdc1a8f36c82c4aa44951b33c9"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.10.1"
|
"version": "==3.10.4"
|
||||||
},
|
},
|
||||||
"pyjwt": {
|
"pyjwt": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -941,10 +955,10 @@
|
|||||||
},
|
},
|
||||||
"pyopenssl": {
|
"pyopenssl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4c231c759543ba02560fcd2480c48dcec4dae34c9da7d3747c508227e0624b51",
|
"sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3",
|
||||||
"sha256:818ae18e06922c066f777a33f1fca45786d85edfe71cd043de6379337a7f274b"
|
"sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"
|
||||||
],
|
],
|
||||||
"version": "==20.0.1"
|
"version": "==21.0.0"
|
||||||
},
|
},
|
||||||
"pyparsing": {
|
"pyparsing": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1081,11 +1095,11 @@
|
|||||||
},
|
},
|
||||||
"sentry-sdk": {
|
"sentry-sdk": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:ebe99144fa9618d4b0e7617e7929b75acd905d258c3c779edcd34c0adfffe26c",
|
"sha256:b9844751e40710e84a457c5bc29b21c383ccb2b63d76eeaad72f7f1c808c8828",
|
||||||
"sha256:f33d34c886d0ba24c75ea8885a8b3a172358853c7cbde05979fc99c29ef7bc52"
|
"sha256:c091cc7115ff25fe3a0e410dbecd7a996f81a3f6137d2272daef32d6c3cfa6dc"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.1"
|
"version": "==1.4.3"
|
||||||
},
|
},
|
||||||
"service-identity": {
|
"service-identity": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1105,11 +1119,11 @@
|
|||||||
},
|
},
|
||||||
"sqlparse": {
|
"sqlparse": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
"sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae",
|
||||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
"sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.5'",
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==0.4.1"
|
"version": "==0.4.2"
|
||||||
},
|
},
|
||||||
"structlog": {
|
"structlog": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1148,11 +1162,11 @@
|
|||||||
},
|
},
|
||||||
"typing-extensions": {
|
"typing-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
|
"sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e",
|
||||||
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
|
"sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7",
|
||||||
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
|
"sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"
|
||||||
],
|
],
|
||||||
"version": "==3.10.0.0"
|
"version": "==3.10.0.2"
|
||||||
},
|
},
|
||||||
"ua-parser": {
|
"ua-parser": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1175,11 +1189,11 @@
|
|||||||
"secure"
|
"secure"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
|
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
|
||||||
"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.26.6"
|
"version": "==1.26.7"
|
||||||
},
|
},
|
||||||
"uvicorn": {
|
"uvicorn": {
|
||||||
"extras": [
|
"extras": [
|
||||||
@ -1253,58 +1267,50 @@
|
|||||||
},
|
},
|
||||||
"websockets": {
|
"websockets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0dd4eb8e0bbf365d6f652711ce21b8fd2b596f873d32aabb0fbb53ec604418cc",
|
"sha256:01db0ecd1a0ca6702d02a5ed40413e18b7d22f94afb3bbe0d323bac86c42c1c8",
|
||||||
"sha256:1d0971cc7251aeff955aa742ec541ee8aaea4bb2ebf0245748fbec62f744a37e",
|
"sha256:085bb8a6e780d30eaa1ba48ac7f3a6707f925edea787cfb761ce5a39e77ac09b",
|
||||||
"sha256:1d6b4fddb12ab9adf87b843cd4316c4bd602db8d5efd2fb83147f0458fe85135",
|
"sha256:1ac35426fe3e7d3d0fac3d63c8965c76ed67a8fd713937be072bf0ce22808539",
|
||||||
"sha256:230a3506df6b5f446fed2398e58dcaafdff12d67fe1397dff196411a9e820d02",
|
"sha256:1f6b814cff6aadc4288297cb3a248614829c6e4ff5556593c44a115e9dd49939",
|
||||||
"sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3",
|
"sha256:2a43072e434c041a99f2e1eb9b692df0232a38c37c61d00e9f24db79474329e4",
|
||||||
"sha256:2cf04601633a4ec176b9cc3d3e73789c037641001dbfaf7c411f89cd3e04fcaf",
|
"sha256:5b2600e01c7ca6f840c42c747ffbe0254f319594ed108db847eb3d75f4aacb80",
|
||||||
"sha256:3ddff38894c7857c476feb3538dd847514379d6dc844961dc99f04b0384b1b1b",
|
"sha256:62160772314920397f9d219147f958b33fa27a12c662d4455c9ccbba9a07e474",
|
||||||
"sha256:48c222feb3ced18f3dc61168ca18952a22fb88e5eb8902d2bf1b50faefdc34a2",
|
"sha256:706e200fc7f03bed99ad0574cd1ea8b0951477dd18cc978ccb190683c69dba76",
|
||||||
"sha256:51d04df04ed9d08077d10ccbe21e6805791b78eac49d16d30a1f1fe2e44ba0af",
|
"sha256:71358c7816e2762f3e4af3adf0040f268e219f5a38cb3487a9d0fc2e554fef6a",
|
||||||
"sha256:597c28f3aa7a09e8c070a86b03107094ee5cdafcc0d55f2f2eac92faac8dc67d",
|
"sha256:7d2e12e4f901f1bc062dfdf91831712c4106ed18a9a4cdb65e2e5f502124ca37",
|
||||||
"sha256:5c8f0d82ea2468282e08b0cf5307f3ad022290ed50c45d5cb7767957ca782880",
|
"sha256:7f79f02c7f9a8320aff7d3321cd1c7e3a7dbc15d922ac996cca827301ee75238",
|
||||||
"sha256:7189e51955f9268b2bdd6cc537e0faa06f8fffda7fb386e5922c6391de51b077",
|
"sha256:82b17524b1ce6ae7f7dd93e4d18e9b9474071e28b65dbf1dfe9b5767778db379",
|
||||||
"sha256:7df3596838b2a0c07c6f6d67752c53859a54993d4f062689fdf547cb56d0f84f",
|
"sha256:82bd921885231f4a30d9bc550552495b3fc36b1235add6d374e7c65c3babd805",
|
||||||
"sha256:826ccf85d4514609219725ba4a7abd569228c2c9f1968e8be05be366f68291ec",
|
"sha256:8bbf8660c3f833ddc8b1afab90213f2e672a9ddac6eecb3cde968e6b2807c1c7",
|
||||||
"sha256:836d14eb53b500fd92bd5db2fc5894f7c72b634f9c2a28f546f75967503d8e25",
|
"sha256:9a4d889162bd48588e80950e07fa5e039eee9deb76a58092e8c3ece96d7ef537",
|
||||||
"sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0",
|
"sha256:b4ade7569b6fd17912452f9c3757d96f8e4044016b6d22b3b8391e641ca50456",
|
||||||
"sha256:85e701a6c316b7067f1e8675c638036a796fe5116783a4c932e7eb8e305a3ffe",
|
"sha256:b8176deb6be540a46695960a765a77c28ac8b2e3ef2ec95d50a4f5df901edb1c",
|
||||||
"sha256:900589e19200be76dd7cbaa95e9771605b5ce3f62512d039fb3bc5da9014912a",
|
"sha256:c4fc9a1d242317892590abe5b61a9127f1a61740477bfb121743f290b8054002",
|
||||||
"sha256:9147868bb0cc01e6846606cd65cbf9c58598f187b96d14dd1ca17338b08793bb",
|
"sha256:c5880442f5fc268f1ef6d37b2c152c114deccca73f48e3a8c48004d2f16f4567",
|
||||||
"sha256:9e7fdc775fe7403dbd8bc883ba59576a6232eac96dacb56512daacf7af5d618d",
|
"sha256:cd8c6f2ec24aedace251017bc7a414525171d4e6578f914acab9349362def4da",
|
||||||
"sha256:ab5ee15d3462198c794c49ccd31773d8a2b8c17d622aa184f669d2b98c2f0857",
|
"sha256:d67646ddd17a86117ae21c27005d83c1895c0cef5d7be548b7549646372f868a",
|
||||||
"sha256:ad893d889bc700a5835e0a95a3e4f2c39e91577ab232a3dc03c262a0f8fc4b5c",
|
"sha256:e42a1f1e03437b017af341e9bbfdc09252cd48ef32a8c3c3ead769eab3b17368",
|
||||||
"sha256:b2e71c4670ebe1067fa8632f0d081e47254ee2d3d409de54168b43b0ba9147e0",
|
"sha256:eb282127e9c136f860c6068a4fba5756eb25e755baffb5940b6f1eae071928b2",
|
||||||
"sha256:b43b13e5622c5a53ab12f3272e6f42f1ce37cd5b6684b2676cb365403295cd40",
|
"sha256:fe83b3ec9ef34063d86dfe1029160a85f24a5a94271036e5714a57acfdd089a1",
|
||||||
"sha256:b4ad84b156cf50529b8ac5cc1638c2cf8680490e3fccb6121316c8c02620a2e4",
|
"sha256:ff59c6bdb87b31f7e2d596f09353d5a38c8c8ff571b0e2238e8ee2d55ad68465"
|
||||||
"sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20",
|
|
||||||
"sha256:caa68c95bc1776d3521f81eeb4d5b9438be92514ec2a79fececda814099c8314",
|
|
||||||
"sha256:d144b350045c53c8ff09aa1cfa955012dd32f00c7e0862c199edcabb1a8b32da",
|
|
||||||
"sha256:d2c2d9b24d3c65b5a02cac12cbb4e4194e590314519ed49db2f67ef561c3cf58",
|
|
||||||
"sha256:e9e5fd6dbdf95d99bc03732ded1fc8ef22ebbc05999ac7e0c7bf57fe6e4e5ae2",
|
|
||||||
"sha256:ebf459a1c069f9866d8569439c06193c586e72c9330db1390af7c6a0a32c4afd",
|
|
||||||
"sha256:f31722f1c033c198aa4a39a01905951c00bd1c74f922e8afc1b1c62adbcdd56a",
|
|
||||||
"sha256:f68c352a68e5fdf1e97288d5cec9296664c590c25932a8476224124aaf90dbcd"
|
|
||||||
],
|
],
|
||||||
"version": "==9.1"
|
"version": "==10.0"
|
||||||
},
|
},
|
||||||
"xmlsec": {
|
"xmlsec": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:23f209260b37bdc2fd96af837494c47dd1e67964f077442b63acd83c0f62e212",
|
"sha256:135724cdce60e6bbd072fca6f09a21f72e2cecc59eebb4eed7740c316ecabc7b",
|
||||||
"sha256:4fb38ab0bf3e47cbae136119674a869e09d61c939b510350f369c8ac46087373",
|
"sha256:1b4377f6d37ad714ba95a227ef40fb54ba1b22ef5170ce04c330fe45ee6ad184",
|
||||||
"sha256:705ab5b848afdf3a5c78b1322276054c885f44dc51601e14cb883a9c86cbe20f",
|
"sha256:2c86ac6ce570c9e04f04da0cd5e7d3db346e4b5b1d006311606368f17c756ef9",
|
||||||
"sha256:843d10bba4c480609da74ee11fff1ee0fc1c12821c656979f12a7a4ecb043e03",
|
"sha256:4e5f565de311afa33aaee4724566e685f951afe301212b6cf82f98cf9d8a1749",
|
||||||
"sha256:86d54b93f8278e2f0c504d0744e39a483c1c7ce9993f2ca70184cc7770faa982",
|
"sha256:9a2b8a780093b0fe8cecae53a81a8cd9edd50c08980d374c5317c91f065042d9",
|
||||||
"sha256:8922fba55a060ee81de4a7f5efc593c5bf121047763aecf0eead02e061c9d2db",
|
"sha256:ce9c681adbc87b4f06c2b16725d9b2edbdbd508117dae4288b5faf78c1406038",
|
||||||
"sha256:c7b49d4fce83186b89f7ce6cec765245d36a70d0acc2f3ed0ba95c735b3667da",
|
"sha256:d22da4d3dcc559fb2e54e782f39c9ddad5f8d5b356f86a79bbb80b0a45115c97",
|
||||||
"sha256:cd2eaaff7f31784a07dd99ce81fa767313df3ba1834faa4143ee2c07000cac7a",
|
"sha256:db3e18ca883c01bbe28c9f5197c66f676c9772cf2d85f667e6122fc4d0702225",
|
||||||
"sha256:dea5bef9b5830c36ccb7a68a0d94d49eaea4d03fbbd04179652bf661b7e6e30f",
|
"sha256:e4783f7814aa2a3e318385cce8ef87c82954b9a59535a48f67da4e2c21c08ce1",
|
||||||
"sha256:eadff662d89c80db409c69d82eb3e695e16d4a5e8ab56b5b22670a54e9c6ff20",
|
"sha256:f32e54065f0404ceff71388daa7fa7df10e1fb800051dfe302d63abb0acf0020",
|
||||||
"sha256:ee233d0bc27fb8f447ca2622b0de2ac2df45b8795f02ef263825912011fe4fe9"
|
"sha256:f5d242b1a19a36078608f5d7f4d561c5ca55cac8061a323a071c06275267dc19"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.3.11"
|
"version": "==1.3.12"
|
||||||
},
|
},
|
||||||
"yarl": {
|
"yarl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1417,11 +1423,11 @@
|
|||||||
},
|
},
|
||||||
"astroid": {
|
"astroid": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b6c2d75cd7c2982d09e7d41d70213e863b3ba34d3bd4014e08f167cee966e99e",
|
"sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471",
|
||||||
"sha256:ecc50f9b3803ebf8ea19aa2c6df5622d8a5c31456a53c741d3be044d96ff0948"
|
"sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708"
|
||||||
],
|
],
|
||||||
"markers": "python_version ~= '3.6'",
|
"markers": "python_version ~= '3.6'",
|
||||||
"version": "==2.7.2"
|
"version": "==2.8.0"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1464,11 +1470,11 @@
|
|||||||
},
|
},
|
||||||
"charset-normalizer": {
|
"charset-normalizer": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b",
|
"sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6",
|
||||||
"sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3"
|
"sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3'",
|
"markers": "python_version >= '3'",
|
||||||
"version": "==2.0.4"
|
"version": "==2.0.6"
|
||||||
},
|
},
|
||||||
"click": {
|
"click": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1554,11 +1560,11 @@
|
|||||||
},
|
},
|
||||||
"gitpython": {
|
"gitpython": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b",
|
"sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647",
|
||||||
"sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"
|
"sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==3.1.18"
|
"version": "==3.1.24"
|
||||||
},
|
},
|
||||||
"idna": {
|
"idna": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1579,7 +1585,7 @@
|
|||||||
"sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899",
|
"sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899",
|
||||||
"sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"
|
"sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
|
"markers": "python_version < '4' and python_full_version >= '3.6.1'",
|
||||||
"version": "==5.9.3"
|
"version": "==5.9.3"
|
||||||
},
|
},
|
||||||
"lazy-object-proxy": {
|
"lazy-object-proxy": {
|
||||||
@ -1649,19 +1655,19 @@
|
|||||||
},
|
},
|
||||||
"platformdirs": {
|
"platformdirs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c",
|
"sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2",
|
||||||
"sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"
|
"sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.2.0"
|
"version": "==2.4.0"
|
||||||
},
|
},
|
||||||
"pluggy": {
|
"pluggy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
|
||||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==0.13.1"
|
"version": "==1.0.0"
|
||||||
},
|
},
|
||||||
"py": {
|
"py": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1673,11 +1679,11 @@
|
|||||||
},
|
},
|
||||||
"pylint": {
|
"pylint": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1",
|
"sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126",
|
||||||
"sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"
|
"sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.10.2"
|
"version": "==2.11.1"
|
||||||
},
|
},
|
||||||
"pylint-django": {
|
"pylint-django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1704,11 +1710,11 @@
|
|||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
|
"sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89",
|
||||||
"sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
|
"sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==6.2.4"
|
"version": "==6.2.5"
|
||||||
},
|
},
|
||||||
"pytest-django": {
|
"pytest-django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1755,49 +1761,49 @@
|
|||||||
},
|
},
|
||||||
"regex": {
|
"regex": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:03840a07a402576b8e3a6261f17eb88abd653ad4e18ec46ef10c9a63f8c99ebd",
|
"sha256:0628ed7d6334e8f896f882a5c1240de8c4d9b0dd7c7fb8e9f4692f5684b7d656",
|
||||||
"sha256:06ba444bbf7ede3890a912bd4904bb65bf0da8f0d8808b90545481362c978642",
|
"sha256:09eb62654030f39f3ba46bc6726bea464069c29d00a9709e28c9ee9623a8da4a",
|
||||||
"sha256:1f9974826aeeda32a76648fc677e3125ade379869a84aa964b683984a2dea9f1",
|
"sha256:0bba1f6df4eafe79db2ecf38835c2626dbd47911e0516f6962c806f83e7a99ae",
|
||||||
"sha256:330836ad89ff0be756b58758878409f591d4737b6a8cef26a162e2a4961c3321",
|
"sha256:10a7a9cbe30bd90b7d9a1b4749ef20e13a3528e4215a2852be35784b6bd070f0",
|
||||||
"sha256:38600fd58c2996829480de7d034fb2d3a0307110e44dae80b6b4f9b3d2eea529",
|
"sha256:17310b181902e0bb42b29c700e2c2346b8d81f26e900b1328f642e225c88bce1",
|
||||||
"sha256:3a195e26df1fbb40ebee75865f9b64ba692a5824ecb91c078cc665b01f7a9a36",
|
"sha256:1e8d1898d4fb817120a5f684363b30108d7b0b46c7261264b100d14ec90a70e7",
|
||||||
"sha256:41acdd6d64cd56f857e271009966c2ffcbd07ec9149ca91f71088574eaa4278a",
|
"sha256:2054dea683f1bda3a804fcfdb0c1c74821acb968093d0be16233873190d459e3",
|
||||||
"sha256:45f97ade892ace20252e5ccecdd7515c7df5feeb42c3d2a8b8c55920c3551c30",
|
"sha256:29385c4dbb3f8b3a55ce13de6a97a3d21bd00de66acd7cdfc0b49cb2f08c906c",
|
||||||
"sha256:4b0c211c55d4aac4309c3209833c803fada3fc21cdf7b74abedda42a0c9dc3ce",
|
"sha256:295bc8a13554a25ad31e44c4bedabd3c3e28bba027e4feeb9bb157647a2344a7",
|
||||||
"sha256:5d5209c3ba25864b1a57461526ebde31483db295fc6195fdfc4f8355e10f7376",
|
"sha256:2cdb3789736f91d0b3333ac54d12a7e4f9efbc98f53cb905d3496259a893a8b3",
|
||||||
"sha256:615fb5a524cffc91ab4490b69e10ae76c1ccbfa3383ea2fad72e54a85c7d47dd",
|
"sha256:3baf3eaa41044d4ced2463fd5d23bf7bd4b03d68739c6c99a59ce1f95599a673",
|
||||||
"sha256:61e734c2bcb3742c3f454dfa930ea60ea08f56fd1a0eb52d8cb189a2f6be9586",
|
"sha256:4e61100200fa6ab7c99b61476f9f9653962ae71b931391d0264acfb4d9527d9c",
|
||||||
"sha256:640ccca4d0a6fcc6590f005ecd7b16c3d8f5d52174e4854f96b16f34c39d6cb7",
|
"sha256:6266fde576e12357b25096351aac2b4b880b0066263e7bc7a9a1b4307991bb0e",
|
||||||
"sha256:6dbd51c3db300ce9d3171f4106da18fe49e7045232630fe3d4c6e37cb2b39ab9",
|
"sha256:650c4f1fc4273f4e783e1d8e8b51a3e2311c2488ba0fcae6425b1e2c248a189d",
|
||||||
"sha256:71a904da8c9c02aee581f4452a5a988c3003207cb8033db426f29e5b2c0b7aea",
|
"sha256:658e3477676009083422042c4bac2bdad77b696e932a3de001c42cc046f8eda2",
|
||||||
"sha256:8021dee64899f993f4b5cca323aae65aabc01a546ed44356a0965e29d7893c94",
|
"sha256:6adc1bd68f81968c9d249aab8c09cdc2cbe384bf2d2cb7f190f56875000cdc72",
|
||||||
"sha256:8b8d551f1bd60b3e1c59ff55b9e8d74607a5308f66e2916948cafd13480b44a3",
|
"sha256:6c4d83d21d23dd854ffbc8154cf293f4e43ba630aa9bd2539c899343d7f59da3",
|
||||||
"sha256:93f9f720081d97acee38a411e861d4ce84cbc8ea5319bc1f8e38c972c47af49f",
|
"sha256:6f74b6d8f59f3cfb8237e25c532b11f794b96f5c89a6f4a25857d85f84fbef11",
|
||||||
"sha256:96f0c79a70642dfdf7e6a018ebcbea7ea5205e27d8e019cad442d2acfc9af267",
|
"sha256:7783d89bd5413d183a38761fbc68279b984b9afcfbb39fa89d91f63763fbfb90",
|
||||||
"sha256:9966337353e436e6ba652814b0a957a517feb492a98b8f9d3b6ba76d22301dcc",
|
"sha256:7e3536f305f42ad6d31fc86636c54c7dafce8d634e56fef790fbacb59d499dd5",
|
||||||
"sha256:a34ba9e39f8269fd66ab4f7a802794ffea6d6ac500568ec05b327a862c21ce23",
|
"sha256:821e10b73e0898544807a0692a276e539e5bafe0a055506a6882814b6a02c3ec",
|
||||||
"sha256:a49f85f0a099a5755d0a2cc6fc337e3cb945ad6390ec892332c691ab0a045882",
|
"sha256:835962f432bce92dc9bf22903d46c50003c8d11b1dc64084c8fae63bca98564a",
|
||||||
"sha256:a795829dc522227265d72b25d6ee6f6d41eb2105c15912c230097c8f5bfdbcdc",
|
"sha256:85c61bee5957e2d7be390392feac7e1d7abd3a49cbaed0c8cee1541b784c8561",
|
||||||
"sha256:a89ca4105f8099de349d139d1090bad387fe2b208b717b288699ca26f179acbe",
|
"sha256:86f9931eb92e521809d4b64ec8514f18faa8e11e97d6c2d1afa1bcf6c20a8eab",
|
||||||
"sha256:ac95101736239260189f426b1e361dc1b704513963357dc474beb0f39f5b7759",
|
"sha256:8a5c2250c0a74428fd5507ae8853706fdde0f23bfb62ee1ec9418eeacf216078",
|
||||||
"sha256:ae87ab669431f611c56e581679db33b9a467f87d7bf197ac384e71e4956b4456",
|
"sha256:8aec4b4da165c4a64ea80443c16e49e3b15df0f56c124ac5f2f8708a65a0eddc",
|
||||||
"sha256:b091dcfee169ad8de21b61eb2c3a75f9f0f859f851f64fdaf9320759a3244239",
|
"sha256:8c268e78d175798cd71d29114b0a1f1391c7d011995267d3b62319ec1a4ecaa1",
|
||||||
"sha256:b511c6009d50d5c0dd0bab85ed25bc8ad6b6f5611de3a63a59786207e82824bb",
|
"sha256:8d80087320632457aefc73f686f66139801959bf5b066b4419b92be85be3543c",
|
||||||
"sha256:b79dc2b2e313565416c1e62807c7c25c67a6ff0a0f8d83a318df464555b65948",
|
"sha256:95e89a8558c8c48626dcffdf9c8abac26b7c251d352688e7ab9baf351e1c7da6",
|
||||||
"sha256:bca14dfcfd9aae06d7d8d7e105539bd77d39d06caaae57a1ce945670bae744e0",
|
"sha256:9c371dd326289d85906c27ec2bc1dcdedd9d0be12b543d16e37bad35754bde48",
|
||||||
"sha256:c835c30f3af5c63a80917b72115e1defb83de99c73bc727bddd979a3b449e183",
|
"sha256:9c7cb25adba814d5f419733fe565f3289d6fa629ab9e0b78f6dff5fa94ab0456",
|
||||||
"sha256:ccd721f1d4fc42b541b633d6e339018a08dd0290dc67269df79552843a06ca92",
|
"sha256:a731552729ee8ae9c546fb1c651c97bf5f759018fdd40d0e9b4d129e1e3a44c8",
|
||||||
"sha256:d6c2b1d78ceceb6741d703508cd0e9197b34f6bf6864dab30f940f8886e04ade",
|
"sha256:aea4006b73b555fc5bdb650a8b92cf486d678afa168cf9b38402bb60bf0f9c18",
|
||||||
"sha256:d6ec4ae13760ceda023b2e5ef1f9bc0b21e4b0830458db143794a117fdbdc044",
|
"sha256:b0e3f59d3c772f2c3baaef2db425e6fc4149d35a052d874bb95ccfca10a1b9f4",
|
||||||
"sha256:d8b623fc429a38a881ab2d9a56ef30e8ea20c72a891c193f5ebbddc016e083ee",
|
"sha256:b15dc34273aefe522df25096d5d087abc626e388a28a28ac75a4404bb7668736",
|
||||||
"sha256:ea9753d64cba6f226947c318a923dadaf1e21cd8db02f71652405263daa1f033",
|
"sha256:c000635fd78400a558bd7a3c2981bb2a430005ebaa909d31e6e300719739a949",
|
||||||
"sha256:ebbceefbffae118ab954d3cd6bf718f5790db66152f95202ebc231d58ad4e2c2",
|
"sha256:c31f35a984caffb75f00a86852951a337540b44e4a22171354fb760cefa09346",
|
||||||
"sha256:ecb6e7c45f9cd199c10ec35262b53b2247fb9a408803ed00ee5bb2b54aa626f5",
|
"sha256:c50a6379763c733562b1fee877372234d271e5c78cd13ade5f25978aa06744db",
|
||||||
"sha256:ef9326c64349e2d718373415814e754183057ebc092261387a2c2f732d9172b2",
|
"sha256:c94722bf403b8da744b7d0bb87e1f2529383003ceec92e754f768ef9323f69ad",
|
||||||
"sha256:f93a9d8804f4cec9da6c26c8cfae2c777028b4fdd9f49de0302e26e00bb86504",
|
"sha256:dcbbc9cfa147d55a577d285fd479b43103188855074552708df7acc31a476dd9",
|
||||||
"sha256:faf08b0341828f6a29b8f7dd94d5cf8cc7c39bfc3e67b78514c54b494b66915a"
|
"sha256:fb9f5844db480e2ef9fce3a72e71122dd010ab7b2920f777966ba25f7eb63819"
|
||||||
],
|
],
|
||||||
"version": "==2021.8.21"
|
"version": "==2021.9.24"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1855,16 +1861,24 @@
|
|||||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==0.10.2"
|
"version": "==0.10.2"
|
||||||
},
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e",
|
||||||
|
"sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7",
|
||||||
|
"sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"
|
||||||
|
],
|
||||||
|
"version": "==3.10.0.2"
|
||||||
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"extras": [
|
"extras": [
|
||||||
"secure"
|
"secure"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4",
|
"sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece",
|
||||||
"sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"
|
"sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.26.6"
|
"version": "==1.26.7"
|
||||||
},
|
},
|
||||||
"wrapt": {
|
"wrapt": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
11
README.md
11
README.md
@ -4,14 +4,15 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[](https://discord.gg/jg33eMhnj6)
|
[](https://discord.gg/jg33eMhnj6)
|
||||||
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6)
|
[](https://github.com/goauthentik/authentik/actions/workflows/ci-main.yml)
|
||||||
[](https://dev.azure.com/beryjuorg/authentik/_build?definitionId=6)
|
[](https://github.com/goauthentik/authentik/actions/workflows/ci-outpost.yml)
|
||||||
|
[](https://github.com/goauthentik/authentik/actions/workflows/ci-web.yml)
|
||||||
[](https://codecov.io/gh/goauthentik/authentik)
|
[](https://codecov.io/gh/goauthentik/authentik)
|
||||||
|
[](https://goauthentik.testspace.com/)
|
||||||

|

|
||||||

|

|
||||||

|
[](https://www.transifex.com/beryjuorg/authentik/)
|
||||||
[Transifex](https://www.transifex.com/beryjuorg/authentik/)
|
|
||||||
|
|
||||||
## What is authentik?
|
## What is authentik?
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,8 @@
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ---------- | ------------------ |
|
| ---------- | ------------------ |
|
||||||
| 2021.5.x | :white_check_mark: |
|
|
||||||
| 2021.6.x | :white_check_mark: |
|
|
||||||
| 2021.7.x | :white_check_mark: |
|
| 2021.7.x | :white_check_mark: |
|
||||||
|
| 2021.8.x | :white_check_mark: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "2021.8.1-rc2"
|
__version__ = "2021.9.6"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|||||||
@ -84,7 +84,7 @@ class SystemSerializer(PassiveSerializer):
|
|||||||
return now()
|
return now()
|
||||||
|
|
||||||
def get_embedded_outpost_host(self, request: Request) -> str:
|
def get_embedded_outpost_host(self, request: Request) -> str:
|
||||||
"""Get the FQDN configured on the embeddded outpost"""
|
"""Get the FQDN configured on the embedded outpost"""
|
||||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||||
if not outposts.exists():
|
if not outposts.exists():
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@ -8,3 +8,8 @@ class AuthentikAdminConfig(AppConfig):
|
|||||||
name = "authentik.admin"
|
name = "authentik.admin"
|
||||||
label = "authentik_admin"
|
label = "authentik_admin"
|
||||||
verbose_name = "authentik Admin"
|
verbose_name = "authentik Admin"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from authentik.admin.tasks import clear_update_notifications
|
||||||
|
|
||||||
|
clear_update_notifications.delay()
|
||||||
|
|||||||
@ -6,12 +6,14 @@ from django.core.cache import cache
|
|||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
from packaging.version import parse
|
from packaging.version import parse
|
||||||
from prometheus_client import Info
|
from prometheus_client import Info
|
||||||
from requests import RequestException, get
|
from requests import RequestException
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction, Notification
|
||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -33,15 +35,32 @@ def _set_prom_info():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task()
|
||||||
|
def clear_update_notifications():
|
||||||
|
"""Clear update notifications on startup if the notification was for the version
|
||||||
|
we're running now."""
|
||||||
|
for notification in Notification.objects.filter(event__action=EventAction.UPDATE_AVAILABLE):
|
||||||
|
if "new_version" not in notification.event.context:
|
||||||
|
continue
|
||||||
|
notification_version = notification.event.context["new_version"]
|
||||||
|
if notification_version == __version__:
|
||||||
|
notification.delete()
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||||
def update_latest_version(self: MonitoredTask):
|
def update_latest_version(self: MonitoredTask):
|
||||||
"""Update latest version info"""
|
"""Update latest version info"""
|
||||||
|
if CONFIG.y_bool("disable_update_check"):
|
||||||
|
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
|
||||||
|
self.set_status(TaskResult(TaskResultStatus.WARNING, messages=["Version check disabled."]))
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
response = get("https://api.github.com/repos/goauthentik/authentik/releases/latest")
|
response = get_http_session().get(
|
||||||
|
"https://version.goauthentik.io/version.json",
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
tag_name = data.get("tag_name")
|
upstream_version = data.get("stable", {}).get("version")
|
||||||
upstream_version = tag_name.split("/")[1]
|
|
||||||
cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT)
|
cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT)
|
||||||
self.set_status(
|
self.set_status(
|
||||||
TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"])
|
TaskResult(TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"])
|
||||||
@ -58,7 +77,7 @@ def update_latest_version(self: MonitoredTask):
|
|||||||
).exists():
|
).exists():
|
||||||
return
|
return
|
||||||
event_dict = {"new_version": upstream_version}
|
event_dict = {"new_version": upstream_version}
|
||||||
if match := re.search(URL_FINDER, data.get("body", "")):
|
if match := re.search(URL_FINDER, data.get("stable", {}).get("changelog", "")):
|
||||||
event_dict["message"] = f"Changelog: {match.group()}"
|
event_dict["message"] = f"Changelog: {match.group()}"
|
||||||
Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save()
|
Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save()
|
||||||
except (RequestException, IndexError) as exc:
|
except (RequestException, IndexError) as exc:
|
||||||
|
|||||||
@ -1,81 +1,58 @@
|
|||||||
"""test admin tasks"""
|
"""test admin tasks"""
|
||||||
import json
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from unittest.mock import Mock, patch
|
|
||||||
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from requests.exceptions import RequestException
|
from requests_mock import Mocker
|
||||||
|
|
||||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
|
||||||
|
RESPONSE_VALID = {
|
||||||
@dataclass
|
"$schema": "https://version.goauthentik.io/schema.json",
|
||||||
class MockResponse:
|
"stable": {
|
||||||
"""Mock class to emulate the methods of requests's Response we need"""
|
"version": "99999999.9999999",
|
||||||
|
"changelog": "See https://goauthentik.io/test",
|
||||||
status_code: int
|
"reason": "bugfix",
|
||||||
response: str
|
},
|
||||||
|
}
|
||||||
def json(self) -> dict:
|
|
||||||
"""Get json parsed response"""
|
|
||||||
return json.loads(self.response)
|
|
||||||
|
|
||||||
def raise_for_status(self):
|
|
||||||
"""raise RequestException if status code is 400 or more"""
|
|
||||||
if self.status_code >= 400:
|
|
||||||
raise RequestException
|
|
||||||
|
|
||||||
|
|
||||||
REQUEST_MOCK_VALID = Mock(
|
|
||||||
return_value=MockResponse(
|
|
||||||
200,
|
|
||||||
"""{
|
|
||||||
"tag_name": "version/99999999.9999999",
|
|
||||||
"body": "https://goauthentik.io/test"
|
|
||||||
}""",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
REQUEST_MOCK_INVALID = Mock(return_value=MockResponse(400, "{}"))
|
|
||||||
|
|
||||||
|
|
||||||
class TestAdminTasks(TestCase):
|
class TestAdminTasks(TestCase):
|
||||||
"""test admin tasks"""
|
"""test admin tasks"""
|
||||||
|
|
||||||
@patch("authentik.admin.tasks.get", REQUEST_MOCK_VALID)
|
|
||||||
def test_version_valid_response(self):
|
def test_version_valid_response(self):
|
||||||
"""Test Update checker with valid response"""
|
"""Test Update checker with valid response"""
|
||||||
update_latest_version.delay().get()
|
with Mocker() as mocker:
|
||||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999")
|
mocker.get("https://version.goauthentik.io/version.json", json=RESPONSE_VALID)
|
||||||
self.assertTrue(
|
update_latest_version.delay().get()
|
||||||
Event.objects.filter(
|
self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999")
|
||||||
action=EventAction.UPDATE_AVAILABLE,
|
self.assertTrue(
|
||||||
context__new_version="99999999.9999999",
|
|
||||||
context__message="Changelog: https://goauthentik.io/test",
|
|
||||||
).exists()
|
|
||||||
)
|
|
||||||
# test that a consecutive check doesn't create a duplicate event
|
|
||||||
update_latest_version.delay().get()
|
|
||||||
self.assertEqual(
|
|
||||||
len(
|
|
||||||
Event.objects.filter(
|
Event.objects.filter(
|
||||||
action=EventAction.UPDATE_AVAILABLE,
|
action=EventAction.UPDATE_AVAILABLE,
|
||||||
context__new_version="99999999.9999999",
|
context__new_version="99999999.9999999",
|
||||||
context__message="Changelog: https://goauthentik.io/test",
|
context__message="Changelog: https://goauthentik.io/test",
|
||||||
)
|
).exists()
|
||||||
),
|
)
|
||||||
1,
|
# test that a consecutive check doesn't create a duplicate event
|
||||||
)
|
update_latest_version.delay().get()
|
||||||
|
self.assertEqual(
|
||||||
|
len(
|
||||||
|
Event.objects.filter(
|
||||||
|
action=EventAction.UPDATE_AVAILABLE,
|
||||||
|
context__new_version="99999999.9999999",
|
||||||
|
context__message="Changelog: https://goauthentik.io/test",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
@patch("authentik.admin.tasks.get", REQUEST_MOCK_INVALID)
|
|
||||||
def test_version_error(self):
|
def test_version_error(self):
|
||||||
"""Test Update checker with invalid response"""
|
"""Test Update checker with invalid response"""
|
||||||
update_latest_version.delay().get()
|
with Mocker() as mocker:
|
||||||
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
|
mocker.get("https://version.goauthentik.io/version.json", status_code=400)
|
||||||
self.assertFalse(
|
update_latest_version.delay().get()
|
||||||
Event.objects.filter(
|
self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0")
|
||||||
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
self.assertFalse(
|
||||||
).exists()
|
Event.objects.filter(
|
||||||
)
|
action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0"
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
|||||||
@ -40,7 +40,6 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
|
|||||||
raise AuthenticationFailed("Malformed header")
|
raise AuthenticationFailed("Malformed header")
|
||||||
tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
|
tokens = Token.filter_not_expired(key=password, intent=TokenIntents.INTENT_API)
|
||||||
if not tokens.exists():
|
if not tokens.exists():
|
||||||
LOGGER.info("Authenticating via secret_key")
|
|
||||||
user = token_secret_key(password)
|
user = token_secret_key(password)
|
||||||
if not user:
|
if not user:
|
||||||
raise AuthenticationFailed("Token invalid/expired")
|
raise AuthenticationFailed("Token invalid/expired")
|
||||||
@ -58,6 +57,7 @@ def token_secret_key(value: str) -> Optional[User]:
|
|||||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||||
if not outposts:
|
if not outposts:
|
||||||
return None
|
return None
|
||||||
|
LOGGER.info("Authenticating via secret_key")
|
||||||
outpost = outposts.first()
|
outpost = outposts.first()
|
||||||
return outpost.user
|
return outpost.user
|
||||||
|
|
||||||
|
|||||||
@ -33,3 +33,12 @@ class OwnerPermissions(BasePermission):
|
|||||||
if owner != request.user:
|
if owner != request.user:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class OwnerSuperuserPermissions(OwnerPermissions):
|
||||||
|
"""Similar to OwnerPermissions, except always allow access for superusers"""
|
||||||
|
|
||||||
|
def has_object_permission(self, request: Request, view, obj: Model) -> bool:
|
||||||
|
if request.user.is_superuser:
|
||||||
|
return True
|
||||||
|
return super().has_object_permission(request, view, obj)
|
||||||
|
|||||||
@ -5,6 +5,9 @@ from typing import Callable, Optional
|
|||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
def permission_required(perm: Optional[str] = None, other_perms: Optional[list[str]] = None):
|
def permission_required(perm: Optional[str] = None, other_perms: Optional[list[str]] = None):
|
||||||
@ -18,10 +21,12 @@ def permission_required(perm: Optional[str] = None, other_perms: Optional[list[s
|
|||||||
if perm:
|
if perm:
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
if not request.user.has_perm(perm, obj):
|
if not request.user.has_perm(perm, obj):
|
||||||
|
LOGGER.debug("denying access for object", user=request.user, perm=perm, obj=obj)
|
||||||
return self.permission_denied(request)
|
return self.permission_denied(request)
|
||||||
if other_perms:
|
if other_perms:
|
||||||
for other_perm in other_perms:
|
for other_perm in other_perms:
|
||||||
if not request.user.has_perm(other_perm):
|
if not request.user.has_perm(other_perm):
|
||||||
|
LOGGER.debug("denying access for other", user=request.user, perm=perm)
|
||||||
return self.permission_denied(request)
|
return self.permission_denied(request)
|
||||||
return func(self, request, *args, **kwargs)
|
return func(self, request, *args, **kwargs)
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from drf_spectacular.types import OpenApiTypes
|
|||||||
|
|
||||||
|
|
||||||
def build_standard_type(obj, **kwargs):
|
def build_standard_type(obj, **kwargs):
|
||||||
"""Build a basic type with optional add ons."""
|
"""Build a basic type with optional add owns."""
|
||||||
schema = build_basic_type(obj)
|
schema = build_basic_type(obj)
|
||||||
schema.update(kwargs)
|
schema.update(kwargs)
|
||||||
return schema
|
return schema
|
||||||
@ -31,7 +31,7 @@ VALIDATION_ERROR = build_object_type(
|
|||||||
"non_field_errors": build_array_type(build_standard_type(OpenApiTypes.STR)),
|
"non_field_errors": build_array_type(build_standard_type(OpenApiTypes.STR)),
|
||||||
"code": build_standard_type(OpenApiTypes.STR),
|
"code": build_standard_type(OpenApiTypes.STR),
|
||||||
},
|
},
|
||||||
required=["detail"],
|
required=[],
|
||||||
additionalProperties={},
|
additionalProperties={},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
19
authentik/api/tasks.py
Normal file
19
authentik/api/tasks.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
"""API tasks"""
|
||||||
|
|
||||||
|
from authentik.lib.utils.http import get_http_session
|
||||||
|
from authentik.root.celery import CELERY_APP
|
||||||
|
|
||||||
|
SENTRY_SESSION = get_http_session()
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task()
|
||||||
|
def sentry_proxy(payload: str):
|
||||||
|
"""Relay data to sentry"""
|
||||||
|
SENTRY_SESSION.post(
|
||||||
|
"https://sentry.beryju.org/api/8/envelope/",
|
||||||
|
data=payload,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
@ -1,8 +1,10 @@
|
|||||||
"""authentik api urls"""
|
"""authentik api urls"""
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from authentik.api.v2.urls import urlpatterns as v2_urls
|
from authentik.api.v3.urls import urlpatterns as v3_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("v2beta/", include(v2_urls)),
|
# Remove in 2022.1
|
||||||
|
path("v2beta/", include(v3_urls)),
|
||||||
|
path("v3/", include(v3_urls)),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,38 +0,0 @@
|
|||||||
"""Sentry tunnel"""
|
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.http.request import HttpRequest
|
|
||||||
from django.http.response import HttpResponse
|
|
||||||
from django.views.generic.base import View
|
|
||||||
from requests import post
|
|
||||||
from requests.exceptions import RequestException
|
|
||||||
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
|
|
||||||
|
|
||||||
class SentryTunnelView(View):
|
|
||||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
|
||||||
# Only allow usage of this endpoint when error reporting is enabled
|
|
||||||
if not CONFIG.y_bool("error_reporting.enabled", False):
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
# Body is 2 json objects separated by \n
|
|
||||||
full_body = request.body
|
|
||||||
header = loads(full_body.splitlines()[0])
|
|
||||||
# Check that the DSN is what we expect
|
|
||||||
dsn = header.get("dsn", "")
|
|
||||||
if dsn != settings.SENTRY_DSN:
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
response = post(
|
|
||||||
"https://sentry.beryju.org/api/8/envelope/",
|
|
||||||
data=full_body,
|
|
||||||
headers={"Content-Type": "application/octet-stream"},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
response.raise_for_status()
|
|
||||||
except RequestException:
|
|
||||||
return HttpResponse(status=500)
|
|
||||||
return HttpResponse(status=response.status_code)
|
|
||||||
@ -63,7 +63,7 @@ class ConfigView(APIView):
|
|||||||
|
|
||||||
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
@extend_schema(responses={200: ConfigSerializer(many=False)})
|
||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request) -> Response:
|
||||||
"""Retrive public configuration options"""
|
"""Retrieve public configuration options"""
|
||||||
config = ConfigSerializer(
|
config = ConfigSerializer(
|
||||||
{
|
{
|
||||||
"error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
|
"error_reporting_enabled": CONFIG.y("error_reporting.enabled"),
|
||||||
65
authentik/api/v3/sentry.py
Normal file
65
authentik/api/v3/sentry.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""Sentry tunnel"""
|
||||||
|
from json import loads
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from django.http.response import HttpResponse
|
||||||
|
from rest_framework.authentication import SessionAuthentication
|
||||||
|
from rest_framework.parsers import BaseParser
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.throttling import AnonRateThrottle
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.api.tasks import sentry_proxy
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class PlainTextParser(BaseParser):
|
||||||
|
"""Plain text parser."""
|
||||||
|
|
||||||
|
media_type = "text/plain"
|
||||||
|
|
||||||
|
def parse(self, stream, media_type=None, parser_context=None) -> str:
|
||||||
|
"""Simply return a string representing the body of the request."""
|
||||||
|
return stream.read()
|
||||||
|
|
||||||
|
|
||||||
|
class CsrfExemptSessionAuthentication(SessionAuthentication):
|
||||||
|
"""CSRF-exempt Session authentication"""
|
||||||
|
|
||||||
|
def enforce_csrf(self, request: Request):
|
||||||
|
return # To not perform the csrf check previously happening
|
||||||
|
|
||||||
|
|
||||||
|
class SentryTunnelView(APIView):
|
||||||
|
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
||||||
|
|
||||||
|
serializer_class = None
|
||||||
|
parser_classes = [PlainTextParser]
|
||||||
|
throttle_classes = [AnonRateThrottle]
|
||||||
|
permission_classes = [AllowAny]
|
||||||
|
authentication_classes = [CsrfExemptSessionAuthentication]
|
||||||
|
|
||||||
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
||||||
|
# Only allow usage of this endpoint when error reporting is enabled
|
||||||
|
if not CONFIG.y_bool("error_reporting.enabled", False):
|
||||||
|
LOGGER.debug("error reporting disabled")
|
||||||
|
return HttpResponse(status=400)
|
||||||
|
# Body is 2 json objects separated by \n
|
||||||
|
full_body = request.body
|
||||||
|
lines = full_body.splitlines()
|
||||||
|
if len(lines) < 1:
|
||||||
|
return HttpResponse(status=400)
|
||||||
|
header = loads(lines[0])
|
||||||
|
# Check that the DSN is what we expect
|
||||||
|
dsn = header.get("dsn", "")
|
||||||
|
if dsn != settings.SENTRY_DSN:
|
||||||
|
LOGGER.debug("Invalid dsn", have=dsn, expected=settings.SENTRY_DSN)
|
||||||
|
return HttpResponse(status=400)
|
||||||
|
sentry_proxy.delay(full_body.decode())
|
||||||
|
return HttpResponse(status=204)
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"""api v2 urls"""
|
"""api v3 urls"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.cache import cache_page
|
||||||
from drf_spectacular.views import SpectacularAPIView
|
from drf_spectacular.views import SpectacularAPIView
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
@ -10,8 +10,8 @@ from authentik.admin.api.system import SystemView
|
|||||||
from authentik.admin.api.tasks import TaskViewSet
|
from authentik.admin.api.tasks import TaskViewSet
|
||||||
from authentik.admin.api.version import VersionView
|
from authentik.admin.api.version import VersionView
|
||||||
from authentik.admin.api.workers import WorkerView
|
from authentik.admin.api.workers import WorkerView
|
||||||
from authentik.api.v2.config import ConfigView
|
from authentik.api.v3.config import ConfigView
|
||||||
from authentik.api.v2.sentry import SentryTunnelView
|
from authentik.api.v3.sentry import SentryTunnelView
|
||||||
from authentik.api.views import APIBrowserView
|
from authentik.api.views import APIBrowserView
|
||||||
from authentik.core.api.applications import ApplicationViewSet
|
from authentik.core.api.applications import ApplicationViewSet
|
||||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||||
@ -24,6 +24,7 @@ from authentik.core.api.users import UserViewSet
|
|||||||
from authentik.crypto.api import CertificateKeyPairViewSet
|
from authentik.crypto.api import CertificateKeyPairViewSet
|
||||||
from authentik.events.api.event import EventViewSet
|
from authentik.events.api.event import EventViewSet
|
||||||
from authentik.events.api.notification import NotificationViewSet
|
from authentik.events.api.notification import NotificationViewSet
|
||||||
|
from authentik.events.api.notification_mapping import NotificationWebhookMappingViewSet
|
||||||
from authentik.events.api.notification_rule import NotificationRuleViewSet
|
from authentik.events.api.notification_rule import NotificationRuleViewSet
|
||||||
from authentik.events.api.notification_transport import NotificationTransportViewSet
|
from authentik.events.api.notification_transport import NotificationTransportViewSet
|
||||||
from authentik.flows.api.bindings import FlowStageBindingViewSet
|
from authentik.flows.api.bindings import FlowStageBindingViewSet
|
||||||
@ -98,6 +99,7 @@ from authentik.stages.user_write.api import UserWriteStageViewSet
|
|||||||
from authentik.tenants.api import TenantViewSet
|
from authentik.tenants.api import TenantViewSet
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
|
router.include_format_suffixes = False
|
||||||
|
|
||||||
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
|
router.register("admin/system_tasks", TaskViewSet, basename="admin_system_tasks")
|
||||||
router.register("admin/apps", AppsViewSet, basename="apps")
|
router.register("admin/apps", AppsViewSet, basename="apps")
|
||||||
@ -159,6 +161,7 @@ router.register("propertymappings/all", PropertyMappingViewSet)
|
|||||||
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
||||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||||
router.register("propertymappings/scope", ScopeMappingViewSet)
|
router.register("propertymappings/scope", ScopeMappingViewSet)
|
||||||
|
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
||||||
|
|
||||||
router.register("authenticators/duo", DuoDeviceViewSet)
|
router.register("authenticators/duo", DuoDeviceViewSet)
|
||||||
router.register("authenticators/static", StaticDeviceViewSet)
|
router.register("authenticators/static", StaticDeviceViewSet)
|
||||||
@ -225,7 +228,7 @@ urlpatterns = (
|
|||||||
FlowExecutorView.as_view(),
|
FlowExecutorView.as_view(),
|
||||||
name="flow-executor",
|
name="flow-executor",
|
||||||
),
|
),
|
||||||
path("sentry/", csrf_exempt(SentryTunnelView.as_view()), name="sentry"),
|
path("sentry/", SentryTunnelView.as_view(), name="sentry"),
|
||||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -4,14 +4,9 @@ from django.db.models import QuerySet
|
|||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import (
|
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
|
||||||
OpenApiParameter,
|
|
||||||
OpenApiResponse,
|
|
||||||
extend_schema,
|
|
||||||
inline_serializer,
|
|
||||||
)
|
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import BooleanField, CharField, FileField, ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -24,6 +19,7 @@ from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
|
|||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.providers import ProviderSerializer
|
from authentik.core.api.providers import ProviderSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.core.api.utils import FilePathSerializer, FileUploadSerializer
|
||||||
from authentik.core.models import Application, User
|
from authentik.core.models import Application, User
|
||||||
from authentik.events.models import EventAction
|
from authentik.events.models import EventAction
|
||||||
from authentik.policies.api.exec import PolicyTestResultSerializer
|
from authentik.policies.api.exec import PolicyTestResultSerializer
|
||||||
@ -71,7 +67,7 @@ class ApplicationSerializer(ModelSerializer):
|
|||||||
class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Application Viewset"""
|
"""Application Viewset"""
|
||||||
|
|
||||||
queryset = Application.objects.all()
|
queryset = Application.objects.all().prefetch_related("provider")
|
||||||
serializer_class = ApplicationSerializer
|
serializer_class = ApplicationSerializer
|
||||||
search_fields = [
|
search_fields = [
|
||||||
"name",
|
"name",
|
||||||
@ -180,13 +176,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
@permission_required("authentik_core.change_application")
|
@permission_required("authentik_core.change_application")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request={
|
request={
|
||||||
"multipart/form-data": inline_serializer(
|
"multipart/form-data": FileUploadSerializer,
|
||||||
"SetIcon",
|
|
||||||
fields={
|
|
||||||
"file": FileField(required=False),
|
|
||||||
"clear": BooleanField(default=False),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
responses={
|
responses={
|
||||||
200: OpenApiResponse(description="Success"),
|
200: OpenApiResponse(description="Success"),
|
||||||
@ -218,7 +208,7 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
|
|
||||||
@permission_required("authentik_core.change_application")
|
@permission_required("authentik_core.change_application")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
|
request=FilePathSerializer,
|
||||||
responses={
|
responses={
|
||||||
200: OpenApiResponse(description="Success"),
|
200: OpenApiResponse(description="Success"),
|
||||||
400: OpenApiResponse(description="Bad request"),
|
400: OpenApiResponse(description="Bad request"),
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from rest_framework.serializers import ModelSerializer
|
|||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from ua_parser import user_agent_parser
|
from ua_parser import user_agent_parser
|
||||||
|
|
||||||
|
from authentik.api.authorization import OwnerSuperuserPermissions
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.models import AuthenticatedSession
|
from authentik.core.models import AuthenticatedSession
|
||||||
from authentik.events.geo import GEOIP_READER, GeoIPDict
|
from authentik.events.geo import GEOIP_READER, GeoIPDict
|
||||||
@ -102,11 +103,8 @@ class AuthenticatedSessionViewSet(
|
|||||||
search_fields = ["user__username", "last_ip", "last_user_agent"]
|
search_fields = ["user__username", "last_ip", "last_user_agent"]
|
||||||
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
|
filterset_fields = ["user__username", "last_ip", "last_user_agent"]
|
||||||
ordering = ["user__username"]
|
ordering = ["user__username"]
|
||||||
filter_backends = [
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
DjangoFilterBackend,
|
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
OrderingFilter,
|
|
||||||
SearchFilter,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user if self.request else get_anonymous_user()
|
user = self.request.user if self.request else get_anonymous_user()
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django_filters.filters import ModelMultipleChoiceFilter
|
from django_filters.filters import ModelMultipleChoiceFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from rest_framework.fields import BooleanField, CharField, JSONField
|
from rest_framework.fields import CharField, JSONField
|
||||||
from rest_framework.serializers import ListSerializer, ModelSerializer
|
from rest_framework.serializers import ListSerializer, ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||||
@ -15,7 +15,6 @@ from authentik.core.models import Group, User
|
|||||||
class GroupMemberSerializer(ModelSerializer):
|
class GroupMemberSerializer(ModelSerializer):
|
||||||
"""Stripped down user serializer to show relevant users for groups"""
|
"""Stripped down user serializer to show relevant users for groups"""
|
||||||
|
|
||||||
is_superuser = BooleanField(read_only=True)
|
|
||||||
avatar = CharField(read_only=True)
|
avatar = CharField(read_only=True)
|
||||||
attributes = JSONField(validators=[is_dict], required=False)
|
attributes = JSONField(validators=[is_dict], required=False)
|
||||||
uid = CharField(read_only=True)
|
uid = CharField(read_only=True)
|
||||||
@ -29,7 +28,6 @@ class GroupMemberSerializer(ModelSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"is_active",
|
"is_active",
|
||||||
"last_login",
|
"last_login",
|
||||||
"is_superuser",
|
|
||||||
"email",
|
"email",
|
||||||
"avatar",
|
"avatar",
|
||||||
"attributes",
|
"attributes",
|
||||||
@ -81,7 +79,7 @@ class GroupFilter(FilterSet):
|
|||||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""Group Viewset"""
|
"""Group Viewset"""
|
||||||
|
|
||||||
queryset = Group.objects.all()
|
queryset = Group.objects.all().select_related("parent").prefetch_related("users")
|
||||||
serializer_class = GroupSerializer
|
serializer_class = GroupSerializer
|
||||||
search_fields = ["name", "is_superuser"]
|
search_fields = ["name", "is_superuser"]
|
||||||
filterset_class = GroupFilter
|
filterset_class = GroupFilter
|
||||||
|
|||||||
@ -95,7 +95,9 @@ class SourceViewSet(
|
|||||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
def user_settings(self, request: Request) -> Response:
|
def user_settings(self, request: Request) -> Response:
|
||||||
"""Get all sources the user can configure"""
|
"""Get all sources the user can configure"""
|
||||||
_all_sources: Iterable[Source] = Source.objects.filter(enabled=True).select_subclasses()
|
_all_sources: Iterable[Source] = (
|
||||||
|
Source.objects.filter(enabled=True).select_subclasses().order_by("name")
|
||||||
|
)
|
||||||
matching_sources: list[UserSettingSerializer] = []
|
matching_sources: list[UserSettingSerializer] = []
|
||||||
for source in _all_sources:
|
for source in _all_sources:
|
||||||
user_settings = source.ui_user_settings
|
user_settings = source.ui_user_settings
|
||||||
|
|||||||
@ -2,15 +2,19 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.api.authorization import OwnerSuperuserPermissions
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.users import UserSerializer
|
from authentik.core.api.users import UserSerializer
|
||||||
@ -23,10 +27,12 @@ from authentik.managed.api import ManagedSerializer
|
|||||||
class TokenSerializer(ManagedSerializer, ModelSerializer):
|
class TokenSerializer(ManagedSerializer, ModelSerializer):
|
||||||
"""Token Serializer"""
|
"""Token Serializer"""
|
||||||
|
|
||||||
user = UserSerializer(required=False)
|
user_obj = UserSerializer(required=False, source="user")
|
||||||
|
|
||||||
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||||
"""Ensure only API or App password tokens are created."""
|
"""Ensure only API or App password tokens are created."""
|
||||||
|
request: Request = self.context["request"]
|
||||||
|
attrs.setdefault("user", request.user)
|
||||||
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
attrs.setdefault("intent", TokenIntents.INTENT_API)
|
||||||
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
if attrs.get("intent") not in [TokenIntents.INTENT_API, TokenIntents.INTENT_APP_PASSWORD]:
|
||||||
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
raise ValidationError(f"Invalid intent {attrs.get('intent')}")
|
||||||
@ -41,11 +47,14 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
|
|||||||
"identifier",
|
"identifier",
|
||||||
"intent",
|
"intent",
|
||||||
"user",
|
"user",
|
||||||
|
"user_obj",
|
||||||
"description",
|
"description",
|
||||||
"expires",
|
"expires",
|
||||||
"expiring",
|
"expiring",
|
||||||
]
|
]
|
||||||
depth = 2
|
extra_kwargs = {
|
||||||
|
"user": {"required": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TokenViewSerializer(PassiveSerializer):
|
class TokenViewSerializer(PassiveSerializer):
|
||||||
@ -73,14 +82,25 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"description",
|
"description",
|
||||||
"expires",
|
"expires",
|
||||||
"expiring",
|
"expiring",
|
||||||
|
"managed",
|
||||||
]
|
]
|
||||||
ordering = ["expires"]
|
ordering = ["identifier", "expires"]
|
||||||
|
permission_classes = [OwnerSuperuserPermissions]
|
||||||
|
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
user = self.request.user if self.request else get_anonymous_user()
|
||||||
|
if user.is_superuser:
|
||||||
|
return super().get_queryset()
|
||||||
|
return super().get_queryset().filter(user=user.pk)
|
||||||
|
|
||||||
def perform_create(self, serializer: TokenSerializer):
|
def perform_create(self, serializer: TokenSerializer):
|
||||||
serializer.save(
|
if not self.request.user.is_superuser:
|
||||||
user=self.request.user,
|
return serializer.save(
|
||||||
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
|
user=self.request.user,
|
||||||
)
|
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
|
||||||
|
)
|
||||||
|
return super().perform_create(serializer)
|
||||||
|
|
||||||
@permission_required("authentik_core.view_token_key")
|
@permission_required("authentik_core.view_token_key")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
"""User API Views"""
|
"""User API Views"""
|
||||||
|
from datetime import timedelta
|
||||||
from json import loads
|
from json import loads
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@ -7,6 +8,8 @@ from django.db.transaction import atomic
|
|||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
|
from django.utils.text import slugify
|
||||||
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
|
from django_filters.filters import BooleanFilter, CharFilter, ModelMultipleChoiceFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
@ -87,6 +90,9 @@ class UserSerializer(ModelSerializer):
|
|||||||
"attributes",
|
"attributes",
|
||||||
"uid",
|
"uid",
|
||||||
]
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
"name": {"allow_blank": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserSelfSerializer(ModelSerializer):
|
class UserSelfSerializer(ModelSerializer):
|
||||||
@ -95,9 +101,25 @@ class UserSelfSerializer(ModelSerializer):
|
|||||||
|
|
||||||
is_superuser = BooleanField(read_only=True)
|
is_superuser = BooleanField(read_only=True)
|
||||||
avatar = CharField(read_only=True)
|
avatar = CharField(read_only=True)
|
||||||
groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
|
groups = SerializerMethodField()
|
||||||
uid = CharField(read_only=True)
|
uid = CharField(read_only=True)
|
||||||
|
|
||||||
|
@extend_schema_field(
|
||||||
|
ListSerializer(
|
||||||
|
child=inline_serializer(
|
||||||
|
"UserSelfGroups",
|
||||||
|
{"name": CharField(read_only=True), "pk": CharField(read_only=True)},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def get_groups(self, user: User):
|
||||||
|
"""Return only the group names a user is member of"""
|
||||||
|
for group in user.ak_groups.all():
|
||||||
|
yield {
|
||||||
|
"name": group.name,
|
||||||
|
"pk": group.pk,
|
||||||
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
@ -114,6 +136,7 @@ class UserSelfSerializer(ModelSerializer):
|
|||||||
]
|
]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"is_active": {"read_only": True},
|
"is_active": {"read_only": True},
|
||||||
|
"name": {"allow_blank": True},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -205,6 +228,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""User Viewset"""
|
"""User Viewset"""
|
||||||
|
|
||||||
queryset = User.objects.none()
|
queryset = User.objects.none()
|
||||||
|
ordering = ["username"]
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
search_fields = ["username", "name", "is_active", "email"]
|
search_fields = ["username", "name", "is_active", "email"]
|
||||||
filterset_class = UsersFilter
|
filterset_class = UsersFilter
|
||||||
@ -271,9 +295,10 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
)
|
)
|
||||||
group.users.add(user)
|
group.users.add(user)
|
||||||
token = Token.objects.create(
|
token = Token.objects.create(
|
||||||
identifier=f"service-account-{username}-password",
|
identifier=slugify(f"service-account-{username}-password"),
|
||||||
intent=TokenIntents.INTENT_APP_PASSWORD,
|
intent=TokenIntents.INTENT_APP_PASSWORD,
|
||||||
user=user,
|
user=user,
|
||||||
|
expires=now() + timedelta(days=360),
|
||||||
)
|
)
|
||||||
return Response({"username": user.username, "token": token.key})
|
return Response({"username": user.username, "token": token.key})
|
||||||
except (IntegrityError) as exc:
|
except (IntegrityError) as exc:
|
||||||
@ -304,13 +329,15 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"""Allow users to change information on their own profile"""
|
"""Allow users to change information on their own profile"""
|
||||||
data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data)
|
data = UserSelfSerializer(instance=User.objects.get(pk=request.user.pk), data=request.data)
|
||||||
if not data.is_valid():
|
if not data.is_valid():
|
||||||
return Response(data.errors)
|
return Response(data.errors, status=400)
|
||||||
new_user = data.save()
|
new_user = data.save()
|
||||||
# If we're impersonating, we need to update that user object
|
# If we're impersonating, we need to update that user object
|
||||||
# since it caches the full object
|
# since it caches the full object
|
||||||
if SESSION_IMPERSONATE_USER in request.session:
|
if SESSION_IMPERSONATE_USER in request.session:
|
||||||
request.session[SESSION_IMPERSONATE_USER] = new_user
|
request.session[SESSION_IMPERSONATE_USER] = new_user
|
||||||
return self.me(request)
|
serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data})
|
||||||
|
serializer.is_valid()
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
||||||
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from rest_framework.fields import CharField, IntegerField
|
from rest_framework.fields import BooleanField, CharField, FileField, IntegerField
|
||||||
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
|
from rest_framework.serializers import Serializer, SerializerMethodField, ValidationError
|
||||||
|
|
||||||
|
|
||||||
@ -22,8 +22,18 @@ class PassiveSerializer(Serializer):
|
|||||||
def update(self, instance: Model, validated_data: dict) -> Model: # pragma: no cover
|
def update(self, instance: Model, validated_data: dict) -> Model: # pragma: no cover
|
||||||
return Model()
|
return Model()
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Model
|
class FileUploadSerializer(PassiveSerializer):
|
||||||
|
"""Serializer to upload file"""
|
||||||
|
|
||||||
|
file = FileField(required=False)
|
||||||
|
clear = BooleanField(default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class FilePathSerializer(PassiveSerializer):
|
||||||
|
"""Serializer to upload file"""
|
||||||
|
|
||||||
|
url = CharField()
|
||||||
|
|
||||||
|
|
||||||
class MetaNameSerializer(PassiveSerializer):
|
class MetaNameSerializer(PassiveSerializer):
|
||||||
|
|||||||
@ -26,7 +26,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"username_link",
|
"username_link",
|
||||||
"Link to a user with identical username address. Can have security implications when a username is used with another source.",
|
"Link to a user with identical username. Can have security implications when a username is used with another source.",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"username_deny",
|
"username_deny",
|
||||||
|
|||||||
@ -283,7 +283,7 @@ class SourceUserMatchingModes(models.TextChoices):
|
|||||||
)
|
)
|
||||||
USERNAME_LINK = "username_link", _(
|
USERNAME_LINK = "username_link", _(
|
||||||
(
|
(
|
||||||
"Link to a user with identical username address. Can have security implications "
|
"Link to a user with identical username. Can have security implications "
|
||||||
"when a username is used with another source."
|
"when a username is used with another source."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -184,7 +184,7 @@ class SourceFlowManager:
|
|||||||
# Ensure redirect is carried through when user was trying to
|
# Ensure redirect is carried through when user was trying to
|
||||||
# authorize application
|
# authorize application
|
||||||
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||||
NEXT_ARG_NAME, "authentik_core:if-admin"
|
NEXT_ARG_NAME, "authentik_core:if-user"
|
||||||
)
|
)
|
||||||
kwargs.update(
|
kwargs.update(
|
||||||
{
|
{
|
||||||
@ -243,9 +243,9 @@ class SourceFlowManager:
|
|||||||
return self.handle_auth_user(connection)
|
return self.handle_auth_user(connection)
|
||||||
return redirect(
|
return redirect(
|
||||||
reverse(
|
reverse(
|
||||||
"authentik_core:if-admin",
|
"authentik_core:if-user",
|
||||||
)
|
)
|
||||||
+ f"#/user;page-{self.source.slug}"
|
+ f"#/settings;page-{self.source.slug}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_enroll(
|
def handle_enroll(
|
||||||
|
|||||||
@ -28,3 +28,7 @@ class PostUserEnrollmentStage(StageView):
|
|||||||
source=connection.source,
|
source=connection.source,
|
||||||
).from_http(self.request)
|
).from_http(self.request)
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|
||||||
|
def post(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
"""Wrapper for post requests"""
|
||||||
|
return self.get(request)
|
||||||
|
|||||||
@ -8,16 +8,15 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
<title>{% block title %}{% trans title|default:tenant.branding_title %}{% endblock %}</title>
|
||||||
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}">
|
<link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-base.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/page.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/empty-state.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/spinner.css' %}">
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
|
||||||
<script src="{% static 'dist/poly.js' %}?v={{ ak_version }}" type="module"></script>
|
<script src="{% static 'dist/poly.js' %}" type="module"></script>
|
||||||
<script>window["polymerSkipLoadingFontRoboto"] = true;</script>
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/AdminInterface.js' %}?v={{ ak_version }}" type="module"></script>
|
<script src="{% static 'dist/AdminInterface.js' %}" type="module"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ You've logged out of {{ application }}.
|
|||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a id="ak-back-home" href="{% url 'authentik_core:if-admin' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to overview' %}</a>
|
<a id="ak-back-home" href="{% url 'authentik_core:root-redirect' %}" class="pf-c-button pf-m-primary">{% trans 'Go back to overview' %}</a>
|
||||||
|
|
||||||
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">{% trans 'Log out of authentik' %}</a>
|
<a id="logout" href="{% url 'authentik_flows:default-invalidation' %}" class="pf-c-button pf-m-secondary">{% trans 'Log out of authentik' %}</a>
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,14 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
|
{{ block.super }}
|
||||||
{% if flow.compatibility_mode %}
|
{% if flow.compatibility_mode %}
|
||||||
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
<script>ShadyDOM = { force: !navigator.webdriver };</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<script src="{% static 'dist/FlowInterface.js' %}?v={{ ak_version }}" type="module"></script>
|
<script src="{% static 'dist/FlowInterface.js' %}" type="module"></script>
|
||||||
<style>
|
<style>
|
||||||
.pf-c-background-image::before {
|
.pf-c-background-image::before {
|
||||||
--ak-flow-background: url("{{ flow.background_url }}");
|
--ak-flow-background: url("{{ flow.background_url }}");
|
||||||
|
|||||||
28
authentik/core/templates/if/user.html
Normal file
28
authentik/core/templates/if/user.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% extends "base/skeleton.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<script src="{% static 'dist/UserInterface.js' %}" type="module"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<ak-message-container></ak-message-container>
|
||||||
|
<ak-interface-user>
|
||||||
|
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||||
|
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||||
|
<div class="pf-c-empty-state__content">
|
||||||
|
<span class="pf-c-spinner pf-m-xl pf-c-empty-state__icon" role="progressbar" aria-valuetext="{% trans 'Loading...' %}">
|
||||||
|
<span class="pf-c-spinner__clipper"></span>
|
||||||
|
<span class="pf-c-spinner__lead-ball"></span>
|
||||||
|
<span class="pf-c-spinner__tail-ball"></span>
|
||||||
|
</span>
|
||||||
|
<h1 class="pf-c-title pf-m-lg">
|
||||||
|
{% trans "Loading..." %}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</ak-interface-user>
|
||||||
|
{% endblock %}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block head_before %}
|
{% block head_before %}
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}?v={{ ak_version }}">
|
<link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
|
|||||||
@ -58,4 +58,4 @@ class TestImpersonation(TestCase):
|
|||||||
self.client.force_login(self.other_user)
|
self.client.force_login(self.other_user)
|
||||||
|
|
||||||
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
||||||
self.assertRedirects(response, reverse("authentik_core:if-admin"))
|
self.assertRedirects(response, reverse("authentik_core:if-user"))
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
"""Test token API"""
|
"""Test token API"""
|
||||||
|
from json import loads
|
||||||
|
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
@ -13,7 +15,8 @@ class TestTokenAPI(APITestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.user = User.objects.get(username="akadmin")
|
self.user = User.objects.create(username="testuser")
|
||||||
|
self.admin = User.objects.get(username="akadmin")
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_token_create(self):
|
def test_token_create(self):
|
||||||
@ -55,3 +58,29 @@ class TestTokenAPI(APITestCase):
|
|||||||
clean_expired_models.delay().get()
|
clean_expired_models.delay().get()
|
||||||
token.refresh_from_db()
|
token.refresh_from_db()
|
||||||
self.assertNotEqual(key, token.key)
|
self.assertNotEqual(key, token.key)
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
"""Test Token List (Test normal authentication)"""
|
||||||
|
token_should: Token = Token.objects.create(
|
||||||
|
identifier="test", expiring=False, user=self.user
|
||||||
|
)
|
||||||
|
Token.objects.create(identifier="test-2", expiring=False, user=get_anonymous_user())
|
||||||
|
response = self.client.get(reverse(("authentik_api:token-list")))
|
||||||
|
body = loads(response.content)
|
||||||
|
self.assertEqual(len(body["results"]), 1)
|
||||||
|
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
||||||
|
|
||||||
|
def test_list_admin(self):
|
||||||
|
"""Test Token List (Test with admin auth)"""
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
token_should: Token = Token.objects.create(
|
||||||
|
identifier="test", expiring=False, user=self.user
|
||||||
|
)
|
||||||
|
token_should_not: Token = Token.objects.create(
|
||||||
|
identifier="test-2", expiring=False, user=get_anonymous_user()
|
||||||
|
)
|
||||||
|
response = self.client.get(reverse(("authentik_api:token-list")))
|
||||||
|
body = loads(response.content)
|
||||||
|
self.assertEqual(len(body["results"]), 2)
|
||||||
|
self.assertEqual(body["results"][0]["identifier"], token_should.identifier)
|
||||||
|
self.assertEqual(body["results"][1]["identifier"], token_should_not.identifier)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ from authentik.core.views.session import EndSessionView
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path(
|
path(
|
||||||
"",
|
"",
|
||||||
login_required(RedirectView.as_view(pattern_name="authentik_core:if-admin")),
|
login_required(RedirectView.as_view(pattern_name="authentik_core:if-user")),
|
||||||
name="root-redirect",
|
name="root-redirect",
|
||||||
),
|
),
|
||||||
# Impersonation
|
# Impersonation
|
||||||
@ -32,6 +32,11 @@ urlpatterns = [
|
|||||||
ensure_csrf_cookie(TemplateView.as_view(template_name="if/admin.html")),
|
ensure_csrf_cookie(TemplateView.as_view(template_name="if/admin.html")),
|
||||||
name="if-admin",
|
name="if-admin",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"if/user/",
|
||||||
|
ensure_csrf_cookie(TemplateView.as_view(template_name="if/user.html")),
|
||||||
|
name="if-user",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"if/flow/<slug:flow_slug>/",
|
"if/flow/<slug:flow_slug>/",
|
||||||
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
ensure_csrf_cookie(FlowInterfaceView.as_view()),
|
||||||
|
|||||||
@ -28,7 +28,7 @@ class ImpersonateInitView(View):
|
|||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||||
|
|
||||||
return redirect("authentik_core:if-admin")
|
return redirect("authentik_core:if-user")
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateEndView(View):
|
class ImpersonateEndView(View):
|
||||||
@ -41,7 +41,7 @@ class ImpersonateEndView(View):
|
|||||||
or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session
|
or SESSION_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||||
):
|
):
|
||||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||||
return redirect("authentik_core:if-admin")
|
return redirect("authentik_core:if-user")
|
||||||
|
|
||||||
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
|
|||||||
@ -78,9 +78,7 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
|||||||
@property
|
@property
|
||||||
def kid(self):
|
def kid(self):
|
||||||
"""Get Key ID used for JWKS"""
|
"""Get Key ID used for JWKS"""
|
||||||
return "{0}".format(
|
return md5(self.key_data.encode("utf-8")).hexdigest() if self.key_data else "" # nosec
|
||||||
md5(self.key_data.encode("utf-8")).hexdigest() if self.key_data else "" # nosec
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Certificate-Key Pair {self.name}"
|
return f"Certificate-Key Pair {self.name}"
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
"""Notification API Views"""
|
"""Notification API Views"""
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from drf_spectacular.types import OpenApiTypes
|
||||||
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
|
|
||||||
@ -53,3 +58,18 @@ class NotificationViewSet(
|
|||||||
]
|
]
|
||||||
permission_classes = [OwnerPermissions]
|
permission_classes = [OwnerPermissions]
|
||||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=OpenApiTypes.NONE,
|
||||||
|
responses={
|
||||||
|
204: OpenApiResponse(description="Marked tasks as read successfully."),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@action(detail=False, methods=["post"])
|
||||||
|
def mark_all_seen(self, request: Request) -> Response:
|
||||||
|
"""Mark all the user's notifications as seen"""
|
||||||
|
notifications = Notification.objects.filter(user=request.user)
|
||||||
|
for notification in notifications:
|
||||||
|
notification.seen = True
|
||||||
|
Notification.objects.bulk_update(notifications, ["seen"])
|
||||||
|
return Response({}, status=204)
|
||||||
|
|||||||
28
authentik/events/api/notification_mapping.py
Normal file
28
authentik/events/api/notification_mapping.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""NotificationWebhookMapping API Views"""
|
||||||
|
from rest_framework.serializers import ModelSerializer
|
||||||
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
|
from authentik.events.models import NotificationWebhookMapping
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationWebhookMappingSerializer(ModelSerializer):
|
||||||
|
"""NotificationWebhookMapping Serializer"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = NotificationWebhookMapping
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"name",
|
||||||
|
"expression",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationWebhookMappingViewSet(UsedByMixin, ModelViewSet):
|
||||||
|
"""NotificationWebhookMapping Viewset"""
|
||||||
|
|
||||||
|
queryset = NotificationWebhookMapping.objects.all()
|
||||||
|
serializer_class = NotificationWebhookMappingSerializer
|
||||||
|
filterset_fields = ["name"]
|
||||||
|
ordering = ["name"]
|
||||||
@ -1,7 +1,10 @@
|
|||||||
"""NotificationTransport API Views"""
|
"""NotificationTransport API Views"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
from rest_framework.fields import CharField, ListField, SerializerMethodField
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -29,6 +32,14 @@ class NotificationTransportSerializer(ModelSerializer):
|
|||||||
"""Return selected mode with a UI Label"""
|
"""Return selected mode with a UI Label"""
|
||||||
return TransportMode(instance.mode).label
|
return TransportMode(instance.mode).label
|
||||||
|
|
||||||
|
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
|
||||||
|
"""Ensure the required fields are set."""
|
||||||
|
mode = attrs.get("mode")
|
||||||
|
if mode in [TransportMode.WEBHOOK, TransportMode.WEBHOOK_SLACK]:
|
||||||
|
if "webhook_url" not in attrs or attrs.get("webhook_url", "") == "":
|
||||||
|
raise ValidationError("Webhook URL may not be empty.")
|
||||||
|
return attrs
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = NotificationTransport
|
model = NotificationTransport
|
||||||
@ -38,6 +49,7 @@ class NotificationTransportSerializer(ModelSerializer):
|
|||||||
"mode",
|
"mode",
|
||||||
"mode_verbose",
|
"mode_verbose",
|
||||||
"webhook_url",
|
"webhook_url",
|
||||||
|
"webhook_mapping",
|
||||||
"send_once",
|
"send_once",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
"""authentik events app"""
|
"""authentik events app"""
|
||||||
from datetime import timedelta
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db import ProgrammingError
|
|
||||||
from django.utils.timezone import now
|
|
||||||
|
|
||||||
|
|
||||||
class AuthentikEventsConfig(AppConfig):
|
class AuthentikEventsConfig(AppConfig):
|
||||||
@ -16,12 +13,3 @@ class AuthentikEventsConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import_module("authentik.events.signals")
|
import_module("authentik.events.signals")
|
||||||
try:
|
|
||||||
from authentik.events.models import Event
|
|
||||||
|
|
||||||
date_from = now() - timedelta(days=1)
|
|
||||||
|
|
||||||
for event in Event.objects.filter(created__gte=date_from):
|
|
||||||
event._set_prom_metrics()
|
|
||||||
except ProgrammingError:
|
|
||||||
pass
|
|
||||||
|
|||||||
46
authentik/events/migrations/0018_auto_20210911_2217.py
Normal file
46
authentik/events/migrations/0018_auto_20210911_2217.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 3.2.6 on 2021-09-11 22:17
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0028_alter_token_intent"),
|
||||||
|
("authentik_events", "0017_alter_event_action"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="NotificationWebhookMapping",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"propertymapping_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="authentik_core.propertymapping",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "Notification Webhook Mapping",
|
||||||
|
"verbose_name_plural": "Notification Webhook Mappings",
|
||||||
|
},
|
||||||
|
bases=("authentik_core.propertymapping",),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="notificationtransport",
|
||||||
|
name="webhook_mapping",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||||
|
to="authentik_events.notificationwebhookmapping",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2021-10-04 15:31
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_events", "0018_auto_20210911_2217"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="notificationtransport",
|
||||||
|
name="webhook_url",
|
||||||
|
field=models.TextField(blank=True, validators=[django.core.validators.URLValidator()]),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -2,25 +2,26 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from inspect import getmodule, stack
|
from inspect import getmodule, stack
|
||||||
from smtplib import SMTPException
|
from smtplib import SMTPException
|
||||||
from typing import Optional, Union
|
from typing import TYPE_CHECKING, Optional, Type, Union
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.validators import URLValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.http.request import QueryDict
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from prometheus_client import Gauge
|
from requests import RequestException
|
||||||
from requests import RequestException, post
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
|
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
|
||||||
from authentik.core.models import ExpiringModel, Group, User
|
from authentik.core.models import ExpiringModel, Group, PropertyMapping, User
|
||||||
from authentik.events.geo import GEOIP_READER
|
from authentik.events.geo import GEOIP_READER
|
||||||
from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
|
from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip, get_http_session
|
||||||
from authentik.lib.utils.time import timedelta_from_string
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
from authentik.policies.models import PolicyBindingModel
|
from authentik.policies.models import PolicyBindingModel
|
||||||
from authentik.stages.email.utils import TemplateEmailMessage
|
from authentik.stages.email.utils import TemplateEmailMessage
|
||||||
@ -28,11 +29,8 @@ from authentik.tenants.models import Tenant
|
|||||||
from authentik.tenants.utils import DEFAULT_TENANT
|
from authentik.tenants.utils import DEFAULT_TENANT
|
||||||
|
|
||||||
LOGGER = get_logger("authentik.events")
|
LOGGER = get_logger("authentik.events")
|
||||||
GAUGE_EVENTS = Gauge(
|
if TYPE_CHECKING:
|
||||||
"authentik_events",
|
from rest_framework.serializers import Serializer
|
||||||
"Events in authentik",
|
|
||||||
["action", "user_username", "app", "client_ip"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def default_event_duration():
|
def default_event_duration():
|
||||||
@ -143,8 +141,9 @@ class Event(ExpiringModel):
|
|||||||
`user` arguments optionally overrides user from requests."""
|
`user` arguments optionally overrides user from requests."""
|
||||||
if request:
|
if request:
|
||||||
self.context["http_request"] = {
|
self.context["http_request"] = {
|
||||||
"path": request.get_full_path(),
|
"path": request.path,
|
||||||
"method": request.method,
|
"method": request.method,
|
||||||
|
"args": QueryDict(request.META.get("QUERY_STRING", "")),
|
||||||
}
|
}
|
||||||
if hasattr(request, "tenant"):
|
if hasattr(request, "tenant"):
|
||||||
tenant: Tenant = request.tenant
|
tenant: Tenant = request.tenant
|
||||||
@ -182,14 +181,6 @@ class Event(ExpiringModel):
|
|||||||
return
|
return
|
||||||
self.context["geo"] = city
|
self.context["geo"] = city
|
||||||
|
|
||||||
def _set_prom_metrics(self):
|
|
||||||
GAUGE_EVENTS.labels(
|
|
||||||
action=self.action,
|
|
||||||
user_username=self.user.get("username"),
|
|
||||||
app=self.app,
|
|
||||||
client_ip=self.client_ip,
|
|
||||||
).set(self.created.timestamp())
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self._state.adding:
|
if self._state.adding:
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
@ -200,7 +191,6 @@ class Event(ExpiringModel):
|
|||||||
user=self.user,
|
user=self.user,
|
||||||
)
|
)
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
self._set_prom_metrics()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def summary(self) -> str:
|
def summary(self) -> str:
|
||||||
@ -234,7 +224,10 @@ class NotificationTransport(models.Model):
|
|||||||
name = models.TextField(unique=True)
|
name = models.TextField(unique=True)
|
||||||
mode = models.TextField(choices=TransportMode.choices)
|
mode = models.TextField(choices=TransportMode.choices)
|
||||||
|
|
||||||
webhook_url = models.TextField(blank=True)
|
webhook_url = models.TextField(blank=True, validators=[URLValidator()])
|
||||||
|
webhook_mapping = models.ForeignKey(
|
||||||
|
"NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None
|
||||||
|
)
|
||||||
send_once = models.BooleanField(
|
send_once = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
@ -254,15 +247,22 @@ class NotificationTransport(models.Model):
|
|||||||
|
|
||||||
def send_webhook(self, notification: "Notification") -> list[str]:
|
def send_webhook(self, notification: "Notification") -> list[str]:
|
||||||
"""Send notification to generic webhook"""
|
"""Send notification to generic webhook"""
|
||||||
|
default_body = {
|
||||||
|
"body": notification.body,
|
||||||
|
"severity": notification.severity,
|
||||||
|
"user_email": notification.user.email,
|
||||||
|
"user_username": notification.user.username,
|
||||||
|
}
|
||||||
|
if self.webhook_mapping:
|
||||||
|
default_body = self.webhook_mapping.evaluate(
|
||||||
|
user=notification.user,
|
||||||
|
request=None,
|
||||||
|
notification=notification,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
response = post(
|
response = get_http_session().post(
|
||||||
self.webhook_url,
|
self.webhook_url,
|
||||||
json={
|
json=default_body,
|
||||||
"body": notification.body,
|
|
||||||
"severity": notification.severity,
|
|
||||||
"user_email": notification.user.email,
|
|
||||||
"user_username": notification.user.username,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
@ -312,7 +312,7 @@ class NotificationTransport(models.Model):
|
|||||||
if notification.event:
|
if notification.event:
|
||||||
body["attachments"][0]["title"] = notification.event.action
|
body["attachments"][0]["title"] = notification.event.action
|
||||||
try:
|
try:
|
||||||
response = post(self.webhook_url, json=body)
|
response = get_http_session().post(self.webhook_url, json=body)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
except RequestException as exc:
|
except RequestException as exc:
|
||||||
text = exc.response.text if exc.response else str(exc)
|
text = exc.response.text if exc.response else str(exc)
|
||||||
@ -429,3 +429,25 @@ class NotificationRule(PolicyBindingModel):
|
|||||||
|
|
||||||
verbose_name = _("Notification Rule")
|
verbose_name = _("Notification Rule")
|
||||||
verbose_name_plural = _("Notification Rules")
|
verbose_name_plural = _("Notification Rules")
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationWebhookMapping(PropertyMapping):
|
||||||
|
"""Modify the schema and layout of the webhook being sent"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def component(self) -> str:
|
||||||
|
return "ak-property-mapping-notification-form"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer(self) -> Type["Serializer"]:
|
||||||
|
from authentik.events.api.notification_mapping import NotificationWebhookMappingSerializer
|
||||||
|
|
||||||
|
return NotificationWebhookMappingSerializer
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Notification Webhook Mapping {self.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
verbose_name = _("Notification Webhook Mapping")
|
||||||
|
verbose_name_plural = _("Notification Webhook Mappings")
|
||||||
|
|||||||
@ -3,7 +3,6 @@ from dataclasses import dataclass, field
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from timeit import default_timer
|
from timeit import default_timer
|
||||||
from traceback import format_tb
|
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from celery import Task
|
from celery import Task
|
||||||
@ -11,6 +10,7 @@ from django.core.cache import cache
|
|||||||
from prometheus_client import Gauge
|
from prometheus_client import Gauge
|
||||||
|
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
|
|
||||||
GAUGE_TASKS = Gauge(
|
GAUGE_TASKS = Gauge(
|
||||||
"authentik_system_tasks",
|
"authentik_system_tasks",
|
||||||
@ -41,8 +41,7 @@ class TaskResult:
|
|||||||
|
|
||||||
def with_error(self, exc: Exception) -> "TaskResult":
|
def with_error(self, exc: Exception) -> "TaskResult":
|
||||||
"""Since errors might not always be pickle-able, set the traceback"""
|
"""Since errors might not always be pickle-able, set the traceback"""
|
||||||
self.messages.extend(format_tb(exc.__traceback__))
|
self.messages.extend(exception_to_string(exc).splitlines())
|
||||||
self.messages.append(str(exc))
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
@ -174,9 +173,7 @@ class MonitoredTask(Task):
|
|||||||
).save(self.result_timeout_hours)
|
).save(self.result_timeout_hours)
|
||||||
Event.new(
|
Event.new(
|
||||||
EventAction.SYSTEM_TASK_EXCEPTION,
|
EventAction.SYSTEM_TASK_EXCEPTION,
|
||||||
message=(
|
message=(f"Task {self.__name__} encountered an error: {exception_to_string(exc)}"),
|
||||||
f"Task {self.__name__} encountered an error: " "\n".join(self._result.messages)
|
|
||||||
),
|
|
||||||
).save()
|
).save()
|
||||||
return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
|
return super().on_failure(exc, task_id, args, kwargs, einfo=einfo)
|
||||||
|
|
||||||
|
|||||||
@ -4,15 +4,21 @@ from django.urls import reverse
|
|||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import (
|
||||||
|
Event,
|
||||||
|
EventAction,
|
||||||
|
Notification,
|
||||||
|
NotificationSeverity,
|
||||||
|
TransportMode,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestEventsAPI(APITestCase):
|
class TestEventsAPI(APITestCase):
|
||||||
"""Test Event API"""
|
"""Test Event API"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
user = User.objects.get(username="akadmin")
|
self.user = User.objects.get(username="akadmin")
|
||||||
self.client.force_login(user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
def test_top_n(self):
|
def test_top_n(self):
|
||||||
"""Test top_per_user"""
|
"""Test top_per_user"""
|
||||||
@ -30,3 +36,34 @@ class TestEventsAPI(APITestCase):
|
|||||||
reverse("authentik_api:event-actions"),
|
reverse("authentik_api:event-actions"),
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_notifications(self):
|
||||||
|
"""Test notifications"""
|
||||||
|
notification = Notification.objects.create(
|
||||||
|
user=self.user, severity=NotificationSeverity.ALERT, body="", seen=False
|
||||||
|
)
|
||||||
|
self.client.post(
|
||||||
|
reverse("authentik_api:notification-mark-all-seen"),
|
||||||
|
)
|
||||||
|
notification.refresh_from_db()
|
||||||
|
self.assertTrue(notification.seen)
|
||||||
|
|
||||||
|
def test_transport(self):
|
||||||
|
"""Test transport API"""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:notificationtransport-list"),
|
||||||
|
data={
|
||||||
|
"name": "foo-with",
|
||||||
|
"mode": TransportMode.WEBHOOK,
|
||||||
|
"webhook_url": "http://foo.com",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("authentik_api:notificationtransport-list"),
|
||||||
|
data={
|
||||||
|
"name": "foo-without",
|
||||||
|
"mode": TransportMode.WEBHOOK,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|||||||
@ -77,7 +77,7 @@ def sanitize_dict(source: dict[Any, Any]) -> dict[Any, Any]:
|
|||||||
final_dict = {}
|
final_dict = {}
|
||||||
for key, value in source.items():
|
for key, value in source.items():
|
||||||
if is_dataclass(value):
|
if is_dataclass(value):
|
||||||
# Because asdict calls `copy.deepcopy(obj)` on everything thats not tuple/dict,
|
# Because asdict calls `copy.deepcopy(obj)` on everything that's not tuple/dict,
|
||||||
# and deepcopy doesn't work with HttpRequests (neither django nor rest_framework).
|
# and deepcopy doesn't work with HttpRequests (neither django nor rest_framework).
|
||||||
# Currently, the only dataclass that actually holds an http request is a PolicyRequest
|
# Currently, the only dataclass that actually holds an http request is a PolicyRequest
|
||||||
if isinstance(value, PolicyRequest):
|
if isinstance(value, PolicyRequest):
|
||||||
|
|||||||
@ -7,10 +7,10 @@ from django.http.response import HttpResponseBadRequest, JsonResponse
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from guardian.shortcuts import get_objects_for_user
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.fields import BooleanField, FileField, ReadOnlyField
|
from rest_framework.fields import ReadOnlyField
|
||||||
from rest_framework.parsers import MultiPartParser
|
from rest_framework.parsers import MultiPartParser
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -20,7 +20,12 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.api.decorators import permission_required
|
from authentik.api.decorators import permission_required
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import CacheSerializer, LinkSerializer
|
from authentik.core.api.utils import (
|
||||||
|
CacheSerializer,
|
||||||
|
FilePathSerializer,
|
||||||
|
FileUploadSerializer,
|
||||||
|
LinkSerializer,
|
||||||
|
)
|
||||||
from authentik.flows.exceptions import FlowNonApplicableException
|
from authentik.flows.exceptions import FlowNonApplicableException
|
||||||
from authentik.flows.models import Flow
|
from authentik.flows.models import Flow
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||||
@ -103,6 +108,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
queryset = Flow.objects.all()
|
queryset = Flow.objects.all()
|
||||||
serializer_class = FlowSerializer
|
serializer_class = FlowSerializer
|
||||||
lookup_field = "slug"
|
lookup_field = "slug"
|
||||||
|
ordering = ["slug", "name"]
|
||||||
search_fields = ["name", "slug", "designation", "title"]
|
search_fields = ["name", "slug", "designation", "title"]
|
||||||
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
|
filterset_fields = ["flow_uuid", "name", "slug", "designation"]
|
||||||
|
|
||||||
@ -147,7 +153,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request={"multipart/form-data": inline_serializer("SetIcon", fields={"file": FileField()})},
|
request={"multipart/form-data": FileUploadSerializer},
|
||||||
responses={
|
responses={
|
||||||
204: OpenApiResponse(description="Successfully imported flow"),
|
204: OpenApiResponse(description="Successfully imported flow"),
|
||||||
400: OpenApiResponse(description="Bad request"),
|
400: OpenApiResponse(description="Bad request"),
|
||||||
@ -259,13 +265,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
@permission_required("authentik_flows.change_flow")
|
@permission_required("authentik_flows.change_flow")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request={
|
request={
|
||||||
"multipart/form-data": inline_serializer(
|
"multipart/form-data": FileUploadSerializer,
|
||||||
"SetIcon",
|
|
||||||
fields={
|
|
||||||
"file": FileField(required=False),
|
|
||||||
"clear": BooleanField(default=False),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
responses={
|
responses={
|
||||||
200: OpenApiResponse(description="Success"),
|
200: OpenApiResponse(description="Success"),
|
||||||
@ -301,7 +301,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
|
|||||||
|
|
||||||
@permission_required("authentik_core.change_application")
|
@permission_required("authentik_core.change_application")
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
request=inline_serializer("SetIconURL", fields={"url": CharField()}),
|
request=FilePathSerializer,
|
||||||
responses={
|
responses={
|
||||||
200: OpenApiResponse(description="Success"),
|
200: OpenApiResponse(description="Success"),
|
||||||
400: OpenApiResponse(description="Bad request"),
|
400: OpenApiResponse(description="Bad request"),
|
||||||
|
|||||||
@ -86,7 +86,7 @@ class StageViewSet(
|
|||||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
def user_settings(self, request: Request) -> Response:
|
def user_settings(self, request: Request) -> Response:
|
||||||
"""Get all stages the user can configure"""
|
"""Get all stages the user can configure"""
|
||||||
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses()
|
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses().order_by("name")
|
||||||
matching_stages: list[dict] = []
|
matching_stages: list[dict] = []
|
||||||
for stage in _all_stages:
|
for stage in _all_stages:
|
||||||
user_settings = stage.ui_user_settings
|
user_settings = stage.ui_user_settings
|
||||||
|
|||||||
@ -31,6 +31,7 @@ class FlowPlanProcess(PROCESS_CLASS): # pragma: no cover
|
|||||||
self.request = RequestFactory().get("/")
|
self.request = RequestFactory().get("/")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
"""Execute 1000 flow plans"""
|
||||||
print(f"Proc {self.index} Running")
|
print(f"Proc {self.index} Running")
|
||||||
|
|
||||||
def test_inner():
|
def test_inner():
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 3.2.6 on 2021-08-30 14:49
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_flows", "0023_alter_flow_background"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="compatibility_mode",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Enable compatibility mode, increases compatibility with password managers on mobile devices.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -125,7 +125,7 @@ class Flow(SerializerModel, PolicyBindingModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
compatibility_mode = models.BooleanField(
|
compatibility_mode = models.BooleanField(
|
||||||
default=True,
|
default=False,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"Enable compatibility mode, increases compatibility with "
|
"Enable compatibility mode, increases compatibility with "
|
||||||
"password managers on mobile devices."
|
"password managers on mobile devices."
|
||||||
|
|||||||
@ -57,11 +57,11 @@ class FlowPlan:
|
|||||||
markers: list[StageMarker] = field(default_factory=list)
|
markers: list[StageMarker] = field(default_factory=list)
|
||||||
|
|
||||||
def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
|
def append_stage(self, stage: Stage, marker: Optional[StageMarker] = None):
|
||||||
"""Append `stage` to all stages, optionall with stage marker"""
|
"""Append `stage` to all stages, optionally with stage marker"""
|
||||||
return self.append(FlowStageBinding(stage=stage), marker)
|
return self.append(FlowStageBinding(stage=stage), marker)
|
||||||
|
|
||||||
def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
|
def append(self, binding: FlowStageBinding, marker: Optional[StageMarker] = None):
|
||||||
"""Append `stage` to all stages, optionall with stage marker"""
|
"""Append `stage` to all stages, optionally with stage marker"""
|
||||||
self.bindings.append(binding)
|
self.bindings.append(binding)
|
||||||
self.markers.append(marker or StageMarker())
|
self.markers.append(marker or StageMarker())
|
||||||
|
|
||||||
|
|||||||
31
authentik/flows/tests/test_stage_views.py
Normal file
31
authentik/flows/tests/test_stage_views.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""stage view tests"""
|
||||||
|
from typing import Callable, Type
|
||||||
|
|
||||||
|
from django.test import RequestFactory, TestCase
|
||||||
|
|
||||||
|
from authentik.flows.stage import StageView
|
||||||
|
from authentik.flows.views import FlowExecutorView
|
||||||
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
|
|
||||||
|
|
||||||
|
class TestViews(TestCase):
|
||||||
|
"""Generic model properties tests"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.exec = FlowExecutorView(request=self.factory.get("/"))
|
||||||
|
|
||||||
|
|
||||||
|
def view_tester_factory(view_class: Type[StageView]) -> Callable:
|
||||||
|
"""Test a form"""
|
||||||
|
|
||||||
|
def tester(self: TestViews):
|
||||||
|
model_class = view_class(self.exec)
|
||||||
|
self.assertIsNotNone(model_class.post)
|
||||||
|
self.assertIsNotNone(model_class.get)
|
||||||
|
|
||||||
|
return tester
|
||||||
|
|
||||||
|
|
||||||
|
for view in all_subclasses(StageView):
|
||||||
|
setattr(TestViews, f"test_view_{view.__name__}", view_tester_factory(view))
|
||||||
@ -2,10 +2,10 @@
|
|||||||
from unittest.mock import MagicMock, PropertyMock, patch
|
from unittest.mock import MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.challenge import ChallengeTypes
|
from authentik.flows.challenge import ChallengeTypes
|
||||||
@ -37,7 +37,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse):
|
|||||||
TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response)
|
TO_STAGE_RESPONSE_MOCK = MagicMock(side_effect=to_stage_response)
|
||||||
|
|
||||||
|
|
||||||
class TestFlowExecutor(TestCase):
|
class TestFlowExecutor(APITestCase):
|
||||||
"""Test views logic"""
|
"""Test views logic"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -438,7 +438,7 @@ class TestFlowExecutor(TestCase):
|
|||||||
|
|
||||||
# third request, this should trigger the re-evaluate
|
# third request, this should trigger the re-evaluate
|
||||||
# A get request will evaluate the policies and this will return stage 4
|
# A get request will evaluate the policies and this will return stage 4
|
||||||
# but it won't save it, hence we cant' check the plan
|
# but it won't save it, hence we can't check the plan
|
||||||
response = self.client.get(exec_url)
|
response = self.client.get(exec_url)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertJSONEqual(
|
self.assertJSONEqual(
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
"""flow views tests"""
|
"""flow views tests"""
|
||||||
from django.test import Client, TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
@ -10,9 +10,6 @@ from authentik.flows.views import SESSION_KEY_PLAN
|
|||||||
class TestHelperView(TestCase):
|
class TestHelperView(TestCase):
|
||||||
"""Test helper views logic"""
|
"""Test helper views logic"""
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client = Client()
|
|
||||||
|
|
||||||
def test_default_view(self):
|
def test_default_view(self):
|
||||||
"""Test that ToDefaultFlow returns the expected URL"""
|
"""Test that ToDefaultFlow returns the expected URL"""
|
||||||
flow = Flow.objects.filter(
|
flow = Flow.objects.filter(
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from authentik.lib.sentry import SentryIgnoredException
|
|||||||
|
|
||||||
|
|
||||||
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
def get_attrs(obj: SerializerModel) -> dict[str, Any]:
|
||||||
"""Get object's attributes via their serializer, and covert it to a normal dict"""
|
"""Get object's attributes via their serializer, and convert it to a normal dict"""
|
||||||
data = dict(obj.serializer(obj).data)
|
data = dict(obj.serializer(obj).data)
|
||||||
to_remove = (
|
to_remove = (
|
||||||
"policies",
|
"policies",
|
||||||
|
|||||||
@ -14,12 +14,7 @@ from django.utils.decorators import method_decorator
|
|||||||
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
from django.views.decorators.clickjacking import xframe_options_sameorigin
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from drf_spectacular.types import OpenApiTypes
|
from drf_spectacular.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import (
|
from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema
|
||||||
OpenApiParameter,
|
|
||||||
OpenApiResponse,
|
|
||||||
PolymorphicProxySerializer,
|
|
||||||
extend_schema,
|
|
||||||
)
|
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
@ -131,12 +126,12 @@ class FlowExecutorView(APIView):
|
|||||||
|
|
||||||
# pylint: disable=unused-argument, too-many-return-statements
|
# pylint: disable=unused-argument, too-many-return-statements
|
||||||
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
||||||
# Early check if theres an active Plan for the current session
|
# Early check if there's an active Plan for the current session
|
||||||
if SESSION_KEY_PLAN in self.request.session:
|
if SESSION_KEY_PLAN in self.request.session:
|
||||||
self.plan = self.request.session[SESSION_KEY_PLAN]
|
self.plan = self.request.session[SESSION_KEY_PLAN]
|
||||||
if self.plan.flow_pk != self.flow.pk.hex:
|
if self.plan.flow_pk != self.flow.pk.hex:
|
||||||
self._logger.warning(
|
self._logger.warning(
|
||||||
"f(exec): Found existing plan for other flow, deleteing plan",
|
"f(exec): Found existing plan for other flow, deleting plan",
|
||||||
)
|
)
|
||||||
# Existing plan is deleted from session and instance
|
# Existing plan is deleted from session and instance
|
||||||
self.plan = None
|
self.plan = None
|
||||||
@ -213,9 +208,6 @@ class FlowExecutorView(APIView):
|
|||||||
serializers=challenge_types(),
|
serializers=challenge_types(),
|
||||||
resource_type_field_name="component",
|
resource_type_field_name="component",
|
||||||
),
|
),
|
||||||
404: OpenApiResponse(
|
|
||||||
description="No Token found"
|
|
||||||
), # This error can be raised by the email stage
|
|
||||||
},
|
},
|
||||||
request=OpenApiTypes.NONE,
|
request=OpenApiTypes.NONE,
|
||||||
parameters=[
|
parameters=[
|
||||||
@ -441,7 +433,7 @@ class ToDefaultFlow(View):
|
|||||||
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
plan: FlowPlan = self.request.session[SESSION_KEY_PLAN]
|
||||||
if plan.flow_pk != flow.pk.hex:
|
if plan.flow_pk != flow.pk.hex:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"f(def): Found existing plan for other flow, deleteing plan",
|
"f(def): Found existing plan for other flow, deleting plan",
|
||||||
flow_slug=flow.slug,
|
flow_slug=flow.slug,
|
||||||
)
|
)
|
||||||
del self.request.session[SESSION_KEY_PLAN]
|
del self.request.session[SESSION_KEY_PLAN]
|
||||||
|
|||||||
@ -9,7 +9,9 @@ postgresql:
|
|||||||
web:
|
web:
|
||||||
listen: 0.0.0.0:9000
|
listen: 0.0.0.0:9000
|
||||||
listen_tls: 0.0.0.0:9443
|
listen_tls: 0.0.0.0:9443
|
||||||
|
listen_metrics: 0.0.0.0:9300
|
||||||
load_local_files: false
|
load_local_files: false
|
||||||
|
outpost_port_offset: 0
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
host: localhost
|
host: localhost
|
||||||
@ -54,6 +56,7 @@ outposts:
|
|||||||
# %(build_hash)s: Build hash if you're running a beta version
|
# %(build_hash)s: Build hash if you're running a beta version
|
||||||
docker_image_base: "ghcr.io/goauthentik/%(type)s:%(version)s"
|
docker_image_base: "ghcr.io/goauthentik/%(type)s:%(version)s"
|
||||||
|
|
||||||
|
disable_update_check: false
|
||||||
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
|
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
|
||||||
geoip: "./GeoLite2-City.mmdb"
|
geoip: "./GeoLite2-City.mmdb"
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,13 @@ from textwrap import indent
|
|||||||
from typing import Any, Iterable, Optional
|
from typing import Any, Iterable, Optional
|
||||||
|
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
from requests import Session
|
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.lib.utils.http import get_http_session
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ class BaseEvaluator:
|
|||||||
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
||||||
"ak_user_by": BaseEvaluator.expr_user_by,
|
"ak_user_by": BaseEvaluator.expr_user_by,
|
||||||
"ak_logger": get_logger(),
|
"ak_logger": get_logger(),
|
||||||
"requests": Session(),
|
"requests": get_http_session(),
|
||||||
}
|
}
|
||||||
self._context = {}
|
self._context = {}
|
||||||
self._filename = "BaseEvalautor"
|
self._filename = "BaseEvalautor"
|
||||||
|
|||||||
@ -93,6 +93,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
|
|||||||
if "exc_info" in hint:
|
if "exc_info" in hint:
|
||||||
_, exc_value, _ = hint["exc_info"]
|
_, exc_value, _ = hint["exc_info"]
|
||||||
if isinstance(exc_value, ignored_classes):
|
if isinstance(exc_value, ignored_classes):
|
||||||
|
LOGGER.debug("dropping exception", exception=exc_value)
|
||||||
return None
|
return None
|
||||||
if "logger" in event:
|
if "logger" in event:
|
||||||
if event["logger"] in [
|
if event["logger"] in [
|
||||||
|
|||||||
@ -32,7 +32,7 @@ class TestConfig(TestCase):
|
|||||||
config = ConfigLoader()
|
config = ConfigLoader()
|
||||||
environ["foo"] = "bar"
|
environ["foo"] = "bar"
|
||||||
self.assertEqual(config.parse_uri("env://foo"), "bar")
|
self.assertEqual(config.parse_uri("env://foo"), "bar")
|
||||||
self.assertEqual(config.parse_uri("env://fo?bar"), "bar")
|
self.assertEqual(config.parse_uri("env://foo?bar"), "bar")
|
||||||
|
|
||||||
def test_uri_file(self):
|
def test_uri_file(self):
|
||||||
"""Test URI parsing (file load)"""
|
"""Test URI parsing (file load)"""
|
||||||
|
|||||||
@ -27,7 +27,7 @@ class TestHTTP(TestCase):
|
|||||||
token = Token.objects.create(
|
token = Token.objects.create(
|
||||||
identifier="test", user=self.user, intent=TokenIntents.INTENT_API
|
identifier="test", user=self.user, intent=TokenIntents.INTENT_API
|
||||||
)
|
)
|
||||||
# Invalid, non-existant token
|
# Invalid, non-existent token
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
"/",
|
"/",
|
||||||
**{
|
**{
|
||||||
@ -36,7 +36,7 @@ class TestHTTP(TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(get_client_ip(request), "127.0.0.1")
|
self.assertEqual(get_client_ip(request), "127.0.0.1")
|
||||||
# Invalid, user doesn't have permisions
|
# Invalid, user doesn't have permissions
|
||||||
request = self.factory.get(
|
request = self.factory.get(
|
||||||
"/",
|
"/",
|
||||||
**{
|
**{
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
"""http helpers"""
|
"""http helpers"""
|
||||||
|
from os import environ
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from requests.sessions import Session
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||||
|
|
||||||
OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP"
|
OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP"
|
||||||
OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN" # nosec
|
OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN" # nosec
|
||||||
DEFAULT_IP = "255.255.255.255"
|
DEFAULT_IP = "255.255.255.255"
|
||||||
@ -60,3 +64,16 @@ def get_client_ip(request: Optional[HttpRequest]) -> str:
|
|||||||
if override:
|
if override:
|
||||||
return override
|
return override
|
||||||
return _get_client_ip_from_meta(request.META)
|
return _get_client_ip_from_meta(request.META)
|
||||||
|
|
||||||
|
|
||||||
|
def authentik_user_agent() -> str:
|
||||||
|
"""Get a common user agent"""
|
||||||
|
build = environ.get(ENV_GIT_HASH_KEY, "tagged")
|
||||||
|
return f"authentik@{__version__} (build={build})"
|
||||||
|
|
||||||
|
|
||||||
|
def get_http_session() -> Session:
|
||||||
|
"""Get a requests session with common headers"""
|
||||||
|
session = Session()
|
||||||
|
session.headers["User-Agent"] = authentik_user_agent()
|
||||||
|
return session
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""authentik lib reflection utilities"""
|
"""authentik lib reflection utilities"""
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@ -19,12 +20,12 @@ def all_subclasses(cls, sort=True):
|
|||||||
return classes
|
return classes
|
||||||
|
|
||||||
|
|
||||||
def class_to_path(cls):
|
def class_to_path(cls: type) -> str:
|
||||||
"""Turn Class (Class or instance) into module path"""
|
"""Turn Class (Class or instance) into module path"""
|
||||||
return f"{cls.__module__}.{cls.__name__}"
|
return f"{cls.__module__}.{cls.__name__}"
|
||||||
|
|
||||||
|
|
||||||
def path_to_class(path):
|
def path_to_class(path: Union[str, None]) -> Union[type, None]:
|
||||||
"""Import module and return class"""
|
"""Import module and return class"""
|
||||||
if not path:
|
if not path:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from authentik.core.channels import AuthJsonConsumer
|
|||||||
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
|
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
|
||||||
|
|
||||||
GAUGE_OUTPOSTS_CONNECTED = Gauge(
|
GAUGE_OUTPOSTS_CONNECTED = Gauge(
|
||||||
"authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid"]
|
"authentik_outposts_connected", "Currently connected outposts", ["outpost", "uid", "expected"]
|
||||||
)
|
)
|
||||||
GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
|
GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
|
||||||
"authentik_outposts_last_update",
|
"authentik_outposts_last_update",
|
||||||
@ -76,6 +76,7 @@ class OutpostConsumer(AuthJsonConsumer):
|
|||||||
GAUGE_OUTPOSTS_CONNECTED.labels(
|
GAUGE_OUTPOSTS_CONNECTED.labels(
|
||||||
outpost=self.outpost.name,
|
outpost=self.outpost.name,
|
||||||
uid=self.last_uid,
|
uid=self.last_uid,
|
||||||
|
expected=self.outpost.config.kubernetes_replicas,
|
||||||
).dec()
|
).dec()
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"removed outpost instance from cache",
|
"removed outpost instance from cache",
|
||||||
@ -100,9 +101,10 @@ class OutpostConsumer(AuthJsonConsumer):
|
|||||||
GAUGE_OUTPOSTS_CONNECTED.labels(
|
GAUGE_OUTPOSTS_CONNECTED.labels(
|
||||||
outpost=self.outpost.name,
|
outpost=self.outpost.name,
|
||||||
uid=self.last_uid,
|
uid=self.last_uid,
|
||||||
|
expected=self.outpost.config.kubernetes_replicas,
|
||||||
).inc()
|
).inc()
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"added outpost instace to cache",
|
"added outpost instance to cache",
|
||||||
outpost=self.outpost,
|
outpost=self.outpost,
|
||||||
instance_uuid=self.last_uid,
|
instance_uuid=self.last_uid,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -29,13 +29,16 @@ class DockerController(BaseController):
|
|||||||
raise ControllerException from exc
|
raise ControllerException from exc
|
||||||
|
|
||||||
def _get_labels(self) -> dict[str, str]:
|
def _get_labels(self) -> dict[str, str]:
|
||||||
return {}
|
return {
|
||||||
|
"io.goauthentik.outpost-uuid": self.outpost.pk.hex,
|
||||||
|
}
|
||||||
|
|
||||||
def _get_env(self) -> dict[str, str]:
|
def _get_env(self) -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
|
"AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
|
||||||
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure).lower(),
|
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure).lower(),
|
||||||
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
||||||
|
"AUTHENTIK_HOST_BROWSER": self.outpost.config.authentik_host_browser,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _comp_env(self, container: Container) -> bool:
|
def _comp_env(self, container: Container) -> bool:
|
||||||
@ -49,6 +52,17 @@ class DockerController(BaseController):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _comp_labels(self, container: Container) -> bool:
|
||||||
|
"""Check if container's labels is equal to what we would set. Return true if container needs
|
||||||
|
to be rebuilt."""
|
||||||
|
should_be = self._get_labels()
|
||||||
|
for key, expected_value in should_be.items():
|
||||||
|
if key not in container.labels:
|
||||||
|
return True
|
||||||
|
if container.labels[key] != expected_value:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def _comp_ports(self, container: Container) -> bool:
|
def _comp_ports(self, container: Container) -> bool:
|
||||||
"""Check that the container has the correct ports exposed. Return true if container needs
|
"""Check that the container has the correct ports exposed. Return true if container needs
|
||||||
to be rebuilt."""
|
to be rebuilt."""
|
||||||
@ -62,6 +76,9 @@ class DockerController(BaseController):
|
|||||||
# {'HostIp': '0.0.0.0', 'HostPort': '389'},
|
# {'HostIp': '0.0.0.0', 'HostPort': '389'},
|
||||||
# {'HostIp': '::', 'HostPort': '389'}
|
# {'HostIp': '::', 'HostPort': '389'}
|
||||||
# ]}
|
# ]}
|
||||||
|
# If no ports are mapped (either mapping disabled, or host network)
|
||||||
|
if not container.ports:
|
||||||
|
return False
|
||||||
for port in self.deployment_ports:
|
for port in self.deployment_ports:
|
||||||
key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
|
key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
|
||||||
if key not in container.ports:
|
if key not in container.ports:
|
||||||
@ -85,16 +102,19 @@ class DockerController(BaseController):
|
|||||||
"image": image_name,
|
"image": image_name,
|
||||||
"name": container_name,
|
"name": container_name,
|
||||||
"detach": True,
|
"detach": True,
|
||||||
"ports": {
|
|
||||||
f"{port.inner_port or port.port}/{port.protocol.lower()}": port.port
|
|
||||||
for port in self.deployment_ports
|
|
||||||
},
|
|
||||||
"environment": self._get_env(),
|
"environment": self._get_env(),
|
||||||
"labels": self._get_labels(),
|
"labels": self._get_labels(),
|
||||||
"restart_policy": {"Name": "unless-stopped"},
|
"restart_policy": {"Name": "unless-stopped"},
|
||||||
|
"network": self.outpost.config.docker_network,
|
||||||
}
|
}
|
||||||
|
if self.outpost.config.docker_map_ports:
|
||||||
|
container_args["ports"] = {
|
||||||
|
f"{port.inner_port or port.port}/{port.protocol.lower()}": str(port.port)
|
||||||
|
for port in self.deployment_ports
|
||||||
|
}
|
||||||
if settings.TEST:
|
if settings.TEST:
|
||||||
del container_args["ports"]
|
del container_args["ports"]
|
||||||
|
del container_args["network"]
|
||||||
container_args["network_mode"] = "host"
|
container_args["network_mode"] = "host"
|
||||||
return (
|
return (
|
||||||
self.client.containers.create(**container_args),
|
self.client.containers.create(**container_args),
|
||||||
@ -133,6 +153,11 @@ class DockerController(BaseController):
|
|||||||
self.logger.info("Container has outdated config, re-creating...")
|
self.logger.info("Container has outdated config, re-creating...")
|
||||||
self.down()
|
self.down()
|
||||||
return self.up(depth + 1)
|
return self.up(depth + 1)
|
||||||
|
# Check that container values match our values
|
||||||
|
if self._comp_labels(container):
|
||||||
|
self.logger.info("Container has outdated labels, re-creating...")
|
||||||
|
self.down()
|
||||||
|
return self.up(depth + 1)
|
||||||
if (
|
if (
|
||||||
container.attrs.get("HostConfig", {})
|
container.attrs.get("HostConfig", {})
|
||||||
.get("RestartPolicy", {})
|
.get("RestartPolicy", {})
|
||||||
@ -144,11 +169,9 @@ class DockerController(BaseController):
|
|||||||
self.down()
|
self.down()
|
||||||
return self.up(depth + 1)
|
return self.up(depth + 1)
|
||||||
# Check that container is healthy
|
# Check that container is healthy
|
||||||
if (
|
if container.status == "running" and container.attrs.get("State", {}).get(
|
||||||
container.status == "running"
|
"Health", {}
|
||||||
and container.attrs.get("State", {}).get("Health", {}).get("Status", "")
|
).get("Status", "") not in ["healthy", "starting"]:
|
||||||
!= "healthy"
|
|
||||||
):
|
|
||||||
# At this point we know the config is correct, but the container isn't healthy,
|
# At this point we know the config is correct, but the container isn't healthy,
|
||||||
# so we just restart it with the same config
|
# so we just restart it with the same config
|
||||||
if has_been_created:
|
if has_been_created:
|
||||||
@ -197,6 +220,7 @@ class DockerController(BaseController):
|
|||||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
||||||
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
|
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
|
||||||
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
||||||
|
"AUTHENTIK_HOST_BROWSER": self.outpost.config.authentik_host_browser,
|
||||||
},
|
},
|
||||||
"labels": self._get_labels(),
|
"labels": self._get_labels(),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,14 @@ from typing import TYPE_CHECKING, Generic, TypeVar
|
|||||||
|
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from kubernetes.client import V1ObjectMeta
|
from kubernetes.client import V1ObjectMeta
|
||||||
|
from kubernetes.client.exceptions import ApiException, OpenApiException
|
||||||
from kubernetes.client.models.v1_deployment import V1Deployment
|
from kubernetes.client.models.v1_deployment import V1Deployment
|
||||||
from kubernetes.client.models.v1_pod import V1Pod
|
from kubernetes.client.models.v1_pod import V1Pod
|
||||||
from kubernetes.client.rest import ApiException
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
from urllib3.exceptions import HTTPError
|
||||||
|
|
||||||
from authentik import __version__
|
from authentik import __version__
|
||||||
from authentik.lib.sentry import SentryIgnoredException
|
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate, NeedsUpdate
|
||||||
from authentik.outposts.managed import MANAGED_OUTPOST
|
from authentik.outposts.managed import MANAGED_OUTPOST
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -19,18 +20,6 @@ if TYPE_CHECKING:
|
|||||||
T = TypeVar("T", V1Pod, V1Deployment)
|
T = TypeVar("T", V1Pod, V1Deployment)
|
||||||
|
|
||||||
|
|
||||||
class ReconcileTrigger(SentryIgnoredException):
|
|
||||||
"""Base trigger raised by child classes to notify us"""
|
|
||||||
|
|
||||||
|
|
||||||
class NeedsRecreate(ReconcileTrigger):
|
|
||||||
"""Exception to trigger a complete recreate of the Kubernetes Object"""
|
|
||||||
|
|
||||||
|
|
||||||
class NeedsUpdate(ReconcileTrigger):
|
|
||||||
"""Exception to trigger an update to the Kubernetes Object"""
|
|
||||||
|
|
||||||
|
|
||||||
class KubernetesObjectReconciler(Generic[T]):
|
class KubernetesObjectReconciler(Generic[T]):
|
||||||
"""Base Kubernetes Reconciler, handles the basic logic."""
|
"""Base Kubernetes Reconciler, handles the basic logic."""
|
||||||
|
|
||||||
@ -72,8 +61,9 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
current = self.retrieve()
|
current = self.retrieve()
|
||||||
except ApiException as exc:
|
except (OpenApiException, HTTPError) as exc:
|
||||||
if exc.status == 404:
|
# pylint: disable=no-member
|
||||||
|
if isinstance(exc, ApiException) and exc.status == 404:
|
||||||
self.logger.debug("Failed to get current, triggering recreate")
|
self.logger.debug("Failed to get current, triggering recreate")
|
||||||
raise NeedsRecreate from exc
|
raise NeedsRecreate from exc
|
||||||
self.logger.debug("Other unhandled error", exc=exc)
|
self.logger.debug("Other unhandled error", exc=exc)
|
||||||
@ -104,9 +94,10 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
current = self.retrieve()
|
current = self.retrieve()
|
||||||
self.delete(current)
|
self.delete(current)
|
||||||
self.logger.debug("Removing")
|
self.logger.debug("Removing")
|
||||||
except ApiException as exc:
|
except (OpenApiException, HTTPError) as exc:
|
||||||
if exc.status == 404:
|
# pylint: disable=no-member
|
||||||
self.logger.debug("Failed to get current, assuming non-existant")
|
if isinstance(exc, ApiException) and exc.status == 404:
|
||||||
|
self.logger.debug("Failed to get current, assuming non-existent")
|
||||||
return
|
return
|
||||||
self.logger.debug("Other unhandled error", exc=exc)
|
self.logger.debug("Other unhandled error", exc=exc)
|
||||||
raise exc
|
raise exc
|
||||||
@ -126,7 +117,7 @@ class KubernetesObjectReconciler(Generic[T]):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def retrieve(self) -> T:
|
def retrieve(self) -> T:
|
||||||
"""API Wrapper to retrive object"""
|
"""API Wrapper to retrieve object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def delete(self, reference: T):
|
def delete(self, reference: T):
|
||||||
|
|||||||
@ -17,7 +17,9 @@ from kubernetes.client import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
|
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||||
|
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
|
||||||
|
from authentik.outposts.controllers.k8s.utils import compare_ports
|
||||||
from authentik.outposts.models import Outpost
|
from authentik.outposts.models import Outpost
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -35,7 +37,10 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
self.outpost = self.controller.outpost
|
self.outpost = self.controller.outpost
|
||||||
|
|
||||||
def reconcile(self, current: V1Deployment, reference: V1Deployment):
|
def reconcile(self, current: V1Deployment, reference: V1Deployment):
|
||||||
super().reconcile(current, reference)
|
compare_ports(
|
||||||
|
current.spec.template.spec.containers[0].ports,
|
||||||
|
reference.spec.template.spec.containers[0].ports,
|
||||||
|
)
|
||||||
if current.spec.replicas != reference.spec.replicas:
|
if current.spec.replicas != reference.spec.replicas:
|
||||||
raise NeedsUpdate()
|
raise NeedsUpdate()
|
||||||
if (
|
if (
|
||||||
@ -43,6 +48,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
!= reference.spec.template.spec.containers[0].image
|
!= reference.spec.template.spec.containers[0].image
|
||||||
):
|
):
|
||||||
raise NeedsUpdate()
|
raise NeedsUpdate()
|
||||||
|
super().reconcile(current, reference)
|
||||||
|
|
||||||
def get_pod_meta(self) -> dict[str, str]:
|
def get_pod_meta(self) -> dict[str, str]:
|
||||||
"""Get common object metadata"""
|
"""Get common object metadata"""
|
||||||
@ -89,6 +95,15 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
V1EnvVar(
|
||||||
|
name="AUTHENTIK_HOST_BROWSER",
|
||||||
|
value_from=V1EnvVarSource(
|
||||||
|
secret_key_ref=V1SecretKeySelector(
|
||||||
|
name=self.name,
|
||||||
|
key="authentik_host_browser",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
V1EnvVar(
|
V1EnvVar(
|
||||||
name="AUTHENTIK_TOKEN",
|
name="AUTHENTIK_TOKEN",
|
||||||
value_from=V1EnvVarSource(
|
value_from=V1EnvVarSource(
|
||||||
|
|||||||
@ -5,7 +5,8 @@ from typing import TYPE_CHECKING
|
|||||||
from kubernetes.client import CoreV1Api, V1Secret
|
from kubernetes.client import CoreV1Api, V1Secret
|
||||||
|
|
||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
|
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||||
|
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||||
@ -26,7 +27,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
|||||||
def reconcile(self, current: V1Secret, reference: V1Secret):
|
def reconcile(self, current: V1Secret, reference: V1Secret):
|
||||||
super().reconcile(current, reference)
|
super().reconcile(current, reference)
|
||||||
for key in reference.data.keys():
|
for key in reference.data.keys():
|
||||||
if current.data[key] != reference.data[key]:
|
if key not in current.data or current.data[key] != reference.data[key]:
|
||||||
raise NeedsUpdate()
|
raise NeedsUpdate()
|
||||||
|
|
||||||
def get_reference_object(self) -> V1Secret:
|
def get_reference_object(self) -> V1Secret:
|
||||||
@ -40,6 +41,9 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
|||||||
str(self.controller.outpost.config.authentik_host_insecure)
|
str(self.controller.outpost.config.authentik_host_insecure)
|
||||||
),
|
),
|
||||||
"token": b64string(self.controller.outpost.token.key),
|
"token": b64string(self.controller.outpost.token.key),
|
||||||
|
"authentik_host_browser": b64string(
|
||||||
|
self.controller.outpost.config.authentik_host_browser
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -3,9 +3,10 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
|
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
|
||||||
|
|
||||||
from authentik.outposts.controllers.base import FIELD_MANAGER, DeploymentPort
|
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler, NeedsUpdate
|
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||||
|
from authentik.outposts.controllers.k8s.utils import compare_ports
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||||
@ -19,46 +20,15 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
|||||||
self.api = CoreV1Api(controller.client)
|
self.api = CoreV1Api(controller.client)
|
||||||
|
|
||||||
def reconcile(self, current: V1Service, reference: V1Service):
|
def reconcile(self, current: V1Service, reference: V1Service):
|
||||||
|
compare_ports(current.spec.ports, reference.spec.ports)
|
||||||
|
# run the base reconcile last, as that will probably raise NeedsUpdate
|
||||||
|
# after an authentik update. However the ports might have also changed during
|
||||||
|
# the update, so this causes the service to be re-created with higher
|
||||||
|
# priority than being updated.
|
||||||
super().reconcile(current, reference)
|
super().reconcile(current, reference)
|
||||||
if len(current.spec.ports) != len(reference.spec.ports):
|
|
||||||
raise NeedsUpdate()
|
|
||||||
for port in reference.spec.ports:
|
|
||||||
if port not in current.spec.ports:
|
|
||||||
raise NeedsUpdate()
|
|
||||||
|
|
||||||
def get_embedded_reference_object(self) -> V1Service:
|
|
||||||
"""Get Service for embedded outpost"""
|
|
||||||
selector_labels = {
|
|
||||||
"app.kubernetes.io/name": "authentik",
|
|
||||||
"app.kubernetes.io/component": "server",
|
|
||||||
}
|
|
||||||
meta = self.get_object_meta(name=self.name)
|
|
||||||
ports = []
|
|
||||||
for port in [
|
|
||||||
DeploymentPort(9000, "http", "tcp"),
|
|
||||||
DeploymentPort(9443, "https", "tcp"),
|
|
||||||
]:
|
|
||||||
ports.append(
|
|
||||||
V1ServicePort(
|
|
||||||
name=port.name,
|
|
||||||
port=port.port,
|
|
||||||
protocol=port.protocol.upper(),
|
|
||||||
target_port=port.inner_port or port.port,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return V1Service(
|
|
||||||
metadata=meta,
|
|
||||||
spec=V1ServiceSpec(
|
|
||||||
ports=ports,
|
|
||||||
selector=selector_labels,
|
|
||||||
type=self.controller.outpost.config.kubernetes_service_type,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_reference_object(self) -> V1Service:
|
def get_reference_object(self) -> V1Service:
|
||||||
"""Get deployment object for outpost"""
|
"""Get deployment object for outpost"""
|
||||||
if self.is_embedded:
|
|
||||||
return self.get_embedded_reference_object()
|
|
||||||
meta = self.get_object_meta(name=self.name)
|
meta = self.get_object_meta(name=self.name)
|
||||||
ports = []
|
ports = []
|
||||||
for port in self.controller.deployment_ports:
|
for port in self.controller.deployment_ports:
|
||||||
@ -70,7 +40,13 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
|||||||
target_port=port.inner_port or port.port,
|
target_port=port.inner_port or port.port,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
|
if self.is_embedded:
|
||||||
|
selector_labels = {
|
||||||
|
"app.kubernetes.io/name": "authentik",
|
||||||
|
"app.kubernetes.io/component": "server",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
|
||||||
return V1Service(
|
return V1Service(
|
||||||
metadata=meta,
|
metadata=meta,
|
||||||
spec=V1ServiceSpec(
|
spec=V1ServiceSpec(
|
||||||
|
|||||||
150
authentik/outposts/controllers/k8s/service_monitor.py
Normal file
150
authentik/outposts/controllers/k8s/service_monitor.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
"""Kubernetes Prometheus ServiceMonitor Reconciler"""
|
||||||
|
from dataclasses import asdict, dataclass, field
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from dacite import from_dict
|
||||||
|
from kubernetes.client import ApiextensionsV1Api, CustomObjectsApi
|
||||||
|
|
||||||
|
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||||
|
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PrometheusServiceMonitorSpecEndpoint:
|
||||||
|
"""Prometheus ServiceMonitor endpoint spec"""
|
||||||
|
|
||||||
|
port: str
|
||||||
|
path: str = field(default="/metrics")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PrometheusServiceMonitorSpecSelector:
|
||||||
|
"""Prometheus ServiceMonitor selector spec"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
matchLabels: dict
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PrometheusServiceMonitorSpec:
|
||||||
|
"""Prometheus ServiceMonitor spec"""
|
||||||
|
|
||||||
|
endpoints: list[PrometheusServiceMonitorSpecEndpoint]
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
selector: PrometheusServiceMonitorSpecSelector
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PrometheusServiceMonitorMetadata:
|
||||||
|
"""Prometheus ServiceMonitor metadata"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
namespace: str
|
||||||
|
labels: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PrometheusServiceMonitor:
|
||||||
|
"""Prometheus ServiceMonitor"""
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
apiVersion: str
|
||||||
|
kind: str
|
||||||
|
metadata: PrometheusServiceMonitorMetadata
|
||||||
|
spec: PrometheusServiceMonitorSpec
|
||||||
|
|
||||||
|
|
||||||
|
CRD_NAME = "servicemonitors.monitoring.coreos.com"
|
||||||
|
CRD_GROUP = "monitoring.coreos.com"
|
||||||
|
CRD_VERSION = "v1"
|
||||||
|
CRD_PLURAL = "servicemonitors"
|
||||||
|
|
||||||
|
|
||||||
|
class PrometheusServiceMonitorReconciler(KubernetesObjectReconciler[PrometheusServiceMonitor]):
|
||||||
|
"""Kubernetes Prometheus ServiceMonitor Reconciler"""
|
||||||
|
|
||||||
|
def __init__(self, controller: "KubernetesController") -> None:
|
||||||
|
super().__init__(controller)
|
||||||
|
self.api_ex = ApiextensionsV1Api(controller.client)
|
||||||
|
self.api = CustomObjectsApi(controller.client)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def noop(self) -> bool:
|
||||||
|
return (not self._crd_exists()) or (self.is_embedded)
|
||||||
|
|
||||||
|
def _crd_exists(self) -> bool:
|
||||||
|
"""Check if the Prometheus ServiceMonitor exists"""
|
||||||
|
return bool(
|
||||||
|
len(
|
||||||
|
self.api_ex.list_custom_resource_definition(
|
||||||
|
field_selector=f"metadata.name={CRD_NAME}"
|
||||||
|
).items
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_reference_object(self) -> PrometheusServiceMonitor:
|
||||||
|
"""Get service monitor object for outpost"""
|
||||||
|
return PrometheusServiceMonitor(
|
||||||
|
apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
|
||||||
|
kind="ServiceMonitor",
|
||||||
|
metadata=PrometheusServiceMonitorMetadata(
|
||||||
|
name=self.name,
|
||||||
|
namespace=self.namespace,
|
||||||
|
labels=self.get_object_meta().labels,
|
||||||
|
),
|
||||||
|
spec=PrometheusServiceMonitorSpec(
|
||||||
|
endpoints=[
|
||||||
|
PrometheusServiceMonitorSpecEndpoint(
|
||||||
|
port="http-metrics",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
selector=PrometheusServiceMonitorSpecSelector(
|
||||||
|
matchLabels=self.get_object_meta(name=self.name).labels,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, reference: PrometheusServiceMonitor):
|
||||||
|
return self.api.create_namespaced_custom_object(
|
||||||
|
group=CRD_GROUP,
|
||||||
|
version=CRD_VERSION,
|
||||||
|
plural=CRD_PLURAL,
|
||||||
|
namespace=self.namespace,
|
||||||
|
body=asdict(reference),
|
||||||
|
field_manager=FIELD_MANAGER,
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, reference: PrometheusServiceMonitor):
|
||||||
|
return self.api.delete_namespaced_custom_object(
|
||||||
|
group=CRD_GROUP,
|
||||||
|
version=CRD_VERSION,
|
||||||
|
namespace=self.namespace,
|
||||||
|
plural=CRD_PLURAL,
|
||||||
|
name=self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def retrieve(self) -> PrometheusServiceMonitor:
|
||||||
|
return from_dict(
|
||||||
|
PrometheusServiceMonitor,
|
||||||
|
self.api.get_namespaced_custom_object(
|
||||||
|
group=CRD_GROUP,
|
||||||
|
version=CRD_VERSION,
|
||||||
|
namespace=self.namespace,
|
||||||
|
plural=CRD_PLURAL,
|
||||||
|
name=self.name,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update(self, current: PrometheusServiceMonitor, reference: PrometheusServiceMonitor):
|
||||||
|
return self.api.patch_namespaced_custom_object(
|
||||||
|
group=CRD_GROUP,
|
||||||
|
version=CRD_VERSION,
|
||||||
|
namespace=self.namespace,
|
||||||
|
plural=CRD_PLURAL,
|
||||||
|
name=self.name,
|
||||||
|
body=asdict(reference),
|
||||||
|
field_manager=FIELD_MANAGER,
|
||||||
|
)
|
||||||
14
authentik/outposts/controllers/k8s/triggers.py
Normal file
14
authentik/outposts/controllers/k8s/triggers.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"""exceptions used by the kubernetes reconciler to trigger updates"""
|
||||||
|
from authentik.lib.sentry import SentryIgnoredException
|
||||||
|
|
||||||
|
|
||||||
|
class ReconcileTrigger(SentryIgnoredException):
|
||||||
|
"""Base trigger raised by child classes to notify us"""
|
||||||
|
|
||||||
|
|
||||||
|
class NeedsRecreate(ReconcileTrigger):
|
||||||
|
"""Exception to trigger a complete recreate of the Kubernetes Object"""
|
||||||
|
|
||||||
|
|
||||||
|
class NeedsUpdate(ReconcileTrigger):
|
||||||
|
"""Exception to trigger an update to the Kubernetes Object"""
|
||||||
@ -1,8 +1,11 @@
|
|||||||
"""k8s utils"""
|
"""k8s utils"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from kubernetes.client.models.v1_container_port import V1ContainerPort
|
||||||
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
|
from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME
|
||||||
|
|
||||||
|
from authentik.outposts.controllers.k8s.triggers import NeedsRecreate
|
||||||
|
|
||||||
|
|
||||||
def get_namespace() -> str:
|
def get_namespace() -> str:
|
||||||
"""Get the namespace if we're running in a pod, otherwise default to default"""
|
"""Get the namespace if we're running in a pod, otherwise default to default"""
|
||||||
@ -11,3 +14,12 @@ def get_namespace() -> str:
|
|||||||
with open(path, "r", encoding="utf8") as _namespace_file:
|
with open(path, "r", encoding="utf8") as _namespace_file:
|
||||||
return _namespace_file.read()
|
return _namespace_file.read()
|
||||||
return "default"
|
return "default"
|
||||||
|
|
||||||
|
|
||||||
|
def compare_ports(current: list[V1ContainerPort], reference: list[V1ContainerPort]):
|
||||||
|
"""Compare ports of a list"""
|
||||||
|
if len(current) != len(reference):
|
||||||
|
raise NeedsRecreate()
|
||||||
|
for port in reference:
|
||||||
|
if port not in current:
|
||||||
|
raise NeedsRecreate()
|
||||||
|
|||||||
@ -3,8 +3,9 @@ from io import StringIO
|
|||||||
from typing import Type
|
from typing import Type
|
||||||
|
|
||||||
from kubernetes.client.api_client import ApiClient
|
from kubernetes.client.api_client import ApiClient
|
||||||
from kubernetes.client.exceptions import ApiException
|
from kubernetes.client.exceptions import OpenApiException
|
||||||
from structlog.testing import capture_logs
|
from structlog.testing import capture_logs
|
||||||
|
from urllib3.exceptions import HTTPError
|
||||||
from yaml import dump_all
|
from yaml import dump_all
|
||||||
|
|
||||||
from authentik.outposts.controllers.base import BaseController, ControllerException
|
from authentik.outposts.controllers.base import BaseController, ControllerException
|
||||||
@ -12,7 +13,8 @@ from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
|||||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||||
from authentik.outposts.controllers.k8s.secret import SecretReconciler
|
from authentik.outposts.controllers.k8s.secret import SecretReconciler
|
||||||
from authentik.outposts.controllers.k8s.service import ServiceReconciler
|
from authentik.outposts.controllers.k8s.service import ServiceReconciler
|
||||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost
|
from authentik.outposts.controllers.k8s.service_monitor import PrometheusServiceMonitorReconciler
|
||||||
|
from authentik.outposts.models import KubernetesServiceConnection, Outpost, ServiceConnectionInvalid
|
||||||
|
|
||||||
|
|
||||||
class KubernetesController(BaseController):
|
class KubernetesController(BaseController):
|
||||||
@ -31,8 +33,9 @@ class KubernetesController(BaseController):
|
|||||||
"secret": SecretReconciler,
|
"secret": SecretReconciler,
|
||||||
"deployment": DeploymentReconciler,
|
"deployment": DeploymentReconciler,
|
||||||
"service": ServiceReconciler,
|
"service": ServiceReconciler,
|
||||||
|
"prometheus servicemonitor": PrometheusServiceMonitorReconciler,
|
||||||
}
|
}
|
||||||
self.reconcile_order = ["secret", "deployment", "service"]
|
self.reconcile_order = ["secret", "deployment", "service", "prometheus servicemonitor"]
|
||||||
|
|
||||||
def up(self):
|
def up(self):
|
||||||
try:
|
try:
|
||||||
@ -40,7 +43,7 @@ class KubernetesController(BaseController):
|
|||||||
reconciler = self.reconcilers[reconcile_key](self)
|
reconciler = self.reconcilers[reconcile_key](self)
|
||||||
reconciler.up()
|
reconciler.up()
|
||||||
|
|
||||||
except ApiException as exc:
|
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
|
||||||
raise ControllerException(str(exc)) from exc
|
raise ControllerException(str(exc)) from exc
|
||||||
|
|
||||||
def up_with_logs(self) -> list[str]:
|
def up_with_logs(self) -> list[str]:
|
||||||
@ -55,7 +58,7 @@ class KubernetesController(BaseController):
|
|||||||
reconciler.up()
|
reconciler.up()
|
||||||
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
||||||
return all_logs
|
return all_logs
|
||||||
except ApiException as exc:
|
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
|
||||||
raise ControllerException(str(exc)) from exc
|
raise ControllerException(str(exc)) from exc
|
||||||
|
|
||||||
def down(self):
|
def down(self):
|
||||||
@ -65,7 +68,7 @@ class KubernetesController(BaseController):
|
|||||||
self.logger.debug("Tearing down object", name=reconcile_key)
|
self.logger.debug("Tearing down object", name=reconcile_key)
|
||||||
reconciler.down()
|
reconciler.down()
|
||||||
|
|
||||||
except ApiException as exc:
|
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
|
||||||
raise ControllerException(str(exc)) from exc
|
raise ControllerException(str(exc)) from exc
|
||||||
|
|
||||||
def down_with_logs(self) -> list[str]:
|
def down_with_logs(self) -> list[str]:
|
||||||
@ -80,7 +83,7 @@ class KubernetesController(BaseController):
|
|||||||
reconciler.down()
|
reconciler.down()
|
||||||
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
all_logs += [f"{reconcile_key.title()}: {x['event']}" for x in logs]
|
||||||
return all_logs
|
return all_logs
|
||||||
except ApiException as exc:
|
except (OpenApiException, HTTPError, ServiceConnectionInvalid) as exc:
|
||||||
raise ControllerException(str(exc)) from exc
|
raise ControllerException(str(exc)) from exc
|
||||||
|
|
||||||
def get_static_deployment(self) -> str:
|
def get_static_deployment(self) -> str:
|
||||||
|
|||||||
@ -30,7 +30,7 @@ class DockerInlineTLS:
|
|||||||
return str(path)
|
return str(path)
|
||||||
|
|
||||||
def write(self) -> TLSConfig:
|
def write(self) -> TLSConfig:
|
||||||
"""Create TLSConfig with Certificate Keypairs"""
|
"""Create TLSConfig with Certificate Key pairs"""
|
||||||
# So yes, this is quite ugly. But sadly, there is no clean way to pass
|
# So yes, this is quite ugly. But sadly, there is no clean way to pass
|
||||||
# docker-py (which is using requests (which is using urllib3)) a certificate
|
# docker-py (which is using requests (which is using urllib3)) a certificate
|
||||||
# for verification or authentication as string.
|
# for verification or authentication as string.
|
||||||
|
|||||||
@ -56,6 +56,7 @@ class ServiceConnectionInvalid(SentryIgnoredException):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
class OutpostConfig:
|
class OutpostConfig:
|
||||||
"""Configuration an outpost uses to configure it self"""
|
"""Configuration an outpost uses to configure it self"""
|
||||||
|
|
||||||
@ -63,12 +64,16 @@ class OutpostConfig:
|
|||||||
|
|
||||||
authentik_host: str = ""
|
authentik_host: str = ""
|
||||||
authentik_host_insecure: bool = False
|
authentik_host_insecure: bool = False
|
||||||
|
authentik_host_browser: str = ""
|
||||||
|
|
||||||
log_level: str = CONFIG.y("log_level")
|
log_level: str = CONFIG.y("log_level")
|
||||||
error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
|
error_reporting_enabled: bool = CONFIG.y_bool("error_reporting.enabled")
|
||||||
error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer")
|
error_reporting_environment: str = CONFIG.y("error_reporting.environment", "customer")
|
||||||
|
|
||||||
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
object_naming_template: str = field(default="ak-outpost-%(name)s")
|
||||||
|
|
||||||
|
docker_network: Optional[str] = field(default=None)
|
||||||
|
docker_map_ports: bool = field(default=True)
|
||||||
|
|
||||||
kubernetes_replicas: int = field(default=1)
|
kubernetes_replicas: int = field(default=1)
|
||||||
kubernetes_namespace: str = field(default_factory=get_namespace)
|
kubernetes_namespace: str = field(default_factory=get_namespace)
|
||||||
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
kubernetes_ingress_annotations: dict[str, str] = field(default_factory=dict)
|
||||||
@ -336,19 +341,8 @@ class Outpost(ManagedModel):
|
|||||||
"""Username for service user"""
|
"""Username for service user"""
|
||||||
return f"ak-outpost-{self.uuid.hex}"
|
return f"ak-outpost-{self.uuid.hex}"
|
||||||
|
|
||||||
@property
|
def build_user_permissions(self, user: User):
|
||||||
def user(self) -> User:
|
"""Create per-object and global permissions for outpost service-account"""
|
||||||
"""Get/create user with access to all required objects"""
|
|
||||||
users = User.objects.filter(username=self.user_identifier)
|
|
||||||
if not users.exists():
|
|
||||||
user: User = User.objects.create(username=self.user_identifier)
|
|
||||||
user.set_unusable_password()
|
|
||||||
user.save()
|
|
||||||
else:
|
|
||||||
user = users.first()
|
|
||||||
user.attributes[USER_ATTRIBUTE_SA] = True
|
|
||||||
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
|
||||||
user.save()
|
|
||||||
# To ensure the user only has the correct permissions, we delete all of them and re-add
|
# To ensure the user only has the correct permissions, we delete all of them and re-add
|
||||||
# the ones the user needs
|
# the ones the user needs
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
@ -362,7 +356,7 @@ class Outpost(ManagedModel):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
assign_perm(code_name, user, model_or_perm)
|
assign_perm(code_name, user, model_or_perm)
|
||||||
except Permission.DoesNotExist as exc:
|
except (Permission.DoesNotExist, AttributeError) as exc:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"permission doesn't exist",
|
"permission doesn't exist",
|
||||||
code_name=code_name,
|
code_name=code_name,
|
||||||
@ -392,6 +386,23 @@ class Outpost(ManagedModel):
|
|||||||
"Updated service account's permissions",
|
"Updated service account's permissions",
|
||||||
perms=UserObjectPermission.objects.filter(user=user),
|
perms=UserObjectPermission.objects.filter(user=user),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user(self) -> User:
|
||||||
|
"""Get/create user with access to all required objects"""
|
||||||
|
users = User.objects.filter(username=self.user_identifier)
|
||||||
|
should_create_user = not users.exists()
|
||||||
|
if should_create_user:
|
||||||
|
user: User = User.objects.create(username=self.user_identifier)
|
||||||
|
user.set_unusable_password()
|
||||||
|
user.save()
|
||||||
|
else:
|
||||||
|
user = users.first()
|
||||||
|
user.attributes[USER_ATTRIBUTE_SA] = True
|
||||||
|
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
||||||
|
user.save()
|
||||||
|
if should_create_user:
|
||||||
|
self.build_user_permissions(user)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@ -100,7 +100,7 @@ def outpost_controller(
|
|||||||
if from_cache:
|
if from_cache:
|
||||||
outpost: Outpost = cache.get(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
|
outpost: Outpost = cache.get(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
|
||||||
else:
|
else:
|
||||||
outpost: Outpost = Outpost.objects.get(pk=outpost_pk)
|
outpost: Outpost = Outpost.objects.filter(pk=outpost_pk).first()
|
||||||
if not outpost:
|
if not outpost:
|
||||||
return
|
return
|
||||||
self.set_uid(slugify(outpost.name))
|
self.set_uid(slugify(outpost.name))
|
||||||
@ -126,6 +126,7 @@ def outpost_token_ensurer(self: MonitoredTask):
|
|||||||
all_outposts = Outpost.objects.all()
|
all_outposts = Outpost.objects.all()
|
||||||
for outpost in all_outposts:
|
for outpost in all_outposts:
|
||||||
_ = outpost.token
|
_ = outpost.token
|
||||||
|
outpost.build_user_permissions(outpost.user)
|
||||||
self.set_status(
|
self.set_status(
|
||||||
TaskResult(
|
TaskResult(
|
||||||
TaskResultStatus.SUCCESSFUL,
|
TaskResultStatus.SUCCESSFUL,
|
||||||
@ -148,10 +149,7 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(instance, Outpost):
|
if isinstance(instance, Outpost):
|
||||||
LOGGER.debug("Ensuring token and permissions for outpost", instance=instance)
|
LOGGER.debug("Trigger reconcile for outpost", instance=instance)
|
||||||
_ = instance.token
|
|
||||||
_ = instance.user
|
|
||||||
LOGGER.debug("Trigger reconcile for outpost")
|
|
||||||
outpost_controller.delay(instance.pk)
|
outpost_controller.delay(instance.pk)
|
||||||
|
|
||||||
if isinstance(instance, (OutpostModel, Outpost)):
|
if isinstance(instance, (OutpostModel, Outpost)):
|
||||||
@ -184,7 +182,7 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
|||||||
|
|
||||||
|
|
||||||
def outpost_send_update(model_instace: Model):
|
def outpost_send_update(model_instace: Model):
|
||||||
"""Send outpost update to all registered outposts, irregardless to which authentik
|
"""Send outpost update to all registered outposts, regardless to which authentik
|
||||||
instance they are connected"""
|
instance they are connected"""
|
||||||
channel_layer = get_channel_layer()
|
channel_layer = get_channel_layer()
|
||||||
if isinstance(model_instace, OutpostModel):
|
if isinstance(model_instace, OutpostModel):
|
||||||
@ -199,7 +197,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
|
|||||||
# Ensure token again, because this function is called when anything related to an
|
# Ensure token again, because this function is called when anything related to an
|
||||||
# OutpostModel is saved, so we can be sure permissions are right
|
# OutpostModel is saved, so we can be sure permissions are right
|
||||||
_ = outpost.token
|
_ = outpost.token
|
||||||
_ = outpost.user
|
outpost.build_user_permissions(outpost.user)
|
||||||
if not layer: # pragma: no cover
|
if not layer: # pragma: no cover
|
||||||
layer = get_channel_layer()
|
layer = get_channel_layer()
|
||||||
for state in OutpostState.for_outpost(outpost):
|
for state in OutpostState.for_outpost(outpost):
|
||||||
@ -211,7 +209,7 @@ def _outpost_single_update(outpost: Outpost, layer=None):
|
|||||||
@CELERY_APP.task()
|
@CELERY_APP.task()
|
||||||
def outpost_local_connection():
|
def outpost_local_connection():
|
||||||
"""Checks the local environment and create Service connections."""
|
"""Checks the local environment and create Service connections."""
|
||||||
# Explicitly check against token filename, as thats
|
# Explicitly check against token filename, as that's
|
||||||
# only present when the integration is enabled
|
# only present when the integration is enabled
|
||||||
if Path(SERVICE_TOKEN_FILENAME).exists():
|
if Path(SERVICE_TOKEN_FILENAME).exists():
|
||||||
LOGGER.debug("Detected in-cluster Kubernetes Config")
|
LOGGER.debug("Detected in-cluster Kubernetes Config")
|
||||||
|
|||||||
@ -87,6 +87,7 @@ class PolicyViewSet(
|
|||||||
"promptstage": ["isnull"],
|
"promptstage": ["isnull"],
|
||||||
}
|
}
|
||||||
search_fields = ["name"]
|
search_fields = ["name"]
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
def get_queryset(self): # pragma: no cover
|
def get_queryset(self): # pragma: no cover
|
||||||
return Policy.objects.select_subclasses().prefetch_related("bindings", "promptstage_set")
|
return Policy.objects.select_subclasses().prefetch_related("bindings", "promptstage_set")
|
||||||
|
|||||||
@ -81,11 +81,11 @@ class PolicyEngine:
|
|||||||
.iterator()
|
.iterator()
|
||||||
)
|
)
|
||||||
|
|
||||||
def _check_policy_type(self, policy: Policy):
|
def _check_policy_type(self, binding: PolicyBinding):
|
||||||
"""Check policy type, make sure it's not the root class as that has no logic implemented"""
|
"""Check policy type, make sure it's not the root class as that has no logic implemented"""
|
||||||
# pyright: reportGeneralTypeIssues=false
|
# pyright: reportGeneralTypeIssues=false
|
||||||
if policy.__class__ == Policy:
|
if binding.policy is not None and binding.policy.__class__ == Policy:
|
||||||
raise TypeError(f"Policy '{policy}' is root type")
|
raise TypeError(f"Policy '{binding.policy}' is root type")
|
||||||
|
|
||||||
def build(self) -> "PolicyEngine":
|
def build(self) -> "PolicyEngine":
|
||||||
"""Build wrapper which monitors performance"""
|
"""Build wrapper which monitors performance"""
|
||||||
@ -102,7 +102,7 @@ class PolicyEngine:
|
|||||||
for binding in self._iter_bindings():
|
for binding in self._iter_bindings():
|
||||||
self.__expected_result_count += 1
|
self.__expected_result_count += 1
|
||||||
|
|
||||||
self._check_policy_type(binding.policy)
|
self._check_policy_type(binding)
|
||||||
key = cache_key(binding, self.request)
|
key = cache_key(binding, self.request)
|
||||||
cached_policy = cache.get(key, None)
|
cached_policy = cache.get(key, None)
|
||||||
if cached_policy and self.use_cache:
|
if cached_policy and self.use_cache:
|
||||||
|
|||||||
@ -3,8 +3,10 @@ from ipaddress import ip_address, ip_network
|
|||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django_otp import devices_for_user
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.core.models import User
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
@ -28,6 +30,7 @@ class PolicyEvaluator(BaseEvaluator):
|
|||||||
self._messages = []
|
self._messages = []
|
||||||
self._context["ak_logger"] = get_logger(policy_name)
|
self._context["ak_logger"] = get_logger(policy_name)
|
||||||
self._context["ak_message"] = self.expr_func_message
|
self._context["ak_message"] = self.expr_func_message
|
||||||
|
self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator
|
||||||
self._context["ip_address"] = ip_address
|
self._context["ip_address"] = ip_address
|
||||||
self._context["ip_network"] = ip_network
|
self._context["ip_network"] = ip_network
|
||||||
self._filename = policy_name or "PolicyEvaluator"
|
self._filename = policy_name or "PolicyEvaluator"
|
||||||
@ -36,6 +39,19 @@ class PolicyEvaluator(BaseEvaluator):
|
|||||||
"""Wrapper to append to messages list, which is returned with PolicyResult"""
|
"""Wrapper to append to messages list, which is returned with PolicyResult"""
|
||||||
self._messages.append(message)
|
self._messages.append(message)
|
||||||
|
|
||||||
|
def expr_func_user_has_authenticator(
|
||||||
|
self, user: User, device_type: Optional[str] = None
|
||||||
|
) -> bool:
|
||||||
|
"""Check if a user has any authenticator devices, optionally matching *device_type*"""
|
||||||
|
user_devices = devices_for_user(user)
|
||||||
|
if device_type:
|
||||||
|
for device in user_devices:
|
||||||
|
device_class = device.__class__.__name__.lower().replace("device", "")
|
||||||
|
if device_class == device_type:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
return len(user_devices) > 0
|
||||||
|
|
||||||
def set_policy_request(self, request: PolicyRequest):
|
def set_policy_request(self, request: PolicyRequest):
|
||||||
"""Update context based on policy request (if http request is given, update that too)"""
|
"""Update context based on policy request (if http request is given, update that too)"""
|
||||||
# update website/docs/expressions/_objects.md
|
# update website/docs/expressions/_objects.md
|
||||||
|
|||||||
@ -3,10 +3,10 @@ from hashlib import sha1
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from requests import get
|
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.policies.models import Policy, PolicyResult
|
from authentik.policies.models import Policy, PolicyResult
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ class HaveIBeenPwendPolicy(Policy):
|
|||||||
|
|
||||||
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
|
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
|
||||||
url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}"
|
url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}"
|
||||||
result = get(url).text
|
result = get_http_session().get(url).text
|
||||||
final_count = 0
|
final_count = 0
|
||||||
for line in result.split("\r\n"):
|
for line in result.split("\r\n"):
|
||||||
full_hash, count = line.split(":")
|
full_hash, count = line.split(":")
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user