Compare commits
61 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
9f2e9e8444 | |||
a3d361f500 | |||
e9bb583b32 | |||
efccf47c83 | |||
a5b144cf8f | |||
afc5a17fc2 | |||
b3e0884b2e | |||
078d648551 | |||
41f9097592 | |||
562175741c | |||
24e24cb97e | |||
69b0a23a7d | |||
f0f3245388 | |||
99ca0d1f9f | |||
c9f0d048a8 | |||
90a94b5e3e | |||
ae1a8842db | |||
a3b17d1ed4 | |||
41576e27be | |||
07082cb3aa | |||
426cb33fab | |||
9e4f840d2d | |||
e120d274e9 | |||
977d3f6ef9 | |||
ecdbc917a5 | |||
0083cd55df | |||
d380194e13 | |||
32f5d5ba72 | |||
e818416863 | |||
7eed70cfe9 | |||
ea6ca23f57 | |||
f056b026d6 | |||
1c0a6efeb1 | |||
17732eea08 | |||
aa5381fd59 | |||
ffee86fcf3 | |||
7ff7398aff | |||
67925a39f2 | |||
3b5e1c7b34 | |||
3e49acf7ae | |||
76764c4374 | |||
9f6f8e1b55 | |||
9590180c6c | |||
aef5c60a7b | |||
d4c9c667c9 | |||
96f0d582f0 | |||
7e8702a71e | |||
1524061480 | |||
434922f702 | |||
d2862ddc93 | |||
6e55431d4c | |||
01548c5e9c | |||
bf1dae2dbe | |||
59c93defcf | |||
a2a1a27502 | |||
e3227e7d54 | |||
1f4a8fffdb | |||
86b1183883 | |||
f781f4848c | |||
19824d693c | |||
0694b911a4 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.13.0-rc1
|
||||
current_version = 0.13.0-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
@ -23,6 +23,8 @@ values =
|
||||
|
||||
[bumpversion:file:helm/values.yaml]
|
||||
|
||||
[bumpversion:file:helm/README.md]
|
||||
|
||||
[bumpversion:file:helm/Chart.yaml]
|
||||
|
||||
[bumpversion:file:.github/workflows/release.yml]
|
||||
|
15
.github/workflows/release.yml
vendored
15
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/authentik:0.13.0-rc1
|
||||
-t beryju/authentik:0.13.0-stable
|
||||
-t beryju/authentik:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik:0.13.0-rc1
|
||||
run: docker push beryju/authentik:0.13.0-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik:latest
|
||||
build-proxy:
|
||||
@ -48,11 +48,11 @@ jobs:
|
||||
cd proxy/
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/authentik-proxy:0.13.0-rc1 \
|
||||
-t beryju/authentik-proxy:0.13.0-stable \
|
||||
-t beryju/authentik-proxy:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik-proxy:0.13.0-rc1
|
||||
run: docker push beryju/authentik-proxy:0.13.0-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik-proxy:latest
|
||||
build-static:
|
||||
@ -69,17 +69,18 @@ jobs:
|
||||
cd web/
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/authentik-static:0.13.0-rc1 \
|
||||
-t beryju/authentik-static:0.13.0-stable \
|
||||
-t beryju/authentik-static:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/authentik-static:0.13.0-rc1
|
||||
run: docker push beryju/authentik-static:0.13.0-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/authentik-static:latest
|
||||
test-release:
|
||||
needs:
|
||||
- build-server
|
||||
- build-static
|
||||
- build-proxy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
@ -106,5 +107,5 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 0.13.0-rc1
|
||||
tagName: 0.13.0-stable
|
||||
environment: beryjuorg-prod
|
||||
|
@ -38,6 +38,7 @@ RUN apt-get update && \
|
||||
|
||||
COPY ./authentik/ /authentik
|
||||
COPY ./pytest.ini /
|
||||
COPY ./xml /xml
|
||||
COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
|
||||
|
5
Makefile
5
Makefile
@ -1,5 +1,10 @@
|
||||
all: lint-fix lint coverage gen
|
||||
|
||||
test-full:
|
||||
coverage run manage.py test --failfast -v 3 .
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
test-integration:
|
||||
k3d cluster create || exit 0
|
||||
k3d kubeconfig write -o ~/.kube/config --overwrite
|
||||
|
120
Pipfile.lock
generated
120
Pipfile.lock
generated
@ -53,10 +53,10 @@
|
||||
},
|
||||
"autobahn": {
|
||||
"hashes": [
|
||||
"sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b",
|
||||
"sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb"
|
||||
"sha256:74ca21d3552825615a65d47ec38d0aa5961a1345f7639f5b0e2abfff40cbfd07",
|
||||
"sha256:85c14b4a404146339ffd171e1ea1f65bf71e2f777d810aaa8a36119273869e3d"
|
||||
],
|
||||
"version": "==20.7.1"
|
||||
"version": "==20.12.1"
|
||||
},
|
||||
"automat": {
|
||||
"hashes": [
|
||||
@ -74,18 +74,18 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:616cde1e326949020da85a5bacaa7ad287e9f117d10ac9c5bfb9150a98dfe1a7",
|
||||
"sha256:ddad9ada00eccae1fc2da28c69531ba202fead562994ddcd9a9a232e993cd8a2"
|
||||
"sha256:2a6e92194bd6f2341908dc9b133af057ea1ff20b7d7e54674f48cdb531d93ca5",
|
||||
"sha256:a35e0915547ea659ddd832c9aaf55038c56fa894c4cc2a2a46cd6c642494012a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.16.34"
|
||||
"version": "==1.16.35"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:49f5e56a7382a65ee0873371edcd91bdba8fc3f70abe102ebc1a0da2e6fbed06",
|
||||
"sha256:4d81d92127ef646ae0f0ee84c9c220c92fa82312e765c29f8cb3b000fdbdd038"
|
||||
"sha256:633aa910509b060717df4130f7e2841f1101c0c47fd5871f4903b4b1dbab7e23",
|
||||
"sha256:d31dce56799edb5796085d5296931faae201e28e14e568d9db4dac237a135fe3"
|
||||
],
|
||||
"version": "==1.19.34"
|
||||
"version": "==1.19.35"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -396,10 +396,10 @@
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:5176db85f1e7e837a646cd9cede72c3c404ccf2e3373d9ee14b2db88febad440",
|
||||
"sha256:b728625ff5dfce8f9e56a499c8a4eb51443a67f20f6d28b67d5774c310ec4b6b"
|
||||
"sha256:0b0e026b412a0ad096e753907559e4bdb180d9ba9f68dd9036164db4fdc4ad2e",
|
||||
"sha256:ce752cc51c31f479dbf9928435ef4b07514b20261b021c7383bee4bda646acb8"
|
||||
],
|
||||
"version": "==1.23.0"
|
||||
"version": "==1.24.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@ -646,26 +646,46 @@
|
||||
},
|
||||
"msgpack": {
|
||||
"hashes": [
|
||||
"sha256:002a0d813e1f7b60da599bdf969e632074f9eec1b96cbed8fb0973a63160a408",
|
||||
"sha256:25b3bc3190f3d9d965b818123b7752c5dfb953f0d774b454fd206c18fe384fb8",
|
||||
"sha256:271b489499a43af001a2e42f42d876bb98ccaa7e20512ff37ca78c8e12e68f84",
|
||||
"sha256:39c54fdebf5fa4dda733369012c59e7d085ebdfe35b6cf648f09d16708f1be5d",
|
||||
"sha256:4233b7f86c1208190c78a525cd3828ca1623359ef48f78a6fea4b91bb995775a",
|
||||
"sha256:5bea44181fc8e18eed1d0cd76e355073f00ce232ff9653a0ae88cb7d9e643322",
|
||||
"sha256:5dba6d074fac9b24f29aaf1d2d032306c27f04187651511257e7831733293ec2",
|
||||
"sha256:7a22c965588baeb07242cb561b63f309db27a07382825fc98aecaf0827c1538e",
|
||||
"sha256:908944e3f038bca67fcfedb7845c4a257c7749bf9818632586b53bcf06ba4b97",
|
||||
"sha256:9534d5cc480d4aff720233411a1f765be90885750b07df772380b34c10ecb5c0",
|
||||
"sha256:aa5c057eab4f40ec47ea6f5a9825846be2ff6bf34102c560bad5cad5a677c5be",
|
||||
"sha256:b3758dfd3423e358bbb18a7cccd1c74228dffa7a697e5be6cb9535de625c0dbf",
|
||||
"sha256:c901e8058dd6653307906c5f157f26ed09eb94a850dddd989621098d347926ab",
|
||||
"sha256:cec8bf10981ed70998d98431cd814db0ecf3384e6b113366e7f36af71a0fca08",
|
||||
"sha256:db685187a415f51d6b937257474ca72199f393dad89534ebbdd7d7a3b000080e",
|
||||
"sha256:e35b051077fc2f3ce12e7c6a34cf309680c63a842db3a0616ea6ed25ad20d272",
|
||||
"sha256:e7bbdd8e2b277b77782f3ce34734b0dfde6cbe94ddb74de8d733d603c7f9e2b1",
|
||||
"sha256:ea41c9219c597f1d2bf6b374d951d310d58684b5de9dc4bd2976db9e1e22c140"
|
||||
"sha256:01835e300967e5ad6fdbfc36eafe74df67ff47e16e0d6dee8766630550315903",
|
||||
"sha256:03c5554315317d76c25a15569dd52ac6047b105df71e861f24faf9675672b72d",
|
||||
"sha256:0968b368a9a9081435bfcb7a57a1e8b75c7bf038ef911b369acd2e038c7f873a",
|
||||
"sha256:1d7ab166401f7789bf11262439336c0a01b878f0d602e48f35c35d2e3a555820",
|
||||
"sha256:1e8d27bac821f8aa909904a704a67e5e8bc2e42b153415fc3621b7afbc06702b",
|
||||
"sha256:1fc9f21da9fd77088ebfd3c9941b044ca3f4a048e85f7ff5727f26bcdbffed61",
|
||||
"sha256:20196229acc193939223118c7420838749d5b0cece49cd397739a3a6ffcfe2d1",
|
||||
"sha256:2933443313289725f16bd7b99a8c3aa6a2cca1549e661d7407f056a0af80bf7b",
|
||||
"sha256:2966b155356fd231fa441131d7301e1596ee38974ad56dc57fd752fdbe2bb63f",
|
||||
"sha256:29a6fb3729215b6fcab786ef4f460a5406a5c056f7021191f70ff7712a3f6ba4",
|
||||
"sha256:35cbefa7d7bddfb4b0770a1b9ff721cd8dfe9a680947a68457974d5e3e6acc2f",
|
||||
"sha256:35ff1ac162a77fb78be360d9f771d36cbf1202e94fc6d70e284ad5db6ab72608",
|
||||
"sha256:40dd1ac7420f071e96b3e4a4a7b8e69546a6f8065ff5995dbacf53f86207eb98",
|
||||
"sha256:4bea1938e484c9caca9585105f447d6807c496c153b7244fa726b3cc4a68ec9e",
|
||||
"sha256:4e58b9f4a99bc3a90859bb006ec4422448a5ce39e5cd6e7498c56de5dcec9c34",
|
||||
"sha256:66d47e952856bfcde46d8351380d0b5b928a73112b66bc06d5367dfcc077c06a",
|
||||
"sha256:69f6aa503378548ea1e760c11aeb6fc91952bf3634fd806a38a0e47edb507fcd",
|
||||
"sha256:7033215267a0e9f60f4a5e4fb2228a932c404f237817caff8dc3115d9e7cd975",
|
||||
"sha256:7b50afd767cc053ad92fad39947c3670db27305fd1c49acded44d9d9ac8b56fd",
|
||||
"sha256:99ea9e65876546743b2b8bb5bc7adefbb03b9da78a899827467da197a48f790b",
|
||||
"sha256:abcc62303ac4d789878d4aac4cdba1bbe2adb478d67be99cd4a6d56ac3a4028f",
|
||||
"sha256:b107f9b36665bf7d7c6176a938a361a7aba16aa179d833919448f77287866484",
|
||||
"sha256:b5b27923b6c98a2616b7e906a29e4e10e1b4424aea87a0e0d5636327dc6ea315",
|
||||
"sha256:bf8eedc7bfbf63cbc9abe58287c32d78780a347835e82c23033c68f11f80bb05",
|
||||
"sha256:c144ff4954a6ea40aa603600c8be175349588fc68696092889fa34ab6e055060",
|
||||
"sha256:c4e5f96a1d0d916ce7a16decb7499e8923ddef007cf7d68412fb68767766648a",
|
||||
"sha256:c60e8b2bf754b8dcc1075c5bee0b177ed9193e7cbd2377faaf507120a948e697",
|
||||
"sha256:c82fc6cdba5737eb6ed0c926a30a5d56e7b050297375a16d6c5ad89b576fd979",
|
||||
"sha256:ce4ebe2c79411cd5671b20862831880e7850a2de699cff6626f48853fde61ae6",
|
||||
"sha256:d113c6b1239c62669ef3063693842605a3edbfebc39a333cf91ba60d314afe6d",
|
||||
"sha256:d3cea07ad16919a44e8d1ea67efa5244855cdce807d672f41694acc24d08834e",
|
||||
"sha256:d76672602db16e3f44bc1a85c7ee5f15d79e02fcf5bc9d133c2954753be6eddc",
|
||||
"sha256:decf2091b75987ca2564e3b742f9614eb7d57e39ff04eaa68af7a3fc5648f7ed",
|
||||
"sha256:e13b9007af66a3f62574bc0a13843df0e4402f5ee4b00a02aa1803f01d26b9fb",
|
||||
"sha256:e157edf4213dacafb0f862e0b7a3a18448250cec91aa1334f432f49028acc650",
|
||||
"sha256:e234ff83628ca3ab345bf97fb36ccbf6d2f1700f5e08868643bf4489edc960f8",
|
||||
"sha256:f08d9dd3ce0c5e972dc4653f0fb66d2703941e65356388c13032b578dd718261",
|
||||
"sha256:f20d7d4f1f0728560408ba6933154abccf0c20f24642a2404b43d5c23e4119ab"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"oauthlib": {
|
||||
"hashes": [
|
||||
@ -676,11 +696,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
|
||||
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
|
||||
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
|
||||
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.7"
|
||||
"version": "==20.8"
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
@ -948,7 +968,7 @@
|
||||
"sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa",
|
||||
"sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.6"
|
||||
},
|
||||
"ruamel.yaml": {
|
||||
@ -1073,11 +1093,11 @@
|
||||
"standard"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:28420526640d800aabe648038f8e2ea8ba2a8bdc363002eecd5dfc57a0f75ab7",
|
||||
"sha256:5123606e0f1d15ffbe0f63161c5078f7c28f350c5eb102435671eae58046db0f"
|
||||
"sha256:2a7b17f4d9848d6557ccc2274a5f7c97f1daf037d130a0c6918f67cd9bc8cdf5",
|
||||
"sha256:6fcce74c00b77d4f4b3ed7ba1b2a370d27133bfdb46f835b7a76dfe0a8c110ae"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.13.0"
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
@ -1263,11 +1283,11 @@
|
||||
},
|
||||
"bandit": {
|
||||
"hashes": [
|
||||
"sha256:2ff3fe35fe3212c0be5fc9c4899bd0108e2b5239c5ff62fb174639e4660fe958",
|
||||
"sha256:d02dfe250f4aa2d166c127ad81d192579e2bfcdb8501717c0e2005e35a6bcf60"
|
||||
"sha256:216be4d044209fa06cf2a3e51b319769a51be8318140659719aa7a115c35ed07",
|
||||
"sha256:8a4c7415254d75df8ff3c3b15cfe9042ecee628a1e40b44c15a98890fbfc2608"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.6.3"
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
@ -1453,11 +1473,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236",
|
||||
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"
|
||||
"sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
|
||||
"sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.7"
|
||||
"version": "==20.8"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
@ -1496,10 +1516,10 @@
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
||||
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
||||
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
|
||||
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
|
||||
],
|
||||
"version": "==1.9.0"
|
||||
"version": "==1.10.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
@ -1566,11 +1586,11 @@
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
|
||||
"sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
|
||||
"sha256:b12e09409c5bdedc28d308469e156127004a436b41e9b44f9bff6446cbab9152",
|
||||
"sha256:d69e1a80b34fe4d596c9142f35d9e523d98a2838976f1a68419a8f051b24cec6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.1.2"
|
||||
"version": "==6.2.0"
|
||||
},
|
||||
"pytest-django": {
|
||||
"hashes": [
|
||||
|
@ -1,4 +1,4 @@
|
||||
<img src="icons/icon_top_brand.svg" height="250" alt="authentik logo">
|
||||
<img src="web/icons/icon_top_brand.svg" height="250" alt="authentik logo">
|
||||
|
||||
---
|
||||
|
||||
|
@ -6,9 +6,9 @@ As authentik is currently in a pre-stable, only the latest "stable" version is s
|
||||
|
||||
| Version | Supported |
|
||||
| -------- | ------------------ |
|
||||
| 0.10.x | :white_check_mark: |
|
||||
| 0.11.x | :white_check_mark: |
|
||||
| 0.12.x | :white_check_mark: |
|
||||
| 0.13.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""authentik"""
|
||||
__version__ = "0.13.0-rc1"
|
||||
__version__ = "0.13.0-stable"
|
||||
|
37
authentik/admin/tests/test_api.py
Normal file
37
authentik/admin/tests/test_api.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""test admin api"""
|
||||
from json import loads
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik import __version__
|
||||
from authentik.core.models import Group, User
|
||||
|
||||
|
||||
class TestAdminAPI(TestCase):
|
||||
"""test admin api"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.user = User.objects.create(username="test-user")
|
||||
self.group = Group.objects.create(name="superusers", is_superuser=True)
|
||||
self.group.users.add(self.user)
|
||||
self.group.save()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_overview(self):
|
||||
"""Test Overview API"""
|
||||
response = self.client.get(reverse("authentik_api:admin_overview-list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = loads(response.content)
|
||||
self.assertEqual(body["version"], __version__)
|
||||
|
||||
def test_metrics(self):
|
||||
"""Test metrics API"""
|
||||
response = self.client.get(reverse("authentik_api:admin_metrics-list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_tasks(self):
|
||||
"""Test tasks metrics API"""
|
||||
response = self.client.get(reverse("authentik_api:admin_system_tasks-list"))
|
||||
self.assertEqual(response.status_code, 200)
|
@ -1,9 +1,13 @@
|
||||
"""admin tests"""
|
||||
from uuid import uuid4
|
||||
|
||||
from django import forms
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from authentik.admin.views.policies_bindings import PolicyBindingCreateView
|
||||
from authentik.core.models import Application
|
||||
from authentik.policies.forms import PolicyBindingForm
|
||||
|
||||
|
||||
class TestPolicyBindingView(TestCase):
|
||||
@ -18,9 +22,22 @@ class TestPolicyBindingView(TestCase):
|
||||
view = PolicyBindingCreateView(request=request)
|
||||
self.assertEqual(view.get_initial(), {})
|
||||
|
||||
def test_with_param(self):
|
||||
def test_with_params_invalid(self):
|
||||
"""Test PolicyBindingCreateView with invalid get params"""
|
||||
request = self.factory.get("/", {"target": uuid4()})
|
||||
view = PolicyBindingCreateView(request=request)
|
||||
self.assertEqual(view.get_initial(), {})
|
||||
|
||||
def test_with_params(self):
|
||||
"""Test PolicyBindingCreateView with get params"""
|
||||
target = Application.objects.create(name="test")
|
||||
request = self.factory.get("/", {"target": target.pk.hex})
|
||||
view = PolicyBindingCreateView(request=request)
|
||||
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
||||
|
||||
self.assertTrue(
|
||||
isinstance(
|
||||
PolicyBindingForm(initial={"target": "foo"}).fields["target"].widget,
|
||||
forms.HiddenInput,
|
||||
)
|
||||
)
|
||||
|
@ -1,8 +1,12 @@
|
||||
"""admin tests"""
|
||||
from uuid import uuid4
|
||||
|
||||
from django import forms
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from authentik.admin.views.stages_bindings import StageBindingCreateView
|
||||
from authentik.flows.forms import FlowStageBindingForm
|
||||
from authentik.flows.models import Flow
|
||||
|
||||
|
||||
@ -18,9 +22,22 @@ class TestStageBindingView(TestCase):
|
||||
view = StageBindingCreateView(request=request)
|
||||
self.assertEqual(view.get_initial(), {})
|
||||
|
||||
def test_with_param(self):
|
||||
def test_with_params_invalid(self):
|
||||
"""Test StageBindingCreateView with invalid get params"""
|
||||
request = self.factory.get("/", {"target": uuid4()})
|
||||
view = StageBindingCreateView(request=request)
|
||||
self.assertEqual(view.get_initial(), {})
|
||||
|
||||
def test_with_params(self):
|
||||
"""Test StageBindingCreateView with get params"""
|
||||
target = Flow.objects.create(name="test", slug="test")
|
||||
request = self.factory.get("/", {"target": target.pk.hex})
|
||||
view = StageBindingCreateView(request=request)
|
||||
self.assertEqual(view.get_initial(), {"target": target, "order": 0})
|
||||
|
||||
self.assertTrue(
|
||||
isinstance(
|
||||
FlowStageBindingForm(initial={"target": "foo"}).fields["target"].widget,
|
||||
forms.HiddenInput,
|
||||
)
|
||||
)
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""API Authentication"""
|
||||
from base64 import b64decode
|
||||
from binascii import Error
|
||||
from typing import Any, Optional, Tuple, Union
|
||||
|
||||
from rest_framework.authentication import BaseAuthentication, get_authorization_header
|
||||
@ -24,7 +25,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]:
|
||||
return None
|
||||
try:
|
||||
auth_credentials = b64decode(auth_credentials.encode()).decode()
|
||||
except UnicodeDecodeError:
|
||||
except (UnicodeDecodeError, Error):
|
||||
return None
|
||||
# Accept credentials with username and without
|
||||
if ":" in auth_credentials:
|
||||
|
37
authentik/api/tests.py
Normal file
37
authentik/api/tests.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""Test API Authentication"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.api.auth import token_from_header
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
|
||||
|
||||
class TestAPIAuth(TestCase):
|
||||
"""Test API Authentication"""
|
||||
|
||||
def test_valid(self):
|
||||
"""Test valid token"""
|
||||
token = Token.objects.create(
|
||||
intent=TokenIntents.INTENT_API, user=get_anonymous_user()
|
||||
)
|
||||
auth = b64encode(f":{token.key}".encode()).decode()
|
||||
self.assertEqual(token_from_header(f"Basic {auth}".encode()), token)
|
||||
|
||||
def test_invalid_type(self):
|
||||
"""Test invalid type"""
|
||||
self.assertIsNone(token_from_header("foo bar".encode()))
|
||||
|
||||
def test_invalid_decode(self):
|
||||
"""Test invalid bas64"""
|
||||
self.assertIsNone(token_from_header("Basic bar".encode()))
|
||||
|
||||
def test_invalid_empty_password(self):
|
||||
"""Test invalid with empty password"""
|
||||
self.assertIsNone(token_from_header("Basic :".encode()))
|
||||
|
||||
def test_invalid_no_token(self):
|
||||
"""Test invalid with no token"""
|
||||
auth = b64encode(":abc".encode()).decode()
|
||||
self.assertIsNone(token_from_header(f"Basic :{auth}".encode()))
|
@ -1,7 +1,10 @@
|
||||
"""Application API Views"""
|
||||
from django.db.models import QuerySet
|
||||
from django.http.response import Http404
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import SerializerMethodField
|
||||
from rest_framework.generics import get_object_or_404
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
@ -71,8 +74,12 @@ class ApplicationViewSet(ModelViewSet):
|
||||
@action(detail=True)
|
||||
def metrics(self, request: Request, slug: str):
|
||||
"""Metrics for application logins"""
|
||||
# TODO: Check app read and audit read perms
|
||||
app = Application.objects.get(slug=slug)
|
||||
app = get_object_or_404(
|
||||
get_objects_for_user(request.user, "authentik_core.view_application"),
|
||||
slug=slug,
|
||||
)
|
||||
if not request.user.has_perm("authentik_audit.view_event"):
|
||||
raise Http404
|
||||
return Response(
|
||||
get_events_per_1h(
|
||||
action=EventAction.AUTHORIZE_APPLICATION,
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Channels base classes"""
|
||||
from channels.exceptions import DenyConnection
|
||||
from channels.generic.websocket import JsonWebsocketConsumer
|
||||
from structlog import get_logger
|
||||
|
||||
@ -17,16 +18,13 @@ class AuthJsonConsumer(JsonWebsocketConsumer):
|
||||
headers = dict(self.scope["headers"])
|
||||
if b"authorization" not in headers:
|
||||
LOGGER.warning("WS Request without authorization header")
|
||||
self.close()
|
||||
return False
|
||||
raise DenyConnection()
|
||||
|
||||
raw_header = headers[b"authorization"]
|
||||
|
||||
token = token_from_header(raw_header)
|
||||
if not token:
|
||||
LOGGER.warning("Failed to authenticate")
|
||||
self.close()
|
||||
return False
|
||||
raise DenyConnection()
|
||||
|
||||
self.user = token.user
|
||||
return True
|
||||
|
@ -6,8 +6,6 @@
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="preload" href="{% static 'dist/assets/fonts/DINEngschriftStd.woff2' %}" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="{% static 'dist/assets/fonts/DINEngschriftStd.woff' %}" as="font" type="font/woff" crossorigin>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title>
|
||||
|
26
authentik/core/templates/user/details.html
Normal file
26
authentik/core/templates/user/details.html
Normal file
@ -0,0 +1,26 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__header pf-c-title pf-m-md">
|
||||
{% trans 'Update details' %}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
||||
{% include 'partials/form_horizontal.html' with form=form %}
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-form__actions">
|
||||
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
|
||||
{% if unenrollment_enabled %}
|
||||
<a class="pf-c-button pf-m-danger"
|
||||
href="{% url 'authentik_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{%
|
||||
trans "Delete account" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
@ -15,29 +15,9 @@
|
||||
<section class="pf-c-page__main-section">
|
||||
<div class="pf-u-display-flex pf-u-justify-content-center">
|
||||
<div class="pf-u-w-75">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__header pf-c-title pf-m-md">
|
||||
{% trans 'Update details' %}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
||||
{% include 'partials/form_horizontal.html' with form=form %}
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-form__actions">
|
||||
<input class="pf-c-button pf-m-primary" type="submit" value="{% trans 'Update' %}" />
|
||||
{% if unenrollment_enabled %}
|
||||
<a class="pf-c-button pf-m-danger"
|
||||
href="{% url 'authentik_flows:default-unenrollment' %}?back={{ request.get_full_path }}">{% trans "Delete account" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<ak-site-shell url="{% url 'authentik_core:user-details' %}">
|
||||
<div slot="body"></div>
|
||||
</ak-site-shell>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -34,9 +34,3 @@ class TestOverviewViews(TestCase):
|
||||
self.assertEqual(
|
||||
self.client.get(reverse("authentik_core:overview")).status_code, 200
|
||||
)
|
||||
|
||||
def test_user_settings(self):
|
||||
"""Test user settings"""
|
||||
self.assertEqual(
|
||||
self.client.get(reverse("authentik_core:user-settings")).status_code, 200
|
||||
)
|
||||
|
@ -28,3 +28,9 @@ class TestUserViews(TestCase):
|
||||
self.assertEqual(
|
||||
self.client.get(reverse("authentik_core:user-settings")).status_code, 200
|
||||
)
|
||||
|
||||
def test_user_details(self):
|
||||
"""Test UserDetailsView"""
|
||||
self.assertEqual(
|
||||
self.client.get(reverse("authentik_core:user-details")).status_code, 200
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ urlpatterns = [
|
||||
path("", shell.ShellView.as_view(), name="shell"),
|
||||
# User views
|
||||
path("-/user/", user.UserSettingsView.as_view(), name="user-settings"),
|
||||
path("-/user/details/", user.UserDetailsView.as_view(), name="user-details"),
|
||||
path("-/user/tokens/", user.TokenListView.as_view(), name="user-tokens"),
|
||||
path(
|
||||
"-/user/tokens/create/",
|
||||
|
@ -11,6 +11,7 @@ from django.http.response import HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from django.views.generic.base import TemplateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
|
||||
@ -26,14 +27,20 @@ from authentik.flows.models import Flow, FlowDesignation
|
||||
from authentik.lib.views import CreateAssignPermView
|
||||
|
||||
|
||||
class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
|
||||
"""Update User settings"""
|
||||
class UserSettingsView(TemplateView):
|
||||
"""Multiple SiteShells for user details and all stages"""
|
||||
|
||||
template_name = "user/settings.html"
|
||||
|
||||
|
||||
class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
|
||||
"""Update User details"""
|
||||
|
||||
template_name = "user/details.html"
|
||||
form_class = UserDetailForm
|
||||
|
||||
success_message = _("Successfully updated user.")
|
||||
success_url = reverse_lazy("authentik_core:user-settings")
|
||||
success_url = reverse_lazy("authentik_core:user-details")
|
||||
|
||||
def get_object(self):
|
||||
return self.request.user
|
||||
|
@ -22,16 +22,15 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
def validate_key_data(self, value):
|
||||
"""Verify that input is a valid PEM RSA Key"""
|
||||
# Since this field is optional, data can be empty.
|
||||
if value == "":
|
||||
return value
|
||||
try:
|
||||
load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in value.split("\n")])),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
)
|
||||
except ValueError:
|
||||
raise ValidationError("Unable to load private key.")
|
||||
if value != "":
|
||||
try:
|
||||
load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in value.split("\n")])),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
)
|
||||
except ValueError:
|
||||
raise ValidationError("Unable to load private key.")
|
||||
return value
|
||||
|
||||
class Meta:
|
||||
|
@ -26,16 +26,15 @@ class CertificateKeyPairForm(forms.ModelForm):
|
||||
"""Verify that input is a valid PEM RSA Key"""
|
||||
key_data = self.cleaned_data["key_data"]
|
||||
# Since this field is optional, data can be empty.
|
||||
if key_data == "":
|
||||
return key_data
|
||||
try:
|
||||
load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in key_data.split("\n")])),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
)
|
||||
except ValueError:
|
||||
raise forms.ValidationError("Unable to load private key.")
|
||||
if key_data != "":
|
||||
try:
|
||||
load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in key_data.split("\n")])),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
)
|
||||
except ValueError:
|
||||
raise forms.ValidationError("Unable to load private key.")
|
||||
return key_data
|
||||
|
||||
class Meta:
|
||||
|
@ -3,14 +3,17 @@ from unittest.mock import MagicMock, PropertyMock, patch
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN
|
||||
from authentik.flows.planner import FlowPlan, FlowPlanner
|
||||
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
|
||||
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
from authentik.policies.http import AccessDeniedResponse
|
||||
@ -35,7 +38,7 @@ class TestFlowExecutor(TestCase):
|
||||
"""Test views logic"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.request_factory = RequestFactory()
|
||||
|
||||
def test_existing_plan_diff_flow(self):
|
||||
"""Check that a plan for a different flow cancels the current plan"""
|
||||
@ -276,6 +279,83 @@ class TestFlowExecutor(TestCase):
|
||||
{"type": "redirect", "to": reverse("authentik_core:shell")},
|
||||
)
|
||||
|
||||
def test_reevaluate_keep(self):
|
||||
"""Test planner with re-evaluate (everything is kept)"""
|
||||
flow = Flow.objects.create(
|
||||
name="test-default-context",
|
||||
slug="test-default-context",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
true_policy = DummyPolicy.objects.create(result=True, wait_min=1, wait_max=2)
|
||||
|
||||
binding = FlowStageBinding.objects.create(
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
|
||||
)
|
||||
binding2 = FlowStageBinding.objects.create(
|
||||
target=flow,
|
||||
stage=DummyStage.objects.create(name="dummy2"),
|
||||
order=1,
|
||||
re_evaluate_policies=True,
|
||||
)
|
||||
binding3 = FlowStageBinding.objects.create(
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
|
||||
)
|
||||
|
||||
PolicyBinding.objects.create(policy=true_policy, target=binding2, order=0)
|
||||
|
||||
# Here we patch the dummy policy to evaluate to true so the stage is included
|
||||
with patch(
|
||||
"authentik.policies.dummy.models.DummyPolicy.passes", POLICY_RETURN_TRUE
|
||||
):
|
||||
|
||||
exec_url = reverse(
|
||||
"authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}
|
||||
)
|
||||
# First request, run the planner
|
||||
response = self.client.get(exec_url)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding.stage)
|
||||
self.assertEqual(plan.stages[1], binding2.stage)
|
||||
self.assertEqual(plan.stages[2], binding3.stage)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], ReevaluateMarker)
|
||||
self.assertIsInstance(plan.markers[2], StageMarker)
|
||||
|
||||
# Second request, this passes the first dummy stage
|
||||
response = self.client.post(exec_url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding2.stage)
|
||||
self.assertEqual(plan.stages[1], binding3.stage)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
self.assertIsInstance(plan.markers[1], StageMarker)
|
||||
|
||||
# Third request, this passes the first dummy stage
|
||||
response = self.client.post(exec_url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
plan: FlowPlan = self.client.session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertEqual(plan.stages[0], binding3.stage)
|
||||
|
||||
self.assertIsInstance(plan.markers[0], StageMarker)
|
||||
|
||||
# third request, this should trigger the re-evaluate
|
||||
# We do this request without the patch, so the policy results in false
|
||||
response = self.client.post(exec_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
force_str(response.content),
|
||||
{"type": "redirect", "to": reverse("authentik_core:shell")},
|
||||
)
|
||||
|
||||
def test_reevaluate_remove_consecutive(self):
|
||||
"""Test planner with re-evaluate (consecutive stages are removed)"""
|
||||
flow = Flow.objects.create(
|
||||
@ -351,3 +431,33 @@ class TestFlowExecutor(TestCase):
|
||||
force_str(response.content),
|
||||
{"type": "redirect", "to": reverse("authentik_core:shell")},
|
||||
)
|
||||
|
||||
def test_stageview_user_identifier(self):
|
||||
"""Test PLAN_CONTEXT_PENDING_USER_IDENTIFIER"""
|
||||
flow = Flow.objects.create(
|
||||
name="test-default-context",
|
||||
slug="test-default-context",
|
||||
designation=FlowDesignation.AUTHENTICATION,
|
||||
)
|
||||
FlowStageBinding.objects.create(
|
||||
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
|
||||
)
|
||||
|
||||
ident = "test-identifier"
|
||||
|
||||
user = User.objects.create(username="test-user")
|
||||
request = self.request_factory.get(
|
||||
reverse("authentik_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
request.user = user
|
||||
planner = FlowPlanner(flow)
|
||||
plan = planner.plan(
|
||||
request, default_context={PLAN_CONTEXT_PENDING_USER_IDENTIFIER: ident}
|
||||
)
|
||||
|
||||
executor = FlowExecutorView()
|
||||
executor.plan = plan
|
||||
executor.flow = flow
|
||||
|
||||
stage_view = StageView(executor)
|
||||
self.assertEqual(ident, stage_view.get_context_data()["user"].username)
|
||||
|
@ -61,7 +61,7 @@ class DataclassEncoder(JSONEncoder):
|
||||
return asdict(o)
|
||||
if isinstance(o, UUID):
|
||||
return str(o)
|
||||
return super().default(o)
|
||||
return super().default(o) # pragma: no cover
|
||||
|
||||
|
||||
class EntryInvalidError(SentryIgnoredException):
|
||||
|
@ -11,7 +11,7 @@ from authentik.flows.transfer.common import (
|
||||
FlowBundle,
|
||||
FlowBundleEntry,
|
||||
)
|
||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
|
||||
from authentik.policies.models import Policy, PolicyBinding
|
||||
from authentik.stages.prompt.models import PromptStage
|
||||
|
||||
|
||||
@ -31,11 +31,6 @@ class FlowExporter:
|
||||
|
||||
def _prepare_pbm(self):
|
||||
self.pbm_uuids = [self.flow.pbm_uuid]
|
||||
for stage_subclass in Stage.__subclasses__():
|
||||
if issubclass(stage_subclass, PolicyBindingModel):
|
||||
self.pbm_uuids += stage_subclass.objects.filter(
|
||||
flow=self.flow
|
||||
).values_list("pbm_uuid", flat=True)
|
||||
self.pbm_uuids += FlowStageBinding.objects.filter(target=self.flow).values_list(
|
||||
"pbm_uuid", flat=True
|
||||
)
|
||||
|
@ -1,55 +0,0 @@
|
||||
"""authentik lib navbar Templatetag"""
|
||||
from django import template
|
||||
from django.http import HttpRequest
|
||||
from structlog import get_logger
|
||||
|
||||
register = template.Library()
|
||||
|
||||
LOGGER = get_logger()
|
||||
ACTIVE_STRING = "pf-m-current"
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def is_active(context, *args: str, **_) -> str:
|
||||
"""Return whether a navbar link is active or not."""
|
||||
request: HttpRequest = context.get("request")
|
||||
if not request.resolver_match:
|
||||
return ""
|
||||
match = request.resolver_match
|
||||
for url in args:
|
||||
if ":" in url:
|
||||
app_name, url = url.split(":")
|
||||
if match.app_name == app_name and match.url_name == url:
|
||||
return ACTIVE_STRING
|
||||
else:
|
||||
if match.url_name == url:
|
||||
return ACTIVE_STRING
|
||||
return ""
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def is_active_url(context, view: str) -> str:
|
||||
"""Return whether a navbar link is active or not."""
|
||||
request: HttpRequest = context.get("request")
|
||||
if not request.resolver_match:
|
||||
return ""
|
||||
|
||||
match = request.resolver_match
|
||||
current_full_url = f"{match.app_name}:{match.url_name}"
|
||||
|
||||
if current_full_url == view:
|
||||
return ACTIVE_STRING
|
||||
return ""
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def is_active_app(context, *args: str) -> str:
|
||||
"""Return True if current link is from app"""
|
||||
|
||||
request: HttpRequest = context.get("request")
|
||||
if not request.resolver_match:
|
||||
return ""
|
||||
for app_name in args:
|
||||
if request.resolver_match.app_name == app_name:
|
||||
return ACTIVE_STRING
|
||||
return ""
|
0
authentik/lib/tests/__init__.py
Normal file
0
authentik/lib/tests/__init__.py
Normal file
18
authentik/lib/tests/test_sentry.py
Normal file
18
authentik/lib/tests/test_sentry.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""test sentry integration"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.lib.sentry import SentryIgnoredException, before_send
|
||||
|
||||
|
||||
class TestSentry(TestCase):
|
||||
"""test sentry integration"""
|
||||
|
||||
def test_error_not_sent(self):
|
||||
"""Test SentryIgnoredError not sent"""
|
||||
self.assertIsNone(
|
||||
before_send(None, {"exc_info": (0, SentryIgnoredException(), 0)})
|
||||
)
|
||||
|
||||
def test_error_sent(self):
|
||||
"""Test error sent"""
|
||||
self.assertIsNone(before_send(None, {"exc_info": (0, ValueError(), 0)}))
|
@ -20,6 +20,8 @@ class TestTimeUtils(TestCase):
|
||||
"""Test invalid expression"""
|
||||
with self.assertRaises(ValueError):
|
||||
timedelta_from_string("foo")
|
||||
with self.assertRaises(ValueError):
|
||||
timedelta_from_string("bar=baz")
|
||||
|
||||
def test_validation(self):
|
||||
"""Test Django model field validator"""
|
@ -35,4 +35,6 @@ def timedelta_from_string(expr: str) -> datetime.timedelta:
|
||||
if key.lower() not in ALLOWED_KEYS:
|
||||
continue
|
||||
kwargs[key.lower()] = float(value)
|
||||
if len(kwargs) < 1:
|
||||
raise ValueError("No valid keys to pass to timedelta")
|
||||
return datetime.timedelta(**kwargs)
|
||||
|
@ -22,7 +22,6 @@ class AuthentikOutpostConfig(AppConfig):
|
||||
|
||||
name = "authentik.outposts"
|
||||
label = "authentik_outposts"
|
||||
mountpoint = "outposts/"
|
||||
verbose_name = "authentik Outpost"
|
||||
|
||||
def ready(self):
|
||||
|
@ -2,8 +2,9 @@
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from channels.exceptions import DenyConnection
|
||||
from dacite import from_dict
|
||||
from dacite.data import Data
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
@ -39,18 +40,16 @@ class WebsocketMessage:
|
||||
class OutpostConsumer(AuthJsonConsumer):
|
||||
"""Handler for Outposts that connect over websockets for health checks and live updates"""
|
||||
|
||||
outpost: Outpost
|
||||
outpost: Optional[Outpost] = None
|
||||
|
||||
def connect(self):
|
||||
if not super().connect():
|
||||
return
|
||||
super().connect()
|
||||
uuid = self.scope["url_route"]["kwargs"]["pk"]
|
||||
outpost = get_objects_for_user(
|
||||
self.user, "authentik_outposts.view_outpost"
|
||||
).filter(pk=uuid)
|
||||
if not outpost.exists():
|
||||
self.close()
|
||||
return
|
||||
raise DenyConnection()
|
||||
self.accept()
|
||||
self.outpost = outpost.first()
|
||||
OutpostState(
|
||||
@ -60,7 +59,8 @@ class OutpostConsumer(AuthJsonConsumer):
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def disconnect(self, close_code):
|
||||
OutpostState.for_channel(self.outpost, self.channel_name).delete()
|
||||
if self.outpost:
|
||||
OutpostState.for_channel(self.outpost, self.channel_name).delete()
|
||||
LOGGER.debug("removed channel from cache", channel_name=self.channel_name)
|
||||
|
||||
def receive_json(self, content: Data):
|
||||
|
@ -46,7 +46,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
|
||||
"authentik_host_insecure": b64string(
|
||||
str(self.controller.outpost.config.authentik_host_insecure)
|
||||
),
|
||||
"token": b64string(self.controller.outpost.token.token_uuid.hex),
|
||||
"token": b64string(self.controller.outpost.token.key),
|
||||
},
|
||||
)
|
||||
|
||||
|
38
authentik/outposts/migrations/0014_auto_20201213_1407.py
Normal file
38
authentik/outposts/migrations/0014_auto_20201213_1407.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-13 14:07
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
|
||||
def update_config_prefix(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
alias = schema_editor.connection.alias
|
||||
Outpost = apps.get_model("authentik_outposts", "Outpost")
|
||||
|
||||
for outpost in Outpost.objects.using(alias).all():
|
||||
config = outpost._config
|
||||
for key in list(config):
|
||||
if "passbook" in key:
|
||||
new_key = key.replace("passbook", "authentik")
|
||||
config[new_key] = config[key]
|
||||
del config[key]
|
||||
outpost._config = config
|
||||
outpost.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_outposts", "0013_auto_20201203_2009"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_config_prefix),
|
||||
migrations.AlterField(
|
||||
model_name="dockerserviceconnection",
|
||||
name="url",
|
||||
field=models.TextField(
|
||||
help_text="Can be in the format of 'unix://<path>' when connecting to a local docker daemon, or 'https://<hostname>:2376' when connecting to a remote system."
|
||||
),
|
||||
),
|
||||
]
|
@ -140,7 +140,14 @@ class OutpostServiceConnection(models.Model):
|
||||
class DockerServiceConnection(OutpostServiceConnection):
|
||||
"""Service Connection to a Docker endpoint"""
|
||||
|
||||
url = models.TextField()
|
||||
url = models.TextField(
|
||||
help_text=_(
|
||||
(
|
||||
"Can be in the format of 'unix://<path>' when connecting to a local docker daemon, "
|
||||
"or 'https://<hostname>:2376' when connecting to a remote system."
|
||||
)
|
||||
)
|
||||
)
|
||||
tls_verification = models.ForeignKey(
|
||||
CertificateKeyPair,
|
||||
null=True,
|
||||
|
@ -12,4 +12,9 @@ CELERY_BEAT_SCHEDULE = {
|
||||
"schedule": crontab(minute=0, hour="*"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
"outpost_token_ensurer": {
|
||||
"task": "authentik.outposts.tasks.outpost_token_ensurer",
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
}
|
||||
|
@ -90,6 +90,21 @@ def outpost_pre_delete(outpost_pk: str):
|
||||
ProxyKubernetesController(outpost, service_connection).down()
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def outpost_token_ensurer(self: MonitoredTask):
|
||||
"""Periodically ensure that all Outposts have valid Service Accounts
|
||||
and Tokens"""
|
||||
all_outposts = Outpost.objects.all()
|
||||
for outpost in all_outposts:
|
||||
_ = outpost.token
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.SUCCESSFUL,
|
||||
[f"Successfully checked {len(all_outposts)} Outposts."],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def outpost_post_save(model_class: str, model_pk: Any):
|
||||
"""If an Outpost is saved, Ensure that token is created/updated
|
||||
|
@ -1,11 +0,0 @@
|
||||
"""authentik outposts urls"""
|
||||
from django.urls import path
|
||||
|
||||
from authentik.outposts.views import KubernetesManifestView, SetupView
|
||||
|
||||
urlpatterns = [
|
||||
path(
|
||||
"<uuid:outpost_pk>/k8s/", KubernetesManifestView.as_view(), name="k8s-manifest"
|
||||
),
|
||||
path("<uuid:outpost_pk>/", SetupView.as_view(), name="setup"),
|
||||
]
|
@ -1,89 +0,0 @@
|
||||
"""authentik outpost views"""
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.views import View
|
||||
from django.views.generic import TemplateView
|
||||
from guardian.shortcuts import get_objects_for_user
|
||||
from structlog import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.outposts.controllers.docker import DockerController
|
||||
from authentik.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
Outpost,
|
||||
OutpostType,
|
||||
)
|
||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def get_object_for_user_or_404(user: User, perm: str, **filters) -> Model:
|
||||
"""Wrapper that combines get_objects_for_user and get_object_or_404"""
|
||||
return get_object_or_404(get_objects_for_user(user, perm), **filters)
|
||||
|
||||
|
||||
class DockerComposeView(LoginRequiredMixin, View):
|
||||
"""Generate docker-compose yaml"""
|
||||
|
||||
def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
|
||||
"""Render docker-compose file"""
|
||||
outpost: Outpost = get_object_for_user_or_404(
|
||||
request.user,
|
||||
"authentik_outposts.view_outpost",
|
||||
pk=outpost_pk,
|
||||
)
|
||||
manifest = ""
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
controller = DockerController(outpost, DockerServiceConnection())
|
||||
manifest = controller.get_static_deployment()
|
||||
|
||||
return HttpResponse(manifest, content_type="text/vnd.yaml")
|
||||
|
||||
|
||||
class KubernetesManifestView(LoginRequiredMixin, View):
|
||||
"""Generate Kubernetes Deployment and SVC for proxy"""
|
||||
|
||||
def get(self, request: HttpRequest, outpost_pk: str) -> HttpResponse:
|
||||
"""Render deployment template"""
|
||||
outpost: Outpost = get_object_for_user_or_404(
|
||||
request.user,
|
||||
"authentik_outposts.view_outpost",
|
||||
pk=outpost_pk,
|
||||
)
|
||||
manifest = ""
|
||||
if outpost.type == OutpostType.PROXY:
|
||||
controller = ProxyKubernetesController(
|
||||
outpost, KubernetesServiceConnection()
|
||||
)
|
||||
manifest = controller.get_static_deployment()
|
||||
|
||||
return HttpResponse(manifest, content_type="text/vnd.yaml")
|
||||
|
||||
|
||||
class SetupView(LoginRequiredMixin, TemplateView):
|
||||
"""Setup view"""
|
||||
|
||||
def get_template_names(self) -> List[str]:
|
||||
allowed = ["dc", "custom", "k8s_manual", "k8s_integration"]
|
||||
setup_type = self.request.GET.get("type", "dc")
|
||||
if setup_type not in allowed:
|
||||
setup_type = allowed[0]
|
||||
return [f"outposts/setup_{setup_type}.html"]
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
outpost: Outpost = get_object_for_user_or_404(
|
||||
self.request.user,
|
||||
"authentik_outposts.view_outpost",
|
||||
pk=self.kwargs["outpost_pk"],
|
||||
)
|
||||
kwargs.update(
|
||||
{"host": self.request.build_absolute_uri("/"), "outpost": outpost}
|
||||
)
|
||||
return kwargs
|
@ -7,7 +7,7 @@
|
||||
<label for="" class="pf-c-form__label"></label>
|
||||
<div class="c-form__horizontal-group">
|
||||
<p>
|
||||
Expression using Python. See <a target="_blank" href="https://goauthentik.io/policies/expression/">here</a> for a list of all variables.
|
||||
Expression using Python. See <a target="_blank" href="https://goauthentik.io/docs/policies/expression/">here</a> for a list of all variables.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -50,6 +50,7 @@ class HaveIBeenPwendPolicy(Policy):
|
||||
field=self.password_field,
|
||||
fields=request.context.keys(),
|
||||
)
|
||||
return PolicyResult(False, _("Password not set in context"))
|
||||
password = request.context[self.password_field]
|
||||
|
||||
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
|
||||
|
@ -10,6 +10,16 @@ from authentik.providers.oauth2.generators import generate_client_secret
|
||||
class TestHIBPPolicy(TestCase):
|
||||
"""Test HIBP Policy"""
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test without password"""
|
||||
policy = HaveIBeenPwendPolicy.objects.create(
|
||||
name="test_invalid",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages[0], "Password not set in context")
|
||||
|
||||
def test_false(self):
|
||||
"""Failing password case"""
|
||||
policy = HaveIBeenPwendPolicy.objects.create(
|
||||
|
@ -50,6 +50,7 @@ class PasswordPolicy(Policy):
|
||||
field=self.password_field,
|
||||
fields=request.context.keys(),
|
||||
)
|
||||
return PolicyResult(False, _("Password not set in context"))
|
||||
password = request.context[self.password_field]
|
||||
|
||||
filter_regex = []
|
||||
|
@ -9,6 +9,21 @@ from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
class TestPasswordPolicy(TestCase):
|
||||
"""Test Password Policy"""
|
||||
|
||||
def test_invalid(self):
|
||||
"""Test without password"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
name="test_invalid",
|
||||
amount_uppercase=1,
|
||||
amount_lowercase=2,
|
||||
amount_symbols=3,
|
||||
length_min=24,
|
||||
error_message="test message",
|
||||
)
|
||||
request = PolicyRequest(get_anonymous_user())
|
||||
result: PolicyResult = policy.passes(request)
|
||||
self.assertFalse(result.passing)
|
||||
self.assertEqual(result.messages[0], "Password not set in context")
|
||||
|
||||
def test_false(self):
|
||||
"""Failing password case"""
|
||||
policy = PasswordPolicy.objects.create(
|
||||
|
@ -7,7 +7,7 @@
|
||||
<label for="" class="pf-c-form__label"></label>
|
||||
<div class="c-form__horizontal-group">
|
||||
<p>
|
||||
Expression using Python. See <a href="https://goauthentik.io/property-mappings/expression/">here</a> for a list of all variables.
|
||||
Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
0
authentik/providers/oauth2/tests/__init__.py
Normal file
0
authentik/providers/oauth2/tests/__init__.py
Normal file
46
authentik/providers/oauth2/tests/test_views_authorize.py
Normal file
46
authentik/providers/oauth2/tests/test_views_authorize.py
Normal file
@ -0,0 +1,46 @@
|
||||
"""Test authorize view"""
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.errors import (
|
||||
AuthorizeError,
|
||||
ClientIdError,
|
||||
RedirectUriError,
|
||||
)
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
from authentik.providers.oauth2.views.authorize import OAuthAuthorizationParams
|
||||
|
||||
|
||||
class TestViewsAuthorize(TestCase):
|
||||
"""Test authorize view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_invalid_grant_type(self):
|
||||
"""Test with invalid grant type"""
|
||||
with self.assertRaises(AuthorizeError):
|
||||
request = self.factory.get("/", data={"response_type": "invalid"})
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_invalid_client_id(self):
|
||||
"""Test invalid client ID"""
|
||||
with self.assertRaises(ClientIdError):
|
||||
request = self.factory.get(
|
||||
"/", data={"response_type": "code", "client_id": "invalid"}
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_missing_redirect_uri(self):
|
||||
"""test missing redirect URI"""
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
)
|
||||
with self.assertRaises(RedirectUriError):
|
||||
request = self.factory.get(
|
||||
"/", data={"response_type": "code", "client_id": "test"}
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
@ -139,7 +139,7 @@ class OAuthAuthorizationParams:
|
||||
is_open_id = SCOPE_OPENID in self.scope
|
||||
|
||||
# Redirect URI validation.
|
||||
if is_open_id and not self.redirect_uri:
|
||||
if not self.redirect_uri:
|
||||
LOGGER.warning("Missing redirect uri.")
|
||||
raise RedirectUriError()
|
||||
if self.redirect_uri.lower() not in [
|
||||
|
@ -1,34 +1,6 @@
|
||||
# Generated by Django 3.1.1 on 2020-09-30 08:10
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
SCOPE_AK_PROXY_EXPRESSION = """return {
|
||||
"ak_proxy": {
|
||||
"user_attributes": user.group_attributes()
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
def create_proxy_scope(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from authentik.providers.proxy.models import SCOPE_AK_PROXY, ProxyProvider
|
||||
|
||||
ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping")
|
||||
|
||||
ScopeMapping.objects.update_or_create(
|
||||
scope_name=SCOPE_AK_PROXY,
|
||||
defaults={
|
||||
"name": "Autogenerated OAuth2 Mapping: authentik Proxy",
|
||||
"scope_name": SCOPE_AK_PROXY,
|
||||
"description": "",
|
||||
"expression": SCOPE_AK_PROXY_EXPRESSION,
|
||||
},
|
||||
)
|
||||
|
||||
for provider in ProxyProvider.objects.all():
|
||||
provider.set_oauth_defaults()
|
||||
provider.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -74,5 +46,4 @@ class Migration(migrations.Migration):
|
||||
verbose_name="HTTP-Basic Username",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(create_proxy_scope),
|
||||
]
|
||||
|
@ -0,0 +1,41 @@
|
||||
# Generated by Django 3.1.4 on 2020-12-14 09:42
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
SCOPE_AK_PROXY_EXPRESSION = """return {
|
||||
"ak_proxy": {
|
||||
"user_attributes": user.group_attributes()
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
def create_proxy_scope(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
from authentik.providers.proxy.models import SCOPE_AK_PROXY, ProxyProvider
|
||||
|
||||
ScopeMapping = apps.get_model("authentik_providers_oauth2", "ScopeMapping")
|
||||
|
||||
ScopeMapping.objects.filter(scope_name="pb_proxy").delete()
|
||||
|
||||
ScopeMapping.objects.update_or_create(
|
||||
scope_name=SCOPE_AK_PROXY,
|
||||
defaults={
|
||||
"name": "Autogenerated OAuth2 Mapping: authentik Proxy",
|
||||
"scope_name": SCOPE_AK_PROXY,
|
||||
"description": "",
|
||||
"expression": SCOPE_AK_PROXY_EXPRESSION,
|
||||
},
|
||||
)
|
||||
|
||||
for provider in ProxyProvider.objects.all():
|
||||
provider.set_oauth_defaults()
|
||||
provider.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_proxy", "0009_auto_20201007_1721"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(create_proxy_scope)]
|
@ -7,7 +7,7 @@
|
||||
<label for="" class="pf-c-form__label"></label>
|
||||
<div class="c-form__horizontal-group">
|
||||
<p>
|
||||
Expression using Python. See <a href="https://goauthentik.io/property-mappings/expression/">here</a> for a list of all variables.
|
||||
Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
84
authentik/providers/saml/tests/test_schema.py
Normal file
84
authentik/providers/saml/tests/test_schema.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""Test Requests and Responses against schema"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import RequestFactory, TestCase
|
||||
from guardian.utils import get_anonymous_user
|
||||
from lxml import etree # nosec
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||
from authentik.providers.saml.tests.test_auth_n_request import dummy_get_response
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.request import RequestProcessor
|
||||
|
||||
|
||||
class TestSchema(TestCase):
|
||||
"""Test Requests and Responses against schema"""
|
||||
|
||||
def setUp(self):
|
||||
cert = CertificateKeyPair.objects.first()
|
||||
self.provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
acs_url="http://testserver/source/saml/provider/acs/",
|
||||
signing_kp=cert,
|
||||
verification_kp=cert,
|
||||
)
|
||||
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
self.provider.save()
|
||||
self.source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
signing_kp=cert,
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_request_schema(self):
|
||||
"""Test generated AuthNRequest against Schema"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
request = request_proc.build_auth_n()
|
||||
|
||||
metadata = etree.fromstring(request) # nosec
|
||||
|
||||
schema = etree.XMLSchema(
|
||||
etree.parse("xml/saml-schema-protocol-2.0.xsd")
|
||||
) # nosec
|
||||
self.assertTrue(schema.validate(metadata))
|
||||
|
||||
def test_response_schema(self):
|
||||
"""Test generated AuthNRequest against Schema"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
request = request_proc.build_auth_n()
|
||||
|
||||
# To get an assertion we need a parsed request (parsed by provider)
|
||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||
b64encode(request.encode()).decode(), "test_state"
|
||||
)
|
||||
# Now create a response and convert it to string (provider)
|
||||
response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
|
||||
response = response_proc.build_response()
|
||||
|
||||
metadata = etree.fromstring(response) # nosec
|
||||
|
||||
schema = etree.XMLSchema(etree.parse("xml/saml-schema-protocol-2.0.xsd"))
|
||||
self.assertTrue(schema.validate(metadata))
|
@ -1,15 +1,6 @@
|
||||
"""Small helper functions"""
|
||||
import uuid
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.template.context import Context
|
||||
|
||||
|
||||
def render_xml(request: HttpRequest, template: str, ctx: Context) -> HttpResponse:
|
||||
"""Render template with content_type application/xml"""
|
||||
return render(request, template, context=ctx, content_type="application/xml")
|
||||
|
||||
|
||||
def get_random_id() -> str:
|
||||
"""Random hex id"""
|
||||
|
@ -32,3 +32,10 @@ class TestRecovery(TestCase):
|
||||
reverse("authentik_recovery:use-token", kwargs={"key": token.key})
|
||||
)
|
||||
self.assertEqual(int(self.client.session["_auth_user_id"]), token.user.pk)
|
||||
|
||||
def test_recovery_view_invalid(self):
|
||||
"""Test recovery view with invalid token"""
|
||||
response = self.client.get(
|
||||
reverse("authentik_recovery:use-token", kwargs={"key": "abc"})
|
||||
)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
@ -4,7 +4,7 @@ from django.conf import settings
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class PytestTestRunner:
|
||||
class PytestTestRunner: # pragma: no cover
|
||||
"""Runs pytest to discover and run tests."""
|
||||
|
||||
def __init__(self, verbosity=1, failfast=False, keepdb=False, **_):
|
||||
|
@ -7,7 +7,7 @@
|
||||
<label for="" class="pf-c-form__label"></label>
|
||||
<div class="c-form__horizontal-group">
|
||||
<p>
|
||||
Expression using Python. See <a href="https://goauthentik.io/property-mappings/expression/">here</a> for a list of all variables.
|
||||
Expression using Python. See <a href="https://goauthentik.io/docs/property-mappings/expression/">here</a> for a list of all variables.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
0
authentik/sources/oauth/tests/__init__.py
Normal file
0
authentik/sources/oauth/tests/__init__.py
Normal file
41
authentik/sources/oauth/tests/test_type_discord.py
Normal file
41
authentik/sources/oauth/tests/test_type_discord.py
Normal file
@ -0,0 +1,41 @@
|
||||
"""Discord Type tests"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.discord import DiscordOAuth2Callback
|
||||
|
||||
# https://discord.com/developers/docs/resources/user#user-object
|
||||
DISCORD_USER = {
|
||||
"id": "80351110224678912",
|
||||
"username": "Nelly",
|
||||
"discriminator": "1337",
|
||||
"avatar": "8342729096ea3675442027381ff50dfe",
|
||||
"verified": True,
|
||||
"email": "nelly@discord.com",
|
||||
"flags": 64,
|
||||
"premium_type": 1,
|
||||
"public_flags": 64,
|
||||
}
|
||||
|
||||
|
||||
class TestTypeGitHub(TestCase):
|
||||
"""OAuth Source tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.source = OAuthSource.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
provider_type="openid-connect",
|
||||
authorization_url="",
|
||||
profile_url="",
|
||||
consumer_key="",
|
||||
)
|
||||
|
||||
def test_enroll_context(self):
|
||||
"""Test GitHub Enrollment context"""
|
||||
ak_context = DiscordOAuth2Callback().get_user_enroll_context(
|
||||
self.source, UserOAuthSourceConnection(), DISCORD_USER
|
||||
)
|
||||
self.assertEqual(ak_context["username"], DISCORD_USER["username"])
|
||||
self.assertEqual(ak_context["email"], DISCORD_USER["email"])
|
||||
self.assertEqual(ak_context["name"], DISCORD_USER["username"])
|
71
authentik/sources/oauth/tests/test_type_github.py
Normal file
71
authentik/sources/oauth/tests/test_type_github.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""GitHub Type tests"""
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.github import GitHubOAuth2Callback
|
||||
|
||||
# https://developer.github.com/v3/users/#get-the-authenticated-user
|
||||
GITHUB_USER = {
|
||||
"login": "octocat",
|
||||
"id": 1,
|
||||
"node_id": "MDQ6VXNlcjE=",
|
||||
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/octocat",
|
||||
"html_url": "https://github.com/octocat",
|
||||
"followers_url": "https://api.github.com/users/octocat/followers",
|
||||
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/octocat/orgs",
|
||||
"repos_url": "https://api.github.com/users/octocat/repos",
|
||||
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/octocat/received_events",
|
||||
"type": "User",
|
||||
"site_admin": False,
|
||||
"name": "monalisa octocat",
|
||||
"company": "GitHub",
|
||||
"blog": "https://github.com/blog",
|
||||
"location": "San Francisco",
|
||||
"email": "octocat@github.com",
|
||||
"hireable": False,
|
||||
"bio": "There once was...",
|
||||
"twitter_username": "monatheoctocat",
|
||||
"public_repos": 2,
|
||||
"public_gists": 1,
|
||||
"followers": 20,
|
||||
"following": 0,
|
||||
"created_at": "2008-01-14T04:33:35Z",
|
||||
"updated_at": "2008-01-14T04:33:35Z",
|
||||
"private_gists": 81,
|
||||
"total_private_repos": 100,
|
||||
"owned_private_repos": 100,
|
||||
"disk_usage": 10000,
|
||||
"collaborators": 8,
|
||||
"two_factor_authentication": True,
|
||||
"plan": {"name": "Medium", "space": 400, "private_repos": 20, "collaborators": 0},
|
||||
}
|
||||
|
||||
|
||||
class TestTypeGitHub(TestCase):
|
||||
"""OAuth Source tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.source = OAuthSource.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
provider_type="openid-connect",
|
||||
authorization_url="",
|
||||
profile_url="",
|
||||
consumer_key="",
|
||||
)
|
||||
|
||||
def test_enroll_context(self):
|
||||
"""Test GitHub Enrollment context"""
|
||||
ak_context = GitHubOAuth2Callback().get_user_enroll_context(
|
||||
self.source, UserOAuthSourceConnection(), GITHUB_USER
|
||||
)
|
||||
self.assertEqual(ak_context["username"], GITHUB_USER["login"])
|
||||
self.assertEqual(ak_context["email"], GITHUB_USER["email"])
|
||||
self.assertEqual(ak_context["name"], GITHUB_USER["name"])
|
112
authentik/sources/oauth/tests/test_type_twitter.py
Normal file
112
authentik/sources/oauth/tests/test_type_twitter.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""Twitter Type tests"""
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.types.twitter import TwitterOAuthCallback
|
||||
|
||||
# https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/ \
|
||||
# api-reference/get-account-verify_credentials
|
||||
TWITTER_USER = {
|
||||
"contributors_enabled": True,
|
||||
"created_at": "Sat May 09 17:58:22 +0000 2009",
|
||||
"default_profile": False,
|
||||
"default_profile_image": False,
|
||||
"description": "I taught your phone that thing you like.",
|
||||
"favourites_count": 588,
|
||||
"follow_request_sent": None,
|
||||
"followers_count": 10625,
|
||||
"following": None,
|
||||
"friends_count": 1181,
|
||||
"geo_enabled": True,
|
||||
"id": 38895958,
|
||||
"id_str": "38895958",
|
||||
"is_translator": False,
|
||||
"lang": "en",
|
||||
"listed_count": 190,
|
||||
"location": "San Francisco",
|
||||
"name": "Sean Cook",
|
||||
"notifications": None,
|
||||
"profile_background_color": "1A1B1F",
|
||||
"profile_background_image_url": "",
|
||||
"profile_background_image_url_https": "",
|
||||
"profile_background_tile": True,
|
||||
"profile_image_url": "",
|
||||
"profile_image_url_https": "",
|
||||
"profile_link_color": "2FC2EF",
|
||||
"profile_sidebar_border_color": "181A1E",
|
||||
"profile_sidebar_fill_color": "252429",
|
||||
"profile_text_color": "666666",
|
||||
"profile_use_background_image": True,
|
||||
"protected": False,
|
||||
"screen_name": "theSeanCook",
|
||||
"show_all_inline_media": True,
|
||||
"status": {
|
||||
"contributors": None,
|
||||
"coordinates": {"coordinates": [-122.45037293, 37.76484123], "type": "Point"},
|
||||
"created_at": "Tue Aug 28 05:44:24 +0000 2012",
|
||||
"favorited": False,
|
||||
"geo": {"coordinates": [37.76484123, -122.45037293], "type": "Point"},
|
||||
"id": 240323931419062272,
|
||||
"id_str": "240323931419062272",
|
||||
"in_reply_to_screen_name": "messl",
|
||||
"in_reply_to_status_id": 240316959173009410,
|
||||
"in_reply_to_status_id_str": "240316959173009410",
|
||||
"in_reply_to_user_id": 18707866,
|
||||
"in_reply_to_user_id_str": "18707866",
|
||||
"place": {
|
||||
"attributes": {},
|
||||
"bounding_box": {
|
||||
"coordinates": [
|
||||
[
|
||||
[-122.45778216, 37.75932999],
|
||||
[-122.44248216, 37.75932999],
|
||||
[-122.44248216, 37.76752899],
|
||||
[-122.45778216, 37.76752899],
|
||||
]
|
||||
],
|
||||
"type": "Polygon",
|
||||
},
|
||||
"country": "United States",
|
||||
"country_code": "US",
|
||||
"full_name": "Ashbury Heights, San Francisco",
|
||||
"id": "866269c983527d5a",
|
||||
"name": "Ashbury Heights",
|
||||
"place_type": "neighborhood",
|
||||
"url": "http://api.twitter.com/1/geo/id/866269c983527d5a.json",
|
||||
},
|
||||
"retweet_count": 0,
|
||||
"retweeted": False,
|
||||
"source": "Twitter for iPhone",
|
||||
"text": "@messl congrats! So happy for all 3 of you.",
|
||||
"truncated": False,
|
||||
},
|
||||
"statuses_count": 2609,
|
||||
"time_zone": "Pacific Time (US & Canada)",
|
||||
"url": None,
|
||||
"utc_offset": -28800,
|
||||
"verified": False,
|
||||
}
|
||||
|
||||
|
||||
class TestTypeGitHub(TestCase):
|
||||
"""OAuth Source tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.source = OAuthSource.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
||||
provider_type="openid-connect",
|
||||
authorization_url="",
|
||||
profile_url="",
|
||||
consumer_key="",
|
||||
)
|
||||
|
||||
def test_enroll_context(self):
|
||||
"""Test Twitter Enrollment context"""
|
||||
ak_context = TwitterOAuthCallback().get_user_enroll_context(
|
||||
self.source, UserOAuthSourceConnection(), TWITTER_USER
|
||||
)
|
||||
self.assertEqual(ak_context["username"], TWITTER_USER["screen_name"])
|
||||
self.assertEqual(ak_context["email"], TWITTER_USER.get("email", None))
|
||||
self.assertEqual(ak_context["name"], TWITTER_USER["name"])
|
@ -1,15 +1,14 @@
|
||||
"""OAuth Source tests"""
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.sources.oauth.models import OAuthSource
|
||||
|
||||
|
||||
class OAuthSourceTests(TestCase):
|
||||
class TestOAuthSource(TestCase):
|
||||
"""OAuth Source tests"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.source = OAuthSource.objects.create(
|
||||
name="test",
|
||||
slug="test",
|
@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
class DiscordOAuthRedirect(OAuthRedirect):
|
||||
"""Discord OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
def get_additional_parameters(self, source): # pragma: no cover
|
||||
return {
|
||||
"scope": "email identify",
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
class FacebookOAuthRedirect(OAuthRedirect):
|
||||
"""Facebook OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
def get_additional_parameters(self, source): # pragma: no cover
|
||||
return {
|
||||
"scope": "email",
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
class GoogleOAuthRedirect(OAuthRedirect):
|
||||
"""Google OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
def get_additional_parameters(self, source): # pragma: no cover
|
||||
return {
|
||||
"scope": "email profile",
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
class OpenIDConnectOAuthRedirect(OAuthRedirect):
|
||||
"""OpenIDConnect OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source: OAuthSource):
|
||||
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
|
||||
return {
|
||||
"scope": "openid email profile",
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
|
||||
class RedditOAuthRedirect(OAuthRedirect):
|
||||
"""Reddit OAuth2 Redirect"""
|
||||
|
||||
def get_additional_parameters(self, source):
|
||||
def get_additional_parameters(self, source): # pragma: no cover
|
||||
return {
|
||||
"scope": "identity",
|
||||
"duration": "permanent",
|
||||
|
@ -18,6 +18,6 @@ class TwitterOAuthCallback(OAuthCallback):
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"username": info.get("screen_name"),
|
||||
"email": info.get("email"),
|
||||
"email": info.get("email", None),
|
||||
"name": info.get("name"),
|
||||
}
|
||||
|
@ -1,26 +0,0 @@
|
||||
"""SAML Source tests"""
|
||||
from defusedxml import ElementTree
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
||||
|
||||
|
||||
class TestMetadataProcessor(TestCase):
|
||||
"""Test MetadataProcessor"""
|
||||
|
||||
def setUp(self):
|
||||
self.source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test Metadata generation being valid"""
|
||||
request = self.factory.get("/")
|
||||
xml = MetadataProcessor(self.source, request).build_entity_descriptor()
|
||||
metadata = ElementTree.fromstring(xml)
|
||||
self.assertEqual(metadata.attrib["entityID"], "authentik")
|
0
authentik/sources/saml/tests/__init__.py
Normal file
0
authentik/sources/saml/tests/__init__.py
Normal file
55
authentik/sources/saml/tests/test_metadata.py
Normal file
55
authentik/sources/saml/tests/test_metadata.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""SAML Source tests"""
|
||||
from defusedxml import ElementTree
|
||||
from django.test import RequestFactory, TestCase
|
||||
from lxml import etree # nosec
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.metadata import MetadataProcessor
|
||||
|
||||
|
||||
class TestMetadataProcessor(TestCase):
|
||||
"""Test MetadataProcessor"""
|
||||
|
||||
def setUp(self):
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_metadata_schema(self):
|
||||
"""Test Metadata generation being valid"""
|
||||
source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
request = self.factory.get("/")
|
||||
xml = MetadataProcessor(source, request).build_entity_descriptor()
|
||||
metadata = etree.fromstring(xml) # nosec
|
||||
|
||||
schema = etree.XMLSchema(
|
||||
etree.parse("xml/saml-schema-metadata-2.0.xsd")
|
||||
) # nosec
|
||||
self.assertTrue(schema.validate(metadata))
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test Metadata generation being valid"""
|
||||
source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
request = self.factory.get("/")
|
||||
xml = MetadataProcessor(source, request).build_entity_descriptor()
|
||||
metadata = ElementTree.fromstring(xml)
|
||||
self.assertEqual(metadata.attrib["entityID"], "authentik")
|
||||
|
||||
def test_metadata_without_signautre(self):
|
||||
"""Test Metadata generation being valid"""
|
||||
source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="authentik",
|
||||
# signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
request = self.factory.get("/")
|
||||
xml = MetadataProcessor(source, request).build_entity_descriptor()
|
||||
metadata = ElementTree.fromstring(xml)
|
||||
self.assertEqual(metadata.attrib["entityID"], "authentik")
|
@ -22,10 +22,10 @@
|
||||
</ul>
|
||||
{% if not state %}
|
||||
{% if stage.configure_flow %}
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={{ request.get_full_path }}" class="pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Static Tokens" %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'authentik_stages_otp_static:disable' stage_uuid=stage.stage_uuid %}" class="pf-c-button pf-m-danger">{% trans "Disable Static Tokens" %}</a>
|
||||
<a href="{% url 'authentik_stages_otp_static:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Static Tokens" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,4 +41,4 @@ class DisableView(LoginRequiredMixin, View):
|
||||
Event.new(
|
||||
"static_otp_disable", message="User disabled Static OTP Tokens."
|
||||
).from_http(request)
|
||||
return redirect("authentik_stages_otp:otp-user-settings")
|
||||
return redirect("authentik_core:user-settings")
|
||||
|
@ -18,10 +18,10 @@
|
||||
<p>
|
||||
{% if not state %}
|
||||
{% if stage.configure_flow %}
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={{ request.get_full_path }}" class="pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
|
||||
<a href="{% url 'authentik_flows:configure' stage_uuid=stage.stage_uuid %}?next={% url 'authentik_core:user-settings' %}" class="ak-root-link pf-c-button pf-m-primary">{% trans "Enable Time-based OTP" %}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a href="{% url 'authentik_stages_otp_time:disable' stage_uuid=stage.stage_uuid %}" class="pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
|
||||
<a href="{% url 'authentik_stages_otp_time:disable' stage_uuid=stage.stage_uuid %}" class="ak-root-pf-c-button pf-m-danger">{% trans "Disable Time-based OTP" %}</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -38,4 +38,4 @@ class DisableView(LoginRequiredMixin, View):
|
||||
Event.new("totp_disable", message="User disabled Time-based OTP.").from_http(
|
||||
request
|
||||
)
|
||||
return redirect("authentik_stages_otp:otp-user-settings")
|
||||
return redirect("authentik_core:user-settings")
|
||||
|
@ -9,7 +9,7 @@
|
||||
{% trans 'Reset your password' %}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<a class="pf-c-button pf-m-primary" href="{{ url }}">
|
||||
<a class="pf-c-button pf-m-primary ak-root-link" href="{{ url }}">
|
||||
{% trans 'Change password' %}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -87,6 +87,7 @@ class TestUserWriteStage(TestCase):
|
||||
"username": "test-user-new",
|
||||
"password": new_password,
|
||||
"attribute_some-custom-attribute": "test",
|
||||
"some_ignored_attribute": "bar",
|
||||
}
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
@ -109,6 +110,7 @@ class TestUserWriteStage(TestCase):
|
||||
self.assertTrue(user_qs.exists())
|
||||
self.assertTrue(user_qs.first().check_password(new_password))
|
||||
self.assertEqual(user_qs.first().attributes["some-custom-attribute"], "test")
|
||||
self.assertNotIn("some_ignored_attribute", user_qs.first().attributes)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.to_stage_response",
|
||||
|
@ -19,7 +19,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-rc1}
|
||||
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-stable}
|
||||
command: server
|
||||
environment:
|
||||
AUTHENTIK_REDIS__HOST: redis
|
||||
@ -42,7 +42,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-rc1}
|
||||
image: beryju/authentik:${AUTHENTIK_TAG:-0.13.0-stable}
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
@ -56,7 +56,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: beryju/authentik-static:${AUTHENTIK_TAG:-0.13.0-rc1}
|
||||
image: beryju/authentik-static:${AUTHENTIK_TAG:-0.13.0-stable}
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
|
@ -4,8 +4,8 @@ name: authentik
|
||||
home: https://goauthentik.io
|
||||
sources:
|
||||
- https://github.com/BeryJu/authentik
|
||||
version: "0.13.0-rc1"
|
||||
icon: https://raw.githubusercontent.com/BeryJu/authentik/master/icons/icon.svg
|
||||
version: "0.13.0-stable"
|
||||
icon: https://raw.githubusercontent.com/BeryJu/authentik/master/web/icons/icon.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 9.4.1
|
||||
|
@ -4,7 +4,8 @@
|
||||
|-----------------------------------|-------------------------|-------------|
|
||||
| image.name | beryju/authentik | Image used to run the authentik server and worker |
|
||||
| image.name_static | beryju/authentik-static | Image used to run the authentik static server (CSS and JS Files) |
|
||||
| image.tag | 0.12.5-stable | Image tag |
|
||||
| image.tag | 0.13.0-stable | Image tag |
|
||||
| image.pullPolicy | IfNotPresent | Image Pull Policy used for all deployments |
|
||||
| serverReplicas | 1 | Replicas for the Server deployment |
|
||||
| workerReplicas | 1 | Replicas for the Worker deployment |
|
||||
| kubernetesIntegration | true | Enable/disable the Kubernetes integration for authentik. This will create a service account for authentik to create and update outposts in authentik |
|
||||
|
@ -36,6 +36,10 @@ spec:
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
- path: /media/
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
- path: /robots.txt
|
||||
backend:
|
||||
serviceName: {{ $fullName }}-static
|
||||
|
@ -24,7 +24,7 @@ spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}-static
|
||||
image: "{{ .Values.image.name_static }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
|
@ -45,6 +45,7 @@ spec:
|
||||
initContainers:
|
||||
- name: authentik-database-migrations
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
args: [migrate]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
@ -69,6 +70,7 @@ spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
args: [server]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
|
@ -48,7 +48,7 @@ spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
imagePullPolicy: "{{ .Values.image.pullPolicy }}"
|
||||
args: [worker]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
|
@ -1,5 +1,6 @@
|
||||
image:
|
||||
tag: gh-master
|
||||
pullPolicy: Always
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
@ -5,7 +5,8 @@ image:
|
||||
name: beryju/authentik
|
||||
name_static: beryju/authentik-static
|
||||
name_outposts: beryju/authentik # Prefix used for Outpost deployments, Outpost type and version is appended
|
||||
tag: 0.13.0-rc1
|
||||
tag: 0.13.0-stable
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
@ -1,4 +1,7 @@
|
||||
# flake8: noqa
|
||||
from redis import Redis
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
from lifecycle.migrate import BaseMigration
|
||||
|
||||
SQL_STATEMENT = """BEGIN TRANSACTION;
|
||||
@ -103,3 +106,16 @@ class Migration(BaseMigration):
|
||||
def run(self):
|
||||
self.cur.execute(SQL_STATEMENT)
|
||||
self.con.commit()
|
||||
# We also need to clean the cache to make sure no pickeled objects still exist
|
||||
for db in [
|
||||
CONFIG.y("redis.message_queue_db"),
|
||||
CONFIG.y("redis.cache_db"),
|
||||
CONFIG.y("redis.ws_db"),
|
||||
]:
|
||||
redis = Redis(
|
||||
host=CONFIG.y("redis.host"),
|
||||
port=6379,
|
||||
db=db,
|
||||
password=CONFIG.y("redis.password"),
|
||||
)
|
||||
redis.flushall()
|
10
proxy/go.mod
10
proxy/go.mod
@ -5,12 +5,12 @@ go 1.14
|
||||
require (
|
||||
cloud.google.com/go v0.64.0 // indirect
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/getsentry/sentry-go v0.7.0
|
||||
github.com/go-openapi/errors v0.19.9
|
||||
github.com/getsentry/sentry-go v0.9.0
|
||||
github.com/go-openapi/errors v0.19.9 // indirect
|
||||
github.com/go-openapi/runtime v0.19.24
|
||||
github.com/go-openapi/strfmt v0.19.11
|
||||
github.com/go-openapi/swag v0.19.12
|
||||
github.com/go-openapi/validate v0.19.15
|
||||
github.com/go-openapi/swag v0.19.12 // indirect
|
||||
github.com/go-openapi/validate v0.19.15 // indirect
|
||||
github.com/go-redis/redis/v7 v7.4.0 // indirect
|
||||
github.com/go-swagger/go-swagger v0.25.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
@ -22,7 +22,7 @@ require (
|
||||
github.com/oauth2-proxy/oauth2-proxy v1.1.2-0.20200817154438-5fa5b3186f39
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f // indirect
|
||||
github.com/recws-org/recws v1.2.1
|
||||
github.com/recws-org/recws v1.2.2
|
||||
github.com/sirupsen/logrus v1.7.0
|
||||
github.com/spf13/afero v1.4.1 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
|
24
proxy/go.sum
24
proxy/go.sum
@ -35,13 +35,17 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BeryJu/authentik v0.0.0-20201213234502-f0f32453882b h1:tLc7ERt2fWSu14nXdsER4EP62KUPXwAB0OeLVAA4Rx0=
|
||||
github.com/BeryJu/authentik v0.0.0-20201214075318-41f9097592da h1:XB/MCgb0K+ngEETjBYKGiXcN2des8YtANetYboeT4Lg=
|
||||
github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb h1:ZVN4Iat3runWOFLaBCDVU5a9X/XikSRBosye++6gojw=
|
||||
github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb/go.mod h1:WsAABbY4HQBgd3mGuG4KMNTbHJCPvx9IVBHzysbknss=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
|
||||
github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMdUywE7VMo=
|
||||
github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc=
|
||||
github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI=
|
||||
github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY=
|
||||
@ -139,6 +143,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/getsentry/sentry-go v0.7.0 h1:MR2yfR4vFfv/2+iBuSnkdQwVg7N9cJzihZ6KJu7srwQ=
|
||||
github.com/getsentry/sentry-go v0.7.0/go.mod h1:pLFpD2Y5RHIKF9Bw3KH6/68DeN2K/XBJd8awjdPnUwg=
|
||||
github.com/getsentry/sentry-go v0.9.0 h1:KIfpY/D9hX3gWAEd3d8z6ImuHNWtqEsjlpdF8zXFsHM=
|
||||
github.com/getsentry/sentry-go v0.9.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
|
||||
@ -409,6 +415,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
|
||||
github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI=
|
||||
github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0=
|
||||
github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI=
|
||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@ -438,9 +446,14 @@ github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q
|
||||
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
|
||||
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
|
||||
github.com/kataras/golog v0.0.9/go.mod h1:12HJgwBIZFNGL0EJnMRhmvGA0PQGx8VFwrZtM4CqbAk=
|
||||
github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8=
|
||||
github.com/kataras/iris/v12 v12.0.1/go.mod h1:udK4vLQKkdDqMGJJVd/msuMtN6hpYJhg/lSzuxjhO+U=
|
||||
github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE=
|
||||
github.com/kataras/neffos v0.0.10/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw=
|
||||
github.com/kataras/neffos v0.0.14/go.mod h1:8lqADm8PnbeFfL7CLXh1WHw53dG27MC3pgi2R1rmoTE=
|
||||
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0=
|
||||
github.com/kataras/pio v0.0.2/go.mod h1:hAoW0t9UmXi4R5Oyq5Z4irTbaTsOemSrDGUtaTl7Dro=
|
||||
github.com/kataras/sitemap v0.0.5/go.mod h1:KY2eugMKiPwsJgx7+U103YZehfvNGOXURubcGyk0Bz8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
@ -448,6 +461,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
@ -496,6 +510,7 @@ github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa h1:hI1uC2A3vJFjwvB
|
||||
github.com/mbland/hmacauth v0.0.0-20170912233209-44256dfd4bfa/go.mod h1:8vxFeeg++MqgCHwehSuwTlYCF0ALyDJbYJ1JsKi7v6s=
|
||||
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg=
|
||||
github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
|
||||
github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8=
|
||||
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
@ -519,8 +534,11 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
|
||||
github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM=
|
||||
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
|
||||
github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
|
||||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
@ -577,6 +595,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/recws-org/recws v1.2.1 h1:bYocRkAsS71hlQ9AMCVS+hYXHEgEyQsAbYKXf394gZ8=
|
||||
github.com/recws-org/recws v1.2.1/go.mod h1:SxTgwQU/jqYSzEgUh4ifDxq/7enApS150f8nZ5Sczk8=
|
||||
github.com/recws-org/recws v1.2.2 h1:TkyyCEgMjsr1D2fnutY/DPhGnUKCLpJeXDAGy6rLmGE=
|
||||
github.com/recws-org/recws v1.2.2/go.mod h1:SxTgwQU/jqYSzEgUh4ifDxq/7enApS150f8nZ5Sczk8=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
@ -584,6 +604,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
@ -711,6 +732,7 @@ golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig=
|
||||
@ -1046,6 +1068,7 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.51.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
@ -1070,6 +1093,7 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -1,3 +1,3 @@
|
||||
package pkg
|
||||
|
||||
const VERSION = "0.13.0-rc1"
|
||||
const VERSION = "0.13.0-stable"
|
||||
|
@ -7087,6 +7087,9 @@ definitions:
|
||||
type: boolean
|
||||
url:
|
||||
title: Url
|
||||
description: Can be in the format of 'unix://<path>' when connecting to a
|
||||
local docker daemon, or 'https://<hostname>:2376' when connecting to a remote
|
||||
system.
|
||||
type: string
|
||||
minLength: 1
|
||||
tls_verification:
|
||||
|
@ -142,7 +142,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
|
||||
# Wait until we've logged in
|
||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||
self.driver.get(self.url("authentik_core:user-settings"))
|
||||
self.driver.get(self.url("authentik_core:user-details"))
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
@ -224,7 +224,7 @@ class TestSourceOAuth2(SeleniumTestCase):
|
||||
|
||||
# Wait until we've logged in
|
||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||
self.driver.get(self.url("authentik_core:user-settings"))
|
||||
self.driver.get(self.url("authentik_core:user-details"))
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||
@ -317,7 +317,7 @@ class TestSourceOAuth1(SeleniumTestCase):
|
||||
sleep(2)
|
||||
# Wait until we've logged in
|
||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||
self.driver.get(self.url("authentik_core:user-settings"))
|
||||
self.driver.get(self.url("authentik_core:user-details"))
|
||||
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"),
|
||||
|
@ -134,7 +134,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||
self.driver.get(self.url("authentik_core:user-settings"))
|
||||
self.driver.get(self.url("authentik_core:user-details"))
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.assertNotEqual(
|
||||
@ -185,7 +185,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||
self.driver.get(self.url("authentik_core:user-settings"))
|
||||
self.driver.get(self.url("authentik_core:user-details"))
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.assertNotEqual(
|
||||
@ -234,7 +234,7 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait_for_url(self.shell_url("authentik_core:overview"))
|
||||
self.driver.get(self.url("authentik_core:user-settings"))
|
||||
self.driver.get(self.url("authentik_core:user-details"))
|
||||
|
||||
# Wait until we've loaded the user info page
|
||||
self.assertNotEqual(
|
||||
|
95
tests/integration/test_outpost_docker.py
Normal file
95
tests/integration/test_outpost_docker.py
Normal file
@ -0,0 +1,95 @@
|
||||
"""outpost tests"""
|
||||
from shutil import rmtree
|
||||
from tempfile import mkdtemp
|
||||
from time import sleep
|
||||
|
||||
from django.test import TestCase
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types.healthcheck import Healthcheck
|
||||
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.outposts.apps import AuthentikOutpostConfig
|
||||
from authentik.outposts.controllers.docker import DockerController
|
||||
from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
|
||||
|
||||
class OutpostDockerTests(TestCase):
|
||||
"""Test Docker Controllers"""
|
||||
|
||||
def _start_container(self, ssl_folder: str) -> Container:
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="docker.beryju.org/proxy/library/docker:dind",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
remove=True,
|
||||
privileged=True,
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "docker", "info"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=5 * 100 * 1000000,
|
||||
),
|
||||
environment={"DOCKER_TLS_CERTDIR": "/ssl"},
|
||||
volumes={
|
||||
f"{ssl_folder}/": {
|
||||
"bind": "/ssl",
|
||||
}
|
||||
},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.ssl_folder = mkdtemp()
|
||||
self.container = self._start_container(self.ssl_folder)
|
||||
# Ensure that local connection have been created
|
||||
AuthentikOutpostConfig.init_local_connection()
|
||||
self.provider: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="test",
|
||||
internal_host="http://localhost",
|
||||
external_host="http://localhost",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
)
|
||||
authentication_kp = CertificateKeyPair.objects.create(
|
||||
name="docker-authentication",
|
||||
certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(),
|
||||
key_data=open(f"{self.ssl_folder}/client/key.pem").read(),
|
||||
)
|
||||
verification_kp = CertificateKeyPair.objects.create(
|
||||
name="docker-verification",
|
||||
certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(),
|
||||
)
|
||||
self.service_connection = DockerServiceConnection.objects.create(
|
||||
url="https://localhost:2376",
|
||||
tls_verification=verification_kp,
|
||||
tls_authentication=authentication_kp,
|
||||
)
|
||||
self.outpost: Outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
type=OutpostType.PROXY,
|
||||
service_connection=self.service_connection,
|
||||
)
|
||||
self.outpost.providers.add(self.provider)
|
||||
self.outpost.save()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
self.container.kill()
|
||||
try:
|
||||
rmtree(self.ssl_folder)
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
def test_docker_controller(self):
|
||||
"""test that deployment requires update"""
|
||||
controller = DockerController(self.outpost, self.service_connection)
|
||||
controller.up()
|
||||
controller.down()
|
@ -8,3 +8,4 @@ FROM nginx
|
||||
|
||||
COPY --from=npm-builder /static/robots.txt /usr/share/nginx/html/robots.txt
|
||||
COPY --from=npm-builder /static/dist/ /usr/share/nginx/html/static/dist/
|
||||
COPY --from=npm-builder /static/authentik/ /usr/share/nginx/html/static/authentik/
|
||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user