Compare commits
170 Commits
version/20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
39ad9d7c9d | |||
20d09c14b2 | |||
3a4d514bae | |||
4932846e14 | |||
bb62aa7c7f | |||
907b837301 | |||
b60a3d45dc | |||
3f5585ca84 | |||
ba9a4efc9b | |||
902378af53 | |||
2352a7f4d6 | |||
d89266a9d2 | |||
d678d33756 | |||
49d0ccd9c7 | |||
ea082ed9ef | |||
d62fc9766c | |||
983747b13b | |||
de4710ea71 | |||
d55b31dd82 | |||
d87871f806 | |||
148194e12b | |||
a2c587be43 | |||
673da2a96e | |||
a9a7b26264 | |||
83d2c442a5 | |||
4029e19b72 | |||
538a466090 | |||
322a343c81 | |||
6ddd6bfa72 | |||
36de302250 | |||
9eb13c50e9 | |||
cffc6a1b88 | |||
ba437beacc | |||
da32b05eba | |||
45b7e7565d | |||
a0b63f50bf | |||
dc5d571c99 | |||
05161db458 | |||
311ffa9f79 | |||
7cbe33d65d | |||
be9ca48de0 | |||
b3159a74e5 | |||
89fafff0af | |||
ae77c872a0 | |||
5f13563e03 | |||
e17c9040bb | |||
280ef3d265 | |||
a5bb583268 | |||
212ff11b6d | |||
1fa9d70945 | |||
eeeaa9317b | |||
09b932100f | |||
aa701c5725 | |||
6f98833150 | |||
30aa24ce6e | |||
a426a1a0b6 | |||
061c549a40 | |||
efa09d5e1d | |||
4fe0bd4b6c | |||
7c2decf5ec | |||
7f39399c32 | |||
7fd78a591d | |||
bdb84b7a8f | |||
84e9748340 | |||
7dfc621ae4 | |||
cd0a6f2d7c | |||
b7835a751b | |||
fd197ceee7 | |||
be5c8341d2 | |||
2036827f04 | |||
35665d248e | |||
bc30b41157 | |||
2af7fab42c | |||
4de205809b | |||
e8433472fd | |||
3896299312 | |||
5cfbb0993a | |||
a62e3557ac | |||
626936636a | |||
85ec713213 | |||
406bbdcfc9 | |||
02f87032cc | |||
b7a929d304 | |||
3c0cc27ea1 | |||
ec254d5927 | |||
92ba77e9e5 | |||
7ddb459030 | |||
076e89b600 | |||
ba5fa2a04f | |||
90fe1c2ce8 | |||
85f88e785f | |||
a7c4f81275 | |||
396fbc4a76 | |||
2dcd0128aa | |||
e5aa9e0774 | |||
53d78d561b | |||
93001d1329 | |||
40428f5a82 | |||
007838fcf2 | |||
5e03b27348 | |||
7c51afa36c | |||
38fd5c5614 | |||
7e3148fab5 | |||
948db46406 | |||
cccddd8c69 | |||
adc4cd9c0d | |||
abed254ca1 | |||
edfab0995f | |||
528dedf99d | |||
5d7eec3049 | |||
ad44567ebe | |||
ac82002339 | |||
df92111296 | |||
da8417a141 | |||
7f32355e3e | |||
5afe88a605 | |||
320dab3425 | |||
ca44f8bd60 | |||
5fd408ca82 | |||
becb9e34b5 | |||
4917ab9985 | |||
bd92505bc2 | |||
30033d1f90 | |||
3e5dfcbd0f | |||
bf0141acc6 | |||
0c8d513567 | |||
d07704fdf1 | |||
086a8753c0 | |||
ae7a6e2fd6 | |||
6a4ddcaba7 | |||
2c9b596f01 | |||
7257108091 | |||
91f7b289cc | |||
77a507d2f8 | |||
3e60e956f4 | |||
84ec70c2a2 | |||
72846f0ae1 | |||
dd53e7e9b1 | |||
9df16a9ae0 | |||
3dc9e247d5 | |||
02dd44eeec | |||
2f78e14381 | |||
ef6f692526 | |||
2dd575874b | |||
84c2ebabaa | |||
3e26170f4b | |||
4709dca33c | |||
6064a481fb | |||
3979b0bde7 | |||
4280847bcc | |||
ade8644da6 | |||
3c3fd53999 | |||
7b823f23ae | |||
a67bea95d4 | |||
775e0ef2fa | |||
d102c59654 | |||
03448a9169 | |||
1e6c081e5c | |||
8b9ce4a745 | |||
2a0bd50e23 | |||
014d93d485 | |||
ff42663d3c | |||
ce49d7ea5b | |||
8429dd19b2 | |||
1554dc9feb | |||
1005f341e4 | |||
b98895ac2c | |||
6dc38b0132 | |||
e154e28611 | |||
690b7be1d8 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.6.3
|
||||
current_version = 2021.7.1-rc1
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
@ -21,14 +21,14 @@ values =
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
[bumpversion:file:schema.yml]
|
||||
|
||||
[bumpversion:file:.github/workflows/release.yml]
|
||||
|
||||
[bumpversion:file:authentik/__init__.py]
|
||||
|
||||
[bumpversion:file:internal/constants/constants.go]
|
||||
|
||||
[bumpversion:file:outpost/pkg/version.go]
|
||||
|
||||
[bumpversion:file:web/src/constants.ts]
|
||||
|
||||
[bumpversion:file:website/docs/outposts/manual-deploy-docker-compose.md]
|
||||
|
@ -3,3 +3,6 @@ static
|
||||
htmlcov
|
||||
*.env.yml
|
||||
**/node_modules
|
||||
dist/**
|
||||
build/**
|
||||
build_docs/**
|
||||
|
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@ -9,7 +9,7 @@ updates:
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: gomod
|
||||
directory: "/outpost"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
@ -48,11 +48,3 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
- package-ecosystem: docker
|
||||
directory: "/outpost"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
||||
assignees:
|
||||
- BeryJu
|
||||
|
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@ -33,14 +33,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.6.3,
|
||||
beryju/authentik:2021.7.1-rc1,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.6.3,
|
||||
ghcr.io/goauthentik/server:2021.7.1-rc1,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.3', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.1-rc1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
@ -75,14 +75,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-proxy:2021.6.3,
|
||||
beryju/authentik-proxy:2021.7.1-rc1,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.6.3,
|
||||
ghcr.io/goauthentik/proxy:2021.7.1-rc1,
|
||||
ghcr.io/goauthentik/proxy:latest
|
||||
file: outpost/proxy.Dockerfile
|
||||
file: proxy.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.3', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.1-rc1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-proxy:latest
|
||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||
@ -117,14 +117,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-ldap:2021.6.3,
|
||||
beryju/authentik-ldap:2021.7.1-rc1,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.6.3,
|
||||
ghcr.io/goauthentik/ldap:2021.7.1-rc1,
|
||||
ghcr.io/goauthentik/ldap:latest
|
||||
file: outpost/ldap.Dockerfile
|
||||
file: ldap.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.6.3', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.1-rc1', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
@ -157,7 +157,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v2.1.5
|
||||
uses: actions/setup-node@v2.3.0
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Build web api client and web ui
|
||||
@ -176,6 +176,6 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.6.3
|
||||
version: authentik@2021.7.1-rc1
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -200,3 +200,4 @@ media/
|
||||
*mmdb
|
||||
|
||||
.idea/
|
||||
api/
|
||||
|
50
Dockerfile
50
Dockerfile
@ -10,8 +10,16 @@ RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -r --dev-only > requirements-dev.txt
|
||||
|
||||
# Stage 2: Build web API
|
||||
FROM openapitools/openapi-generator-cli as api-builder
|
||||
# Stage 2: Build website
|
||||
FROM node as website-builder
|
||||
|
||||
COPY ./website /static/
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build-docs-only
|
||||
|
||||
# Stage 3: Build web API
|
||||
FROM openapitools/openapi-generator-cli as web-api-builder
|
||||
|
||||
COPY ./schema.yml /local/schema.yml
|
||||
|
||||
@ -21,34 +29,52 @@ RUN docker-entrypoint.sh generate \
|
||||
-o /local/web/api \
|
||||
--additional-properties=typescriptThreePlus=true,supportsES6=true,npmName=authentik-api,npmVersion=1.0.0
|
||||
|
||||
# Stage 3: Build webui
|
||||
FROM node as npm-builder
|
||||
# Stage 3: Generate API Client
|
||||
FROM openapitools/openapi-generator-cli as go-api-builder
|
||||
|
||||
COPY ./schema.yml /local/schema.yml
|
||||
|
||||
RUN docker-entrypoint.sh generate \
|
||||
--git-host goauthentik.io \
|
||||
--git-repo-id outpost \
|
||||
--git-user-id api \
|
||||
-i /local/schema.yml \
|
||||
-g go \
|
||||
-o /local/api \
|
||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true && \
|
||||
rm -f /local/api/go.mod /local/api/go.sum
|
||||
|
||||
# Stage 4: Build webui
|
||||
FROM node as web-builder
|
||||
|
||||
COPY ./web /static/
|
||||
COPY --from=api-builder /local/web/api /static/api
|
||||
COPY --from=web-api-builder /local/web/api /static/api
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build
|
||||
|
||||
# Stage 4: Build go proxy
|
||||
FROM golang:1.16.5 AS builder
|
||||
# Stage 5: Build go proxy
|
||||
FROM golang:1.16.6 AS builder
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
COPY --from=npm-builder /static/robots.txt /work/web/robots.txt
|
||||
COPY --from=npm-builder /static/security.txt /work/web/security.txt
|
||||
COPY --from=npm-builder /static/dist/ /work/web/dist/
|
||||
COPY --from=npm-builder /static/authentik/ /work/web/authentik/
|
||||
COPY --from=web-builder /static/robots.txt /work/web/robots.txt
|
||||
COPY --from=web-builder /static/security.txt /work/web/security.txt
|
||||
COPY --from=web-builder /static/dist/ /work/web/dist/
|
||||
COPY --from=web-builder /static/authentik/ /work/web/authentik/
|
||||
COPY --from=website-builder /static/help/ /work/website/help/
|
||||
|
||||
COPY --from=go-api-builder /local/api api
|
||||
COPY ./cmd /work/cmd
|
||||
COPY ./web/static.go /work/web/static.go
|
||||
COPY ./website/static.go /work/website/static.go
|
||||
COPY ./internal /work/internal
|
||||
COPY ./go.mod /work/go.mod
|
||||
COPY ./go.sum /work/go.sum
|
||||
|
||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||
|
||||
# Stage 5: Run
|
||||
# Stage 6: Run
|
||||
FROM python:3.9-slim-buster
|
||||
|
||||
WORKDIR /
|
||||
|
9
Makefile
9
Makefile
@ -32,7 +32,7 @@ gen-build:
|
||||
|
||||
gen-clean:
|
||||
rm -rf web/api/src/
|
||||
rm -rf outpost/api/
|
||||
rm -rf api/
|
||||
|
||||
gen-web:
|
||||
docker run \
|
||||
@ -55,11 +55,14 @@ gen-outpost:
|
||||
--git-user-id api \
|
||||
-i /local/schema.yml \
|
||||
-g go \
|
||||
-o /local/outpost/api \
|
||||
-o /local/api \
|
||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true
|
||||
rm -f outpost/api/go.mod outpost/api/go.sum
|
||||
rm -f api/go.mod api/go.sum
|
||||
|
||||
gen: gen-build gen-clean gen-web gen-outpost
|
||||
|
||||
migrate:
|
||||
python -m lifecycle.migrate
|
||||
|
||||
run:
|
||||
go run -v cmd/server/main.go
|
||||
|
1
Pipfile
1
Pipfile
@ -47,6 +47,7 @@ xmlsec = "*"
|
||||
duo-client = "*"
|
||||
ua-parser = "*"
|
||||
deepmerge = "*"
|
||||
colorama = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.9"
|
||||
|
371
Pipfile.lock
generated
371
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "f90d9fb4713eaf9c5ffe6a3858e64843670f79ab5007e7debf914c1f094c8d63"
|
||||
"sha256": "e4f2e57bd5c709809515ab2b95eb3f5fa337d4a9334f4110a24bf28c3f9d5f8f"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -76,11 +76,11 @@
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:05914d0fa65a21711e732adc6572edad6c8da5f1435c3f0c060689ced5e85195",
|
||||
"sha256:d36fa91dd90e3aa3c81a6bd426ccc8fb20bd3d22b0cf14a12800289e9c3e2563"
|
||||
"sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9",
|
||||
"sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.4.0"
|
||||
"version": "==3.4.1"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
@ -122,19 +122,19 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:6300e9ee9a404038113250bd218e2c4827f5e676efb14e77de2ad2dcb67679bc",
|
||||
"sha256:be4714f0475c1f5183eea09ddbf568ced6fa41b0fc9976f2698b8442e1b17303"
|
||||
"sha256:13e60f88d13161df951d6e52bd483cdbe1a36a31f818746289d8ba0879465710",
|
||||
"sha256:3be2f259b279d69495433e3288db3670817fdb1813cfde92abf867bba3ad8148"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.17.102"
|
||||
"version": "==1.18.3"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:2f57f7ceed1598d96cc497aeb45317db5d3b21a5aafea4732d0e561d0fc2a8fa",
|
||||
"sha256:bdf08a4f7f01ead00d386848f089c08270499711447569c18d0db60023619c06"
|
||||
"sha256:0b6f378c9efbc72eee61aba1e16cab90bde53a37bd2d861f6435552fd7030adf",
|
||||
"sha256:285ab9459cdd49d4a9322692c6e13772b97af723a03c0eed519b589446491a5b"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.20.102"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.21.3"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -180,73 +180,69 @@
|
||||
},
|
||||
"cffi": {
|
||||
"hashes": [
|
||||
"sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813",
|
||||
"sha256:04c468b622ed31d408fea2346bec5bbffba2cc44226302a0de1ade9f5ea3d373",
|
||||
"sha256:06d7cd1abac2ffd92e65c0609661866709b4b2d82dd15f611e602b9b188b0b69",
|
||||
"sha256:06db6321b7a68b2bd6df96d08a5adadc1fa0e8f419226e25b2a5fbf6ccc7350f",
|
||||
"sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06",
|
||||
"sha256:0f861a89e0043afec2a51fd177a567005847973be86f709bbb044d7f42fc4e05",
|
||||
"sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea",
|
||||
"sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee",
|
||||
"sha256:1bf1ac1984eaa7675ca8d5745a8cb87ef7abecb5592178406e55858d411eadc0",
|
||||
"sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396",
|
||||
"sha256:24a570cd11895b60829e941f2613a4f79df1a27344cbbb82164ef2e0116f09c7",
|
||||
"sha256:24ec4ff2c5c0c8f9c6b87d5bb53555bf267e1e6f70e52e5a9740d32861d36b6f",
|
||||
"sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73",
|
||||
"sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315",
|
||||
"sha256:293e7ea41280cb28c6fcaaa0b1aa1f533b8ce060b9e701d78511e1e6c4a1de76",
|
||||
"sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1",
|
||||
"sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49",
|
||||
"sha256:3c3f39fa737542161d8b0d680df2ec249334cd70a8f420f71c9304bd83c3cbed",
|
||||
"sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892",
|
||||
"sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482",
|
||||
"sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058",
|
||||
"sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5",
|
||||
"sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53",
|
||||
"sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045",
|
||||
"sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3",
|
||||
"sha256:681d07b0d1e3c462dd15585ef5e33cb021321588bebd910124ef4f4fb71aef55",
|
||||
"sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5",
|
||||
"sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e",
|
||||
"sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c",
|
||||
"sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369",
|
||||
"sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827",
|
||||
"sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053",
|
||||
"sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa",
|
||||
"sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4",
|
||||
"sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322",
|
||||
"sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132",
|
||||
"sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62",
|
||||
"sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa",
|
||||
"sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0",
|
||||
"sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396",
|
||||
"sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e",
|
||||
"sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991",
|
||||
"sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6",
|
||||
"sha256:cc5a8e069b9ebfa22e26d0e6b97d6f9781302fe7f4f2b8776c3e1daea35f1adc",
|
||||
"sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1",
|
||||
"sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406",
|
||||
"sha256:df5052c5d867c1ea0b311fb7c3cd28b19df469c056f7fdcfe88c7473aa63e333",
|
||||
"sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d",
|
||||
"sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"
|
||||
"sha256:06c54a68935738d206570b20da5ef2b6b6d92b38ef3ec45c5422c0ebaf338d4d",
|
||||
"sha256:0c0591bee64e438883b0c92a7bed78f6290d40bf02e54c5bf0978eaf36061771",
|
||||
"sha256:19ca0dbdeda3b2615421d54bef8985f72af6e0c47082a8d26122adac81a95872",
|
||||
"sha256:22b9c3c320171c108e903d61a3723b51e37aaa8c81255b5e7ce102775bd01e2c",
|
||||
"sha256:26bb2549b72708c833f5abe62b756176022a7b9a7f689b571e74c8478ead51dc",
|
||||
"sha256:33791e8a2dc2953f28b8d8d300dde42dd929ac28f974c4b4c6272cb2955cb762",
|
||||
"sha256:3c8d896becff2fa653dc4438b54a5a25a971d1f4110b32bd3068db3722c80202",
|
||||
"sha256:4373612d59c404baeb7cbd788a18b2b2a8331abcc84c3ba40051fcd18b17a4d5",
|
||||
"sha256:487d63e1454627c8e47dd230025780e91869cfba4c753a74fda196a1f6ad6548",
|
||||
"sha256:48916e459c54c4a70e52745639f1db524542140433599e13911b2f329834276a",
|
||||
"sha256:4922cd707b25e623b902c86188aca466d3620892db76c0bdd7b99a3d5e61d35f",
|
||||
"sha256:55af55e32ae468e9946f741a5d51f9896da6b9bf0bbdd326843fec05c730eb20",
|
||||
"sha256:57e555a9feb4a8460415f1aac331a2dc833b1115284f7ded7278b54afc5bd218",
|
||||
"sha256:5d4b68e216fc65e9fe4f524c177b54964af043dde734807586cf5435af84045c",
|
||||
"sha256:64fda793737bc4037521d4899be780534b9aea552eb673b9833b01f945904c2e",
|
||||
"sha256:6d6169cb3c6c2ad50db5b868db6491a790300ade1ed5d1da29289d73bbe40b56",
|
||||
"sha256:7bcac9a2b4fdbed2c16fa5681356d7121ecabf041f18d97ed5b8e0dd38a80224",
|
||||
"sha256:80b06212075346b5546b0417b9f2bf467fea3bfe7352f781ffc05a8ab24ba14a",
|
||||
"sha256:818014c754cd3dba7229c0f5884396264d51ffb87ec86e927ef0be140bfdb0d2",
|
||||
"sha256:8eb687582ed7cd8c4bdbff3df6c0da443eb89c3c72e6e5dcdd9c81729712791a",
|
||||
"sha256:99f27fefe34c37ba9875f224a8f36e31d744d8083e00f520f133cab79ad5e819",
|
||||
"sha256:9f3e33c28cd39d1b655ed1ba7247133b6f7fc16fa16887b120c0c670e35ce346",
|
||||
"sha256:a8661b2ce9694ca01c529bfa204dbb144b275a31685a075ce123f12331be790b",
|
||||
"sha256:a9da7010cec5a12193d1af9872a00888f396aba3dc79186604a09ea3ee7c029e",
|
||||
"sha256:aedb15f0a5a5949ecb129a82b72b19df97bbbca024081ed2ef88bd5c0a610534",
|
||||
"sha256:b315d709717a99f4b27b59b021e6207c64620790ca3e0bde636a6c7f14618abb",
|
||||
"sha256:ba6f2b3f452e150945d58f4badd92310449876c4c954836cfb1803bdd7b422f0",
|
||||
"sha256:c33d18eb6e6bc36f09d793c0dc58b0211fccc6ae5149b808da4a62660678b156",
|
||||
"sha256:c9a875ce9d7fe32887784274dd533c57909b7b1dcadcc128a2ac21331a9765dd",
|
||||
"sha256:c9e005e9bd57bc987764c32a1bee4364c44fdc11a3cc20a40b93b444984f2b87",
|
||||
"sha256:d2ad4d668a5c0645d281dcd17aff2be3212bc109b33814bbb15c4939f44181cc",
|
||||
"sha256:d950695ae4381ecd856bcaf2b1e866720e4ab9a1498cba61c602e56630ca7195",
|
||||
"sha256:e22dcb48709fc51a7b58a927391b23ab37eb3737a98ac4338e2448bef8559b33",
|
||||
"sha256:e8c6a99be100371dbb046880e7a282152aa5d6127ae01783e37662ef73850d8f",
|
||||
"sha256:e9dc245e3ac69c92ee4c167fbdd7428ec1956d4e754223124991ef29eb57a09d",
|
||||
"sha256:eb687a11f0a7a1839719edd80f41e459cc5366857ecbed383ff376c4e3cc6afd",
|
||||
"sha256:eb9e2a346c5238a30a746893f23a9535e700f8192a68c07c0258e7ece6ff3728",
|
||||
"sha256:ed38b924ce794e505647f7c331b22a693bee1538fdf46b0222c4717b42f744e7",
|
||||
"sha256:f0010c6f9d1a4011e429109fda55a225921e3206e7f62a0c22a35344bfd13cca",
|
||||
"sha256:f0c5d1acbfca6ebdd6b1e3eded8d261affb6ddcf2186205518f1428b8569bb99",
|
||||
"sha256:f10afb1004f102c7868ebfe91c28f4a712227fe4cb24974350ace1f90e1febbf",
|
||||
"sha256:f174135f5609428cc6e1b9090f9268f5c8935fddb1b25ccb8255a2d50de6789e",
|
||||
"sha256:f3ebe6e73c319340830a9b2825d32eb6d8475c1dac020b4f0aa774ee3b898d1c",
|
||||
"sha256:f627688813d0a4140153ff532537fbe4afea5a3dffce1f9deb7f91f848a832b5",
|
||||
"sha256:fd4305f86f53dfd8cd3522269ed7fc34856a8ee3709a5e28b2836b2db9d4cd69"
|
||||
],
|
||||
"version": "==1.14.5"
|
||||
"version": "==1.14.6"
|
||||
},
|
||||
"channels": {
|
||||
"hashes": [
|
||||
"sha256:056b72e51080a517a0f33a0a30003e03833b551d75394d6636c885d4edb8188f",
|
||||
"sha256:3f15bdd2138bb4796e76ea588a0a344b12a7964ea9b2e456f992fddb988a4317"
|
||||
"sha256:0ff0422b4224d10efac76e451575517f155fe7c97d369b5973b116f22eeaf86c",
|
||||
"sha256:fdd9a94987a23d8d7ebd97498ed8b8cc83163f37e53fc6c85098aba7a3bb8b75"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.3"
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"channels-redis": {
|
||||
"hashes": [
|
||||
"sha256:18d63f6462a58011740dc8eeb57ea4b31ec220eb551cb71b27de9c6779a549de",
|
||||
"sha256:2fb31a63b05373f6402da2e6a91a22b9e66eb8b56626c6bfc93e156c734c5ae6"
|
||||
"sha256:0a18ce279c15ba79b7985bb12b2d6dd0ac8a14e4ad6952681f4422a4cc4a5ea9",
|
||||
"sha256:1abd5820ff1ed4ac627f8a219ad389e4c87e52e47a230929a7a474e95dd2c6c2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.0"
|
||||
"version": "==3.3.0"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@ -256,6 +252,14 @@
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.0.0"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1",
|
||||
"sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"
|
||||
],
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.3"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
@ -284,6 +288,14 @@
|
||||
],
|
||||
"version": "==0.2.0"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
|
||||
"sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.4"
|
||||
},
|
||||
"constantly": {
|
||||
"hashes": [
|
||||
"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",
|
||||
@ -342,11 +354,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:66c9d8db8cc6fe938a28b7887c1596e42d522e27618562517cc8929eb7e7f296",
|
||||
"sha256:ea735cbbbb3b2fba6d4da4784a0043d84c67c92f1fdf15ad6db69900e792c10f"
|
||||
"sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd",
|
||||
"sha256:c58b5f19c5ae0afe6d75cbdd7df561e6eb929339985dbbda2565e1cabb19a62e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.4"
|
||||
"version": "==3.2.5"
|
||||
},
|
||||
"django-dbbackup": {
|
||||
"git": "https://github.com/django-dbbackup/django-dbbackup.git",
|
||||
@ -473,11 +485,11 @@
|
||||
},
|
||||
"google-auth": {
|
||||
"hashes": [
|
||||
"sha256:b3a67fa9ba5b768861dacf374c2135eb09fa14a0e40c851c3b8ea7abe6fc8fef",
|
||||
"sha256:e34e5f5de5610b202f9b40ebd9f8b27571d5c5537db9afed3a72b2db5a345039"
|
||||
"sha256:036dd68c1e8baa422b6b61619b8e02793da2e20f55e69514612de6c080468755",
|
||||
"sha256:7665c04f2df13cc938dc7d9066cddb1f8af62b038bc8b2306848c1b23121865f"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==1.32.0"
|
||||
"version": "==1.33.1"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
@ -571,10 +583,10 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
|
||||
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||
],
|
||||
"version": "==2.10"
|
||||
"version": "==3.2"
|
||||
},
|
||||
"incremental": {
|
||||
"hashes": [
|
||||
@ -624,14 +636,14 @@
|
||||
},
|
||||
"ldap3": {
|
||||
"hashes": [
|
||||
"sha256:18c3ee656a6775b9b0d60f7c6c5b094d878d1d90fc03d56731039f0a4b546a91",
|
||||
"sha256:4139c91f0eef9782df7b77c8cbc6243086affcb6a8a249b768a9658438e5da59",
|
||||
"sha256:8c949edbad2be8a03e719ba48bd6779f327ec156929562814b3e84ab56889c8c",
|
||||
"sha256:afc6fc0d01f02af82cd7bfabd3bbfd5dc96a6ae91e97db0a2dab8a0f1b436056",
|
||||
"sha256:c1df41d89459be6f304e0ceec4b00fdea533dbbcd83c802b1272dcdb94620b57"
|
||||
"sha256:2bc966556fc4d4fa9f445a1c31dc484ee81d44a51ab0e2d0fd05b62cac75daa6",
|
||||
"sha256:5630d1383e09ba94839e253e013f1aa1a2cf7a547628ba1265cb7b9a844b5687",
|
||||
"sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70",
|
||||
"sha256:5ab7febc00689181375de40c396dcad4f2659cd260fc5e94c508b6d77c17e9d5",
|
||||
"sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.9"
|
||||
"version": "==2.9.1"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
@ -778,11 +790,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.9"
|
||||
"version": "==21.0"
|
||||
},
|
||||
"prometheus-client": {
|
||||
"hashes": [
|
||||
@ -975,11 +987,11 @@
|
||||
},
|
||||
"python-dateutil": {
|
||||
"hashes": [
|
||||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
|
||||
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.1"
|
||||
"version": "==2.8.2"
|
||||
},
|
||||
"python-dotenv": {
|
||||
"hashes": [
|
||||
@ -1040,11 +1052,11 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
|
||||
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.1"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==2.26.0"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
@ -1065,18 +1077,19 @@
|
||||
},
|
||||
"s3transfer": {
|
||||
"hashes": [
|
||||
"sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc",
|
||||
"sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2"
|
||||
"sha256:50ed823e1dc5868ad40c8dc92072f757aa0e653a192845c94a3b676f4a62da4c",
|
||||
"sha256:9c1dc369814391a6bda20ebbf4b70a0f34630592c9aa520856bf384916af2803"
|
||||
],
|
||||
"version": "==0.4.2"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:c1227d38dca315ba35182373f129c3e2722e8ed999e52584e6aca7d287870739",
|
||||
"sha256:c7d380a21281e15be3d9f67a3c4fbb4f800c481d88ff8d8931f39486dd7b4ada"
|
||||
"sha256:5210a712dd57d88d225c1fc3fe3a3626fee493637bcd54e204826cf04b8d769c",
|
||||
"sha256:6864dcb6f7dec692635e5518c2a5c80010adf673c70340817f1a1b713d65bb41"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.1.0"
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"service-identity": {
|
||||
"hashes": [
|
||||
@ -1206,18 +1219,18 @@
|
||||
},
|
||||
"uvloop": {
|
||||
"hashes": [
|
||||
"sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc",
|
||||
"sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69",
|
||||
"sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01",
|
||||
"sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d",
|
||||
"sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760",
|
||||
"sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c",
|
||||
"sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47",
|
||||
"sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c",
|
||||
"sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c",
|
||||
"sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7"
|
||||
"sha256:0de811931e90ae2da9e19ce70ffad73047ab0c1dba7c6e74f9ae1a3aabeb89bd",
|
||||
"sha256:1ff05116ede1ebdd81802df339e5b1d4cab1dfbd99295bf27e90b4cec64d70e9",
|
||||
"sha256:2d8ffe44ae709f839c54bacf14ed283f41bee90430c3b398e521e10f8d117b3a",
|
||||
"sha256:5cda65fc60a645470b8525ce014516b120b7057b576fa876cdfdd5e60ab1efbb",
|
||||
"sha256:63a3288abbc9c8ee979d7e34c34e780b2fbab3e7e53d00b6c80271119f277399",
|
||||
"sha256:7522df4e45e4f25b50adbbbeb5bb9847495c438a628177099d2721f2751ff825",
|
||||
"sha256:7f4b8a905df909a407c5791fb582f6c03b0d3b491ecdc1cdceaefbc9bf9e08f6",
|
||||
"sha256:905f0adb0c09c9f44222ee02f6b96fd88b493478fffb7a345287f9444e926030",
|
||||
"sha256:ae2b325c0f6d748027f7463077e457006b4fdb35a8788f01754aadba825285ee",
|
||||
"sha256:e71fb9038bfcd7646ca126c5ef19b17e48d4af9e838b2bcfda7a9f55a6552a32"
|
||||
],
|
||||
"version": "==0.15.2"
|
||||
"version": "==0.15.3"
|
||||
},
|
||||
"vine": {
|
||||
"hashes": [
|
||||
@ -1423,11 +1436,11 @@
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:4db03ab5fc3340cf619dbc25e42c2cc3755154ce6009469766d7143d1fc2ee4e",
|
||||
"sha256:8a398dfce302c13f14bab13e2b14fe385d32b73f4e4853b9bdfb64598baa1975"
|
||||
"sha256:6021561b2e87ed6b3c93c2682ac50079c65ab08f1e4e0277ba38f97e0e492185",
|
||||
"sha256:a670dd7af3fe603f51aa7117462588b7c3bdcd58007edfaee752bf82eceecd28"
|
||||
],
|
||||
"markers": "python_version ~= '3.6'",
|
||||
"version": "==2.5.6"
|
||||
"version": "==2.6.4"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@ -1468,13 +1481,13 @@
|
||||
],
|
||||
"version": "==2021.5.30"
|
||||
},
|
||||
"chardet": {
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
"sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1",
|
||||
"sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==4.0.0"
|
||||
"markers": "python_version >= '3'",
|
||||
"version": "==2.0.3"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
@ -1568,10 +1581,10 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
"sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a",
|
||||
"sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"
|
||||
],
|
||||
"version": "==2.10"
|
||||
"version": "==3.2"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
@ -1582,11 +1595,11 @@
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56",
|
||||
"sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"
|
||||
"sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813",
|
||||
"sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"
|
||||
],
|
||||
"markers": "python_version < '4' and python_full_version >= '3.6.1'",
|
||||
"version": "==5.9.1"
|
||||
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
|
||||
"version": "==5.9.2"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
"hashes": [
|
||||
@ -1632,18 +1645,18 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
|
||||
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
|
||||
"sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7",
|
||||
"sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.9"
|
||||
"version": "==21.0"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd",
|
||||
"sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"
|
||||
"sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
|
||||
"sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
|
||||
],
|
||||
"version": "==0.8.1"
|
||||
"version": "==0.9.0"
|
||||
},
|
||||
"pbr": {
|
||||
"hashes": [
|
||||
@ -1671,11 +1684,11 @@
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:0a049c5d47b629d9070c3932d13bff482b12119b6a241a93bc460b0be16953c8",
|
||||
"sha256:792b38ff30903884e4a9eab814ee3523731abd3c463f3ba48d7b627e87013484"
|
||||
"sha256:2a971129fb2d594068913a7e531d4b6d2785b2a68c6857e2baa40d3214da30f4",
|
||||
"sha256:a622c4c4c79dc8fe5e784efccacec3afe9d5e5ffab5fda2264fb5afa7c9b5797"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.3"
|
||||
"version": "==2.9.4"
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
@ -1753,57 +1766,57 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5",
|
||||
"sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79",
|
||||
"sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31",
|
||||
"sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500",
|
||||
"sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11",
|
||||
"sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14",
|
||||
"sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3",
|
||||
"sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439",
|
||||
"sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c",
|
||||
"sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82",
|
||||
"sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711",
|
||||
"sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093",
|
||||
"sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a",
|
||||
"sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb",
|
||||
"sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8",
|
||||
"sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17",
|
||||
"sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000",
|
||||
"sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d",
|
||||
"sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480",
|
||||
"sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc",
|
||||
"sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0",
|
||||
"sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9",
|
||||
"sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765",
|
||||
"sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e",
|
||||
"sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a",
|
||||
"sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07",
|
||||
"sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f",
|
||||
"sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac",
|
||||
"sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7",
|
||||
"sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed",
|
||||
"sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968",
|
||||
"sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7",
|
||||
"sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2",
|
||||
"sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4",
|
||||
"sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87",
|
||||
"sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8",
|
||||
"sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10",
|
||||
"sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29",
|
||||
"sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605",
|
||||
"sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6",
|
||||
"sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"
|
||||
"sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f",
|
||||
"sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad",
|
||||
"sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a",
|
||||
"sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf",
|
||||
"sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59",
|
||||
"sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d",
|
||||
"sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895",
|
||||
"sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4",
|
||||
"sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3",
|
||||
"sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222",
|
||||
"sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0",
|
||||
"sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c",
|
||||
"sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417",
|
||||
"sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d",
|
||||
"sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d",
|
||||
"sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761",
|
||||
"sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0",
|
||||
"sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026",
|
||||
"sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854",
|
||||
"sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb",
|
||||
"sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d",
|
||||
"sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068",
|
||||
"sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde",
|
||||
"sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d",
|
||||
"sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec",
|
||||
"sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa",
|
||||
"sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd",
|
||||
"sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b",
|
||||
"sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26",
|
||||
"sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2",
|
||||
"sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f",
|
||||
"sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694",
|
||||
"sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0",
|
||||
"sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407",
|
||||
"sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874",
|
||||
"sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035",
|
||||
"sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d",
|
||||
"sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c",
|
||||
"sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5",
|
||||
"sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985",
|
||||
"sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58"
|
||||
],
|
||||
"version": "==2021.4.4"
|
||||
"version": "==2021.7.6"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
"sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24",
|
||||
"sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.1"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==2.26.0"
|
||||
},
|
||||
"requests-mock": {
|
||||
"hashes": [
|
||||
|
@ -1,3 +1,3 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.6.3"
|
||||
__version__ = "2021.7.1-rc1"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
@ -49,7 +49,7 @@ class ConfigView(APIView):
|
||||
caps.append(Capabilities.CAN_GEO_IP)
|
||||
if SERVICE_HOST_ENV_NAME in environ:
|
||||
# Running in k8s, only s3 backup is supported
|
||||
if CONFIG.y("postgresql.s3_backup"):
|
||||
if CONFIG.y_bool("postgresql.s3_backup"):
|
||||
caps.append(Capabilities.CAN_BACKUP)
|
||||
else:
|
||||
# Running in compose, backup is always supported
|
||||
|
38
authentik/api/v2/sentry.py
Normal file
38
authentik/api/v2/sentry.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""Sentry tunnel"""
|
||||
from json import loads
|
||||
|
||||
from django.conf import settings
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
from django.views.generic.base import View
|
||||
from requests import post
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
|
||||
class SentryTunnelView(View):
|
||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
||||
# Only allow usage of this endpoint when error reporting is enabled
|
||||
if not CONFIG.y_bool("error_reporting.enabled", False):
|
||||
return HttpResponse(status=400)
|
||||
# Body is 2 json objects separated by \n
|
||||
full_body = request.body
|
||||
header = loads(full_body.splitlines()[0])
|
||||
# Check that the DSN is what we expect
|
||||
dsn = header.get("dsn", "")
|
||||
if dsn != settings.SENTRY_DSN:
|
||||
return HttpResponse(status=400)
|
||||
response = post(
|
||||
"https://sentry.beryju.org/api/8/envelope/",
|
||||
data=full_body,
|
||||
headers={"Content-Type": "application/octet-stream"},
|
||||
)
|
||||
try:
|
||||
response.raise_for_status()
|
||||
except RequestException:
|
||||
return HttpResponse(status=500)
|
||||
return HttpResponse(status=response.status_code)
|
@ -1,5 +1,6 @@
|
||||
"""api v2 urls"""
|
||||
from django.urls import path
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from drf_spectacular.views import SpectacularAPIView
|
||||
from rest_framework import routers
|
||||
|
||||
@ -10,6 +11,7 @@ from authentik.admin.api.tasks import TaskViewSet
|
||||
from authentik.admin.api.version import VersionView
|
||||
from authentik.admin.api.workers import WorkerView
|
||||
from authentik.api.v2.config import ConfigView
|
||||
from authentik.api.v2.sentry import SentryTunnelView
|
||||
from authentik.api.views import APIBrowserView
|
||||
from authentik.core.api.applications import ApplicationViewSet
|
||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||
@ -235,6 +237,7 @@ urlpatterns = (
|
||||
FlowExecutorView.as_view(),
|
||||
name="flow-executor",
|
||||
),
|
||||
path("sentry/", csrf_exempt(SentryTunnelView.as_view()), name="sentry"),
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
]
|
||||
)
|
||||
|
@ -1,24 +1,60 @@
|
||||
"""Groups API Viewset"""
|
||||
from django.db.models.query import QuerySet
|
||||
from rest_framework.fields import JSONField
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.fields import BooleanField, CharField, JSONField
|
||||
from rest_framework.serializers import ListSerializer, ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
|
||||
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import is_dict
|
||||
from authentik.core.models import Group
|
||||
from authentik.core.models import Group, User
|
||||
|
||||
|
||||
class GroupMemberSerializer(ModelSerializer):
|
||||
"""Stripped down user serializer to show relevant users for groups"""
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
avatar = CharField(read_only=True)
|
||||
attributes = JSONField(validators=[is_dict], required=False)
|
||||
uid = CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = [
|
||||
"pk",
|
||||
"username",
|
||||
"name",
|
||||
"is_active",
|
||||
"last_login",
|
||||
"is_superuser",
|
||||
"email",
|
||||
"avatar",
|
||||
"attributes",
|
||||
"uid",
|
||||
]
|
||||
|
||||
|
||||
class GroupSerializer(ModelSerializer):
|
||||
"""Group Serializer"""
|
||||
|
||||
attributes = JSONField(validators=[is_dict], required=False)
|
||||
users_obj = ListSerializer(
|
||||
child=GroupMemberSerializer(), read_only=True, source="users", required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
fields = ["pk", "name", "is_superuser", "parent", "users", "attributes"]
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"is_superuser",
|
||||
"parent",
|
||||
"users",
|
||||
"attributes",
|
||||
"users_obj",
|
||||
]
|
||||
|
||||
|
||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -12,7 +12,7 @@ from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.users import UserSerializer
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
from authentik.core.models import Token, TokenIntents
|
||||
from authentik.core.models import USER_ATTRIBUTE_TOKEN_EXPIRING, Token, TokenIntents
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.managed.api import ManagedSerializer
|
||||
|
||||
@ -61,11 +61,19 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
"intent",
|
||||
"user__username",
|
||||
"description",
|
||||
"expires",
|
||||
"expiring",
|
||||
]
|
||||
ordering = ["expires"]
|
||||
|
||||
def perform_create(self, serializer: TokenSerializer):
|
||||
serializer.save(user=self.request.user, intent=TokenIntents.INTENT_API)
|
||||
serializer.save(
|
||||
user=self.request.user,
|
||||
intent=TokenIntents.INTENT_API,
|
||||
expiring=self.request.user.attributes.get(
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING, True
|
||||
),
|
||||
)
|
||||
|
||||
@permission_required("authentik_core.view_token_key")
|
||||
@extend_schema(
|
||||
|
@ -2,12 +2,11 @@
|
||||
from json import loads
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http.response import Http404
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.http import urlencode
|
||||
from django_filters.filters import BooleanFilter, CharFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from guardian.utils import get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||
@ -173,7 +172,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
@extend_schema(
|
||||
responses={
|
||||
"200": LinkSerializer(many=False),
|
||||
"404": OpenApiResponse(description="No recovery flow found."),
|
||||
"404": LinkSerializer(many=False),
|
||||
},
|
||||
)
|
||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||
@ -184,7 +183,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
# Check that there is a recovery flow, if not return an error
|
||||
flow = tenant.flow_recovery
|
||||
if not flow:
|
||||
raise Http404
|
||||
return Response({"link": ""}, status=404)
|
||||
user: User = self.get_object()
|
||||
token, __ = Token.objects.get_or_create(
|
||||
identifier=f"{user.uid}-password-reset",
|
||||
|
@ -14,7 +14,9 @@ def is_dict(value: Any):
|
||||
"""Ensure a value is a dictionary, useful for JSONFields"""
|
||||
if isinstance(value, dict):
|
||||
return
|
||||
raise ValidationError("Value must be a dictionary.")
|
||||
raise ValidationError(
|
||||
"Value must be a dictionary, and not have any duplicate keys."
|
||||
)
|
||||
|
||||
|
||||
class PassiveSerializer(Serializer):
|
||||
|
@ -0,0 +1,20 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-09 17:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0025_alter_application_meta_icon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="application",
|
||||
name="meta_icon",
|
||||
field=models.FileField(
|
||||
default=None, max_length=500, null=True, upload_to="application-icons/"
|
||||
),
|
||||
),
|
||||
]
|
@ -5,14 +5,13 @@ from typing import Any, Optional, Type
|
||||
from urllib.parse import urlencode
|
||||
from uuid import uuid4
|
||||
|
||||
import django.db.models.options as options
|
||||
from deepmerge import always_merger
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.contrib.auth.models import UserManager as DjangoUserManager
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
from django.db.models import Q, QuerySet
|
||||
from django.db.models import Q, QuerySet, options
|
||||
from django.http import HttpRequest
|
||||
from django.templatetags.static import static
|
||||
from django.utils.functional import cached_property
|
||||
@ -38,6 +37,8 @@ LOGGER = get_logger()
|
||||
USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
||||
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
||||
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
||||
|
||||
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||
DEFAULT_AVATAR = static("dist/assets/images/user_default.png")
|
||||
@ -229,7 +230,10 @@ class Application(PolicyBindingModel):
|
||||
)
|
||||
# For template applications, this can be set to /static/authentik/applications/*
|
||||
meta_icon = models.FileField(
|
||||
upload_to="application-icons/", default=None, null=True
|
||||
upload_to="application-icons/",
|
||||
default=None,
|
||||
null=True,
|
||||
max_length=500,
|
||||
)
|
||||
meta_description = models.TextField(default="", blank=True)
|
||||
meta_publisher = models.TextField(default="", blank=True)
|
||||
@ -378,6 +382,13 @@ class ExpiringModel(models.Model):
|
||||
expires = models.DateTimeField(default=default_token_duration)
|
||||
expiring = models.BooleanField(default=True)
|
||||
|
||||
def expire_action(self, *args, **kwargs):
|
||||
"""Handler which is called when this object is expired. By
|
||||
default the object is deleted. This is less efficient compared
|
||||
to bulk deleting objects, but classes like Token() need to change
|
||||
values instead of being deleted."""
|
||||
return self.delete(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def filter_not_expired(cls, **kwargs) -> QuerySet:
|
||||
"""Filer for tokens which are not expired yet or are not expiring,
|
||||
@ -422,6 +433,18 @@ class Token(ManagedModel, ExpiringModel):
|
||||
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
|
||||
description = models.TextField(default="", blank=True)
|
||||
|
||||
def expire_action(self, *args, **kwargs):
|
||||
"""Handler which is called when this object is expired."""
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
self.key = default_token_key()
|
||||
self.save(*args, **kwargs)
|
||||
Event.new(
|
||||
action=EventAction.SECRET_ROTATE,
|
||||
token=self,
|
||||
message=f"Token {self.identifier}'s secret was rotated.",
|
||||
).save()
|
||||
|
||||
def __str__(self):
|
||||
description = f"{self.identifier}"
|
||||
if self.expiring:
|
||||
|
@ -26,14 +26,16 @@ def clean_expired_models(self: MonitoredTask):
|
||||
messages = []
|
||||
for cls in ExpiringModel.__subclasses__():
|
||||
cls: ExpiringModel
|
||||
amount, _ = (
|
||||
objects = (
|
||||
cls.objects.all()
|
||||
.exclude(expiring=False)
|
||||
.exclude(expiring=True, expires__gt=now())
|
||||
.delete()
|
||||
)
|
||||
LOGGER.debug("Deleted expired models", model=cls, amount=amount)
|
||||
messages.append(f"Deleted {amount} expired {cls._meta.verbose_name_plural}")
|
||||
for obj in objects:
|
||||
obj.expire_action()
|
||||
amount = objects.count()
|
||||
LOGGER.debug("Expired models", model=cls, amount=amount)
|
||||
messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}")
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
|
||||
|
||||
|
||||
|
@ -1,18 +0,0 @@
|
||||
"""authentik core task tests"""
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.models import Token
|
||||
from authentik.core.tasks import clean_expired_models
|
||||
|
||||
|
||||
class TestTasks(TestCase):
|
||||
"""Test Tasks"""
|
||||
|
||||
def test_token_cleanup(self):
|
||||
"""Test Token cleanup task"""
|
||||
Token.objects.create(expires=now(), user=get_anonymous_user())
|
||||
self.assertEqual(Token.objects.all().count(), 1)
|
||||
clean_expired_models.delay().get()
|
||||
self.assertEqual(Token.objects.all().count(), 0)
|
@ -1,8 +1,16 @@
|
||||
"""Test token API"""
|
||||
from django.urls.base import reverse
|
||||
from django.utils.timezone import now
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import Token, TokenIntents, User
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
)
|
||||
from authentik.core.tasks import clean_expired_models
|
||||
|
||||
|
||||
class TestTokenAPI(APITestCase):
|
||||
@ -22,3 +30,25 @@ class TestTokenAPI(APITestCase):
|
||||
token = Token.objects.get(identifier="test-token")
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, True)
|
||||
|
||||
def test_token_create_non_expiring(self):
|
||||
"""Test token creation endpoint"""
|
||||
self.user.attributes[USER_ATTRIBUTE_TOKEN_EXPIRING] = False
|
||||
self.user.save()
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:token-list"), {"identifier": "test-token"}
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
token = Token.objects.get(identifier="test-token")
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, False)
|
||||
|
||||
def test_token_expire(self):
|
||||
"""Test Token expire task"""
|
||||
token: Token = Token.objects.create(expires=now(), user=get_anonymous_user())
|
||||
key = token.key
|
||||
clean_expired_models.delay().get()
|
||||
token.refresh_from_db()
|
||||
self.assertNotEqual(key, token.key)
|
||||
|
@ -97,7 +97,8 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
fields = [
|
||||
"pk",
|
||||
"name",
|
||||
"fingerprint",
|
||||
"fingerprint_sha256",
|
||||
"fingerprint_sha1",
|
||||
"certificate_data",
|
||||
"key_data",
|
||||
"cert_expiry",
|
||||
|
@ -16,11 +16,6 @@ from authentik.crypto.models import CertificateKeyPair
|
||||
class CertificateBuilder:
|
||||
"""Build self-signed certificates"""
|
||||
|
||||
__public_key = None
|
||||
__private_key = None
|
||||
__builder = None
|
||||
__certificate = None
|
||||
|
||||
common_name: str
|
||||
|
||||
def __init__(self):
|
||||
|
@ -68,12 +68,19 @@ class CertificateKeyPair(CreatedUpdatedModel):
|
||||
return self._private_key
|
||||
|
||||
@property
|
||||
def fingerprint(self) -> str:
|
||||
def fingerprint_sha256(self) -> str:
|
||||
"""Get SHA256 Fingerprint of certificate_data"""
|
||||
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
@property
|
||||
def fingerprint_sha1(self) -> str:
|
||||
"""Get SHA1 Fingerprint of certificate_data"""
|
||||
return hexlify(
|
||||
self.certificate.fingerprint(hashes.SHA1()), ":" # nosec
|
||||
).decode("utf-8")
|
||||
|
||||
@property
|
||||
def kid(self):
|
||||
"""Get Key ID used for JWKS"""
|
||||
|
@ -46,7 +46,7 @@ class NotificationTransportTestSerializer(Serializer):
|
||||
|
||||
messages = ListField(child=CharField())
|
||||
|
||||
def create(self, request: Request) -> Response:
|
||||
def create(self, validated_data: Request) -> Response:
|
||||
raise NotImplementedError
|
||||
|
||||
def update(self, request: Request) -> Response:
|
||||
|
@ -27,10 +27,9 @@ class GeoIPDict(TypedDict):
|
||||
class GeoIPReader:
|
||||
"""Slim wrapper around GeoIP API"""
|
||||
|
||||
__reader: Optional[Reader] = None
|
||||
__last_mtime: float = 0.0
|
||||
|
||||
def __init__(self):
|
||||
self.__reader: Optional[Reader] = None
|
||||
self.__last_mtime: float = 0.0
|
||||
self.__open()
|
||||
|
||||
def __open(self):
|
||||
|
@ -3,6 +3,7 @@ from functools import partial
|
||||
from typing import Callable
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
@ -63,7 +64,15 @@ class AuditMiddleware:
|
||||
|
||||
if settings.DEBUG:
|
||||
return
|
||||
if before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
||||
# Special case for SuspiciousOperation, we have a special event action for that
|
||||
if isinstance(exception, SuspiciousOperation):
|
||||
thread = EventNewThread(
|
||||
EventAction.SUSPICIOUS_REQUEST,
|
||||
request,
|
||||
message=str(exception),
|
||||
)
|
||||
thread.run()
|
||||
elif before_send({}, {"exc_info": (None, exception, None)}) is not None:
|
||||
thread = EventNewThread(
|
||||
EventAction.SYSTEM_EXCEPTION,
|
||||
request,
|
||||
|
47
authentik/events/migrations/0017_alter_event_action.py
Normal file
47
authentik/events/migrations/0017_alter_event_action.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-14 19:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_events", "0016_add_tenant"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("secret_view", "Secret View"),
|
||||
("secret_rotate", "Secret Rotate"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("system_exception", "System Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("email_sent", "Email Sent"),
|
||||
("update_available", "Update Available"),
|
||||
("custom_", "Custom Prefix"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
@ -62,6 +62,7 @@ class EventAction(models.TextChoices):
|
||||
PASSWORD_SET = "password_set" # noqa # nosec
|
||||
|
||||
SECRET_VIEW = "secret_view" # noqa # nosec
|
||||
SECRET_ROTATE = "secret_rotate" # noqa # nosec
|
||||
|
||||
INVITE_USED = "invitation_used"
|
||||
|
||||
@ -313,7 +314,8 @@ class NotificationTransport(models.Model):
|
||||
response = post(self.webhook_url, json=body)
|
||||
response.raise_for_status()
|
||||
except RequestException as exc:
|
||||
raise NotificationTransportError(exc.response.text) from exc
|
||||
text = exc.response.text if exc.response else str(exc)
|
||||
raise NotificationTransportError(text) from exc
|
||||
return [
|
||||
response.status_code,
|
||||
response.text,
|
||||
|
@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-03 13:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0021_flowstagebinding_invalid_response_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="flowstagebinding",
|
||||
name="invalid_response_action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("retry", "Retry"),
|
||||
("restart", "Restart"),
|
||||
("restart_with_context", "Restart With Context"),
|
||||
],
|
||||
default="retry",
|
||||
help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT restarts the flow while keeping the current context.",
|
||||
),
|
||||
),
|
||||
]
|
24
authentik/flows/migrations/0023_alter_flow_background.py
Normal file
24
authentik/flows/migrations/0023_alter_flow_background.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-09 17:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0022_alter_flowstagebinding_invalid_response_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="flow",
|
||||
name="background",
|
||||
field=models.FileField(
|
||||
default=None,
|
||||
help_text="Background shown during execution",
|
||||
max_length=500,
|
||||
null=True,
|
||||
upload_to="flow-backgrounds/",
|
||||
),
|
||||
),
|
||||
]
|
@ -121,6 +121,7 @@ class Flow(SerializerModel, PolicyBindingModel):
|
||||
default=None,
|
||||
null=True,
|
||||
help_text=_("Background shown during execution"),
|
||||
max_length=500,
|
||||
)
|
||||
|
||||
compatibility_mode = models.BooleanField(
|
||||
|
@ -17,7 +17,7 @@ from authentik.flows.challenge import (
|
||||
WithUserInfoChallenge,
|
||||
)
|
||||
from authentik.flows.models import InvalidResponseAction
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_PENDING_USER
|
||||
from authentik.flows.views import FlowExecutorView
|
||||
|
||||
PLAN_CONTEXT_PENDING_USER_IDENTIFIER = "pending_user_identifier"
|
||||
@ -102,12 +102,18 @@ class ChallengeStageView(StageView):
|
||||
return self.challenge_invalid(challenge)
|
||||
return self.challenge_valid(challenge)
|
||||
|
||||
def format_title(self) -> str:
|
||||
"""Allow usage of placeholder in flow title."""
|
||||
return self.executor.flow.title % {
|
||||
"app": self.executor.plan.context.get(PLAN_CONTEXT_APPLICATION, "")
|
||||
}
|
||||
|
||||
def _get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
challenge = self.get_challenge(*args, **kwargs)
|
||||
if "flow_info" not in challenge.initial_data:
|
||||
flow_info = ContextualFlowInfo(
|
||||
data={
|
||||
"title": self.executor.flow.title,
|
||||
"title": self.format_title(),
|
||||
"background": self.executor.flow.background_url,
|
||||
"cancel_url": reverse("authentik_flows:cancel"),
|
||||
}
|
||||
|
@ -40,15 +40,11 @@ def transaction_rollback():
|
||||
class FlowImporter:
|
||||
"""Import Flow from json"""
|
||||
|
||||
__import: FlowBundle
|
||||
|
||||
__pk_map: dict[Any, Model]
|
||||
|
||||
logger: BoundLogger
|
||||
|
||||
def __init__(self, json_input: str):
|
||||
self.__pk_map: dict[Any, Model] = {}
|
||||
self.logger = get_logger()
|
||||
self.__pk_map = {}
|
||||
import_dict = loads(json_input)
|
||||
try:
|
||||
self.__import = from_dict(FlowBundle, import_dict)
|
||||
|
@ -134,7 +134,7 @@ class FlowExecutorView(APIView):
|
||||
message = exc.__doc__ if exc.__doc__ else str(exc)
|
||||
return self.stage_invalid(error_message=message)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=unused-argument, too-many-return-statements
|
||||
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
|
||||
# Early check if theres an active Plan for the current session
|
||||
if SESSION_KEY_PLAN in self.request.session:
|
||||
@ -167,7 +167,18 @@ class FlowExecutorView(APIView):
|
||||
request.session[SESSION_KEY_GET] = QueryDict(request.GET.get("query", ""))
|
||||
# We don't save the Plan after getting the next stage
|
||||
# as it hasn't been successfully passed yet
|
||||
next_binding = self.plan.next(self.request)
|
||||
try:
|
||||
# This is the first time we actually access any attribute on the selected plan
|
||||
# if the cached plan is from an older version, it might have different attributes
|
||||
# in which case we just delete the plan and invalidate everything
|
||||
next_binding = self.plan.next(self.request)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
self._logger.warning(
|
||||
"f(exec): found incompatible flow plan, invalidating run", exc=exc
|
||||
)
|
||||
keys = cache.keys("flow_*")
|
||||
cache.delete_many(keys)
|
||||
return self.stage_invalid()
|
||||
if not next_binding:
|
||||
self._logger.debug("f(exec): no more stages, flow is done.")
|
||||
return self._flow_done()
|
||||
|
@ -26,10 +26,9 @@ class ConfigLoader:
|
||||
|
||||
loaded_file = []
|
||||
|
||||
__config = {}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.__config = {}
|
||||
base_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "../.."))
|
||||
for path in SEARCH_PATHS:
|
||||
# Check if path is relative, and if so join with base_dir
|
||||
|
@ -13,7 +13,10 @@ web:
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ''
|
||||
tls: false
|
||||
tls_reqs: "none"
|
||||
cache_db: 0
|
||||
message_queue_db: 1
|
||||
ws_db: 2
|
||||
|
@ -2,10 +2,12 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.http import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP"
|
||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
||||
OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN" # nosec
|
||||
DEFAULT_IP = "255.255.255.255"
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
|
||||
@ -27,13 +29,25 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
|
||||
"""Get the actual remote IP when set by an outpost. Only
|
||||
allowed when the request is authenticated, by a user with USER_ATTRIBUTE_CAN_OVERRIDE_IP set
|
||||
to outpost"""
|
||||
if not hasattr(request, "user"):
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP,
|
||||
Token,
|
||||
TokenIntents,
|
||||
)
|
||||
|
||||
if (
|
||||
OUTPOST_REMOTE_IP_HEADER not in request.META
|
||||
or OUTPOST_TOKEN_HEADER not in request.META
|
||||
):
|
||||
return None
|
||||
if not request.user.is_authenticated:
|
||||
tokens = Token.filter_not_expired(
|
||||
key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API
|
||||
)
|
||||
if not tokens.exists():
|
||||
LOGGER.warning("Attempted remote-ip override without token")
|
||||
return None
|
||||
if OUTPOST_REMOTE_IP_HEADER not in request.META:
|
||||
return None
|
||||
if request.user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
||||
user = tokens.first().user
|
||||
if user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
||||
return None
|
||||
return request.META[OUTPOST_REMOTE_IP_HEADER]
|
||||
|
||||
|
@ -51,7 +51,7 @@ class OutpostSerializer(ModelSerializer):
|
||||
raise ValidationError(
|
||||
(
|
||||
f"Outpost type {self.initial_data['type']} can't be used with "
|
||||
f"{type(provider)} providers."
|
||||
f"{provider.__class__.__name__} providers."
|
||||
)
|
||||
)
|
||||
return providers
|
||||
@ -77,6 +77,7 @@ class OutpostSerializer(ModelSerializer):
|
||||
"service_connection_obj",
|
||||
"token_identifier",
|
||||
"config",
|
||||
"managed",
|
||||
]
|
||||
extra_kwargs = {"type": {"required": True}}
|
||||
|
||||
|
@ -69,7 +69,7 @@ class OutpostConsumer(AuthJsonConsumer):
|
||||
self.last_uid = self.channel_name
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def disconnect(self, close_code):
|
||||
def disconnect(self, code):
|
||||
if self.outpost and self.last_uid:
|
||||
state = OutpostState.for_instance_uid(self.outpost, self.last_uid)
|
||||
if self.channel_name in state.channel_ids:
|
||||
|
@ -36,8 +36,10 @@ class DockerController(BaseController):
|
||||
|
||||
def _get_env(self) -> dict[str, str]:
|
||||
return {
|
||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host,
|
||||
"AUTHENTIK_INSECURE": str(self.outpost.config.authentik_host_insecure),
|
||||
"AUTHENTIK_HOST": self.outpost.config.authentik_host.lower(),
|
||||
"AUTHENTIK_INSECURE": str(
|
||||
self.outpost.config.authentik_host_insecure
|
||||
).lower(),
|
||||
"AUTHENTIK_TOKEN": self.outpost.token.key,
|
||||
}
|
||||
|
||||
@ -45,11 +47,10 @@ class DockerController(BaseController):
|
||||
"""Check if container's env is equal to what we would set. Return true if container needs
|
||||
to be rebuilt."""
|
||||
should_be = self._get_env()
|
||||
container_env = container.attrs.get("Config", {}).get("Env", {})
|
||||
container_env = container.attrs.get("Config", {}).get("Env", [])
|
||||
for key, expected_value in should_be.items():
|
||||
if key not in container_env:
|
||||
continue
|
||||
if container_env[key] != expected_value:
|
||||
entry = f"{key.upper()}={expected_value}"
|
||||
if entry not in container_env:
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -62,14 +63,17 @@ class DockerController(BaseController):
|
||||
# When the container isn't running, the API doesn't report any port mappings
|
||||
if container.status != "running":
|
||||
return False
|
||||
# {'6379/tcp': [{'HostIp': '127.0.0.1', 'HostPort': '6379'}]}
|
||||
# {'3389/tcp': [
|
||||
# {'HostIp': '0.0.0.0', 'HostPort': '389'},
|
||||
# {'HostIp': '::', 'HostPort': '389'}
|
||||
# ]}
|
||||
for port in self.deployment_ports:
|
||||
key = f"{port.inner_port or port.port}/{port.protocol.lower()}"
|
||||
if key not in container.ports:
|
||||
return True
|
||||
host_matching = False
|
||||
for host_port in container.ports[key]:
|
||||
host_matching = host_port.get("HostPort") == port.port
|
||||
host_matching = host_port.get("HostPort") == str(port.port)
|
||||
if not host_matching:
|
||||
return True
|
||||
return False
|
||||
@ -79,7 +83,7 @@ class DockerController(BaseController):
|
||||
try:
|
||||
return self.client.containers.get(container_name), False
|
||||
except NotFound:
|
||||
self.logger.info("Container does not exist, creating")
|
||||
self.logger.info("(Re-)creating container...")
|
||||
image_name = self.get_container_image()
|
||||
self.client.images.pull(image_name)
|
||||
container_args = {
|
||||
@ -107,6 +111,7 @@ class DockerController(BaseController):
|
||||
try:
|
||||
container, has_been_created = self._get_container()
|
||||
if has_been_created:
|
||||
container.start()
|
||||
return None
|
||||
# Check if the container is out of date, delete it and retry
|
||||
if len(container.image.tags) > 0:
|
||||
@ -164,6 +169,7 @@ class DockerController(BaseController):
|
||||
self.logger.info("Container is not running, restarting...")
|
||||
container.start()
|
||||
return None
|
||||
self.logger.info("Container is running")
|
||||
return None
|
||||
except DockerException as exc:
|
||||
raise ControllerException(str(exc)) from exc
|
||||
|
18
authentik/outposts/managed.py
Normal file
18
authentik/outposts/managed.py
Normal file
@ -0,0 +1,18 @@
|
||||
"""Outpost managed objects"""
|
||||
from authentik.managed.manager import EnsureExists, ObjectManager
|
||||
from authentik.outposts.models import Outpost, OutpostType
|
||||
|
||||
|
||||
class OutpostManager(ObjectManager):
|
||||
"""Outpost managed objects"""
|
||||
|
||||
def reconcile(self):
|
||||
return [
|
||||
EnsureExists(
|
||||
Outpost,
|
||||
"goauthentik.io/outposts/inbuilt",
|
||||
name="authentik Bundeled Outpost",
|
||||
object_field="name",
|
||||
type=OutpostType.PROXY,
|
||||
),
|
||||
]
|
24
authentik/outposts/migrations/0017_outpost_managed.py
Normal file
24
authentik/outposts/migrations/0017_outpost_managed.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.4 on 2021-06-23 19:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_outposts", "0016_alter_outpost_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="outpost",
|
||||
name="managed",
|
||||
field=models.TextField(
|
||||
default=None,
|
||||
help_text="Objects which are managed by authentik. These objects are created and updated automatically. This is flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update.",
|
||||
null=True,
|
||||
unique=True,
|
||||
verbose_name="Managed by authentik",
|
||||
),
|
||||
),
|
||||
]
|
@ -28,12 +28,19 @@ from structlog.stdlib import get_logger
|
||||
from urllib3.exceptions import HTTPError
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik.core.models import USER_ATTRIBUTE_SA, Provider, Token, TokenIntents, User
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP,
|
||||
USER_ATTRIBUTE_SA,
|
||||
Provider,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
)
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.models import InheritanceForeignKey
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.http import USER_ATTRIBUTE_CAN_OVERRIDE_IP
|
||||
from authentik.managed.models import ManagedModel
|
||||
from authentik.outposts.controllers.k8s.utils import get_namespace
|
||||
from authentik.outposts.docker_tls import DockerInlineTLS
|
||||
|
||||
@ -281,7 +288,7 @@ class KubernetesServiceConnection(OutpostServiceConnection):
|
||||
verbose_name_plural = _("Kubernetes Service-Connections")
|
||||
|
||||
|
||||
class Outpost(models.Model):
|
||||
class Outpost(ManagedModel):
|
||||
"""Outpost instance which manages a service user and token"""
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
|
||||
|
@ -9,7 +9,7 @@ CELERY_BEAT_SCHEDULE = {
|
||||
},
|
||||
"outposts_service_connection_check": {
|
||||
"task": "authentik.outposts.tasks.outpost_service_connection_monitor",
|
||||
"schedule": crontab(minute="*/60"),
|
||||
"schedule": crontab(minute="*/5"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
"outpost_token_ensurer": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""authentik outpost signals"""
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, pre_delete, pre_save
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
|
||||
from django.dispatch import receiver
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -46,6 +46,14 @@ def pre_save_outpost(sender, instance: Outpost, **_):
|
||||
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Outpost.providers.through)
|
||||
# pylint: disable=unused-argument
|
||||
def m2m_changed_update(sender, instance: Model, action: str, **_):
|
||||
"""Update outpost on m2m change, when providers are added or removed"""
|
||||
if action in ["post_add", "post_remove", "post_clear"]:
|
||||
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_update(sender, instance: Model, **_):
|
||||
|
@ -82,13 +82,13 @@ class PolicyBindingSerializer(ModelSerializer):
|
||||
"timeout",
|
||||
]
|
||||
|
||||
def validate(self, data: OrderedDict) -> OrderedDict:
|
||||
def validate(self, attrs: OrderedDict) -> OrderedDict:
|
||||
"""Check that either policy, group or user is set."""
|
||||
count = sum(
|
||||
[
|
||||
bool(data.get("policy", None)),
|
||||
bool(data.get("group", None)),
|
||||
bool(data.get("user", None)),
|
||||
bool(attrs.get("policy", None)),
|
||||
bool(attrs.get("group", None)),
|
||||
bool(attrs.get("user", None)),
|
||||
]
|
||||
)
|
||||
invalid = count > 1
|
||||
@ -97,7 +97,7 @@ class PolicyBindingSerializer(ModelSerializer):
|
||||
raise ValidationError("Only one of 'policy', 'group' or 'user' can be set.")
|
||||
if empty:
|
||||
raise ValidationError("One of 'policy', 'group' or 'user' must be set.")
|
||||
return data
|
||||
return attrs
|
||||
|
||||
|
||||
class PolicyBindingViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -62,12 +62,6 @@ class PolicyEngine:
|
||||
# Allow objects with no policies attached to pass
|
||||
empty_result: bool
|
||||
|
||||
__pbm: PolicyBindingModel
|
||||
__cached_policies: list[PolicyResult]
|
||||
__processes: list[PolicyProcessInfo]
|
||||
|
||||
__expected_result_count: int
|
||||
|
||||
def __init__(
|
||||
self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
|
||||
):
|
||||
@ -83,8 +77,8 @@ class PolicyEngine:
|
||||
self.request.obj = pbm
|
||||
if request:
|
||||
self.request.set_http_request(request)
|
||||
self.__cached_policies = []
|
||||
self.__processes = []
|
||||
self.__cached_policies: list[PolicyResult] = []
|
||||
self.__processes: list[PolicyProcessInfo] = []
|
||||
self.use_cache = True
|
||||
self.__expected_result_count = 0
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-14 19:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_policies_event_matcher", "0017_alter_eventmatcherpolicy_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="eventmatcherpolicy",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("login", "Login"),
|
||||
("login_failed", "Login Failed"),
|
||||
("logout", "Logout"),
|
||||
("user_write", "User Write"),
|
||||
("suspicious_request", "Suspicious Request"),
|
||||
("password_set", "Password Set"),
|
||||
("secret_view", "Secret View"),
|
||||
("secret_rotate", "Secret Rotate"),
|
||||
("invitation_used", "Invite Used"),
|
||||
("authorize_application", "Authorize Application"),
|
||||
("source_linked", "Source Linked"),
|
||||
("impersonation_started", "Impersonation Started"),
|
||||
("impersonation_ended", "Impersonation Ended"),
|
||||
("policy_execution", "Policy Execution"),
|
||||
("policy_exception", "Policy Exception"),
|
||||
("property_mapping_exception", "Property Mapping Exception"),
|
||||
("system_task_execution", "System Task Execution"),
|
||||
("system_task_exception", "System Task Exception"),
|
||||
("system_exception", "System Exception"),
|
||||
("configuration_error", "Configuration Error"),
|
||||
("model_created", "Model Created"),
|
||||
("model_updated", "Model Updated"),
|
||||
("model_deleted", "Model Deleted"),
|
||||
("email_sent", "Email Sent"),
|
||||
("update_available", "Update Available"),
|
||||
("custom_", "Custom Prefix"),
|
||||
],
|
||||
help_text="Match created events with this action type. When left empty, all action types will be matched.",
|
||||
),
|
||||
),
|
||||
]
|
@ -21,12 +21,15 @@ def update_score(request: HttpRequest, username: str, amount: int):
|
||||
"""Update score for IP and User"""
|
||||
remote_ip = get_client_ip(request)
|
||||
|
||||
# We only update the cache here, as its faster than writing to the DB
|
||||
cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0, CACHE_TIMEOUT)
|
||||
cache.incr(CACHE_KEY_IP_PREFIX + remote_ip, amount)
|
||||
try:
|
||||
# We only update the cache here, as its faster than writing to the DB
|
||||
cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0, CACHE_TIMEOUT)
|
||||
cache.incr(CACHE_KEY_IP_PREFIX + remote_ip, amount)
|
||||
|
||||
cache.get_or_set(CACHE_KEY_USER_PREFIX + username, 0, CACHE_TIMEOUT)
|
||||
cache.incr(CACHE_KEY_USER_PREFIX + username, amount)
|
||||
cache.get_or_set(CACHE_KEY_USER_PREFIX + username, 0, CACHE_TIMEOUT)
|
||||
cache.incr(CACHE_KEY_USER_PREFIX + username, amount)
|
||||
except ValueError as exc:
|
||||
LOGGER.warning("failed to set reputation", exc=exc)
|
||||
|
||||
LOGGER.debug("Updated score", amount=amount, for_user=username, for_ip=remote_ip)
|
||||
|
||||
|
@ -17,6 +17,10 @@ class LDAPProviderSerializer(ProviderSerializer):
|
||||
fields = ProviderSerializer.Meta.fields + [
|
||||
"base_dn",
|
||||
"search_group",
|
||||
"certificate",
|
||||
"tls_server_name",
|
||||
"uid_start_number",
|
||||
"gid_start_number",
|
||||
]
|
||||
|
||||
|
||||
@ -44,6 +48,10 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
|
||||
"bind_flow_slug",
|
||||
"application_slug",
|
||||
"search_group",
|
||||
"certificate",
|
||||
"tls_server_name",
|
||||
"uid_start_number",
|
||||
"gid_start_number",
|
||||
]
|
||||
|
||||
|
||||
|
@ -11,4 +11,5 @@ class LDAPDockerController(DockerController):
|
||||
super().__init__(outpost, connection)
|
||||
self.deployment_ports = [
|
||||
DeploymentPort(389, "ldap", "tcp", 3389),
|
||||
DeploymentPort(636, "ldaps", "tcp", 6636),
|
||||
]
|
||||
|
@ -11,4 +11,5 @@ class LDAPKubernetesController(KubernetesController):
|
||||
super().__init__(outpost, connection)
|
||||
self.deployment_ports = [
|
||||
DeploymentPort(389, "ldap", "tcp", 3389),
|
||||
DeploymentPort(636, "ldaps", "tcp", 6636),
|
||||
]
|
||||
|
@ -0,0 +1,30 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-13 11:38
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_crypto", "0002_create_self_signed_kp"),
|
||||
("authentik_providers_ldap", "0002_ldapprovider_search_group"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="ldapprovider",
|
||||
name="certificate",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="authentik_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ldapprovider",
|
||||
name="tls_server_name",
|
||||
field=models.TextField(blank=True, default=""),
|
||||
),
|
||||
]
|
@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-13 21:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_ldap", "0003_auto_20210713_1138"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="ldapprovider",
|
||||
name="gid_start_number",
|
||||
field=models.IntegerField(
|
||||
default=4000,
|
||||
help_text="The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ldapprovider",
|
||||
name="uid_start_number",
|
||||
field=models.IntegerField(
|
||||
default=2000,
|
||||
help_text="The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
|
||||
),
|
||||
),
|
||||
]
|
@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import Group, Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.outposts.models import OutpostModel
|
||||
|
||||
|
||||
@ -28,6 +29,36 @@ class LDAPProvider(OutpostModel, Provider):
|
||||
),
|
||||
)
|
||||
|
||||
tls_server_name = models.TextField(
|
||||
default="",
|
||||
blank=True,
|
||||
)
|
||||
certificate = models.ForeignKey(
|
||||
CertificateKeyPair,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
uid_start_number = models.IntegerField(
|
||||
default=2000,
|
||||
help_text=_(
|
||||
"The start for uidNumbers, this number is added to the user.Pk to make sure that the "
|
||||
"numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't "
|
||||
"collide with local users uidNumber"
|
||||
),
|
||||
)
|
||||
|
||||
gid_start_number = models.IntegerField(
|
||||
default=4000,
|
||||
help_text=_(
|
||||
"The start for gidNumbers, this number is added to a number generated from the "
|
||||
"group.Pk to make sure that the numbers aren't too low for POSIX groups. Default "
|
||||
"is 4000 to ensure that we don't collide with local groups or users "
|
||||
"primary groups gidNumber"
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def launch_url(self) -> Optional[str]:
|
||||
"""LDAP never has a launch URL"""
|
||||
|
@ -51,6 +51,7 @@ class RefreshTokenModelSerializer(ExpiringBaseGrantModelSerializer):
|
||||
"expires",
|
||||
"scope",
|
||||
"id_token",
|
||||
"revoked",
|
||||
]
|
||||
depth = 2
|
||||
|
||||
|
@ -109,6 +109,7 @@ class Migration(migrations.Migration):
|
||||
"redirect_uris",
|
||||
models.TextField(
|
||||
default="",
|
||||
blank=True,
|
||||
help_text="Enter each URI on a new line.",
|
||||
verbose_name="Redirect URIs",
|
||||
),
|
||||
|
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.2.4 on 2021-07-03 13:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0014_alter_oauth2provider_rsa_key"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="authorizationcode",
|
||||
name="revoked",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="refreshtoken",
|
||||
name="revoked",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-20 22:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_providers_oauth2", "0015_auto_20210703_1313"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="authorizationcode",
|
||||
name="nonce",
|
||||
field=models.TextField(default=None, null=True, verbose_name="Nonce"),
|
||||
),
|
||||
]
|
@ -158,6 +158,7 @@ class OAuth2Provider(Provider):
|
||||
)
|
||||
redirect_uris = models.TextField(
|
||||
default="",
|
||||
blank=True,
|
||||
verbose_name=_("Redirect URIs"),
|
||||
help_text=_("Enter each URI on a new line."),
|
||||
)
|
||||
@ -278,7 +279,7 @@ class OAuth2Provider(Provider):
|
||||
"""Guess launch_url based on first redirect_uri"""
|
||||
if self.redirect_uris == "":
|
||||
return None
|
||||
main_url = self.redirect_uris.split("\n")[0]
|
||||
main_url = self.redirect_uris.split("\n", maxsplit=1)[0]
|
||||
launch_url = urlparse(main_url)
|
||||
return main_url.replace(launch_url.path, "")
|
||||
|
||||
@ -318,6 +319,7 @@ class BaseGrantModel(models.Model):
|
||||
provider = models.ForeignKey(OAuth2Provider, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, verbose_name=_("User"), on_delete=models.CASCADE)
|
||||
_scope = models.TextField(default="", verbose_name=_("Scopes"))
|
||||
revoked = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def scope(self) -> list[str]:
|
||||
@ -336,7 +338,7 @@ class AuthorizationCode(ExpiringModel, BaseGrantModel):
|
||||
"""OAuth2 Authorization Code"""
|
||||
|
||||
code = models.CharField(max_length=255, unique=True, verbose_name=_("Code"))
|
||||
nonce = models.TextField(blank=True, default="", verbose_name=_("Nonce"))
|
||||
nonce = models.TextField(null=True, default=None, verbose_name=_("Nonce"))
|
||||
is_open_id = models.BooleanField(
|
||||
default=False, verbose_name=_("Is Authentication?")
|
||||
)
|
||||
@ -473,9 +475,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
|
||||
# Convert datetimes into timestamps.
|
||||
now = int(time.time())
|
||||
iat_time = now
|
||||
exp_time = int(
|
||||
now + timedelta_from_string(self.provider.token_validity).total_seconds()
|
||||
)
|
||||
exp_time = int(dateformat.format(self.expires, "U"))
|
||||
# We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
|
||||
auth_events = Event.objects.filter(
|
||||
action=EventAction.LOGIN, user=get_user(user)
|
||||
|
@ -67,7 +67,7 @@ class TestAuthorize(OAuthTestCase):
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_redirect_uri(self):
|
||||
def test_invalid_redirect_uri(self):
|
||||
"""test missing/invalid redirect URI"""
|
||||
OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
@ -91,6 +91,28 @@ class TestAuthorize(OAuthTestCase):
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_empty_redirect_uri(self):
|
||||
"""test empty redirect URI (configure in provider)"""
|
||||
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)
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
data={
|
||||
"response_type": "code",
|
||||
"client_id": "test",
|
||||
"redirect_uri": "http://localhost",
|
||||
},
|
||||
)
|
||||
OAuthAuthorizationParams.from_request(request)
|
||||
|
||||
def test_response_type(self):
|
||||
"""test response_type"""
|
||||
OAuth2Provider.objects.create(
|
||||
|
@ -6,6 +6,8 @@ from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
@ -39,7 +41,8 @@ class TestToken(OAuthTestCase):
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
@ -53,11 +56,13 @@ class TestToken(OAuthTestCase):
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
"code": code.code,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
"redirect_uri": "http://testserver",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
params = TokenParams.from_request(request)
|
||||
params = TokenParams.parse(
|
||||
request, provider, provider.client_id, provider.client_secret
|
||||
)
|
||||
self.assertEqual(params.provider, provider)
|
||||
|
||||
def test_request_refresh_token(self):
|
||||
@ -68,6 +73,7 @@ class TestToken(OAuthTestCase):
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
@ -87,7 +93,9 @@ class TestToken(OAuthTestCase):
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
params = TokenParams.from_request(request)
|
||||
params = TokenParams.parse(
|
||||
request, provider, provider.client_id, provider.client_secret
|
||||
)
|
||||
self.assertEqual(params.provider, provider)
|
||||
|
||||
def test_auth_code_view(self):
|
||||
@ -98,6 +106,7 @@ class TestToken(OAuthTestCase):
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
@ -141,6 +150,7 @@ class TestToken(OAuthTestCase):
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
@ -193,6 +203,7 @@ class TestToken(OAuthTestCase):
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
@ -230,3 +241,65 @@ class TestToken(OAuthTestCase):
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
def test_refresh_token_revoke(self):
|
||||
"""test request param"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://testserver",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = provider
|
||||
self.app.save()
|
||||
header = b64encode(
|
||||
f"{provider.client_id}:{provider.client_secret}".encode()
|
||||
).decode()
|
||||
user = User.objects.get(username="akadmin")
|
||||
token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=provider,
|
||||
user=user,
|
||||
refresh_token=generate_client_id(),
|
||||
)
|
||||
# Create initial refresh token
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||
"refresh_token": token.refresh_token,
|
||||
"redirect_uri": "http://testserver",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
new_token: RefreshToken = (
|
||||
RefreshToken.objects.filter(user=user).exclude(pk=token.pk).first()
|
||||
)
|
||||
# Post again with initial token -> get new refresh token
|
||||
# and revoke old one
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||
"refresh_token": new_token.refresh_token,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Post again with old token, is now revoked and should error
|
||||
response = self.client.post(
|
||||
reverse("authentik_providers_oauth2:token"),
|
||||
data={
|
||||
"grant_type": GRANT_TYPE_REFRESH_TOKEN,
|
||||
"refresh_token": new_token.refresh_token,
|
||||
"redirect_uri": "http://local.invalid",
|
||||
},
|
||||
HTTP_AUTHORIZATION=f"Basic {header}",
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertTrue(
|
||||
Event.objects.filter(action=EventAction.SUSPICIOUS_REQUEST).exists()
|
||||
)
|
||||
|
@ -10,6 +10,7 @@ from django.http.response import HttpResponseRedirect
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.providers.oauth2.errors import BearerTokenError
|
||||
from authentik.providers.oauth2.models import RefreshToken
|
||||
|
||||
@ -50,7 +51,7 @@ def cors_allow(request: HttpRequest, response: HttpResponse, *allowed_origins: s
|
||||
if not allowed:
|
||||
LOGGER.warning(
|
||||
"CORS: Origin is not an allowed origin",
|
||||
requested=origin,
|
||||
requested=received_origin,
|
||||
allowed=allowed_origins,
|
||||
)
|
||||
return response
|
||||
@ -132,22 +133,31 @@ def protected_resource_view(scopes: list[str]):
|
||||
raise BearerTokenError("invalid_token")
|
||||
|
||||
try:
|
||||
kwargs["token"] = RefreshToken.objects.get(
|
||||
token: RefreshToken = RefreshToken.objects.get(
|
||||
access_token=access_token
|
||||
)
|
||||
except RefreshToken.DoesNotExist:
|
||||
LOGGER.debug("Token does not exist", access_token=access_token)
|
||||
raise BearerTokenError("invalid_token")
|
||||
|
||||
if kwargs["token"].is_expired:
|
||||
if token.is_expired:
|
||||
LOGGER.debug("Token has expired", access_token=access_token)
|
||||
raise BearerTokenError("invalid_token")
|
||||
|
||||
if not set(scopes).issubset(set(kwargs["token"].scope)):
|
||||
if token.revoked:
|
||||
LOGGER.warning("Revoked token was used", access_token=access_token)
|
||||
Event.new(
|
||||
action=EventAction.SUSPICIOUS_REQUEST,
|
||||
message="Revoked refresh token was used",
|
||||
token=access_token,
|
||||
).from_http(request)
|
||||
raise BearerTokenError("invalid_token")
|
||||
|
||||
if not set(scopes).issubset(set(token.scope)):
|
||||
LOGGER.warning(
|
||||
"Scope missmatch.",
|
||||
required=set(scopes),
|
||||
token_has=set(kwargs["token"].scope),
|
||||
token_has=set(token.scope),
|
||||
)
|
||||
raise BearerTokenError("insufficient_scope")
|
||||
except BearerTokenError as error:
|
||||
@ -156,7 +166,7 @@ def protected_resource_view(scopes: list[str]):
|
||||
"WWW-Authenticate"
|
||||
] = f'error="{error.code}", error_description="{error.description}"'
|
||||
return response
|
||||
|
||||
kwargs["token"] = token
|
||||
return view(request, *args, **kwargs)
|
||||
|
||||
return view_wrapper
|
||||
|
@ -156,20 +156,23 @@ class OAuthAuthorizationParams:
|
||||
|
||||
def check_redirect_uri(self):
|
||||
"""Redirect URI validation."""
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
if not self.redirect_uri:
|
||||
LOGGER.warning("Missing redirect uri.")
|
||||
raise RedirectUriError("", self.provider.redirect_uris.split())
|
||||
if self.redirect_uri.lower() not in [
|
||||
x.lower() for x in self.provider.redirect_uris.split()
|
||||
]:
|
||||
raise RedirectUriError("", allowed_redirect_urls)
|
||||
if len(allowed_redirect_urls) < 1:
|
||||
LOGGER.warning(
|
||||
"Provider has no allowed redirect_uri set, allowing all.",
|
||||
allow=self.redirect_uri.lower(),
|
||||
)
|
||||
return
|
||||
if self.redirect_uri.lower() not in [x.lower() for x in allowed_redirect_urls]:
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri",
|
||||
redirect_uri=self.redirect_uri,
|
||||
excepted=self.provider.redirect_uris.split(),
|
||||
)
|
||||
raise RedirectUriError(
|
||||
self.redirect_uri, self.provider.redirect_uris.split()
|
||||
excepted=allowed_redirect_urls,
|
||||
)
|
||||
raise RedirectUriError(self.redirect_uri, allowed_redirect_urls)
|
||||
if self.request:
|
||||
raise AuthorizeError(
|
||||
self.redirect_uri, "request_not_supported", self.grant_type, self.state
|
||||
@ -189,6 +192,10 @@ class OAuthAuthorizationParams:
|
||||
|
||||
def check_nonce(self):
|
||||
"""Nonce parameter validation."""
|
||||
# https://openid.net/specs/openid-connect-core-1_0.html#ImplicitIDTValidation
|
||||
# Nonce is only required for Implicit flows
|
||||
if self.grant_type != GrantTypes.IMPLICIT:
|
||||
return
|
||||
if not self.nonce:
|
||||
self.nonce = self.state
|
||||
LOGGER.warning("Using state as nonce for OpenID Request")
|
||||
|
@ -8,6 +8,7 @@ from django.http import HttpRequest, HttpResponse
|
||||
from django.views import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.oauth2.constants import (
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
@ -30,6 +31,7 @@ LOGGER = get_logger()
|
||||
|
||||
|
||||
@dataclass
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class TokenParams:
|
||||
"""Token params"""
|
||||
|
||||
@ -40,6 +42,8 @@ class TokenParams:
|
||||
state: str
|
||||
scope: list[str]
|
||||
|
||||
provider: OAuth2Provider
|
||||
|
||||
authorization_code: Optional[AuthorizationCode] = None
|
||||
refresh_token: Optional[RefreshToken] = None
|
||||
|
||||
@ -47,35 +51,34 @@ class TokenParams:
|
||||
|
||||
raw_code: InitVar[str] = ""
|
||||
raw_token: InitVar[str] = ""
|
||||
request: InitVar[Optional[HttpRequest]] = None
|
||||
|
||||
@staticmethod
|
||||
def from_request(request: HttpRequest) -> "TokenParams":
|
||||
"""Extract Token Parameters from http request"""
|
||||
client_id, client_secret = extract_client_auth(request)
|
||||
|
||||
def parse(
|
||||
request: HttpRequest,
|
||||
provider: OAuth2Provider,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
) -> "TokenParams":
|
||||
"""Parse params for request"""
|
||||
return TokenParams(
|
||||
# Init vars
|
||||
raw_code=request.POST.get("code", ""),
|
||||
raw_token=request.POST.get("refresh_token", ""),
|
||||
request=request,
|
||||
# Regular params
|
||||
provider=provider,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uri=request.POST.get("redirect_uri", ""),
|
||||
grant_type=request.POST.get("grant_type", ""),
|
||||
raw_code=request.POST.get("code", ""),
|
||||
raw_token=request.POST.get("refresh_token", ""),
|
||||
state=request.POST.get("state", ""),
|
||||
scope=request.POST.get("scope", "").split(),
|
||||
# PKCE parameter.
|
||||
code_verifier=request.POST.get("code_verifier"),
|
||||
)
|
||||
|
||||
def __post_init__(self, raw_code, raw_token):
|
||||
try:
|
||||
provider: OAuth2Provider = OAuth2Provider.objects.get(
|
||||
client_id=self.client_id
|
||||
)
|
||||
self.provider = provider
|
||||
except OAuth2Provider.DoesNotExist:
|
||||
LOGGER.warning("OAuth2Provider does not exist", client_id=self.client_id)
|
||||
raise TokenError("invalid_client")
|
||||
|
||||
def __post_init__(self, raw_code: str, raw_token: str, request: HttpRequest):
|
||||
if self.provider.client_type == ClientTypes.CONFIDENTIAL:
|
||||
if self.provider.client_secret != self.client_secret:
|
||||
LOGGER.warning(
|
||||
@ -87,7 +90,6 @@ class TokenParams:
|
||||
|
||||
if self.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
||||
self.__post_init_code(raw_code)
|
||||
|
||||
elif self.grant_type == GRANT_TYPE_REFRESH_TOKEN:
|
||||
if not raw_token:
|
||||
LOGGER.warning("Missing refresh token")
|
||||
@ -107,7 +109,14 @@ class TokenParams:
|
||||
token=raw_token,
|
||||
)
|
||||
raise TokenError("invalid_grant")
|
||||
|
||||
if self.refresh_token.revoked:
|
||||
LOGGER.warning("Refresh token is revoked", token=raw_token)
|
||||
Event.new(
|
||||
action=EventAction.SUSPICIOUS_REQUEST,
|
||||
message="Revoked refresh token was used",
|
||||
token=raw_token,
|
||||
).from_http(request)
|
||||
raise TokenError("invalid_grant")
|
||||
else:
|
||||
LOGGER.warning("Invalid grant type", grant_type=self.grant_type)
|
||||
raise TokenError("unsupported_grant_type")
|
||||
@ -159,13 +168,14 @@ class TokenParams:
|
||||
class TokenView(View):
|
||||
"""Generate tokens for clients"""
|
||||
|
||||
provider: Optional[OAuth2Provider] = None
|
||||
params: Optional[TokenParams] = None
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
allowed_origins = []
|
||||
if self.params:
|
||||
allowed_origins = self.params.provider.redirect_uris.split("\n")
|
||||
if self.provider:
|
||||
allowed_origins = self.provider.redirect_uris.split("\n")
|
||||
cors_allow(self.request, response, *allowed_origins)
|
||||
return response
|
||||
|
||||
@ -175,19 +185,32 @@ class TokenView(View):
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Generate tokens for clients"""
|
||||
try:
|
||||
self.params = TokenParams.from_request(request)
|
||||
client_id, client_secret = extract_client_auth(request)
|
||||
try:
|
||||
self.provider = OAuth2Provider.objects.get(client_id=client_id)
|
||||
except OAuth2Provider.DoesNotExist:
|
||||
LOGGER.warning(
|
||||
"OAuth2Provider does not exist", client_id=self.client_id
|
||||
)
|
||||
raise TokenError("invalid_client")
|
||||
|
||||
if not self.provider:
|
||||
raise ValueError
|
||||
self.params = TokenParams.parse(
|
||||
request, self.provider, client_id, client_secret
|
||||
)
|
||||
|
||||
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
|
||||
return TokenResponse(self.create_code_response_dic())
|
||||
return TokenResponse(self.create_code_response())
|
||||
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
|
||||
return TokenResponse(self.create_refresh_response_dic())
|
||||
return TokenResponse(self.create_refresh_response())
|
||||
raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
|
||||
except TokenError as error:
|
||||
return TokenResponse(error.create_dict(), status=400)
|
||||
except UserAuthError as error:
|
||||
return TokenResponse(error.create_dict(), status=403)
|
||||
|
||||
def create_code_response_dic(self) -> dict[str, Any]:
|
||||
def create_code_response(self) -> dict[str, Any]:
|
||||
"""See https://tools.ietf.org/html/rfc6749#section-4.1"""
|
||||
|
||||
refresh_token = self.params.authorization_code.provider.create_refresh_token(
|
||||
@ -211,7 +234,7 @@ class TokenView(View):
|
||||
# We don't need to store the code anymore.
|
||||
self.params.authorization_code.delete()
|
||||
|
||||
response_dict = {
|
||||
return {
|
||||
"access_token": refresh_token.access_token,
|
||||
"refresh_token": refresh_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
@ -223,9 +246,7 @@ class TokenView(View):
|
||||
"id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
|
||||
}
|
||||
|
||||
return response_dict
|
||||
|
||||
def create_refresh_response_dic(self) -> dict[str, Any]:
|
||||
def create_refresh_response(self) -> dict[str, Any]:
|
||||
"""See https://tools.ietf.org/html/rfc6749#section-6"""
|
||||
|
||||
unauthorized_scopes = set(self.params.scope) - set(
|
||||
@ -253,10 +274,11 @@ class TokenView(View):
|
||||
# Store the refresh_token.
|
||||
refresh_token.save()
|
||||
|
||||
# Forget the old token.
|
||||
self.params.refresh_token.delete()
|
||||
# Mark old token as revoked
|
||||
self.params.refresh_token.revoked = True
|
||||
self.params.refresh_token.save()
|
||||
|
||||
dic = {
|
||||
return {
|
||||
"access_token": refresh_token.access_token,
|
||||
"refresh_token": refresh_token.refresh_token,
|
||||
"token_type": "bearer",
|
||||
@ -267,5 +289,3 @@ class TokenView(View):
|
||||
),
|
||||
"id_token": self.params.provider.encode(refresh_token.id_token.to_dict()),
|
||||
}
|
||||
|
||||
return dic
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""authentik OAuth2 OpenID Userinfo views"""
|
||||
from typing import Any, Optional
|
||||
|
||||
from deepmerge import always_merger
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.views import View
|
||||
@ -78,7 +79,7 @@ class UserInfoView(View):
|
||||
)
|
||||
continue
|
||||
LOGGER.debug("updated scope", scope=scope)
|
||||
final_claims.update(value)
|
||||
always_merger.merge(final_claims, value)
|
||||
return final_claims
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""SAML AuthNRequest Parser and dataclass"""
|
||||
from base64 import b64decode
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import xmlsec
|
||||
@ -54,7 +54,9 @@ class AuthNRequestParser:
|
||||
def __init__(self, provider: SAMLProvider):
|
||||
self.provider = provider
|
||||
|
||||
def _parse_xml(self, decoded_xml: str, relay_state: Optional[str]) -> AuthNRequest:
|
||||
def _parse_xml(
|
||||
self, decoded_xml: Union[str, bytes], relay_state: Optional[str]
|
||||
) -> AuthNRequest:
|
||||
root = ElementTree.fromstring(decoded_xml)
|
||||
|
||||
request_acs_url = root.attrib["AssertionConsumerServiceURL"]
|
||||
@ -79,10 +81,12 @@ class AuthNRequestParser:
|
||||
|
||||
return auth_n_request
|
||||
|
||||
def parse(self, saml_request: str, relay_state: Optional[str]) -> AuthNRequest:
|
||||
def parse(
|
||||
self, saml_request: str, relay_state: Optional[str] = None
|
||||
) -> AuthNRequest:
|
||||
"""Validate and parse raw request with enveloped signautre."""
|
||||
try:
|
||||
decoded_xml = b64decode(saml_request.encode()).decode()
|
||||
decoded_xml = b64decode(saml_request.encode())
|
||||
except UnicodeDecodeError:
|
||||
raise CannotHandleAssertion(ERROR_CANNOT_DECODE_REQUEST)
|
||||
|
||||
@ -93,8 +97,9 @@ class AuthNRequestParser:
|
||||
signature_nodes = root.xpath(
|
||||
"/samlp:AuthnRequest/ds:Signature", namespaces=NS_MAP
|
||||
)
|
||||
if len(signature_nodes) != 1:
|
||||
raise CannotHandleAssertion(ERROR_SIGNATURE_REQUIRED_BUT_ABSENT)
|
||||
# No signatures, no verifier configured -> decode xml directly
|
||||
if len(signature_nodes) < 1 and not verifier:
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
|
||||
signature_node = signature_nodes[0]
|
||||
|
||||
|
@ -14,13 +14,29 @@ from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||
from authentik.sources.saml.exceptions import MismatchedRequestID
|
||||
from authentik.sources.saml.models import SAMLSource
|
||||
from authentik.sources.saml.processors.constants import SAML_NAME_ID_FORMAT_UNSPECIFIED
|
||||
from authentik.sources.saml.processors.constants import (
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_UNSPECIFIED,
|
||||
)
|
||||
from authentik.sources.saml.processors.request import (
|
||||
SESSION_REQUEST_ID,
|
||||
RequestProcessor,
|
||||
)
|
||||
from authentik.sources.saml.processors.response import ResponseProcessor
|
||||
|
||||
POST_REQUEST = (
|
||||
"PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1sMn"
|
||||
"A9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgQXNzZXJ0aW9uQ29uc3VtZXJTZXJ2aWNlVVJMPSJo"
|
||||
"dHRwczovL2V1LWNlbnRyYWwtMS5zaWduaW4uYXdzLmFtYXpvbi5jb20vcGxhdGZvcm0vc2FtbC9hY3MvMmQ3MzdmOTYtNT"
|
||||
"VmYi00MDM1LTk1M2UtNWUyNDEzNGViNzc4IiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9pZC5iZXJ5anUub3JnL2FwcGxpY2F0"
|
||||
"aW9uL3NhbWwvYXdzLXNzby9zc28vYmluZGluZy9wb3N0LyIgSUQ9ImF3c19MRHhMR2V1YnBjNWx4MTJneENnUzZ1UGJpeD"
|
||||
"F5ZDVyZSIgSXNzdWVJbnN0YW50PSIyMDIxLTA3LTA2VDE0OjIzOjA2LjM4OFoiIFByb3RvY29sQmluZGluZz0idXJuOm9h"
|
||||
"c2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCIgVmVyc2lvbj0iMi4wIj48c2FtbDI6SXNzdWVyIH"
|
||||
"htbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwczovL2V1LWNlbnRyYWwt"
|
||||
"MS5zaWduaW4uYXdzLmFtYXpvbi5jb20vcGxhdGZvcm0vc2FtbC9kLTk5NjcyZjgyNzg8L3NhbWwyOklzc3Vlcj48c2FtbD"
|
||||
"JwOk5hbWVJRFBvbGljeSBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWls"
|
||||
"QWRkcmVzcyIvPjwvc2FtbDJwOkF1dGhuUmVxdWVzdD4="
|
||||
)
|
||||
REDIRECT_REQUEST = (
|
||||
"fZLNbsIwEIRfJfIdbKeFgEUipXAoEm0jSHvopTLJplhK7NTr9Oft6yRUKhekPdk73+yOdoWyqVuRdu6k9/DRAbrgu6k1iu"
|
||||
"EjJp3VwkhUKLRsAIUrxCF92IlwykRrjTOFqUmQIoJ1yui10dg1YA9gP1UBz/tdTE7OtSgo5WzKQzYditGeP8GW9rSQZk+H"
|
||||
@ -208,3 +224,22 @@ class TestAuthNRequest(TestCase):
|
||||
self.assertEqual(parsed_request.id, "_dcf55fcd27a887e60a7ef9ee6fd3adab")
|
||||
self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_UNSPECIFIED)
|
||||
self.assertEqual(parsed_request.relay_state, REDIRECT_RELAY_STATE)
|
||||
|
||||
def test_signed_static(self):
|
||||
"""Test post request with static request"""
|
||||
provider = SAMLProvider(
|
||||
name="aws",
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
acs_url=(
|
||||
"https://eu-central-1.signin.aws.amazon.com/platform/"
|
||||
"saml/acs/2d737f96-55fb-4035-953e-5e24134eb778"
|
||||
),
|
||||
audience="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
issuer="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
parsed_request = AuthNRequestParser(provider).parse(POST_REQUEST)
|
||||
self.assertEqual(parsed_request.id, "aws_LDxLGeubpc5lx12gxCgS6uPbix1yd5re")
|
||||
self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL)
|
||||
|
@ -15,7 +15,7 @@ class MessageConsumer(JsonWebsocketConsumer):
|
||||
cache.set(f"user_{self.session_key}_messages_{self.channel_name}", True, None)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def disconnect(self, close_code):
|
||||
def disconnect(self, code):
|
||||
cache.delete(f"user_{self.session_key}_messages_{self.channel_name}")
|
||||
|
||||
def event_update(self, event: dict):
|
||||
|
94
authentik/root/middleware.py
Normal file
94
authentik/root/middleware.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""Dynamically set SameSite depending if the upstream connection is TLS or not"""
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.backends.base import UpdateError
|
||||
from django.contrib.sessions.exceptions import SessionInterrupted
|
||||
from django.contrib.sessions.middleware import (
|
||||
SessionMiddleware as UpstreamSessionMiddleware,
|
||||
)
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.http import http_date
|
||||
|
||||
|
||||
class SessionMiddleware(UpstreamSessionMiddleware):
|
||||
"""Dynamically set SameSite depending if the upstream connection is TLS or not"""
|
||||
|
||||
@staticmethod
|
||||
def is_secure(request: HttpRequest) -> bool:
|
||||
"""Check if request is TLS'd or localhost"""
|
||||
if request.is_secure():
|
||||
return True
|
||||
host, _, _ = request.get_host().partition(":")
|
||||
if host == "localhost" and settings.DEBUG:
|
||||
# Since go does not consider localhost with http a secure origin
|
||||
# we can't set the secure flag.
|
||||
user_agent = request.META.get("HTTP_USER_AGENT", "")
|
||||
if user_agent.startswith("authentik-outpost@"):
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
def process_response(
|
||||
self, request: HttpRequest, response: HttpResponse
|
||||
) -> HttpResponse:
|
||||
"""
|
||||
If request.session was modified, or if the configuration is to save the
|
||||
session every time, save the changes and set a session cookie or delete
|
||||
the session cookie if the session has been emptied.
|
||||
"""
|
||||
try:
|
||||
accessed = request.session.accessed
|
||||
modified = request.session.modified
|
||||
empty = request.session.is_empty()
|
||||
except AttributeError:
|
||||
return response
|
||||
# Set SameSite based on whether or not the request is secure
|
||||
secure = SessionMiddleware.is_secure(request)
|
||||
same_site = "None" if secure else "Lax"
|
||||
# First check if we need to delete this cookie.
|
||||
# The session should be deleted only if the session is entirely empty.
|
||||
if settings.SESSION_COOKIE_NAME in request.COOKIES and empty:
|
||||
response.delete_cookie(
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
samesite=same_site,
|
||||
)
|
||||
patch_vary_headers(response, ("Cookie",))
|
||||
else:
|
||||
if accessed:
|
||||
patch_vary_headers(response, ("Cookie",))
|
||||
if (modified or settings.SESSION_SAVE_EVERY_REQUEST) and not empty:
|
||||
if request.session.get_expire_at_browser_close():
|
||||
max_age = None
|
||||
expires = None
|
||||
else:
|
||||
max_age = request.session.get_expiry_age()
|
||||
expires_time = time.time() + max_age
|
||||
expires = http_date(expires_time)
|
||||
# Save the session data and refresh the client cookie.
|
||||
# Skip session save for 500 responses, refs #3881.
|
||||
if response.status_code != 500:
|
||||
try:
|
||||
request.session.save()
|
||||
except UpdateError:
|
||||
raise SessionInterrupted(
|
||||
"The request's session was deleted before the "
|
||||
"request completed. The user may have logged "
|
||||
"out in a concurrent request, for example."
|
||||
)
|
||||
response.set_cookie(
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
request.session.session_key,
|
||||
max_age=max_age,
|
||||
expires=expires,
|
||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=secure,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None,
|
||||
samesite=same_site,
|
||||
)
|
||||
return response
|
@ -188,12 +188,19 @@ REST_FRAMEWORK = {
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
REDIS_PROTOCOL_PREFIX = "redis://"
|
||||
REDIS_CELERY_TLS_REQUIREMENTS = ""
|
||||
if CONFIG.y_bool("redis.tls", False):
|
||||
REDIS_PROTOCOL_PREFIX = "rediss://"
|
||||
REDIS_CELERY_TLS_REQUIREMENTS = f"?ssl_cert_reqs={CONFIG.y('redis.tls_reqs')}"
|
||||
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": (
|
||||
f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379"
|
||||
f"/{CONFIG.y('redis.cache_db')}"
|
||||
f"{REDIS_PROTOCOL_PREFIX}:"
|
||||
f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
|
||||
f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.cache_db')}"
|
||||
),
|
||||
"TIMEOUT": int(CONFIG.y("redis.cache_timeout", 300)),
|
||||
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
|
||||
@ -203,14 +210,16 @@ DJANGO_REDIS_IGNORE_EXCEPTIONS = True
|
||||
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
||||
SESSION_CACHE_ALIAS = "default"
|
||||
SESSION_COOKIE_SAMESITE = "lax"
|
||||
# Configured via custom SessionMiddleware
|
||||
# SESSION_COOKIE_SAMESITE = "None"
|
||||
# SESSION_COOKIE_SECURE = True
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
|
||||
MESSAGE_STORAGE = "authentik.root.messages.storage.ChannelsStorage"
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django_prometheus.middleware.PrometheusBeforeMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"authentik.root.middleware.SessionMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"authentik.core.middleware.RequestIDMiddleware",
|
||||
"authentik.tenants.middleware.TenantMiddleware",
|
||||
@ -250,8 +259,9 @@ CHANNEL_LAYERS = {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [
|
||||
f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379"
|
||||
f"/{CONFIG.y('redis.ws_db')}"
|
||||
f"{REDIS_PROTOCOL_PREFIX}:"
|
||||
f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
|
||||
f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.ws_db')}"
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -329,12 +339,16 @@ CELERY_BEAT_SCHEDULE = {
|
||||
CELERY_TASK_CREATE_MISSING_QUEUES = True
|
||||
CELERY_TASK_DEFAULT_QUEUE = "authentik"
|
||||
CELERY_BROKER_URL = (
|
||||
f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}"
|
||||
f":6379/{CONFIG.y('redis.message_queue_db')}"
|
||||
f"{REDIS_PROTOCOL_PREFIX}:"
|
||||
f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
|
||||
f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.message_queue_db')}"
|
||||
f"{REDIS_CELERY_TLS_REQUIREMENTS}"
|
||||
)
|
||||
CELERY_RESULT_BACKEND = (
|
||||
f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}"
|
||||
f":6379/{CONFIG.y('redis.message_queue_db')}"
|
||||
f"{REDIS_PROTOCOL_PREFIX}:"
|
||||
f"{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:"
|
||||
f"{int(CONFIG.y('redis.port'))}/{CONFIG.y('redis.message_queue_db')}"
|
||||
f"{REDIS_CELERY_TLS_REQUIREMENTS}"
|
||||
)
|
||||
|
||||
# Database backup
|
||||
@ -362,11 +376,12 @@ if CONFIG.y("postgresql.s3_backup"):
|
||||
)
|
||||
|
||||
# Sentry integration
|
||||
SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
|
||||
_ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False)
|
||||
if _ERROR_REPORTING:
|
||||
# pylint: disable=abstract-class-instantiated
|
||||
sentry_init(
|
||||
dsn="https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
|
||||
dsn=SENTRY_DSN,
|
||||
integrations=[
|
||||
DjangoIntegration(transaction_style="function_name"),
|
||||
CeleryIntegration(),
|
||||
@ -401,9 +416,8 @@ MEDIA_URL = "/media/"
|
||||
|
||||
TEST = False
|
||||
TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
|
||||
|
||||
LOG_LEVEL = CONFIG.y("log_level").upper() if not TEST else "DEBUG"
|
||||
|
||||
# We can't check TEST here as its set later by the test runner
|
||||
LOG_LEVEL = CONFIG.y("log_level").upper() if "TF_BUILD" not in os.environ else "DEBUG"
|
||||
|
||||
structlog.configure_once(
|
||||
processors=[
|
||||
|
@ -36,7 +36,8 @@ class SourceType:
|
||||
class SourceTypeManager:
|
||||
"""Manager to hold all Source types."""
|
||||
|
||||
__sources: list[SourceType] = []
|
||||
def __init__(self) -> None:
|
||||
self.__sources: list[SourceType] = []
|
||||
|
||||
def type(self):
|
||||
"""Class decorator to register classes inline."""
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""OAuth Callback Views"""
|
||||
from json import JSONDecodeError
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.conf import settings
|
||||
@ -10,6 +11,7 @@ from django.views.generic import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.sources.flow_manager import SourceFlowManager
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.views.base import OAuthClientMixin
|
||||
|
||||
@ -42,8 +44,16 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||
if "error" in token:
|
||||
return self.handle_login_failure(token["error"])
|
||||
# Fetch profile info
|
||||
raw_info = client.get_profile_info(token)
|
||||
if raw_info is None:
|
||||
try:
|
||||
raw_info = client.get_profile_info(token)
|
||||
if raw_info is None:
|
||||
return self.handle_login_failure("Could not retrieve profile.")
|
||||
except JSONDecodeError as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message="Failed to JSON-decode profile.",
|
||||
raw_profile=exc.doc,
|
||||
).from_http(self.request)
|
||||
return self.handle_login_failure("Could not retrieve profile.")
|
||||
identifier = self.get_user_id(raw_info)
|
||||
if identifier is None:
|
||||
|
@ -74,12 +74,12 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
||||
duo, self.stage.request, self.stage.get_pending_user()
|
||||
)
|
||||
|
||||
def validate(self, data: dict):
|
||||
def validate(self, attrs: dict):
|
||||
# Checking if the given data is from a valid device class is done above
|
||||
# Here we only check if the any data was sent at all
|
||||
if "code" not in data and "webauthn" not in data and "duo" not in data:
|
||||
if "code" not in attrs and "webauthn" not in attrs and "duo" not in attrs:
|
||||
raise ValidationError("Empty response")
|
||||
return data
|
||||
return attrs
|
||||
|
||||
|
||||
class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
@ -163,7 +163,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def challenge_valid(
|
||||
self, challenge: AuthenticatorValidationChallengeResponse
|
||||
self, response: AuthenticatorValidationChallengeResponse
|
||||
) -> HttpResponse:
|
||||
# All validation is done by the serializer
|
||||
return self.executor.stage_ok()
|
||||
|
@ -38,7 +38,7 @@ class EmailChallengeResponse(ChallengeResponse):
|
||||
|
||||
component = CharField(default="ak-stage-email")
|
||||
|
||||
def validate(self, data):
|
||||
def validate(self, attrs):
|
||||
raise ValidationError("")
|
||||
|
||||
|
||||
|
@ -73,9 +73,9 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
|
||||
pre_user: Optional[User] = None
|
||||
|
||||
def validate(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate that user exists, and optionally their password"""
|
||||
uid_field = data["uid_field"]
|
||||
uid_field = attrs["uid_field"]
|
||||
current_stage: IdentificationStage = self.stage.executor.current_stage
|
||||
|
||||
pre_user = self.stage.get_user(uid_field)
|
||||
@ -101,9 +101,9 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
self.pre_user = pre_user
|
||||
if not current_stage.password_stage:
|
||||
# No password stage select, don't validate the password
|
||||
return data
|
||||
return attrs
|
||||
|
||||
password = data["password"]
|
||||
password = attrs["password"]
|
||||
try:
|
||||
user = authenticate(
|
||||
self.stage.request,
|
||||
@ -116,7 +116,7 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
||||
self.pre_user = user
|
||||
except PermissionDenied as exc:
|
||||
raise ValidationError(str(exc)) from exc
|
||||
return data
|
||||
return attrs
|
||||
|
||||
|
||||
class IdentificationStageView(ChallengeStageView):
|
||||
|
@ -146,8 +146,6 @@ def password_single_validator_factory() -> Callable[[PromptChallenge, str], Any]
|
||||
class ListPolicyEngine(PolicyEngine):
|
||||
"""Slightly modified policy engine, which uses a list instead of a PolicyBindingModel"""
|
||||
|
||||
__list: list[Policy]
|
||||
|
||||
def __init__(
|
||||
self, policies: list[Policy], user: User, request: HttpRequest = None
|
||||
) -> None:
|
||||
|
@ -24,6 +24,10 @@ LOGGER = get_logger()
|
||||
class UserWriteStageView(StageView):
|
||||
"""Finalise Enrollment flow by creating a user object."""
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Wrapper for post requests"""
|
||||
return self.get(request)
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Save data in the current flow to the currently pending user. If no user is pending,
|
||||
a new user is created."""
|
||||
|
@ -27,7 +27,7 @@ stages:
|
||||
script: make gen-outpost
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: 'outpost/api/'
|
||||
targetPath: 'api/'
|
||||
artifact: 'go_api_client'
|
||||
publishLocation: 'pipeline'
|
||||
- stage: lint
|
||||
@ -43,17 +43,19 @@ stages:
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'go_api_client'
|
||||
path: "outpost/api/"
|
||||
path: "api/"
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
mkdir -p web/dist
|
||||
mkdir -p website/help
|
||||
touch web/dist/test website/help/test
|
||||
docker run \
|
||||
--rm \
|
||||
-v $(pwd):/app \
|
||||
-w /app \
|
||||
golangci/golangci-lint:v1.39.0 \
|
||||
golangci-lint run -v --timeout 200s
|
||||
workingDirectory: 'outpost/'
|
||||
- stage: build_docker
|
||||
jobs:
|
||||
- job: proxy_build_docker
|
||||
@ -73,7 +75,7 @@ stages:
|
||||
containerRegistry: 'beryjuorg-harbor'
|
||||
repository: 'authentik/outpost-proxy'
|
||||
command: 'build'
|
||||
Dockerfile: 'outpost/proxy.Dockerfile'
|
||||
Dockerfile: 'proxy.Dockerfile'
|
||||
buildContext: '$(Build.SourcesDirectory)'
|
||||
tags: |
|
||||
gh-$(branchName)
|
||||
@ -106,7 +108,7 @@ stages:
|
||||
containerRegistry: 'beryjuorg-harbor'
|
||||
repository: 'authentik/outpost-ldap'
|
||||
command: 'build'
|
||||
Dockerfile: 'outpost/ldap.Dockerfile'
|
||||
Dockerfile: 'ldap.Dockerfile'
|
||||
buildContext: '$(Build.SourcesDirectory)'
|
||||
tags: |
|
||||
gh-$(branchName)
|
@ -2,16 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"goauthentik.io/outpost/pkg/ak"
|
||||
"goauthentik.io/outpost/pkg/ldap"
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/ldap"
|
||||
)
|
||||
|
||||
const helpMessage = `authentik ldap
|
||||
@ -23,32 +21,30 @@ Required environment variables:
|
||||
|
||||
func main() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
pbURL, found := os.LookupEnv("AUTHENTIK_HOST")
|
||||
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
|
||||
if !found {
|
||||
fmt.Println("env AUTHENTIK_HOST not set!")
|
||||
fmt.Println(helpMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
pbToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
|
||||
akToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
|
||||
if !found {
|
||||
fmt.Println("env AUTHENTIK_TOKEN not set!")
|
||||
fmt.Println(helpMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pbURLActual, err := url.Parse(pbURL)
|
||||
akURLActual, err := url.Parse(akURL)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(helpMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
ex := common.Init()
|
||||
defer common.Defer()
|
||||
|
||||
ac := ak.NewAPIController(*pbURLActual, pbToken)
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
ac := ak.NewAPIController(*akURLActual, akToken)
|
||||
|
||||
ac.Server = ldap.NewServer(ac)
|
||||
|
||||
@ -58,7 +54,7 @@ func main() {
|
||||
}
|
||||
|
||||
for {
|
||||
<-interrupt
|
||||
<-ex
|
||||
ac.Shutdown()
|
||||
os.Exit(0)
|
||||
}
|
@ -2,16 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"goauthentik.io/outpost/pkg/ak"
|
||||
"goauthentik.io/outpost/pkg/proxy"
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/proxy"
|
||||
)
|
||||
|
||||
const helpMessage = `authentik proxy
|
||||
@ -23,32 +21,30 @@ Required environment variables:
|
||||
|
||||
func main() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
pbURL, found := os.LookupEnv("AUTHENTIK_HOST")
|
||||
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
|
||||
if !found {
|
||||
fmt.Println("env AUTHENTIK_HOST not set!")
|
||||
fmt.Println(helpMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
pbToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
|
||||
akToken, found := os.LookupEnv("AUTHENTIK_TOKEN")
|
||||
if !found {
|
||||
fmt.Println("env AUTHENTIK_TOKEN not set!")
|
||||
fmt.Println(helpMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pbURLActual, err := url.Parse(pbURL)
|
||||
akURLActual, err := url.Parse(akURL)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println(helpMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
ex := common.Init()
|
||||
defer common.Defer()
|
||||
|
||||
ac := ak.NewAPIController(*pbURLActual, pbToken)
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
ac := ak.NewAPIController(*akURLActual, akToken)
|
||||
|
||||
ac.Server = proxy.NewServer(ac)
|
||||
|
||||
@ -58,7 +54,7 @@ func main() {
|
||||
}
|
||||
|
||||
for {
|
||||
<-interrupt
|
||||
<-ex
|
||||
ac.Shutdown()
|
||||
os.Exit(0)
|
||||
}
|
@ -2,51 +2,106 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/gounicorn"
|
||||
"goauthentik.io/internal/web"
|
||||
)
|
||||
|
||||
var running = true
|
||||
|
||||
func main() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
config.DefaultConfig()
|
||||
config.LoadConfig("./authentik/lib/default.yml")
|
||||
config.LoadConfig("./local.env.yml")
|
||||
err := config.LoadConfig("./authentik/lib/default.yml")
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to load default config")
|
||||
}
|
||||
err = config.LoadConfig("./local.env.yml")
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("failed to local config")
|
||||
}
|
||||
config.ConfigureLogger()
|
||||
|
||||
if config.G.ErrorReporting.Enabled {
|
||||
sentry.Init(sentry.ClientOptions{
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
|
||||
AttachStacktrace: true,
|
||||
TracesSampleRate: 0.6,
|
||||
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
|
||||
Environment: config.G.ErrorReporting.Environment,
|
||||
})
|
||||
defer sentry.Flush(time.Second * 5)
|
||||
defer sentry.Recover()
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to init sentry")
|
||||
}
|
||||
}
|
||||
|
||||
rl := log.WithField("logger", "authentik.g")
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
g := gounicorn.NewGoUnicorn()
|
||||
for {
|
||||
err := g.Start()
|
||||
rl.WithError(err).Warning("gunicorn process died, restarting")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ws := web.NewWebServer()
|
||||
ws.Run()
|
||||
}()
|
||||
wg.Wait()
|
||||
ex := common.Init()
|
||||
defer common.Defer()
|
||||
|
||||
// u, _ := url.Parse("http://localhost:8000")
|
||||
|
||||
g := gounicorn.NewGoUnicorn()
|
||||
ws := web.NewWebServer()
|
||||
defer g.Kill()
|
||||
defer ws.Shutdown()
|
||||
for {
|
||||
go attemptStartBackend(g)
|
||||
ws.Start()
|
||||
// go attemptProxyStart(u)
|
||||
|
||||
<-ex
|
||||
running = false
|
||||
log.WithField("logger", "authentik").Info("shutting down webserver")
|
||||
go ws.Shutdown()
|
||||
log.WithField("logger", "authentik").Info("killing gunicorn")
|
||||
g.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
func attemptStartBackend(g *gounicorn.GoUnicorn) {
|
||||
for {
|
||||
err := g.Start()
|
||||
if !running {
|
||||
return
|
||||
}
|
||||
log.WithField("logger", "authentik.g").WithError(err).Warning("gunicorn process died, restarting")
|
||||
}
|
||||
}
|
||||
|
||||
// func attemptProxyStart(u *url.URL) error {
|
||||
// maxTries := 100
|
||||
// attempt := 0
|
||||
// for {
|
||||
// log.WithField("logger", "authentik").Debug("attempting to init outpost")
|
||||
// ac := ak.NewAPIController(*u, config.G.SecretKey)
|
||||
// if ac == nil {
|
||||
// attempt += 1
|
||||
// time.Sleep(1 * time.Second)
|
||||
// if attempt > maxTries {
|
||||
// break
|
||||
// }
|
||||
// continue
|
||||
// }
|
||||
// ac.Server = proxy.NewServer(ac)
|
||||
// err := ac.Start()
|
||||
// log.WithField("logger", "authentik").Debug("attempting to start outpost")
|
||||
// if err != nil {
|
||||
// attempt += 1
|
||||
// time.Sleep(5 * time.Second)
|
||||
// if attempt > maxTries {
|
||||
// break
|
||||
// }
|
||||
// continue
|
||||
// }
|
||||
// if !running {
|
||||
// ac.Shutdown()
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
@ -21,7 +21,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.3}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.1-rc1}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -44,7 +44,7 @@ services:
|
||||
- "0.0.0.0:9000:9000"
|
||||
- "0.0.0.0:9443:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.6.3}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.1-rc1}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
networks:
|
||||
|
50
go.mod
50
go.mod
@ -3,11 +3,49 @@ module goauthentik.io
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/getsentry/sentry-go v0.10.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/getsentry/sentry-go v0.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.3.0
|
||||
github.com/go-openapi/analysis v0.20.1 // indirect
|
||||
github.com/go-openapi/errors v0.20.0 // indirect
|
||||
github.com/go-openapi/runtime v0.19.29
|
||||
github.com/go-openapi/strfmt v0.20.1
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-openapi/validate v0.20.2 // indirect
|
||||
github.com/go-redis/redis/v7 v7.4.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a
|
||||
github.com/justinas/alice v1.2.0
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 // indirect
|
||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3
|
||||
github.com/oauth2-proxy/oauth2-proxy v0.0.0-20200831161845-e4e5580852dc
|
||||
github.com/pelletier/go-toml v1.9.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.6.0 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
|
||||
github.com/recws-org/recws v1.3.1
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.7.1 // indirect
|
||||
go.mongodb.org/mongo-driver v1.5.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 // indirect
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
22
internal/common/global.go
Normal file
22
internal/common/global.go
Normal file
@ -0,0 +1,22 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
func Init() chan os.Signal {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, os.Interrupt)
|
||||
return interrupt
|
||||
}
|
||||
|
||||
func Defer() {
|
||||
defer sentry.Flush(time.Second * 5)
|
||||
defer sentry.Recover()
|
||||
}
|
@ -2,6 +2,7 @@ package config
|
||||
|
||||
type Config struct {
|
||||
Debug bool `yaml:"debug"`
|
||||
SecretKey string `yaml:"secret_key"`
|
||||
Web WebConfig `yaml:"web"`
|
||||
Paths PathsConfig `yaml:"paths"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
|
@ -1,3 +1,20 @@
|
||||
package constants
|
||||
|
||||
const VERSION = "2021.6.3"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func BUILD() string {
|
||||
build := os.Getenv("GIT_BUILD_HASH")
|
||||
if build == "" {
|
||||
return "tagged"
|
||||
}
|
||||
return build
|
||||
}
|
||||
|
||||
func OutpostUserAgent() string {
|
||||
return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD())
|
||||
}
|
||||
|
||||
const VERSION = "2021.7.1-rc1"
|
||||
|
@ -9,16 +9,24 @@ import (
|
||||
)
|
||||
|
||||
type GoUnicorn struct {
|
||||
log *log.Entry
|
||||
log *log.Entry
|
||||
p *exec.Cmd
|
||||
started bool
|
||||
killed bool
|
||||
}
|
||||
|
||||
func NewGoUnicorn() *GoUnicorn {
|
||||
return &GoUnicorn{
|
||||
log: log.WithField("logger", "authentik.g.unicorn"),
|
||||
logger := log.WithField("logger", "authentik.g.unicorn")
|
||||
g := &GoUnicorn{
|
||||
log: logger,
|
||||
started: false,
|
||||
killed: false,
|
||||
}
|
||||
g.initCmd()
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GoUnicorn) Start() error {
|
||||
func (g *GoUnicorn) initCmd() {
|
||||
command := "gunicorn"
|
||||
args := []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi:application"}
|
||||
if config.G.Debug {
|
||||
@ -26,11 +34,28 @@ func (g *GoUnicorn) Start() error {
|
||||
args = []string{"manage.py", "runserver", "localhost:8000"}
|
||||
}
|
||||
g.log.WithField("args", args).WithField("cmd", command).Debug("Starting gunicorn")
|
||||
p := exec.Command(command, args...)
|
||||
p.Env = append(os.Environ(),
|
||||
"WORKERS=2",
|
||||
)
|
||||
p.Stdout = os.Stdout
|
||||
p.Stderr = os.Stderr
|
||||
return p.Run()
|
||||
g.p = exec.Command(command, args...)
|
||||
g.p.Env = os.Environ()
|
||||
g.p.Stdout = os.Stdout
|
||||
g.p.Stderr = os.Stderr
|
||||
}
|
||||
|
||||
func (g *GoUnicorn) Start() error {
|
||||
if g.killed {
|
||||
g.log.Debug("Not restarting gunicorn since we're killed")
|
||||
return nil
|
||||
}
|
||||
if g.started {
|
||||
g.initCmd()
|
||||
}
|
||||
g.started = true
|
||||
return g.p.Run()
|
||||
}
|
||||
|
||||
func (g *GoUnicorn) Kill() {
|
||||
g.killed = true
|
||||
err := g.p.Process.Kill()
|
||||
if err != nil {
|
||||
g.log.WithError(err).Warning("failed to kill gunicorn")
|
||||
}
|
||||
}
|
||||
|
@ -6,15 +6,14 @@ import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/recws-org/recws"
|
||||
"goauthentik.io/outpost/api"
|
||||
"goauthentik.io/outpost/pkg"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/constants"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -41,10 +40,11 @@ type APIController struct {
|
||||
// NewAPIController initialise new API Controller instance from URL and API token
|
||||
func NewAPIController(akURL url.URL, token string) *APIController {
|
||||
config := api.NewConfiguration()
|
||||
config.UserAgent = constants.OutpostUserAgent()
|
||||
config.Host = akURL.Host
|
||||
config.Scheme = akURL.Scheme
|
||||
config.HTTPClient = &http.Client{
|
||||
Transport: SetUserAgent(GetTLSTransport(), pkg.UserAgent()),
|
||||
Transport: GetTLSTransport(),
|
||||
}
|
||||
config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
@ -59,7 +59,7 @@ func NewAPIController(akURL url.URL, token string) *APIController {
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to fetch configuration")
|
||||
os.Exit(1)
|
||||
return nil
|
||||
}
|
||||
outpost := outposts.Results[0]
|
||||
doGlobalSetup(outpost.Config)
|
@ -3,7 +3,7 @@ package ak
|
||||
import (
|
||||
"context"
|
||||
|
||||
"goauthentik.io/outpost/api"
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
|
||||
func (a *APIController) Update() ([]api.ProxyOutpostConfig, error) {
|
@ -12,7 +12,7 @@ import (
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/recws-org/recws"
|
||||
"goauthentik.io/outpost/pkg"
|
||||
"goauthentik.io/internal/constants"
|
||||
)
|
||||
|
||||
func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
|
||||
@ -23,7 +23,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
|
||||
|
||||
header := http.Header{
|
||||
"Authorization": []string{authHeader},
|
||||
"User-Agent": []string{pkg.UserAgent()},
|
||||
"User-Agent": []string{constants.OutpostUserAgent()},
|
||||
}
|
||||
|
||||
value, set := os.LookupEnv("AUTHENTIK_INSECURE")
|
||||
@ -46,8 +46,8 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
|
||||
msg := websocketMessage{
|
||||
Instruction: WebsocketInstructionHello,
|
||||
Args: map[string]interface{}{
|
||||
"version": pkg.VERSION,
|
||||
"buildHash": pkg.BUILD(),
|
||||
"version": constants.VERSION,
|
||||
"buildHash": constants.BUILD(),
|
||||
"uuid": ac.instanceUUID.String(),
|
||||
},
|
||||
}
|
||||
@ -101,8 +101,8 @@ func (ac *APIController) startWSHealth() {
|
||||
aliveMsg := websocketMessage{
|
||||
Instruction: WebsocketInstructionHello,
|
||||
Args: map[string]interface{}{
|
||||
"version": pkg.VERSION,
|
||||
"buildHash": pkg.BUILD(),
|
||||
"version": constants.VERSION,
|
||||
"buildHash": constants.BUILD(),
|
||||
"uuid": ac.instanceUUID.String(),
|
||||
},
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package ak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@ -9,7 +11,8 @@ import (
|
||||
"github.com/getsentry/sentry-go"
|
||||
httptransport "github.com/go-openapi/runtime/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/outpost/pkg"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/constants"
|
||||
)
|
||||
|
||||
func doGlobalSetup(config map[string]interface{}) {
|
||||
@ -33,7 +36,7 @@ func doGlobalSetup(config map[string]interface{}) {
|
||||
default:
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
log.WithField("buildHash", pkg.BUILD()).WithField("version", pkg.VERSION).Info("Starting authentik outpost")
|
||||
log.WithField("buildHash", constants.BUILD()).WithField("version", constants.VERSION).Info("Starting authentik outpost")
|
||||
|
||||
var dsn string
|
||||
if config[ConfigErrorReportingEnabled].(bool) {
|
||||
@ -66,3 +69,21 @@ func GetTLSTransport() http.RoundTripper {
|
||||
}
|
||||
return tlsTransport
|
||||
}
|
||||
|
||||
// ParseCertificate Load certificate from Keyepair UUID and parse it into a go Certificate
|
||||
func ParseCertificate(kpUuid string, cryptoApi *api.CryptoApiService) (*tls.Certificate, error) {
|
||||
cert, _, err := cryptoApi.CryptoCertificatekeypairsViewCertificateRetrieve(context.Background(), kpUuid).Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, _, err := cryptoApi.CryptoCertificatekeypairsViewPrivateKeyRetrieve(context.Background(), kpUuid).Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x509cert, err := tls.X509KeyPair([]byte(cert.Data), []byte(key.Data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &x509cert, nil
|
||||
}
|
177
internal/outpost/flow.go
Normal file
177
internal/outpost/flow.go
Normal file
@ -0,0 +1,177 @@
|
||||
package outpost
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
)
|
||||
|
||||
type StageComponent string
|
||||
|
||||
const (
|
||||
StageIdentification = StageComponent("ak-stage-identification")
|
||||
StagePassword = StageComponent("ak-stage-password")
|
||||
StageAuthenticatorValidate = StageComponent("ak-stage-authenticator-validate")
|
||||
StageAccessDenied = StageComponent("ak-stage-access-denied")
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderAuthentikRemoteIP = "X-authentik-remote-ip"
|
||||
HeaderAuthentikOutpostToken = "X-authentik-outpost-token"
|
||||
)
|
||||
|
||||
type FlowExecutor struct {
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
|
||||
api *api.APIClient
|
||||
flowSlug string
|
||||
log *log.Entry
|
||||
token string
|
||||
}
|
||||
|
||||
func NewFlowExecutor(flowSlug string, refConfig *api.Configuration, logFields log.Fields) *FlowExecutor {
|
||||
l := log.WithField("flow", flowSlug).WithFields(logFields)
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
l.WithError(err).Warning("Failed to create cookiejar")
|
||||
panic(err)
|
||||
}
|
||||
// Create new http client that also sets the correct ip
|
||||
config := api.NewConfiguration()
|
||||
config.Host = refConfig.Host
|
||||
config.Scheme = refConfig.Scheme
|
||||
config.UserAgent = constants.OutpostUserAgent()
|
||||
config.HTTPClient = &http.Client{
|
||||
Jar: jar,
|
||||
Transport: ak.GetTLSTransport(),
|
||||
}
|
||||
apiClient := api.NewAPIClient(config)
|
||||
return &FlowExecutor{
|
||||
Params: url.Values{},
|
||||
Answers: make(map[StageComponent]string),
|
||||
api: apiClient,
|
||||
flowSlug: flowSlug,
|
||||
log: l,
|
||||
token: strings.Split(refConfig.DefaultHeader["Authorization"], " ")[1],
|
||||
}
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) ApiClient() *api.APIClient {
|
||||
return fe.api
|
||||
}
|
||||
|
||||
type ChallengeInt interface {
|
||||
GetComponent() string
|
||||
GetType() api.ChallengeChoices
|
||||
GetResponseErrors() map[string][]api.ErrorDetail
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) DelegateClientIP(a net.Addr) {
|
||||
host, _, err := net.SplitHostPort(a.String())
|
||||
if err != nil {
|
||||
fe.log.WithError(err).Warning("Failed to get remote IP")
|
||||
return
|
||||
}
|
||||
fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, host)
|
||||
fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikOutpostToken, fe.token)
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) {
|
||||
p, _, err := fe.api.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), appSlug).Execute()
|
||||
if !p.Passing {
|
||||
fe.log.Info("Access denied for user")
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to check access: %w", err)
|
||||
}
|
||||
fe.log.Debug("User has access")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) getAnswer(stage StageComponent) string {
|
||||
if v, o := fe.Answers[stage]; o {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) Execute() (bool, error) {
|
||||
return fe.solveFlowChallenge(1)
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) {
|
||||
req := fe.api.FlowsApi.FlowsExecutorGet(context.Background(), fe.flowSlug).Query(fe.Params.Encode())
|
||||
challenge, _, err := req.Execute()
|
||||
if err != nil {
|
||||
return false, errors.New("failed to get challenge")
|
||||
}
|
||||
ch := challenge.GetActualInstance().(ChallengeInt)
|
||||
fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got challenge")
|
||||
responseReq := fe.api.FlowsApi.FlowsExecutorSolve(context.Background(), fe.flowSlug).Query(fe.Params.Encode())
|
||||
switch ch.GetComponent() {
|
||||
case string(StageIdentification):
|
||||
responseReq = responseReq.FlowChallengeResponseRequest(api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewIdentificationChallengeResponseRequest(fe.getAnswer(StageIdentification))))
|
||||
case string(StagePassword):
|
||||
responseReq = responseReq.FlowChallengeResponseRequest(api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword))))
|
||||
case string(StageAuthenticatorValidate):
|
||||
// We only support duo as authenticator, check if that's allowed
|
||||
var deviceChallenge *api.DeviceChallenge
|
||||
for _, devCh := range challenge.AuthenticatorValidationChallenge.DeviceChallenges {
|
||||
if devCh.DeviceClass == string(api.DEVICECLASSESENUM_DUO) {
|
||||
deviceChallenge = &devCh
|
||||
}
|
||||
}
|
||||
if deviceChallenge == nil {
|
||||
return false, errors.New("got ak-stage-authenticator-validate without duo")
|
||||
}
|
||||
devId, err := strconv.Atoi(deviceChallenge.DeviceUid)
|
||||
if err != nil {
|
||||
return false, errors.New("failed to convert duo device id to int")
|
||||
}
|
||||
devId32 := int32(devId)
|
||||
inner := api.NewAuthenticatorValidationChallengeResponseRequest()
|
||||
inner.Duo = &devId32
|
||||
responseReq = responseReq.FlowChallengeResponseRequest(api.AuthenticatorValidationChallengeResponseRequestAsFlowChallengeResponseRequest(inner))
|
||||
case string(StageAccessDenied):
|
||||
return false, errors.New("got ak-stage-access-denied")
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
}
|
||||
response, _, err := responseReq.Execute()
|
||||
ch = response.GetActualInstance().(ChallengeInt)
|
||||
fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got response")
|
||||
switch ch.GetComponent() {
|
||||
case string(StageAccessDenied):
|
||||
return false, errors.New("got ak-stage-access-denied")
|
||||
}
|
||||
if ch.GetType() == "redirect" {
|
||||
return true, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to submit challenge %w", err)
|
||||
}
|
||||
if len(ch.GetResponseErrors()) > 0 {
|
||||
for key, errs := range ch.GetResponseErrors() {
|
||||
for _, err := range errs {
|
||||
return false, fmt.Errorf("flow error %s: %s", key, err.String)
|
||||
}
|
||||
}
|
||||
}
|
||||
if depth >= 10 {
|
||||
return false, errors.New("exceeded stage recursion depth")
|
||||
}
|
||||
return fe.solveFlowChallenge(depth + 1)
|
||||
}
|
@ -2,14 +2,18 @@ package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/pires/go-proxyproto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) Refresh() error {
|
||||
@ -24,6 +28,7 @@ func (ls *LDAPServer) Refresh() error {
|
||||
for idx, provider := range outposts.Results {
|
||||
userDN := strings.ToLower(fmt.Sprintf("ou=users,%s", *provider.BaseDn))
|
||||
groupDN := strings.ToLower(fmt.Sprintf("ou=groups,%s", *provider.BaseDn))
|
||||
logger := log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name)
|
||||
providers[idx] = &ProviderInstance{
|
||||
BaseDN: *provider.BaseDn,
|
||||
GroupDN: groupDN,
|
||||
@ -34,7 +39,20 @@ func (ls *LDAPServer) Refresh() error {
|
||||
boundUsersMutex: sync.RWMutex{},
|
||||
boundUsers: make(map[string]UserFlags),
|
||||
s: ls,
|
||||
log: log.WithField("logger", "authentik.outpost.ldap").WithField("provider", provider.Name),
|
||||
log: logger,
|
||||
tlsServerName: provider.TlsServerName,
|
||||
uidStartNumber: *provider.UidStartNumber,
|
||||
gidStartNumber: *provider.GidStartNumber,
|
||||
}
|
||||
if provider.Certificate.Get() != nil {
|
||||
logger.WithField("provider", provider.Name).Debug("Enabling TLS")
|
||||
cert, err := ak.ParseCertificate(*provider.Certificate.Get(), ls.ac.Client.CryptoApi)
|
||||
if err != nil {
|
||||
logger.WithField("provider", provider.Name).WithError(err).Warning("Failed to fetch certificate")
|
||||
} else {
|
||||
providers[idx].cert = cert
|
||||
logger.WithField("provider", provider.Name).Debug("Loaded certificates")
|
||||
}
|
||||
}
|
||||
}
|
||||
ls.providers = providers
|
||||
@ -54,13 +72,53 @@ func (ls *LDAPServer) StartHTTPServer() error {
|
||||
|
||||
func (ls *LDAPServer) StartLDAPServer() error {
|
||||
listen := "0.0.0.0:3389"
|
||||
|
||||
ln, err := net.Listen("tcp", listen)
|
||||
if err != nil {
|
||||
ls.log.Fatalf("FATAL: listen (%s) failed - %s", listen, err)
|
||||
}
|
||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||
defer proxyListener.Close()
|
||||
|
||||
ls.log.WithField("listen", listen).Info("Starting ldap server")
|
||||
err = ls.s.Serve(proxyListener)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls.log.Printf("closing %s", ln.Addr())
|
||||
return ls.s.ListenAndServe(listen)
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) StartLDAPTLSServer() error {
|
||||
listen := "0.0.0.0:6636"
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MaxVersion: tls.VersionTLS12,
|
||||
GetCertificate: ls.getCertificates,
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", listen)
|
||||
if err != nil {
|
||||
ls.log.Fatalf("FATAL: listen (%s) failed - %s", listen, err)
|
||||
}
|
||||
|
||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||
defer proxyListener.Close()
|
||||
|
||||
tln := tls.NewListener(proxyListener, tlsConfig)
|
||||
|
||||
ls.log.WithField("listen", listen).Info("Starting ldap tls server")
|
||||
err = ls.s.Serve(tln)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls.log.Printf("closing %s", ln.Addr())
|
||||
return ls.s.ListenAndServe(listen)
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Start() error {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ls.StartHTTPServer()
|
||||
@ -75,24 +133,13 @@ func (ls *LDAPServer) Start() error {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := ls.StartLDAPTLSServer()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
type transport struct {
|
||||
headers map[string]string
|
||||
inner http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
for key, value := range t.headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
return t.inner.RoundTrip(req)
|
||||
}
|
||||
func newTransport(inner http.RoundTripper, headers map[string]string) *transport {
|
||||
return &transport{
|
||||
inner: inner,
|
||||
headers: headers,
|
||||
}
|
||||
}
|
23
internal/outpost/ldap/api_tls.go
Normal file
23
internal/outpost/ldap/api_tls.go
Normal file
@ -0,0 +1,23 @@
|
||||
package ldap
|
||||
|
||||
import "crypto/tls"
|
||||
|
||||
func (ls *LDAPServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if len(ls.providers) == 1 {
|
||||
if ls.providers[0].cert != nil {
|
||||
ls.log.WithField("server-name", info.ServerName).Debug("We only have a single provider, using their cert")
|
||||
return ls.providers[0].cert, nil
|
||||
}
|
||||
}
|
||||
for _, provider := range ls.providers {
|
||||
if provider.tlsServerName == &info.ServerName {
|
||||
if provider.cert == nil {
|
||||
ls.log.WithField("server-name", info.ServerName).Debug("Handler does not have a certificate")
|
||||
return ls.defaultCert, nil
|
||||
}
|
||||
return provider.cert, nil
|
||||
}
|
||||
}
|
||||
ls.log.WithField("server-name", info.ServerName).Debug("Fallback to default cert")
|
||||
return ls.defaultCert, nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user