Compare commits
206 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
| ab2b13938e | |||
| 5c97a3aef3 | |||
| e6963c543d | |||
| 9ca15983a2 | |||
| 99ef94b7aa | |||
| 133bedafba | |||
| c3faa61ed9 | |||
| da74304221 | |||
| ed6659a46d | |||
| 0abb1f94a4 | |||
| c7e299e0bf | |||
| 8a6590bac8 | |||
| ed717dcfa2 | |||
| b6df42f580 | |||
| 2ea85bd0c4 | |||
| 68fa8105e1 | |||
| 79db0ce4c1 | |||
| 5e23b11764 | |||
| c4e029ffe2 | |||
| 61b5b36192 | |||
| c6cc1b1728 | |||
| 77dd652160 | |||
| 1144944adb | |||
| 7751be284e | |||
| 74382c6287 | |||
| 011babbbd9 | |||
| 3c01a1dd7b | |||
| 6e832be2de | |||
| 46017f2f86 | |||
| da50eb0369 | |||
| b996e3cee7 | |||
| 12735cc14c | |||
| 4d36699b78 | |||
| 8110d2861b | |||
| 1cc60f572d | |||
| 90151a13ae | |||
| f958aa6930 | |||
| 13fbac30a2 | |||
| 4f4cdf16f1 | |||
| 7d75599627 | |||
| 924a13e832 | |||
| ae83c35dfd | |||
| e9102f4e28 | |||
| 9b8c1cbea5 | |||
| 6424bf98da | |||
| 74fb0f9e2a | |||
| 4380f37a77 | |||
| 17fccd44e6 | |||
| 217a8b5610 | |||
| 2cef220a3e | |||
| 5a8c66d325 | |||
| 8de13d3f67 | |||
| 5c22bedbaf | |||
| 8a0f993f0b | |||
| abcf515a69 | |||
| 894f704c27 | |||
| 7798292aa8 | |||
| 3005ca17bd | |||
| 909461e533 | |||
| df838a4023 | |||
| 0f86b62dd3 | |||
| a40c3aeb68 | |||
| 4080738ded | |||
| 4a89be3048 | |||
| e587c53e18 | |||
| 023b97aa69 | |||
| 51365dba74 | |||
| 0d3705685e | |||
| 738e4d5c74 | |||
| b14b9cb0dd | |||
| 2a21ebf7b0 | |||
| 5bc1301043 | |||
| e0e4bf6972 | |||
| 337677ad12 | |||
| 3712d5aee2 | |||
| dd82d55725 | |||
| 8d766efecb | |||
| 9ac3b29418 | |||
| 5000c5b061 | |||
| b362d2af03 | |||
| bcd42fce13 | |||
| 6deddd038f | |||
| 3b47cb64da | |||
| cf5e70c759 | |||
| 20bc38a54b | |||
| 672a4ab1f4 | |||
| 47dd667261 | |||
| d1ac69789b | |||
| 08abf81c6d | |||
| 76bd987e6f | |||
| 5374352411 | |||
| 08eff4cc5d | |||
| c87a9f9489 | |||
| 8f6d700aa8 | |||
| c6843b026c | |||
| 3769c33ef0 | |||
| 8982afaf44 | |||
| 58c221e867 | |||
| 108d3e56e3 | |||
| 145b32c480 | |||
| c788504bb0 | |||
| 34782b31e5 | |||
| 5a3ca13d76 | |||
| 5dc0f3b91b | |||
| f51515f3de | |||
| f978575293 | |||
| cb64eed90d | |||
| db1f7f0400 | |||
| 0d02dbf55c | |||
| 6da78b8c32 | |||
| 3a80bc8bda | |||
| 1aa9c0f9ca | |||
| 2da7a8fede | |||
| 89cb402f42 | |||
| b617fd213f | |||
| 97b0f58f25 | |||
| 49a98bb744 | |||
| f93a00d773 | |||
| 8de40a8a21 | |||
| b9c54e97fa | |||
| f1c55465f7 | |||
| 40c2b2860b | |||
| a92bce322d | |||
| af83308fd4 | |||
| 73d991e75a | |||
| 1eba3f1334 | |||
| b86251255d | |||
| ccab41a6ca | |||
| 0e051031b1 | |||
| aecbe8c585 | |||
| da98022704 | |||
| e13f9c0b38 | |||
| 7941fb9d95 | |||
| d2392b0881 | |||
| b2044d75fb | |||
| 617b64b7db | |||
| 2bf5f2709a | |||
| f03325df28 | |||
| 2b71e5bdfd | |||
| f861737b85 | |||
| 6036d88392 | |||
| bfc8a56a0b | |||
| 8d995011b8 | |||
| 5646141fe2 | |||
| 96b0bc324e | |||
| 335d6edd11 | |||
| 5d9bed130a | |||
| 0a1ab74707 | |||
| ef24b94585 | |||
| 77b0438aa4 | |||
| 2788329880 | |||
| 15ab11be70 | |||
| 8d5460a132 | |||
| 5ba2c80813 | |||
| 06766bdb25 | |||
| fdae13316c | |||
| ae21886e8e | |||
| f5dc81907a | |||
| 40f8ce3c4c | |||
| c934915776 | |||
| d70c8fbcc3 | |||
| 12b26e49ec | |||
| 0ac548d56e | |||
| e771e1857f | |||
| 479e9750c7 | |||
| c5e7801247 | |||
| 48ea15a946 | |||
| e4c06f7356 | |||
| 4d7d866e4b | |||
| 72a93c0959 | |||
| 73733b20b6 | |||
| 3872314931 | |||
| 85c6ede448 | |||
| 49c2bee9d6 | |||
| 6b2c9d7c44 | |||
| 381010600f | |||
| 2a265f706a | |||
| 1b21b50b77 | |||
| fa6324ab1d | |||
| 9e0daf2bcf | |||
| 0273ae16df | |||
| f2f12ef0ba | |||
| 61d3df5f02 | |||
| 971de4fcb9 | |||
| 9c0bc78ca0 | |||
| 92085f1a3c | |||
| 6067406e96 | |||
| 9ccd4d69fe | |||
| 17ec48332d | |||
| d3f5253a6b | |||
| 7a70726d57 | |||
| be303937fb | |||
| 2326fc9ae2 | |||
| 9374b0bcf2 | |||
| 47e6028099 | |||
| 24114e8304 | |||
| 921d9c79a1 | |||
| 1119989ab7 | |||
| e17594f0f7 | |||
| 5ae3b868d4 | |||
| 37ee4af5ff | |||
| 829aaca317 | |||
| 8eb4d53810 | |||
| e60dfc5b3c | |||
| cc403d8777 | |||
| b81e2e69d1 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 2021.10.1-rc2
|
current_version = 2021.10.3
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||||
|
|||||||
2
.github/workflows/ci-main.yml
vendored
2
.github/workflows/ci-main.yml
vendored
@ -147,6 +147,8 @@ jobs:
|
|||||||
# Copy current, latest config to local
|
# Copy current, latest config to local
|
||||||
cp authentik/lib/default.yml local.env.yml
|
cp authentik/lib/default.yml local.env.yml
|
||||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||||
|
git checkout ${{ steps.ev.outputs.branchName }} -- .github
|
||||||
|
git checkout ${{ steps.ev.outputs.branchName }} -- scripts
|
||||||
- name: prepare
|
- name: prepare
|
||||||
env:
|
env:
|
||||||
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}
|
||||||
|
|||||||
20
.github/workflows/release-publish.yml
vendored
20
.github/workflows/release-publish.yml
vendored
@ -30,14 +30,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik:2021.10.1-rc2,
|
beryju/authentik:2021.10.3,
|
||||||
beryju/authentik:latest,
|
beryju/authentik:latest,
|
||||||
ghcr.io/goauthentik/server:2021.10.1-rc2,
|
ghcr.io/goauthentik/server:2021.10.3,
|
||||||
ghcr.io/goauthentik/server:latest
|
ghcr.io/goauthentik/server:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
context: .
|
context: .
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.10.1-rc2', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.10.3', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik:latest
|
docker pull beryju/authentik:latest
|
||||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||||
@ -72,14 +72,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-proxy:2021.10.1-rc2,
|
beryju/authentik-proxy:2021.10.3,
|
||||||
beryju/authentik-proxy:latest,
|
beryju/authentik-proxy:latest,
|
||||||
ghcr.io/goauthentik/proxy:2021.10.1-rc2,
|
ghcr.io/goauthentik/proxy:2021.10.3,
|
||||||
ghcr.io/goauthentik/proxy:latest
|
ghcr.io/goauthentik/proxy:latest
|
||||||
file: proxy.Dockerfile
|
file: proxy.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.10.1-rc2', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.10.3', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-proxy:latest
|
docker pull beryju/authentik-proxy:latest
|
||||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||||
@ -114,14 +114,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
push: ${{ github.event_name == 'release' }}
|
push: ${{ github.event_name == 'release' }}
|
||||||
tags: |
|
tags: |
|
||||||
beryju/authentik-ldap:2021.10.1-rc2,
|
beryju/authentik-ldap:2021.10.3,
|
||||||
beryju/authentik-ldap:latest,
|
beryju/authentik-ldap:latest,
|
||||||
ghcr.io/goauthentik/ldap:2021.10.1-rc2,
|
ghcr.io/goauthentik/ldap:2021.10.3,
|
||||||
ghcr.io/goauthentik/ldap:latest
|
ghcr.io/goauthentik/ldap:latest
|
||||||
file: ldap.Dockerfile
|
file: ldap.Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
- name: Building Docker Image (stable)
|
- name: Building Docker Image (stable)
|
||||||
if: ${{ github.event_name == 'release' && !contains('2021.10.1-rc2', 'rc') }}
|
if: ${{ github.event_name == 'release' && !contains('2021.10.3', 'rc') }}
|
||||||
run: |
|
run: |
|
||||||
docker pull beryju/authentik-ldap:latest
|
docker pull beryju/authentik-ldap:latest
|
||||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||||
@ -170,7 +170,7 @@ jobs:
|
|||||||
SENTRY_PROJECT: authentik
|
SENTRY_PROJECT: authentik
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
version: authentik@2021.10.1-rc2
|
version: authentik@2021.10.3
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
sourcemaps: './web/dist'
|
sourcemaps: './web/dist'
|
||||||
url_prefix: '~/static/dist'
|
url_prefix: '~/static/dist'
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@ -1,17 +1,17 @@
|
|||||||
# Stage 1: Lock python dependencies
|
# Stage 1: Lock python dependencies
|
||||||
FROM docker.io/python:3.9-slim-buster as locker
|
FROM docker.io/python:3.9-bullseye as locker
|
||||||
|
|
||||||
COPY ./Pipfile /app/
|
COPY ./Pipfile /app/
|
||||||
COPY ./Pipfile.lock /app/
|
COPY ./Pipfile.lock /app/
|
||||||
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
RUN pip install pipenv && \
|
RUN pip install pipenv==2021.5.29 && \
|
||||||
pipenv lock -r > requirements.txt && \
|
pipenv lock -r > requirements.txt && \
|
||||||
pipenv lock -r --dev-only > requirements-dev.txt
|
pipenv lock -r --dev-only > requirements-dev.txt
|
||||||
|
|
||||||
# Stage 2: Build website
|
# Stage 2: Build website
|
||||||
FROM docker.io/node as website-builder
|
FROM docker.io/node:16 as website-builder
|
||||||
|
|
||||||
COPY ./website /static/
|
COPY ./website /static/
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ ENV NODE_ENV=production
|
|||||||
RUN cd /static && npm i && npm run build-docs-only
|
RUN cd /static && npm i && npm run build-docs-only
|
||||||
|
|
||||||
# Stage 3: Build webui
|
# Stage 3: Build webui
|
||||||
FROM docker.io/node as web-builder
|
FROM docker.io/node:16 as web-builder
|
||||||
|
|
||||||
COPY ./web /static/
|
COPY ./web /static/
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ ENV NODE_ENV=production
|
|||||||
RUN cd /static && npm i && npm run build
|
RUN cd /static && npm i && npm run build
|
||||||
|
|
||||||
# Stage 4: Build go proxy
|
# Stage 4: Build go proxy
|
||||||
FROM docker.io/golang:1.17.2 AS builder
|
FROM docker.io/golang:1.17.3-bullseye AS builder
|
||||||
|
|
||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ COPY ./go.sum /work/go.sum
|
|||||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||||
|
|
||||||
# Stage 5: Run
|
# Stage 5: Run
|
||||||
FROM docker.io/python:3.9-slim-buster
|
FROM docker.io/python:3.9-bullseye
|
||||||
|
|
||||||
WORKDIR /
|
WORKDIR /
|
||||||
COPY --from=locker /app/requirements.txt /
|
COPY --from=locker /app/requirements.txt /
|
||||||
@ -59,7 +59,7 @@ ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
|||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends curl ca-certificates gnupg git runit && \
|
apt-get install -y --no-install-recommends curl ca-certificates gnupg git runit && \
|
||||||
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
||||||
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
||||||
apt-get update && \
|
apt-get update && \
|
||||||
apt-get install -y --no-install-recommends libpq-dev postgresql-client build-essential libxmlsec1-dev pkg-config libmaxminddb0 && \
|
apt-get install -y --no-install-recommends libpq-dev postgresql-client build-essential libxmlsec1-dev pkg-config libmaxminddb0 && \
|
||||||
pip install -r /requirements.txt --no-cache-dir && \
|
pip install -r /requirements.txt --no-cache-dir && \
|
||||||
|
|||||||
14
Makefile
14
Makefile
@ -30,7 +30,6 @@ lint-fix:
|
|||||||
website/developer-docs
|
website/developer-docs
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
pyright authentik tests lifecycle
|
|
||||||
bandit -r authentik tests lifecycle -x node_modules
|
bandit -r authentik tests lifecycle -x node_modules
|
||||||
pylint authentik tests lifecycle
|
pylint authentik tests lifecycle
|
||||||
|
|
||||||
@ -49,7 +48,7 @@ gen-web:
|
|||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
openapitools/openapi-generator-cli generate \
|
ghcr.io/beryju/openapi-generator generate \
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g typescript-fetch \
|
-g typescript-fetch \
|
||||||
-o /local/web-api \
|
-o /local/web-api \
|
||||||
@ -61,18 +60,19 @@ gen-web:
|
|||||||
\cp -rfv web-api/* web/node_modules/@goauthentik/api
|
\cp -rfv web-api/* web/node_modules/@goauthentik/api
|
||||||
|
|
||||||
gen-outpost:
|
gen-outpost:
|
||||||
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
|
||||||
|
mkdir -p templates
|
||||||
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache
|
||||||
|
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache
|
||||||
docker run \
|
docker run \
|
||||||
--rm -v ${PWD}:/local \
|
--rm -v ${PWD}:/local \
|
||||||
--user ${UID}:${GID} \
|
--user ${UID}:${GID} \
|
||||||
openapitools/openapi-generator-cli generate \
|
openapitools/openapi-generator-cli generate \
|
||||||
--git-host goauthentik.io \
|
|
||||||
--git-repo-id outpost \
|
|
||||||
--git-user-id api \
|
|
||||||
-i /local/schema.yml \
|
-i /local/schema.yml \
|
||||||
-g go \
|
-g go \
|
||||||
-o /local/api \
|
-o /local/api \
|
||||||
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false
|
-c /local/config.yaml
|
||||||
rm -f api/go.mod api/go.sum
|
go mod edit -replace goauthentik.io/api=./api
|
||||||
|
|
||||||
gen: gen-build gen-clean gen-web
|
gen: gen-build gen-clean gen-web
|
||||||
|
|
||||||
|
|||||||
2
Pipfile
2
Pipfile
@ -26,7 +26,7 @@ drf-spectacular = "*"
|
|||||||
facebook-sdk = "*"
|
facebook-sdk = "*"
|
||||||
geoip2 = "*"
|
geoip2 = "*"
|
||||||
gunicorn = "*"
|
gunicorn = "*"
|
||||||
kubernetes = "==v19.15.0b1"
|
kubernetes = "==v19.15.0"
|
||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = "*"
|
lxml = "*"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
|
|||||||
909
Pipfile.lock
generated
909
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,3 @@
|
|||||||
"""authentik"""
|
"""authentik"""
|
||||||
__version__ = "2021.10.1-rc2"
|
__version__ = "2021.10.3"
|
||||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||||
|
|||||||
@ -27,6 +27,7 @@ VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
|
|||||||
# Chop of the first ^ because we want to search the entire string
|
# Chop of the first ^ because we want to search the entire string
|
||||||
URL_FINDER = URLValidator.regex.pattern[1:]
|
URL_FINDER = URLValidator.regex.pattern[1:]
|
||||||
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
PROM_INFO = Info("authentik_version", "Currently running authentik version")
|
||||||
|
LOCAL_VERSION = parse(__version__)
|
||||||
|
|
||||||
|
|
||||||
def _set_prom_info():
|
def _set_prom_info():
|
||||||
@ -48,7 +49,7 @@ def clear_update_notifications():
|
|||||||
if "new_version" not in notification.event.context:
|
if "new_version" not in notification.event.context:
|
||||||
continue
|
continue
|
||||||
notification_version = notification.event.context["new_version"]
|
notification_version = notification.event.context["new_version"]
|
||||||
if notification_version == __version__:
|
if LOCAL_VERSION >= parse(notification_version):
|
||||||
notification.delete()
|
notification.delete()
|
||||||
|
|
||||||
|
|
||||||
@ -74,8 +75,7 @@ def update_latest_version(self: MonitoredTask):
|
|||||||
_set_prom_info()
|
_set_prom_info()
|
||||||
# Check if upstream version is newer than what we're running,
|
# Check if upstream version is newer than what we're running,
|
||||||
# and if no event exists yet, create one.
|
# and if no event exists yet, create one.
|
||||||
local_version = parse(__version__)
|
if LOCAL_VERSION < parse(upstream_version):
|
||||||
if local_version < parse(upstream_version):
|
|
||||||
# Event has already been created, don't create duplicate
|
# Event has already been created, don't create duplicate
|
||||||
if Event.objects.filter(
|
if Event.objects.filter(
|
||||||
action=EventAction.UPDATE_AVAILABLE,
|
action=EventAction.UPDATE_AVAILABLE,
|
||||||
|
|||||||
@ -45,6 +45,7 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
|
|||||||
if not user:
|
if not user:
|
||||||
raise AuthenticationFailed("Token invalid/expired")
|
raise AuthenticationFailed("Token invalid/expired")
|
||||||
return user
|
return user
|
||||||
|
if hasattr(LOCAL, "authentik"):
|
||||||
LOCAL.authentik[KEY_AUTH_VIA] = "api_token"
|
LOCAL.authentik[KEY_AUTH_VIA] = "api_token"
|
||||||
return tokens.first().user
|
return tokens.first().user
|
||||||
|
|
||||||
@ -59,6 +60,7 @@ def token_secret_key(value: str) -> Optional[User]:
|
|||||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||||
if not outposts:
|
if not outposts:
|
||||||
return None
|
return None
|
||||||
|
if hasattr(LOCAL, "authentik"):
|
||||||
LOCAL.authentik[KEY_AUTH_VIA] = "secret_key"
|
LOCAL.authentik[KEY_AUTH_VIA] = "secret_key"
|
||||||
outpost = outposts.first()
|
outpost = outposts.first()
|
||||||
return outpost.user
|
return outpost.user
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
"""API tasks"""
|
|
||||||
|
|
||||||
from authentik.lib.utils.http import get_http_session
|
|
||||||
from authentik.root.celery import CELERY_APP
|
|
||||||
|
|
||||||
SENTRY_SESSION = get_http_session()
|
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
|
||||||
def sentry_proxy(payload: str):
|
|
||||||
"""Relay data to sentry"""
|
|
||||||
SENTRY_SESSION.post(
|
|
||||||
"https://sentry.beryju.org/api/8/envelope/",
|
|
||||||
data=payload,
|
|
||||||
headers={
|
|
||||||
"Content-Type": "application/octet-stream",
|
|
||||||
},
|
|
||||||
timeout=10,
|
|
||||||
)
|
|
||||||
@ -4,7 +4,7 @@ from django.urls import include, path
|
|||||||
from authentik.api.v3.urls import urlpatterns as v3_urls
|
from authentik.api.v3.urls import urlpatterns as v3_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Remove in 2022.1
|
# TODO: Remove in 2022.1
|
||||||
path("v2beta/", include(v3_urls)),
|
path("v2beta/", include(v3_urls)),
|
||||||
path("v3/", include(v3_urls)),
|
path("v3/", include(v3_urls)),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
"""Sentry tunnel"""
|
|
||||||
from json import loads
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.http.request import HttpRequest
|
|
||||||
from django.http.response import HttpResponse
|
|
||||||
from rest_framework.authentication import SessionAuthentication
|
|
||||||
from rest_framework.parsers import BaseParser
|
|
||||||
from rest_framework.permissions import AllowAny
|
|
||||||
from rest_framework.request import Request
|
|
||||||
from rest_framework.throttling import AnonRateThrottle
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.api.tasks import sentry_proxy
|
|
||||||
from authentik.lib.config import CONFIG
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class PlainTextParser(BaseParser):
|
|
||||||
"""Plain text parser."""
|
|
||||||
|
|
||||||
media_type = "text/plain"
|
|
||||||
|
|
||||||
def parse(self, stream, media_type=None, parser_context=None) -> str:
|
|
||||||
"""Simply return a string representing the body of the request."""
|
|
||||||
return stream.read()
|
|
||||||
|
|
||||||
|
|
||||||
class CsrfExemptSessionAuthentication(SessionAuthentication):
|
|
||||||
"""CSRF-exempt Session authentication"""
|
|
||||||
|
|
||||||
def enforce_csrf(self, request: Request):
|
|
||||||
return # To not perform the csrf check previously happening
|
|
||||||
|
|
||||||
|
|
||||||
class SentryTunnelView(APIView):
|
|
||||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
|
||||||
|
|
||||||
serializer_class = None
|
|
||||||
parser_classes = [PlainTextParser]
|
|
||||||
throttle_classes = [AnonRateThrottle]
|
|
||||||
permission_classes = [AllowAny]
|
|
||||||
authentication_classes = [CsrfExemptSessionAuthentication]
|
|
||||||
|
|
||||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
|
||||||
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
|
|
||||||
# Only allow usage of this endpoint when error reporting is enabled
|
|
||||||
if not CONFIG.y_bool("error_reporting.enabled", False):
|
|
||||||
LOGGER.debug("error reporting disabled")
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
# Body is 2 json objects separated by \n
|
|
||||||
full_body = request.body
|
|
||||||
lines = full_body.splitlines()
|
|
||||||
if len(lines) < 1:
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
header = loads(lines[0])
|
|
||||||
# Check that the DSN is what we expect
|
|
||||||
dsn = header.get("dsn", "")
|
|
||||||
if dsn != settings.SENTRY_DSN:
|
|
||||||
LOGGER.debug("Invalid dsn", have=dsn, expected=settings.SENTRY_DSN)
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
sentry_proxy.delay(full_body.decode())
|
|
||||||
return HttpResponse(status=204)
|
|
||||||
@ -11,14 +11,14 @@ from authentik.admin.api.tasks import TaskViewSet
|
|||||||
from authentik.admin.api.version import VersionView
|
from authentik.admin.api.version import VersionView
|
||||||
from authentik.admin.api.workers import WorkerView
|
from authentik.admin.api.workers import WorkerView
|
||||||
from authentik.api.v3.config import ConfigView
|
from authentik.api.v3.config import ConfigView
|
||||||
from authentik.api.v3.sentry import SentryTunnelView
|
|
||||||
from authentik.api.views import APIBrowserView
|
from authentik.api.views import APIBrowserView
|
||||||
from authentik.core.api.applications import ApplicationViewSet
|
from authentik.core.api.applications import ApplicationViewSet
|
||||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
|
||||||
|
from authentik.core.api.devices import DeviceViewSet
|
||||||
from authentik.core.api.groups import GroupViewSet
|
from authentik.core.api.groups import GroupViewSet
|
||||||
from authentik.core.api.propertymappings import PropertyMappingViewSet
|
from authentik.core.api.propertymappings import PropertyMappingViewSet
|
||||||
from authentik.core.api.providers import ProviderViewSet
|
from authentik.core.api.providers import ProviderViewSet
|
||||||
from authentik.core.api.sources import SourceViewSet
|
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
|
||||||
from authentik.core.api.tokens import TokenViewSet
|
from authentik.core.api.tokens import TokenViewSet
|
||||||
from authentik.core.api.users import UserViewSet
|
from authentik.core.api.users import UserViewSet
|
||||||
from authentik.crypto.api import CertificateKeyPairViewSet
|
from authentik.crypto.api import CertificateKeyPairViewSet
|
||||||
@ -136,6 +136,7 @@ router.register("events/transports", NotificationTransportViewSet)
|
|||||||
router.register("events/rules", NotificationRuleViewSet)
|
router.register("events/rules", NotificationRuleViewSet)
|
||||||
|
|
||||||
router.register("sources/all", SourceViewSet)
|
router.register("sources/all", SourceViewSet)
|
||||||
|
router.register("sources/user_connections/all", UserSourceConnectionViewSet)
|
||||||
router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet)
|
router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet)
|
||||||
router.register("sources/user_connections/plex", PlexSourceConnectionViewSet)
|
router.register("sources/user_connections/plex", PlexSourceConnectionViewSet)
|
||||||
router.register("sources/ldap", LDAPSourceViewSet)
|
router.register("sources/ldap", LDAPSourceViewSet)
|
||||||
@ -169,6 +170,7 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
|||||||
router.register("propertymappings/scope", ScopeMappingViewSet)
|
router.register("propertymappings/scope", ScopeMappingViewSet)
|
||||||
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
|
||||||
|
|
||||||
|
router.register("authenticators/all", DeviceViewSet, basename="device")
|
||||||
router.register("authenticators/duo", DuoDeviceViewSet)
|
router.register("authenticators/duo", DuoDeviceViewSet)
|
||||||
router.register("authenticators/sms", SMSDeviceViewSet)
|
router.register("authenticators/sms", SMSDeviceViewSet)
|
||||||
router.register("authenticators/static", StaticDeviceViewSet)
|
router.register("authenticators/static", StaticDeviceViewSet)
|
||||||
@ -246,7 +248,6 @@ urlpatterns = (
|
|||||||
FlowInspectorView.as_view(),
|
FlowInspectorView.as_view(),
|
||||||
name="flow-inspector",
|
name="flow-inspector",
|
||||||
),
|
),
|
||||||
path("sentry/", SentryTunnelView.as_view(), name="sentry"),
|
|
||||||
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
|
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
36
authentik/core/api/devices.py
Normal file
36
authentik/core/api/devices.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
"""Authenticator Devices API Views"""
|
||||||
|
from django_otp import devices_for_user
|
||||||
|
from django_otp.models import Device
|
||||||
|
from drf_spectacular.utils import extend_schema
|
||||||
|
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.request import Request
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
|
from authentik.core.api.utils import MetaNameSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceSerializer(MetaNameSerializer):
|
||||||
|
"""Serializer for Duo authenticator devices"""
|
||||||
|
|
||||||
|
pk = IntegerField()
|
||||||
|
name = CharField()
|
||||||
|
type = SerializerMethodField()
|
||||||
|
|
||||||
|
def get_type(self, instance: Device) -> str:
|
||||||
|
"""Get type of device"""
|
||||||
|
return instance._meta.label
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceViewSet(ViewSet):
|
||||||
|
"""Viewset for authenticator devices"""
|
||||||
|
|
||||||
|
serializer_class = DeviceSerializer
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
|
@extend_schema(responses={200: DeviceSerializer(many=True)})
|
||||||
|
def list(self, request: Request) -> Response:
|
||||||
|
"""Get all devices for current user"""
|
||||||
|
devices = devices_for_user(request.user)
|
||||||
|
return Response(DeviceSerializer(devices, many=True).data)
|
||||||
@ -42,6 +42,7 @@ class GroupSerializer(ModelSerializer):
|
|||||||
users_obj = ListSerializer(
|
users_obj = ListSerializer(
|
||||||
child=GroupMemberSerializer(), read_only=True, source="users", required=False
|
child=GroupMemberSerializer(), read_only=True, source="users", required=False
|
||||||
)
|
)
|
||||||
|
parent_name = CharField(source="parent.name", read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
@ -51,6 +52,7 @@ class GroupSerializer(ModelSerializer):
|
|||||||
"name",
|
"name",
|
||||||
"is_superuser",
|
"is_superuser",
|
||||||
"parent",
|
"parent",
|
||||||
|
"parent_name",
|
||||||
"users",
|
"users",
|
||||||
"attributes",
|
"attributes",
|
||||||
"users_obj",
|
"users_obj",
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
"""Source API Views"""
|
"""Source API Views"""
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
|
from authentik.api.authorization import OwnerFilter, OwnerPermissions
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||||
from authentik.core.models import Source
|
from authentik.core.models import Source, UserSourceConnection
|
||||||
from authentik.core.types import UserSettingSerializer
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
@ -113,3 +116,39 @@ class SourceViewSet(
|
|||||||
LOGGER.warning(source_settings.errors)
|
LOGGER.warning(source_settings.errors)
|
||||||
matching_sources.append(source_settings.validated_data)
|
matching_sources.append(source_settings.validated_data)
|
||||||
return Response(matching_sources)
|
return Response(matching_sources)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSourceConnectionSerializer(SourceSerializer):
|
||||||
|
"""OAuth Source Serializer"""
|
||||||
|
|
||||||
|
source = SourceSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = UserSourceConnection
|
||||||
|
fields = [
|
||||||
|
"pk",
|
||||||
|
"user",
|
||||||
|
"source",
|
||||||
|
"created",
|
||||||
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
"user": {"read_only": True},
|
||||||
|
"created": {"read_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UserSourceConnectionViewSet(
|
||||||
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
|
UsedByMixin,
|
||||||
|
mixins.ListModelMixin,
|
||||||
|
GenericViewSet,
|
||||||
|
):
|
||||||
|
"""User-source connection Viewset"""
|
||||||
|
|
||||||
|
queryset = UserSourceConnection.objects.all()
|
||||||
|
serializer_class = UserSourceConnectionSerializer
|
||||||
|
permission_classes = [OwnerPermissions]
|
||||||
|
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
|
ordering = ["pk"]
|
||||||
|
|||||||
@ -45,6 +45,7 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
|
||||||
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
|
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
|
||||||
from authentik.core.models import (
|
from authentik.core.models import (
|
||||||
|
USER_ATTRIBUTE_CHANGE_EMAIL,
|
||||||
USER_ATTRIBUTE_CHANGE_USERNAME,
|
USER_ATTRIBUTE_CHANGE_USERNAME,
|
||||||
USER_ATTRIBUTE_SA,
|
USER_ATTRIBUTE_SA,
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
USER_ATTRIBUTE_TOKEN_EXPIRING,
|
||||||
@ -122,6 +123,14 @@ class UserSelfSerializer(ModelSerializer):
|
|||||||
"pk": group.pk,
|
"pk": group.pk,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def validate_email(self, email: str):
|
||||||
|
"""Check if the user is allowed to change their email"""
|
||||||
|
if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_EMAIL, True):
|
||||||
|
return email
|
||||||
|
if email != self.instance.email:
|
||||||
|
raise ValidationError("Not allowed to change email.")
|
||||||
|
return email
|
||||||
|
|
||||||
def validate_username(self, username: str):
|
def validate_username(self, username: str):
|
||||||
"""Check if the user is allowed to change their username"""
|
"""Check if the user is allowed to change their username"""
|
||||||
if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_USERNAME, True):
|
if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_USERNAME, True):
|
||||||
@ -320,13 +329,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def me(self, request: Request) -> Response:
|
def me(self, request: Request) -> Response:
|
||||||
"""Get information about current user"""
|
"""Get information about current user"""
|
||||||
serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data})
|
serializer = SessionUserSerializer(
|
||||||
|
data={"user": UserSelfSerializer(instance=request.user).data}
|
||||||
|
)
|
||||||
if SESSION_IMPERSONATE_USER in request._request.session:
|
if SESSION_IMPERSONATE_USER in request._request.session:
|
||||||
serializer.initial_data["original"] = UserSelfSerializer(
|
serializer.initial_data["original"] = UserSelfSerializer(
|
||||||
request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
instance=request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||||
).data
|
).data
|
||||||
serializer.is_valid()
|
return Response(serializer.initial_data)
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@extend_schema(request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)})
|
@extend_schema(request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)})
|
||||||
@action(
|
@action(
|
||||||
@ -346,9 +356,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||||||
# since it caches the full object
|
# since it caches the full object
|
||||||
if SESSION_IMPERSONATE_USER in request.session:
|
if SESSION_IMPERSONATE_USER in request.session:
|
||||||
request.session[SESSION_IMPERSONATE_USER] = new_user
|
request.session[SESSION_IMPERSONATE_USER] = new_user
|
||||||
serializer = SessionUserSerializer(data={"user": data.data})
|
return Response({"user": data.data})
|
||||||
serializer.is_valid()
|
|
||||||
return Response(serializer.data)
|
|
||||||
|
|
||||||
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
||||||
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||||
|
|||||||
@ -55,5 +55,5 @@ class TokenBackend(InbuiltBackend):
|
|||||||
if not tokens.exists():
|
if not tokens.exists():
|
||||||
return None
|
return None
|
||||||
token = tokens.first()
|
token = tokens.first()
|
||||||
self.set_method("password", request, token=token)
|
self.set_method("token", request, token=token)
|
||||||
return token.user
|
return token.user
|
||||||
|
|||||||
0
authentik/core/management/__init__.py
Normal file
0
authentik/core/management/__init__.py
Normal file
0
authentik/core/management/commands/__init__.py
Normal file
0
authentik/core/management/commands/__init__.py
Normal file
15
authentik/core/management/commands/dump_config.py
Normal file
15
authentik/core/management/commands/dump_config.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""Output full config"""
|
||||||
|
from json import dumps
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand, no_translations
|
||||||
|
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand): # pragma: no cover
|
||||||
|
"""Output full config"""
|
||||||
|
|
||||||
|
@no_translations
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""Check permissions for all apps"""
|
||||||
|
print(dumps(CONFIG.raw, indent=4))
|
||||||
@ -39,7 +39,8 @@ USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
|
|||||||
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
|
||||||
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
|
||||||
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
|
||||||
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username" # nosec
|
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
|
||||||
|
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
|
||||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
|
||||||
|
|
||||||
GRAVATAR_URL = "https://secure.gravatar.com"
|
GRAVATAR_URL = "https://secure.gravatar.com"
|
||||||
@ -80,6 +81,27 @@ class Group(models.Model):
|
|||||||
)
|
)
|
||||||
attributes = models.JSONField(default=dict, blank=True)
|
attributes = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
def is_member(self, user: "User") -> bool:
|
||||||
|
"""Recursively check if `user` is member of us, or any parent."""
|
||||||
|
query = """
|
||||||
|
WITH RECURSIVE parents AS (
|
||||||
|
SELECT authentik_core_group.*, 0 AS relative_depth
|
||||||
|
FROM authentik_core_group
|
||||||
|
WHERE authentik_core_group.group_uuid = %s
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT authentik_core_group.*, parents.relative_depth - 1
|
||||||
|
FROM authentik_core_group,parents
|
||||||
|
WHERE authentik_core_group.parent_id = parents.group_uuid
|
||||||
|
)
|
||||||
|
SELECT group_uuid
|
||||||
|
FROM parents
|
||||||
|
GROUP BY group_uuid;
|
||||||
|
"""
|
||||||
|
groups = Group.objects.raw(query, [self.group_uuid])
|
||||||
|
return user.ak_groups.filter(pk__in=[group.pk for group in groups]).exists()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Group {self.name}"
|
return f"Group {self.name}"
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from os import environ
|
|||||||
from boto3.exceptions import Boto3Error
|
from boto3.exceptions import Boto3Error
|
||||||
from botocore.exceptions import BotoCoreError, ClientError
|
from botocore.exceptions import BotoCoreError, ClientError
|
||||||
from dbbackup.db.exceptions import CommandConnectorError
|
from dbbackup.db.exceptions import CommandConnectorError
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||||
from django.core import management
|
from django.core import management
|
||||||
@ -55,24 +56,25 @@ def clean_expired_models(self: MonitoredTask):
|
|||||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
|
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
|
||||||
|
|
||||||
|
|
||||||
|
def should_backup() -> bool:
|
||||||
|
"""Check if we should be doing backups"""
|
||||||
|
if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup.bucket"):
|
||||||
|
LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
|
||||||
|
return False
|
||||||
|
if not CONFIG.y_bool("postgresql.backup.enabled"):
|
||||||
|
return False
|
||||||
|
if settings.DEBUG:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||||
@prefill_task()
|
@prefill_task()
|
||||||
def backup_database(self: MonitoredTask): # pragma: no cover
|
def backup_database(self: MonitoredTask): # pragma: no cover
|
||||||
"""Database backup"""
|
"""Database backup"""
|
||||||
self.result_timeout_hours = 25
|
self.result_timeout_hours = 25
|
||||||
if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup.bucket"):
|
if not should_backup():
|
||||||
LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
|
self.set_status(TaskResult(TaskResultStatus.UNKNOWN, ["Backups are not configured."]))
|
||||||
self.set_status(
|
|
||||||
TaskResult(
|
|
||||||
TaskResultStatus.WARNING,
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"Skipping backup as authentik is running in Kubernetes "
|
|
||||||
"without S3 backups configured."
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
start = datetime.now()
|
start = datetime.now()
|
||||||
|
|||||||
40
authentik/core/tests/test_groups.py
Normal file
40
authentik/core/tests/test_groups.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
"""group tests"""
|
||||||
|
from django.test.testcases import TestCase
|
||||||
|
|
||||||
|
from authentik.core.models import Group, User
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroups(TestCase):
|
||||||
|
"""Test group membership"""
|
||||||
|
|
||||||
|
def test_group_membership_simple(self):
|
||||||
|
"""Test simple membership"""
|
||||||
|
user = User.objects.create(username="user")
|
||||||
|
user2 = User.objects.create(username="user2")
|
||||||
|
group = Group.objects.create(name="group")
|
||||||
|
group.users.add(user)
|
||||||
|
self.assertTrue(group.is_member(user))
|
||||||
|
self.assertFalse(group.is_member(user2))
|
||||||
|
|
||||||
|
def test_group_membership_parent(self):
|
||||||
|
"""Test parent membership"""
|
||||||
|
user = User.objects.create(username="user")
|
||||||
|
user2 = User.objects.create(username="user2")
|
||||||
|
first = Group.objects.create(name="first")
|
||||||
|
second = Group.objects.create(name="second", parent=first)
|
||||||
|
second.users.add(user)
|
||||||
|
self.assertTrue(first.is_member(user))
|
||||||
|
self.assertFalse(first.is_member(user2))
|
||||||
|
|
||||||
|
def test_group_membership_parent_extra(self):
|
||||||
|
"""Test parent membership"""
|
||||||
|
user = User.objects.create(username="user")
|
||||||
|
user2 = User.objects.create(username="user2")
|
||||||
|
first = Group.objects.create(name="first")
|
||||||
|
second = Group.objects.create(name="second", parent=first)
|
||||||
|
third = Group.objects.create(name="third", parent=second)
|
||||||
|
second.users.add(user)
|
||||||
|
self.assertTrue(first.is_member(user))
|
||||||
|
self.assertFalse(first.is_member(user2))
|
||||||
|
self.assertFalse(third.is_member(user))
|
||||||
|
self.assertFalse(third.is_member(user2))
|
||||||
@ -2,7 +2,7 @@
|
|||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from authentik.core.models import USER_ATTRIBUTE_CHANGE_USERNAME, User
|
from authentik.core.models import USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, User
|
||||||
from authentik.flows.models import Flow, FlowDesignation
|
from authentik.flows.models import Flow, FlowDesignation
|
||||||
from authentik.stages.email.models import EmailStage
|
from authentik.stages.email.models import EmailStage
|
||||||
from authentik.tenants.models import Tenant
|
from authentik.tenants.models import Tenant
|
||||||
@ -33,6 +33,16 @@ class TestUsersAPI(APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 400)
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
|
def test_update_self_email_denied(self):
|
||||||
|
"""Test update_self"""
|
||||||
|
self.admin.attributes[USER_ATTRIBUTE_CHANGE_EMAIL] = False
|
||||||
|
self.admin.save()
|
||||||
|
self.client.force_login(self.admin)
|
||||||
|
response = self.client.put(
|
||||||
|
reverse("authentik_api:user-update-self"), data={"email": "foo", "name": "foo"}
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
|
||||||
def test_metrics(self):
|
def test_metrics(self):
|
||||||
"""Test user's metrics"""
|
"""Test user's metrics"""
|
||||||
self.client.force_login(self.admin)
|
self.client.force_login(self.admin)
|
||||||
|
|||||||
@ -141,7 +141,7 @@ class CertificateKeyPairFilter(FilterSet):
|
|||||||
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""CertificateKeyPair Viewset"""
|
"""CertificateKeyPair Viewset"""
|
||||||
|
|
||||||
queryset = CertificateKeyPair.objects.all()
|
queryset = CertificateKeyPair.objects.exclude(managed__isnull=False)
|
||||||
serializer_class = CertificateKeyPairSerializer
|
serializer_class = CertificateKeyPairSerializer
|
||||||
filterset_class = CertificateKeyPairFilter
|
filterset_class = CertificateKeyPairFilter
|
||||||
|
|
||||||
|
|||||||
@ -7,16 +7,25 @@ from django.core.exceptions import SuspiciousOperation
|
|||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, pre_delete
|
from django.db.models.signals import post_save, pre_delete
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django_otp.plugins.otp_static.models import StaticToken
|
||||||
from guardian.models import UserObjectPermission
|
from guardian.models import UserObjectPermission
|
||||||
|
|
||||||
from authentik.core.middleware import LOCAL
|
from authentik.core.middleware import LOCAL
|
||||||
from authentik.core.models import User
|
from authentik.core.models import AuthenticatedSession, User
|
||||||
from authentik.events.models import Event, EventAction, Notification
|
from authentik.events.models import Event, EventAction, Notification
|
||||||
from authentik.events.signals import EventNewThread
|
from authentik.events.signals import EventNewThread
|
||||||
from authentik.events.utils import model_to_dict
|
from authentik.events.utils import model_to_dict
|
||||||
from authentik.lib.sentry import before_send
|
from authentik.lib.sentry import before_send
|
||||||
from authentik.lib.utils.errors import exception_to_string
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
|
|
||||||
|
IGNORED_MODELS = (
|
||||||
|
Event,
|
||||||
|
Notification,
|
||||||
|
UserObjectPermission,
|
||||||
|
AuthenticatedSession,
|
||||||
|
StaticToken,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AuditMiddleware:
|
class AuditMiddleware:
|
||||||
"""Register handlers for duration of request-response that log creation/update/deletion
|
"""Register handlers for duration of request-response that log creation/update/deletion
|
||||||
@ -82,7 +91,7 @@ class AuditMiddleware:
|
|||||||
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
||||||
):
|
):
|
||||||
"""Signal handler for all object's post_save"""
|
"""Signal handler for all object's post_save"""
|
||||||
if isinstance(instance, (Event, Notification, UserObjectPermission)):
|
if isinstance(instance, IGNORED_MODELS):
|
||||||
return
|
return
|
||||||
|
|
||||||
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||||
@ -92,7 +101,7 @@ class AuditMiddleware:
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def pre_delete_handler(user: User, request: HttpRequest, sender, instance: Model, **_):
|
def pre_delete_handler(user: User, request: HttpRequest, sender, instance: Model, **_):
|
||||||
"""Signal handler for all object's pre_delete"""
|
"""Signal handler for all object's pre_delete"""
|
||||||
if isinstance(instance, (Event, Notification, UserObjectPermission)): # pragma: no cover
|
if isinstance(instance, IGNORED_MODELS): # pragma: no cover
|
||||||
return
|
return
|
||||||
|
|
||||||
EventNewThread(
|
EventNewThread(
|
||||||
|
|||||||
@ -98,7 +98,9 @@ def notification_transport(self: MonitoredTask, notification_pk: int, transport_
|
|||||||
notification: Notification = Notification.objects.filter(pk=notification_pk).first()
|
notification: Notification = Notification.objects.filter(pk=notification_pk).first()
|
||||||
if not notification:
|
if not notification:
|
||||||
return
|
return
|
||||||
transport: NotificationTransport = NotificationTransport.objects.get(pk=transport_pk)
|
transport = NotificationTransport.objects.filter(pk=transport_pk).first()
|
||||||
|
if not transport:
|
||||||
|
return
|
||||||
transport.send(notification)
|
transport.send(notification)
|
||||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
|
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
|
||||||
except NotificationTransportError as exc:
|
except NotificationTransportError as exc:
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
"""Flow Stage API Views"""
|
"""Flow Stage API Views"""
|
||||||
from typing import Iterable
|
|
||||||
|
|
||||||
from django.urls.base import reverse
|
from django.urls.base import reverse
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
@ -15,7 +13,7 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
|
||||||
from authentik.core.types import UserSettingSerializer
|
from authentik.core.types import UserSettingSerializer
|
||||||
from authentik.flows.api.flows import FlowSerializer
|
from authentik.flows.api.flows import FlowSerializer
|
||||||
from authentik.flows.models import Stage
|
from authentik.flows.models import ConfigurableStage, Stage
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -86,9 +84,11 @@ class StageViewSet(
|
|||||||
@action(detail=False, pagination_class=None, filter_backends=[])
|
@action(detail=False, pagination_class=None, filter_backends=[])
|
||||||
def user_settings(self, request: Request) -> Response:
|
def user_settings(self, request: Request) -> Response:
|
||||||
"""Get all stages the user can configure"""
|
"""Get all stages the user can configure"""
|
||||||
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses().order_by("name")
|
stages = []
|
||||||
|
for configurable_stage in all_subclasses(ConfigurableStage):
|
||||||
|
stages += list(configurable_stage.objects.all().order_by("name"))
|
||||||
matching_stages: list[dict] = []
|
matching_stages: list[dict] = []
|
||||||
for stage in _all_stages:
|
for stage in stages:
|
||||||
user_settings = stage.ui_user_settings
|
user_settings = stage.ui_user_settings
|
||||||
if not user_settings:
|
if not user_settings:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -545,6 +545,7 @@ class TestFlowExecutor(APITestCase):
|
|||||||
"password_fields": False,
|
"password_fields": False,
|
||||||
"primary_action": "Log in",
|
"primary_action": "Log in",
|
||||||
"sources": [],
|
"sources": [],
|
||||||
|
"show_source_labels": False,
|
||||||
"user_fields": [UserFields.E_MAIL],
|
"user_fields": [UserFields.E_MAIL],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -60,6 +60,7 @@ class TestFlowInspector(APITestCase):
|
|||||||
"password_fields": False,
|
"password_fields": False,
|
||||||
"primary_action": "Log in",
|
"primary_action": "Log in",
|
||||||
"sources": [],
|
"sources": [],
|
||||||
|
"show_source_labels": False,
|
||||||
"user_fields": ["username"],
|
"user_fields": ["username"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,6 +5,16 @@ postgresql:
|
|||||||
user: authentik
|
user: authentik
|
||||||
port: 5432
|
port: 5432
|
||||||
password: 'env://POSTGRES_PASSWORD'
|
password: 'env://POSTGRES_PASSWORD'
|
||||||
|
backup:
|
||||||
|
enabled: true
|
||||||
|
s3_backup:
|
||||||
|
access_key: ""
|
||||||
|
secret_key: ""
|
||||||
|
bucket: ""
|
||||||
|
region: eu-central-1
|
||||||
|
host: ""
|
||||||
|
location: ""
|
||||||
|
insecure_skip_verify: false
|
||||||
|
|
||||||
web:
|
web:
|
||||||
listen: 0.0.0.0:9000
|
listen: 0.0.0.0:9000
|
||||||
@ -58,6 +68,7 @@ outposts:
|
|||||||
|
|
||||||
cookie_domain: null
|
cookie_domain: null
|
||||||
disable_update_check: false
|
disable_update_check: false
|
||||||
|
disable_startup_analytics: false
|
||||||
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
|
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
|
||||||
geoip: "./GeoLite2-City.mmdb"
|
geoip: "./GeoLite2-City.mmdb"
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ from django.db import InternalError, OperationalError, ProgrammingError
|
|||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from django_redis.exceptions import ConnectionInterrupted
|
from django_redis.exceptions import ConnectionInterrupted
|
||||||
from docker.errors import DockerException
|
from docker.errors import DockerException
|
||||||
|
from h11 import LocalProtocolError
|
||||||
from ldap3.core.exceptions import LDAPException
|
from ldap3.core.exceptions import LDAPException
|
||||||
from redis.exceptions import ConnectionError as RedisConnectionError
|
from redis.exceptions import ConnectionError as RedisConnectionError
|
||||||
from redis.exceptions import RedisError, ResponseError
|
from redis.exceptions import RedisError, ResponseError
|
||||||
@ -72,6 +73,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
|
|||||||
# websocket errors
|
# websocket errors
|
||||||
ChannelFull,
|
ChannelFull,
|
||||||
WebSocketException,
|
WebSocketException,
|
||||||
|
LocalProtocolError,
|
||||||
# rest_framework error
|
# rest_framework error
|
||||||
APIException,
|
APIException,
|
||||||
# celery errors
|
# celery errors
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
"""authentik lib reflection utilities"""
|
"""authentik lib reflection utilities"""
|
||||||
|
import os
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from pathlib import Path
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||||
|
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
def all_subclasses(cls, sort=True):
|
def all_subclasses(cls, sort=True):
|
||||||
@ -42,3 +47,16 @@ def get_apps():
|
|||||||
for _app in apps.get_app_configs():
|
for _app in apps.get_app_configs():
|
||||||
if _app.name.startswith("authentik"):
|
if _app.name.startswith("authentik"):
|
||||||
yield _app
|
yield _app
|
||||||
|
|
||||||
|
|
||||||
|
def get_env() -> str:
|
||||||
|
"""Get environment in which authentik is currently running"""
|
||||||
|
if SERVICE_HOST_ENV_NAME in os.environ:
|
||||||
|
return "kubernetes"
|
||||||
|
if "CI" in os.environ:
|
||||||
|
return "ci"
|
||||||
|
if Path("/tmp/authentik-mode").exists(): # nosec
|
||||||
|
return "compose"
|
||||||
|
if CONFIG.y_bool("debug"):
|
||||||
|
return "dev"
|
||||||
|
return "custom"
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.text import slugify
|
||||||
from docker import DockerClient
|
from docker import DockerClient
|
||||||
from docker.errors import DockerException, NotFound
|
from docker.errors import DockerException, NotFound
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
@ -28,6 +29,17 @@ class DockerController(BaseController):
|
|||||||
except ServiceConnectionInvalid as exc:
|
except ServiceConnectionInvalid as exc:
|
||||||
raise ControllerException from exc
|
raise ControllerException from exc
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Get the name of the object this reconciler manages"""
|
||||||
|
return (
|
||||||
|
self.outpost.config.object_naming_template
|
||||||
|
% {
|
||||||
|
"name": slugify(self.outpost.name),
|
||||||
|
"uuid": self.outpost.uuid.hex,
|
||||||
|
}
|
||||||
|
).lower()
|
||||||
|
|
||||||
def _get_labels(self) -> dict[str, str]:
|
def _get_labels(self) -> dict[str, str]:
|
||||||
return {
|
return {
|
||||||
"io.goauthentik.outpost-uuid": self.outpost.pk.hex,
|
"io.goauthentik.outpost-uuid": self.outpost.pk.hex,
|
||||||
@ -102,15 +114,14 @@ class DockerController(BaseController):
|
|||||||
return image
|
return image
|
||||||
|
|
||||||
def _get_container(self) -> tuple[Container, bool]:
|
def _get_container(self) -> tuple[Container, bool]:
|
||||||
container_name = f"authentik-proxy-{self.outpost.uuid.hex}"
|
|
||||||
try:
|
try:
|
||||||
return self.client.containers.get(container_name), False
|
return self.client.containers.get(self.name), False
|
||||||
except NotFound:
|
except NotFound:
|
||||||
self.logger.info("(Re-)creating container...")
|
self.logger.info("(Re-)creating container...")
|
||||||
image_name = self.try_pull_image()
|
image_name = self.try_pull_image()
|
||||||
container_args = {
|
container_args = {
|
||||||
"image": image_name,
|
"image": image_name,
|
||||||
"name": container_name,
|
"name": self.name,
|
||||||
"detach": True,
|
"detach": True,
|
||||||
"environment": self._get_env(),
|
"environment": self._get_env(),
|
||||||
"labels": self._get_labels(),
|
"labels": self._get_labels(),
|
||||||
@ -131,12 +142,23 @@ class DockerController(BaseController):
|
|||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _migrate_container_name(self):
|
||||||
|
"""Migrate 2021.9 to 2021.10+"""
|
||||||
|
old_name = f"authentik-proxy-{self.outpost.uuid.hex}"
|
||||||
|
try:
|
||||||
|
old_container: Container = self.client.containers.get(old_name)
|
||||||
|
old_container.kill()
|
||||||
|
old_container.remove()
|
||||||
|
except NotFound:
|
||||||
|
return
|
||||||
|
|
||||||
# pylint: disable=too-many-return-statements
|
# pylint: disable=too-many-return-statements
|
||||||
def up(self, depth=1):
|
def up(self, depth=1):
|
||||||
if self.outpost.managed == MANAGED_OUTPOST:
|
if self.outpost.managed == MANAGED_OUTPOST:
|
||||||
return None
|
return None
|
||||||
if depth >= 10:
|
if depth >= 10:
|
||||||
raise ControllerException("Giving up since we exceeded recursion limit.")
|
raise ControllerException("Giving up since we exceeded recursion limit.")
|
||||||
|
self._migrate_container_name()
|
||||||
try:
|
try:
|
||||||
container, has_been_created = self._get_container()
|
container, has_been_created = self._get_container()
|
||||||
if has_been_created:
|
if has_been_created:
|
||||||
|
|||||||
@ -65,14 +65,14 @@ class PolicyBinding(SerializerModel):
|
|||||||
# This is quite an ugly hack to prevent pylint from trying
|
# This is quite an ugly hack to prevent pylint from trying
|
||||||
# to resolve authentik_core.models.Group
|
# to resolve authentik_core.models.Group
|
||||||
# as python import path
|
# as python import path
|
||||||
"authentik_core." + "Group",
|
"authentik_core.Group",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
default=None,
|
default=None,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
"authentik_core." + "User",
|
"authentik_core.User",
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
default=None,
|
default=None,
|
||||||
null=True,
|
null=True,
|
||||||
@ -96,7 +96,7 @@ class PolicyBinding(SerializerModel):
|
|||||||
self.policy: Policy
|
self.policy: Policy
|
||||||
return self.policy.passes(request)
|
return self.policy.passes(request)
|
||||||
if self.group:
|
if self.group:
|
||||||
return PolicyResult(self.group.users.filter(pk=request.user.pk).exists())
|
return PolicyResult(self.group.is_member(request.user))
|
||||||
if self.user:
|
if self.user:
|
||||||
return PolicyResult(request.user == self.user)
|
return PolicyResult(request.user == self.user)
|
||||||
return PolicyResult(False)
|
return PolicyResult(False)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
"""LDAPProvider API Views"""
|
"""LDAPProvider API Views"""
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField, ListField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
|
||||||
|
|
||||||
@ -11,6 +11,8 @@ from authentik.providers.ldap.models import LDAPProvider
|
|||||||
class LDAPProviderSerializer(ProviderSerializer):
|
class LDAPProviderSerializer(ProviderSerializer):
|
||||||
"""LDAPProvider Serializer"""
|
"""LDAPProvider Serializer"""
|
||||||
|
|
||||||
|
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = LDAPProvider
|
model = LDAPProvider
|
||||||
@ -21,6 +23,8 @@ class LDAPProviderSerializer(ProviderSerializer):
|
|||||||
"tls_server_name",
|
"tls_server_name",
|
||||||
"uid_start_number",
|
"uid_start_number",
|
||||||
"gid_start_number",
|
"gid_start_number",
|
||||||
|
"outpost_set",
|
||||||
|
"search_mode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -65,6 +69,7 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
|
|||||||
"tls_server_name",
|
"tls_server_name",
|
||||||
"uid_start_number",
|
"uid_start_number",
|
||||||
"gid_start_number",
|
"gid_start_number",
|
||||||
|
"search_mode",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,93 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-05 09:41
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
replaces = [
|
||||||
|
("authentik_providers_ldap", "0001_initial"),
|
||||||
|
("authentik_providers_ldap", "0002_ldapprovider_search_group"),
|
||||||
|
("authentik_providers_ldap", "0003_auto_20210713_1138"),
|
||||||
|
("authentik_providers_ldap", "0004_auto_20210713_2115"),
|
||||||
|
("authentik_providers_ldap", "0005_ldapprovider_search_mode"),
|
||||||
|
]
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_core", "0019_source_managed"),
|
||||||
|
("authentik_crypto", "0002_create_self_signed_kp"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="LDAPProvider",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"provider_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="authentik_core.provider",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"base_dn",
|
||||||
|
models.TextField(
|
||||||
|
default="DC=ldap,DC=goauthentik,DC=io",
|
||||||
|
help_text="DN under which objects are accessible.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"search_group",
|
||||||
|
models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
help_text="Users in this group can do search queries. If not set, every user can execute search queries.",
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||||
|
to="authentik_core.group",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"certificate",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
to="authentik_crypto.certificatekeypair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("tls_server_name", models.TextField(blank=True, default="")),
|
||||||
|
(
|
||||||
|
"gid_start_number",
|
||||||
|
models.IntegerField(
|
||||||
|
default=4000,
|
||||||
|
help_text="The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"uid_start_number",
|
||||||
|
models.IntegerField(
|
||||||
|
default=2000,
|
||||||
|
help_text="The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"search_mode",
|
||||||
|
models.TextField(
|
||||||
|
choices=[("direct", "Direct"), ("cached", "Cached")], default="direct"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "LDAP Provider",
|
||||||
|
"verbose_name_plural": "LDAP Providers",
|
||||||
|
},
|
||||||
|
bases=("authentik_core.provider", models.Model),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-11-05 09:40
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_providers_ldap", "0004_auto_20210713_2115"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="ldapprovider",
|
||||||
|
name="search_mode",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[("direct", "Direct"), ("cached", "Cached")], default="direct"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -10,6 +10,13 @@ from authentik.crypto.models import CertificateKeyPair
|
|||||||
from authentik.outposts.models import OutpostModel
|
from authentik.outposts.models import OutpostModel
|
||||||
|
|
||||||
|
|
||||||
|
class SearchModes(models.TextChoices):
|
||||||
|
"""Search modes"""
|
||||||
|
|
||||||
|
DIRECT = "direct"
|
||||||
|
CACHED = "cached"
|
||||||
|
|
||||||
|
|
||||||
class LDAPProvider(OutpostModel, Provider):
|
class LDAPProvider(OutpostModel, Provider):
|
||||||
"""Allow applications to authenticate against authentik's users using LDAP."""
|
"""Allow applications to authenticate against authentik's users using LDAP."""
|
||||||
|
|
||||||
@ -59,6 +66,8 @@ class LDAPProvider(OutpostModel, Provider):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
search_mode = models.TextField(default=SearchModes.DIRECT, choices=SearchModes.choices)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def launch_url(self) -> Optional[str]:
|
def launch_url(self) -> Optional[str]:
|
||||||
"""LDAP never has a launch URL"""
|
"""LDAP never has a launch URL"""
|
||||||
|
|||||||
@ -448,7 +448,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
|
|||||||
elif self.provider.sub_mode == SubModes.USER_USERNAME:
|
elif self.provider.sub_mode == SubModes.USER_USERNAME:
|
||||||
sub = user.username
|
sub = user.username
|
||||||
elif self.provider.sub_mode == SubModes.USER_UPN:
|
elif self.provider.sub_mode == SubModes.USER_UPN:
|
||||||
sub = user.attributes["upn"]
|
sub = user.attributes.get("upn", user.uid)
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
(
|
(
|
||||||
|
|||||||
@ -386,6 +386,9 @@ class AuthorizationFlowInitView(PolicyAccessView):
|
|||||||
def pre_permission_check(self):
|
def pre_permission_check(self):
|
||||||
"""Check prompt parameter before checking permission/authentication,
|
"""Check prompt parameter before checking permission/authentication,
|
||||||
see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6"""
|
see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6"""
|
||||||
|
# Quick sanity check at the beginning to prevent event spamming
|
||||||
|
if len(self.request.GET) < 1:
|
||||||
|
raise Http404
|
||||||
try:
|
try:
|
||||||
self.params = OAuthAuthorizationParams.from_request(self.request)
|
self.params = OAuthAuthorizationParams.from_request(self.request)
|
||||||
except AuthorizeError as error:
|
except AuthorizeError as error:
|
||||||
|
|||||||
@ -36,6 +36,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
|||||||
"""ProxyProvider Serializer"""
|
"""ProxyProvider Serializer"""
|
||||||
|
|
||||||
redirect_uris = CharField(read_only=True)
|
redirect_uris = CharField(read_only=True)
|
||||||
|
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
|
||||||
|
|
||||||
def validate(self, attrs) -> dict[Any, str]:
|
def validate(self, attrs) -> dict[Any, str]:
|
||||||
"""Check that internal_host is set when mode is Proxy"""
|
"""Check that internal_host is set when mode is Proxy"""
|
||||||
@ -74,6 +75,7 @@ class ProxyProviderSerializer(ProviderSerializer):
|
|||||||
"redirect_uris",
|
"redirect_uris",
|
||||||
"cookie_domain",
|
"cookie_domain",
|
||||||
"token_validity",
|
"token_validity",
|
||||||
|
"outpost_set",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -138,7 +138,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
|
|||||||
SCOPE_AK_PROXY,
|
SCOPE_AK_PROXY,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.property_mappings.set(scopes)
|
self.property_mappings.add(*list(scopes))
|
||||||
self.redirect_uris = _get_callback_url(self.external_host)
|
self.redirect_uris = _get_callback_url(self.external_host)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@ -59,10 +59,12 @@ class AuthNRequestParser:
|
|||||||
) -> AuthNRequest:
|
) -> AuthNRequest:
|
||||||
root = ElementTree.fromstring(decoded_xml)
|
root = ElementTree.fromstring(decoded_xml)
|
||||||
|
|
||||||
|
# http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
|
||||||
|
# `AssertionConsumerServiceURL` can be omitted, and we should fallback to the
|
||||||
|
# default ACS URL
|
||||||
if "AssertionConsumerServiceURL" not in root.attrib:
|
if "AssertionConsumerServiceURL" not in root.attrib:
|
||||||
msg = "Missing 'AssertionConsumerServiceURL' attribute"
|
request_acs_url = self.provider.acs_url.lower()
|
||||||
LOGGER.warning(msg)
|
else:
|
||||||
raise CannotHandleAssertion(msg)
|
|
||||||
request_acs_url = root.attrib["AssertionConsumerServiceURL"]
|
request_acs_url = root.attrib["AssertionConsumerServiceURL"]
|
||||||
|
|
||||||
if self.provider.acs_url.lower() != request_acs_url.lower():
|
if self.provider.acs_url.lower() != request_acs_url.lower():
|
||||||
|
|||||||
30
authentik/recovery/management/commands/create_admin_group.py
Normal file
30
authentik/recovery/management/commands/create_admin_group.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""authentik recovery create_admin_group"""
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from authentik.core.models import Group, User
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Create admin group if the default group gets deleted"""
|
||||||
|
|
||||||
|
help = _("Create admin group if the default group gets deleted.")
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("user", action="store", help="User to add to the admin group.")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""Create admin group if the default group gets deleted"""
|
||||||
|
username = options.get("user")
|
||||||
|
user = User.objects.filter(username=username).first()
|
||||||
|
if not user:
|
||||||
|
self.stderr.write(f"User '{username}' not found.")
|
||||||
|
return
|
||||||
|
group, _ = Group.objects.update_or_create(
|
||||||
|
name="authentik Admins",
|
||||||
|
defaults={
|
||||||
|
"is_superuser": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
group.users.add(user)
|
||||||
|
self.stdout.write(f"User '{username}' successfully added to the group 'authentik Admins'.")
|
||||||
@ -7,12 +7,9 @@ from django.urls import reverse
|
|||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from structlog.stdlib import get_logger
|
|
||||||
|
|
||||||
from authentik.core.models import Token, TokenIntents, User
|
from authentik.core.models import Token, TokenIntents, User
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""Create Token used to recover access"""
|
"""Create Token used to recover access"""
|
||||||
|
|||||||
@ -14,13 +14,13 @@ import importlib
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from hashlib import sha512
|
||||||
from json import dumps
|
from json import dumps
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
|
||||||
from sentry_sdk import init as sentry_init
|
from sentry_sdk import init as sentry_init
|
||||||
from sentry_sdk.api import set_tag
|
from sentry_sdk.api import set_tag
|
||||||
from sentry_sdk.integrations.celery import CeleryIntegration
|
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||||
@ -32,6 +32,8 @@ from authentik.core.middleware import structlog_add_request_id
|
|||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.logging import add_process_id
|
from authentik.lib.logging import add_process_id
|
||||||
from authentik.lib.sentry import before_send
|
from authentik.lib.sentry import before_send
|
||||||
|
from authentik.lib.utils.http import get_http_session
|
||||||
|
from authentik.lib.utils.reflection import get_env
|
||||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||||
|
|
||||||
|
|
||||||
@ -176,6 +178,7 @@ SPECTACULAR_SETTINGS = {
|
|||||||
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
|
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
|
||||||
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
|
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
|
||||||
"ProxyMode": "authentik.providers.proxy.models.ProxyMode",
|
"ProxyMode": "authentik.providers.proxy.models.ProxyMode",
|
||||||
|
"PromptTypeEnum": "authentik.stages.prompt.models.FieldTypes",
|
||||||
},
|
},
|
||||||
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
|
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
|
||||||
"POSTPROCESSING_HOOKS": [
|
"POSTPROCESSING_HOOKS": [
|
||||||
@ -307,7 +310,7 @@ EMAIL_HOST = CONFIG.y("email.host")
|
|||||||
EMAIL_PORT = int(CONFIG.y("email.port"))
|
EMAIL_PORT = int(CONFIG.y("email.port"))
|
||||||
EMAIL_HOST_USER = CONFIG.y("email.username")
|
EMAIL_HOST_USER = CONFIG.y("email.username")
|
||||||
EMAIL_HOST_PASSWORD = CONFIG.y("email.password")
|
EMAIL_HOST_PASSWORD = CONFIG.y("email.password")
|
||||||
EMAIL_USE_TLS = CONFIG.y_bool("email.use_tls", True)
|
EMAIL_USE_TLS = CONFIG.y_bool("email.use_tls", False)
|
||||||
EMAIL_USE_SSL = CONFIG.y_bool("email.use_ssl", False)
|
EMAIL_USE_SSL = CONFIG.y_bool("email.use_ssl", False)
|
||||||
EMAIL_TIMEOUT = int(CONFIG.y("email.timeout"))
|
EMAIL_TIMEOUT = int(CONFIG.y("email.timeout"))
|
||||||
DEFAULT_FROM_EMAIL = CONFIG.y("email.from")
|
DEFAULT_FROM_EMAIL = CONFIG.y("email.from")
|
||||||
@ -380,7 +383,8 @@ DBBACKUP_CONNECTOR_MAPPING = {
|
|||||||
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
|
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
|
||||||
}
|
}
|
||||||
DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp" # nosec
|
DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp" # nosec
|
||||||
if CONFIG.y("postgresql.s3_backup"):
|
DBBACKUP_CLEANUP_KEEP = 30
|
||||||
|
if CONFIG.y("postgresql.s3_backup.bucket", "") != "":
|
||||||
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||||
DBBACKUP_STORAGE_OPTIONS = {
|
DBBACKUP_STORAGE_OPTIONS = {
|
||||||
"access_key": CONFIG.y("postgresql.s3_backup.access_key"),
|
"access_key": CONFIG.y("postgresql.s3_backup.access_key"),
|
||||||
@ -399,6 +403,12 @@ if CONFIG.y("postgresql.s3_backup"):
|
|||||||
|
|
||||||
# Sentry integration
|
# Sentry integration
|
||||||
SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
|
SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
|
||||||
|
# Default to empty string as that is what docker has
|
||||||
|
build_hash = os.environ.get(ENV_GIT_HASH_KEY, "")
|
||||||
|
if build_hash == "":
|
||||||
|
build_hash = "tagged"
|
||||||
|
|
||||||
|
env = get_env()
|
||||||
_ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False)
|
_ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False)
|
||||||
if _ERROR_REPORTING:
|
if _ERROR_REPORTING:
|
||||||
# pylint: disable=abstract-class-instantiated
|
# pylint: disable=abstract-class-instantiated
|
||||||
@ -415,18 +425,29 @@ if _ERROR_REPORTING:
|
|||||||
environment=CONFIG.y("error_reporting.environment", "customer"),
|
environment=CONFIG.y("error_reporting.environment", "customer"),
|
||||||
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
|
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
|
||||||
)
|
)
|
||||||
# Default to empty string as that is what docker has
|
|
||||||
build_hash = os.environ.get(ENV_GIT_HASH_KEY, "")
|
|
||||||
if build_hash == "":
|
|
||||||
build_hash = "tagged"
|
|
||||||
set_tag("authentik.build_hash", build_hash)
|
set_tag("authentik.build_hash", build_hash)
|
||||||
set_tag("authentik.env", "kubernetes" if SERVICE_HOST_ENV_NAME in os.environ else "compose")
|
set_tag("authentik.env", env)
|
||||||
set_tag("authentik.component", "backend")
|
set_tag("authentik.component", "backend")
|
||||||
j_print(
|
j_print(
|
||||||
"Error reporting is enabled",
|
"Error reporting is enabled",
|
||||||
env=CONFIG.y("error_reporting.environment", "customer"),
|
env=CONFIG.y("error_reporting.environment", "customer"),
|
||||||
)
|
)
|
||||||
|
if not CONFIG.y_bool("disable_startup_analytics", False):
|
||||||
|
should_send = env not in ["dev", "ci"]
|
||||||
|
if should_send:
|
||||||
|
get_http_session().post(
|
||||||
|
"https://goauthentik.io/api/event",
|
||||||
|
json={
|
||||||
|
"domain": "authentik",
|
||||||
|
"name": "pageview",
|
||||||
|
"url": f"http://localhost/{env}",
|
||||||
|
"referrer": f"{__version__} ({build_hash})",
|
||||||
|
},
|
||||||
|
headers={
|
||||||
|
"User-Agent": sha512(SECRET_KEY.encode("ascii")).hexdigest()[:16],
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/2.1/howto/static-files/
|
# https://docs.djangoproject.com/en/2.1/howto/static-files/
|
||||||
|
|||||||
@ -82,6 +82,8 @@ class BaseLDAPSynchronizer:
|
|||||||
value = mapping.evaluate(user=None, request=None, ldap=kwargs, dn=object_dn)
|
value = mapping.evaluate(user=None, request=None, ldap=kwargs, dn=object_dn)
|
||||||
if value is None:
|
if value is None:
|
||||||
continue
|
continue
|
||||||
|
if isinstance(value, (bytes)):
|
||||||
|
continue
|
||||||
object_field = mapping.object_field
|
object_field = mapping.object_field
|
||||||
if object_field.startswith("attributes."):
|
if object_field.startswith("attributes."):
|
||||||
# Because returning a list might desired, we can't
|
# Because returning a list might desired, we can't
|
||||||
|
|||||||
@ -40,6 +40,9 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
|
|||||||
self._logger.debug("Creating group with attributes", **defaults)
|
self._logger.debug("Creating group with attributes", **defaults)
|
||||||
if "name" not in defaults:
|
if "name" not in defaults:
|
||||||
raise IntegrityError("Name was not set by propertymappings")
|
raise IntegrityError("Name was not set by propertymappings")
|
||||||
|
# Special check for `users` field, as this is an M2M relation, and cannot be sync'd
|
||||||
|
if "users" in defaults:
|
||||||
|
del defaults["users"]
|
||||||
ak_group, created = Group.objects.update_or_create(
|
ak_group, created = Group.objects.update_or_create(
|
||||||
**{
|
**{
|
||||||
f"attributes__{LDAP_UNIQUENESS}": uniq,
|
f"attributes__{LDAP_UNIQUENESS}": uniq,
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
"""LDAP Sync tasks"""
|
"""LDAP Sync tasks"""
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from ldap3.core.exceptions import LDAPException
|
from ldap3.core.exceptions import LDAPException
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
@ -31,8 +29,7 @@ def ldap_sync_all():
|
|||||||
@CELERY_APP.task(
|
@CELERY_APP.task(
|
||||||
bind=True, base=MonitoredTask, soft_time_limit=60 * 60 * 2, task_time_limit=60 * 60 * 2
|
bind=True, base=MonitoredTask, soft_time_limit=60 * 60 * 2, task_time_limit=60 * 60 * 2
|
||||||
)
|
)
|
||||||
# TODO: remove Optional[str] in 2021.10
|
def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
|
||||||
def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: Optional[str] = None):
|
|
||||||
"""Synchronization of an LDAP Source"""
|
"""Synchronization of an LDAP Source"""
|
||||||
self.result_timeout_hours = 2
|
self.result_timeout_hours = 2
|
||||||
try:
|
try:
|
||||||
@ -41,8 +38,6 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: Optional[str] = N
|
|||||||
# Because the source couldn't be found, we don't have a UID
|
# Because the source couldn't be found, we don't have a UID
|
||||||
# to set the state with
|
# to set the state with
|
||||||
return
|
return
|
||||||
if not sync_class:
|
|
||||||
return
|
|
||||||
sync = path_to_class(sync_class)
|
sync = path_to_class(sync_class)
|
||||||
self.set_uid(f"{slugify(source.name)}-{sync.__name__}")
|
self.set_uid(f"{slugify(source.name)}-{sync.__name__}")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -21,6 +21,9 @@ class UserOAuthSourceConnectionSerializer(SourceSerializer):
|
|||||||
"source",
|
"source",
|
||||||
"identifier",
|
"identifier",
|
||||||
]
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
"user": {"read_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserOAuthSourceConnectionViewSet(
|
class UserOAuthSourceConnectionViewSet(
|
||||||
|
|||||||
@ -12,6 +12,7 @@ class DiscordOAuthRedirect(OAuthRedirect):
|
|||||||
def get_additional_parameters(self, source): # pragma: no cover
|
def get_additional_parameters(self, source): # pragma: no cover
|
||||||
return {
|
return {
|
||||||
"scope": "email identify",
|
"scope": "email identify",
|
||||||
|
"prompt": "none",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_sche
|
|||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import PermissionDenied
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.fields import CharField
|
from rest_framework.fields import CharField
|
||||||
from rest_framework.permissions import AllowAny
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
|
|||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.flows.challenge import RedirectChallenge
|
from authentik.flows.challenge import RedirectChallenge
|
||||||
from authentik.flows.views.executor import to_stage_response
|
from authentik.flows.views.executor import to_stage_response
|
||||||
from authentik.sources.plex.models import PlexSource
|
from authentik.sources.plex.models import PlexSource, PlexSourceConnection
|
||||||
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager
|
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -98,21 +98,11 @@ class PlexSourceViewSet(UsedByMixin, ModelViewSet):
|
|||||||
user_info, identifier = auth_api.get_user_info()
|
user_info, identifier = auth_api.get_user_info()
|
||||||
# Check friendship first, then check server overlay
|
# Check friendship first, then check server overlay
|
||||||
friends_allowed = False
|
friends_allowed = False
|
||||||
owner_id = None
|
|
||||||
if source.allow_friends:
|
if source.allow_friends:
|
||||||
owner_api = PlexAuth(source, source.plex_token)
|
owner_api = PlexAuth(source, source.plex_token)
|
||||||
owner_id = owner_api.get_user_info
|
friends_allowed = owner_api.check_friends_overlap(identifier)
|
||||||
owner_friends = owner_api.get_friends()
|
|
||||||
for friend in owner_friends:
|
|
||||||
if int(friend.get("id", "0")) == int(identifier):
|
|
||||||
friends_allowed = True
|
|
||||||
LOGGER.info(
|
|
||||||
"allowing user for plex because of friend",
|
|
||||||
user=user_info["username"],
|
|
||||||
)
|
|
||||||
servers_allowed = auth_api.check_server_overlap()
|
servers_allowed = auth_api.check_server_overlap()
|
||||||
owner_allowed = owner_id == identifier
|
if any([friends_allowed, servers_allowed]):
|
||||||
if any([friends_allowed, servers_allowed, owner_allowed]):
|
|
||||||
sfm = PlexSourceFlowManager(
|
sfm = PlexSourceFlowManager(
|
||||||
source=source,
|
source=source,
|
||||||
request=request,
|
request=request,
|
||||||
@ -125,3 +115,57 @@ class PlexSourceViewSet(UsedByMixin, ModelViewSet):
|
|||||||
user=user_info["username"],
|
user=user_info["username"],
|
||||||
)
|
)
|
||||||
raise PermissionDenied("Access denied.")
|
raise PermissionDenied("Access denied.")
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
request=PlexTokenRedeemSerializer(),
|
||||||
|
responses={
|
||||||
|
204: OpenApiResponse(),
|
||||||
|
400: OpenApiResponse(description="Token not found"),
|
||||||
|
403: OpenApiResponse(description="Access denied"),
|
||||||
|
},
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="slug",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@action(
|
||||||
|
methods=["POST"],
|
||||||
|
detail=False,
|
||||||
|
pagination_class=None,
|
||||||
|
filter_backends=[],
|
||||||
|
permission_classes=[IsAuthenticated],
|
||||||
|
)
|
||||||
|
def redeem_token_authenticated(self, request: Request) -> Response:
|
||||||
|
"""Redeem a plex token for an authenticated user, creating a connection"""
|
||||||
|
source: PlexSource = get_object_or_404(
|
||||||
|
PlexSource, slug=request.query_params.get("slug", "")
|
||||||
|
)
|
||||||
|
plex_token = request.data.get("plex_token", None)
|
||||||
|
if not plex_token:
|
||||||
|
raise ValidationError("No plex token given")
|
||||||
|
auth_api = PlexAuth(source, plex_token)
|
||||||
|
user_info, identifier = auth_api.get_user_info()
|
||||||
|
# Check friendship first, then check server overlay
|
||||||
|
friends_allowed = False
|
||||||
|
if source.allow_friends:
|
||||||
|
owner_api = PlexAuth(source, source.plex_token)
|
||||||
|
friends_allowed = owner_api.check_friends_overlap(identifier)
|
||||||
|
servers_allowed = auth_api.check_server_overlap()
|
||||||
|
if any([friends_allowed, servers_allowed]):
|
||||||
|
PlexSourceConnection.objects.create(
|
||||||
|
plex_token=plex_token,
|
||||||
|
user=request.user,
|
||||||
|
identifier=identifier,
|
||||||
|
source=source,
|
||||||
|
)
|
||||||
|
return Response(status=204)
|
||||||
|
LOGGER.warning(
|
||||||
|
"Denying plex connection because no server overlay and no friends and not owner",
|
||||||
|
user=user_info["username"],
|
||||||
|
friends_allowed=friends_allowed,
|
||||||
|
servers_allowed=servers_allowed,
|
||||||
|
)
|
||||||
|
raise PermissionDenied("Access denied.")
|
||||||
|
|||||||
@ -22,6 +22,9 @@ class PlexSourceConnectionSerializer(SourceSerializer):
|
|||||||
"identifier",
|
"identifier",
|
||||||
"plex_token",
|
"plex_token",
|
||||||
]
|
]
|
||||||
|
extra_kwargs = {
|
||||||
|
"user": {"read_only": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PlexSourceConnectionViewSet(
|
class PlexSourceConnectionViewSet(
|
||||||
@ -39,3 +42,4 @@ class PlexSourceConnectionViewSet(
|
|||||||
filterset_fields = ["source__slug"]
|
filterset_fields = ["source__slug"]
|
||||||
permission_classes = [OwnerPermissions]
|
permission_classes = [OwnerPermissions]
|
||||||
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
|
ordering = ["pk"]
|
||||||
|
|||||||
@ -83,6 +83,7 @@ class PlexSource(Source):
|
|||||||
data={
|
data={
|
||||||
"title": f"Plex {self.name}",
|
"title": f"Plex {self.name}",
|
||||||
"component": "ak-user-settings-source-plex",
|
"component": "ak-user-settings-source-plex",
|
||||||
|
"configure_url": self.client_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ class PlexAuth:
|
|||||||
return {
|
return {
|
||||||
"X-Plex-Product": "authentik",
|
"X-Plex-Product": "authentik",
|
||||||
"X-Plex-Version": __version__,
|
"X-Plex-Version": __version__,
|
||||||
"X-Plex-Device-Vendor": "BeryJu.org",
|
"X-Plex-Device-Vendor": "goauthentik.io",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_resources(self) -> list[dict]:
|
def get_resources(self) -> list[dict]:
|
||||||
@ -96,6 +96,21 @@ class PlexAuth:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def check_friends_overlap(self, user_ident: int) -> bool:
|
||||||
|
"""Check if the user is a friend of the owner, or the owner themselves"""
|
||||||
|
friends_allowed = False
|
||||||
|
_, owner_id = self.get_user_info()
|
||||||
|
owner_friends = self.get_friends()
|
||||||
|
for friend in owner_friends:
|
||||||
|
if int(friend.get("id", "0")) == user_ident:
|
||||||
|
friends_allowed = True
|
||||||
|
LOGGER.info(
|
||||||
|
"allowing user for plex because of friend",
|
||||||
|
user=user_ident,
|
||||||
|
)
|
||||||
|
owner_allowed = owner_id == user_ident
|
||||||
|
return any([friends_allowed, owner_allowed])
|
||||||
|
|
||||||
|
|
||||||
class PlexSourceFlowManager(SourceFlowManager):
|
class PlexSourceFlowManager(SourceFlowManager):
|
||||||
"""Flow manager for plex sources"""
|
"""Flow manager for plex sources"""
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from requests import RequestException
|
|||||||
|
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||||
|
from authentik.lib.utils.errors import exception_to_string
|
||||||
from authentik.root.celery import CELERY_APP
|
from authentik.root.celery import CELERY_APP
|
||||||
from authentik.sources.plex.models import PlexSource
|
from authentik.sources.plex.models import PlexSource
|
||||||
from authentik.sources.plex.plex import PlexAuth
|
from authentik.sources.plex.plex import PlexAuth
|
||||||
@ -31,7 +32,7 @@ def check_plex_token(self: MonitoredTask, source_slug: int):
|
|||||||
self.set_status(
|
self.set_status(
|
||||||
TaskResult(
|
TaskResult(
|
||||||
TaskResultStatus.ERROR,
|
TaskResultStatus.ERROR,
|
||||||
["Plex token is invalid/an error occurred:", str(exc)],
|
["Plex token is invalid/an error occurred:", exception_to_string(exc)],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Event.new(
|
Event.new(
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-31 16:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_stages_authenticator_sms", "0001_squashed_0004_auto_20211014_0936"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="authenticatorsmsstage",
|
||||||
|
name="from_number",
|
||||||
|
field=models.TextField(),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -101,7 +101,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
|
|||||||
stage: AuthenticatorSMSStage = self.executor.current_stage
|
stage: AuthenticatorSMSStage = self.executor.current_stage
|
||||||
|
|
||||||
if SESSION_SMS_DEVICE not in self.request.session:
|
if SESSION_SMS_DEVICE not in self.request.session:
|
||||||
device = SMSDevice(user=user, confirmed=False, stage=stage)
|
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
|
||||||
device.generate_token(commit=False)
|
device.generate_token(commit=False)
|
||||||
if phone_number := self._has_phone_number():
|
if phone_number := self._has_phone_number():
|
||||||
device.phone_number = phone_number
|
device.phone_number = phone_number
|
||||||
|
|||||||
@ -55,7 +55,7 @@ class AuthenticatorStaticStageView(ChallengeStageView):
|
|||||||
stage: AuthenticatorStaticStage = self.executor.current_stage
|
stage: AuthenticatorStaticStage = self.executor.current_stage
|
||||||
|
|
||||||
if SESSION_STATIC_DEVICE not in self.request.session:
|
if SESSION_STATIC_DEVICE not in self.request.session:
|
||||||
device = StaticDevice(user=user, confirmed=True)
|
device = StaticDevice(user=user, confirmed=True, name="Static Token")
|
||||||
tokens = []
|
tokens = []
|
||||||
for _ in range(0, stage.token_count):
|
for _ in range(0, stage.token_count):
|
||||||
tokens.append(StaticToken(device=device, token=StaticToken.random_token()))
|
tokens.append(StaticToken(device=device, token=StaticToken.random_token()))
|
||||||
|
|||||||
@ -81,7 +81,9 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
|
|||||||
stage: AuthenticatorTOTPStage = self.executor.current_stage
|
stage: AuthenticatorTOTPStage = self.executor.current_stage
|
||||||
|
|
||||||
if SESSION_TOTP_DEVICE not in self.request.session:
|
if SESSION_TOTP_DEVICE not in self.request.session:
|
||||||
device = TOTPDevice(user=user, confirmed=True, digits=stage.digits)
|
device = TOTPDevice(
|
||||||
|
user=user, confirmed=True, digits=stage.digits, name="TOTP Authenticator"
|
||||||
|
)
|
||||||
|
|
||||||
self.request.session[SESSION_TOTP_DEVICE] = device
|
self.request.session[SESSION_TOTP_DEVICE] = device
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|||||||
@ -75,6 +75,7 @@ class AuthenticatorValidateStageTests(APITestCase):
|
|||||||
},
|
},
|
||||||
"user_fields": ["username"],
|
"user_fields": ["username"],
|
||||||
"sources": [],
|
"sources": [],
|
||||||
|
"show_source_labels": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -136,6 +136,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
|||||||
credential_id=bytes_to_base64url(webauthn_credential.credential_id),
|
credential_id=bytes_to_base64url(webauthn_credential.credential_id),
|
||||||
sign_count=webauthn_credential.sign_count,
|
sign_count=webauthn_credential.sign_count,
|
||||||
rp_id=get_rp_id(self.request),
|
rp_id=get_rp_id(self.request),
|
||||||
|
name="WebAuthn Device",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return self.executor.stage_invalid("Device with Credential ID already exists.")
|
return self.executor.stage_invalid("Device with Credential ID already exists.")
|
||||||
|
|||||||
@ -20,6 +20,7 @@ class IdentificationStageSerializer(StageSerializer):
|
|||||||
"enrollment_flow",
|
"enrollment_flow",
|
||||||
"recovery_flow",
|
"recovery_flow",
|
||||||
"sources",
|
"sources",
|
||||||
|
"show_source_labels",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -35,5 +36,6 @@ class IdentificationStageViewSet(UsedByMixin, ModelViewSet):
|
|||||||
"show_matched_user",
|
"show_matched_user",
|
||||||
"enrollment_flow",
|
"enrollment_flow",
|
||||||
"recovery_flow",
|
"recovery_flow",
|
||||||
|
"show_source_labels",
|
||||||
]
|
]
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.8 on 2021-10-31 16:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_stages_identification", "0011_alter_identificationstage_user_fields"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="identificationstage",
|
||||||
|
name="show_source_labels",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -81,6 +81,7 @@ class IdentificationStage(Stage):
|
|||||||
sources = models.ManyToManyField(
|
sources = models.ManyToManyField(
|
||||||
Source, default=list, help_text=_("Specify which sources should be shown.")
|
Source, default=list, help_text=_("Specify which sources should be shown.")
|
||||||
)
|
)
|
||||||
|
show_source_labels = models.BooleanField(default=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer(self) -> BaseSerializer:
|
def serializer(self) -> BaseSerializer:
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""Identification stage logic"""
|
"""Identification stage logic"""
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
from random import SystemRandom
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
@ -15,10 +16,16 @@ from structlog.stdlib import get_logger
|
|||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import Application, Source, User
|
from authentik.core.models import Application, Source, User
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
from authentik.flows.challenge import (
|
||||||
|
Challenge,
|
||||||
|
ChallengeResponse,
|
||||||
|
ChallengeTypes,
|
||||||
|
RedirectChallenge,
|
||||||
|
)
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
|
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
|
||||||
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, challenge_types
|
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
|
||||||
|
from authentik.sources.plex.models import PlexAuthenticationChallenge
|
||||||
from authentik.stages.identification.models import IdentificationStage
|
from authentik.stages.identification.models import IdentificationStage
|
||||||
from authentik.stages.identification.signals import identification_failed
|
from authentik.stages.identification.signals import identification_failed
|
||||||
from authentik.stages.password.stage import authenticate
|
from authentik.stages.password.stage import authenticate
|
||||||
@ -28,8 +35,11 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
@extend_schema_field(
|
@extend_schema_field(
|
||||||
PolymorphicProxySerializer(
|
PolymorphicProxySerializer(
|
||||||
component_name="ChallengeTypes",
|
component_name="LoginChallengeTypes",
|
||||||
serializers=challenge_types(),
|
serializers={
|
||||||
|
RedirectChallenge().fields["component"].default: RedirectChallenge,
|
||||||
|
PlexAuthenticationChallenge().fields["component"].default: PlexAuthenticationChallenge,
|
||||||
|
},
|
||||||
resource_type_field_name="component",
|
resource_type_field_name="component",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -57,6 +67,7 @@ class IdentificationChallenge(Challenge):
|
|||||||
recovery_url = CharField(required=False)
|
recovery_url = CharField(required=False)
|
||||||
primary_action = CharField()
|
primary_action = CharField()
|
||||||
sources = LoginSourceSerializer(many=True, required=False)
|
sources = LoginSourceSerializer(many=True, required=False)
|
||||||
|
show_source_labels = BooleanField()
|
||||||
|
|
||||||
component = CharField(default="ak-stage-identification")
|
component = CharField(default="ak-stage-identification")
|
||||||
|
|
||||||
@ -77,7 +88,8 @@ class IdentificationChallengeResponse(ChallengeResponse):
|
|||||||
|
|
||||||
pre_user = self.stage.get_user(uid_field)
|
pre_user = self.stage.get_user(uid_field)
|
||||||
if not pre_user:
|
if not pre_user:
|
||||||
sleep(0.150)
|
# Sleep a random time (between 90 and 210ms) to "prevent" user enumeration attacks
|
||||||
|
sleep(0.30 * SystemRandom().randint(3, 7))
|
||||||
LOGGER.debug("invalid_login", identifier=uid_field)
|
LOGGER.debug("invalid_login", identifier=uid_field)
|
||||||
identification_failed.send(sender=self, request=self.stage.request, uid_field=uid_field)
|
identification_failed.send(sender=self, request=self.stage.request, uid_field=uid_field)
|
||||||
# We set the pending_user even on failure so it's part of the context, even
|
# We set the pending_user even on failure so it's part of the context, even
|
||||||
@ -152,6 +164,7 @@ class IdentificationStageView(ChallengeStageView):
|
|||||||
"component": "ak-stage-identification",
|
"component": "ak-stage-identification",
|
||||||
"user_fields": current_stage.user_fields,
|
"user_fields": current_stage.user_fields,
|
||||||
"password_fields": bool(current_stage.password_stage),
|
"password_fields": bool(current_stage.password_stage),
|
||||||
|
"show_source_labels": current_stage.show_source_labels,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# If the user has been redirected to us whilst trying to access an
|
# If the user has been redirected to us whilst trying to access an
|
||||||
|
|||||||
@ -123,6 +123,7 @@ class TestIdentificationStage(APITestCase):
|
|||||||
"name": "test",
|
"name": "test",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"show_source_labels": False,
|
||||||
"user_fields": ["email"],
|
"user_fields": ["email"],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -158,6 +159,7 @@ class TestIdentificationStage(APITestCase):
|
|||||||
{"code": "invalid", "string": "Failed to " "authenticate."}
|
{"code": "invalid", "string": "Failed to " "authenticate."}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"show_source_labels": False,
|
||||||
"flow_info": {
|
"flow_info": {
|
||||||
"background": self.flow.background_url,
|
"background": self.flow.background_url,
|
||||||
"cancel_url": reverse("authentik_flows:cancel"),
|
"cancel_url": reverse("authentik_flows:cancel"),
|
||||||
@ -218,6 +220,7 @@ class TestIdentificationStage(APITestCase):
|
|||||||
"authentik_core:if-flow",
|
"authentik_core:if-flow",
|
||||||
kwargs={"flow_slug": "unique-enrollment-string"},
|
kwargs={"flow_slug": "unique-enrollment-string"},
|
||||||
),
|
),
|
||||||
|
"show_source_labels": False,
|
||||||
"primary_action": "Log in",
|
"primary_action": "Log in",
|
||||||
"flow_info": {
|
"flow_info": {
|
||||||
"background": flow.background_url,
|
"background": flow.background_url,
|
||||||
@ -267,6 +270,7 @@ class TestIdentificationStage(APITestCase):
|
|||||||
"authentik_core:if-flow",
|
"authentik_core:if-flow",
|
||||||
kwargs={"flow_slug": "unique-recovery-string"},
|
kwargs={"flow_slug": "unique-recovery-string"},
|
||||||
),
|
),
|
||||||
|
"show_source_labels": False,
|
||||||
"primary_action": "Log in",
|
"primary_action": "Log in",
|
||||||
"flow_info": {
|
"flow_info": {
|
||||||
"background": flow.background_url,
|
"background": flow.background_url,
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
"""Invitation Stage API Views"""
|
"""Invitation Stage API Views"""
|
||||||
|
from django_filters.filters import BooleanFilter
|
||||||
|
from django_filters.filterset import FilterSet
|
||||||
from rest_framework.fields import JSONField
|
from rest_framework.fields import JSONField
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
@ -21,12 +23,23 @@ class InvitationStageSerializer(StageSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class InvitationStageFilter(FilterSet):
|
||||||
|
"""invitation filter"""
|
||||||
|
|
||||||
|
no_flows = BooleanFilter("flow", "isnull")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
model = InvitationStage
|
||||||
|
fields = ["name", "no_flows", "continue_flow_without_invitation", "stage_uuid"]
|
||||||
|
|
||||||
|
|
||||||
class InvitationStageViewSet(UsedByMixin, ModelViewSet):
|
class InvitationStageViewSet(UsedByMixin, ModelViewSet):
|
||||||
"""InvitationStage Viewset"""
|
"""InvitationStage Viewset"""
|
||||||
|
|
||||||
queryset = InvitationStage.objects.all()
|
queryset = InvitationStage.objects.all()
|
||||||
serializer_class = InvitationStageSerializer
|
serializer_class = InvitationStageSerializer
|
||||||
filterset_fields = "__all__"
|
filterset_class = InvitationStageFilter
|
||||||
ordering = ["name"]
|
ordering = ["name"]
|
||||||
|
|
||||||
|
|
||||||
@ -53,7 +66,7 @@ class InvitationViewSet(UsedByMixin, ModelViewSet):
|
|||||||
|
|
||||||
queryset = Invitation.objects.all()
|
queryset = Invitation.objects.all()
|
||||||
serializer_class = InvitationSerializer
|
serializer_class = InvitationSerializer
|
||||||
order = ["-expires"]
|
ordering = ["-expires"]
|
||||||
search_fields = ["created_by__username", "expires"]
|
search_fields = ["created_by__username", "expires"]
|
||||||
filterset_fields = ["created_by__username", "expires"]
|
filterset_fields = ["created_by__username", "expires"]
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,6 @@ from typing import Optional
|
|||||||
from deepmerge import always_merger
|
from deepmerge import always_merger
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.http.response import HttpResponseBadRequest
|
from django.http.response import HttpResponseBadRequest
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.flows.models import in_memory_stage
|
from authentik.flows.models import in_memory_stage
|
||||||
@ -50,7 +49,12 @@ class InvitationStageView(StageView):
|
|||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
return self.executor.stage_invalid()
|
return self.executor.stage_invalid()
|
||||||
|
|
||||||
invite: Invitation = get_object_or_404(Invitation, pk=token)
|
invite: Invitation = Invitation.objects.filter(pk=token).first()
|
||||||
|
if not invite:
|
||||||
|
LOGGER.debug("invalid invitation", token=token)
|
||||||
|
if stage.continue_flow_without_invitation:
|
||||||
|
return self.executor.stage_ok()
|
||||||
|
return self.executor.stage_invalid()
|
||||||
self.executor.plan.context[INVITATION_IN_EFFECT] = True
|
self.executor.plan.context[INVITATION_IN_EFFECT] = True
|
||||||
self.executor.plan.context[INVITATION] = invite
|
self.executor.plan.context[INVITATION] = invite
|
||||||
|
|
||||||
@ -79,7 +83,9 @@ class InvitationFinalStageView(StageView):
|
|||||||
if not invitation:
|
if not invitation:
|
||||||
LOGGER.warning("InvitationFinalStageView stage called without invitation")
|
LOGGER.warning("InvitationFinalStageView stage called without invitation")
|
||||||
return HttpResponseBadRequest
|
return HttpResponseBadRequest
|
||||||
if not invitation.single_use:
|
token = invitation.invite_uuid.hex
|
||||||
return self.executor.stage_ok()
|
if invitation.single_use:
|
||||||
invitation.delete()
|
invitation.delete()
|
||||||
|
LOGGER.debug("Deleted invitation", token=token)
|
||||||
|
del self.executor.plan.context[INVITATION]
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
"""prompt models"""
|
"""prompt models"""
|
||||||
from typing import Type
|
from typing import Any, Optional, Type
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -13,6 +13,7 @@ from rest_framework.fields import (
|
|||||||
EmailField,
|
EmailField,
|
||||||
HiddenField,
|
HiddenField,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
|
ReadOnlyField,
|
||||||
)
|
)
|
||||||
from rest_framework.serializers import BaseSerializer
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
|
||||||
@ -26,6 +27,10 @@ class FieldTypes(models.TextChoices):
|
|||||||
|
|
||||||
# Simple text field
|
# Simple text field
|
||||||
TEXT = "text", _("Text: Simple Text input")
|
TEXT = "text", _("Text: Simple Text input")
|
||||||
|
# Simple text field
|
||||||
|
TEXT_READ_ONLY = "text_read_only", _(
|
||||||
|
"Text (read-only): Simple Text input, but cannot be edited."
|
||||||
|
)
|
||||||
# Same as text, but has autocomplete for password managers
|
# Same as text, but has autocomplete for password managers
|
||||||
USERNAME = (
|
USERNAME = (
|
||||||
"username",
|
"username",
|
||||||
@ -74,13 +79,15 @@ class Prompt(SerializerModel):
|
|||||||
|
|
||||||
return PromptSerializer
|
return PromptSerializer
|
||||||
|
|
||||||
@property
|
def field(self, default: Optional[Any]) -> CharField:
|
||||||
def field(self) -> CharField:
|
|
||||||
"""Get field type for Challenge and response"""
|
"""Get field type for Challenge and response"""
|
||||||
field_class = CharField
|
field_class = CharField
|
||||||
kwargs = {
|
kwargs = {
|
||||||
"required": self.required,
|
"required": self.required,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.type == FieldTypes.TEXT_READ_ONLY:
|
||||||
|
field_class = ReadOnlyField
|
||||||
if self.type == FieldTypes.EMAIL:
|
if self.type == FieldTypes.EMAIL:
|
||||||
field_class = EmailField
|
field_class = EmailField
|
||||||
if self.type == FieldTypes.NUMBER:
|
if self.type == FieldTypes.NUMBER:
|
||||||
@ -97,12 +104,14 @@ class Prompt(SerializerModel):
|
|||||||
if self.type == FieldTypes.DATE_TIME:
|
if self.type == FieldTypes.DATE_TIME:
|
||||||
field_class = DateTimeField
|
field_class = DateTimeField
|
||||||
if self.type == FieldTypes.STATIC:
|
if self.type == FieldTypes.STATIC:
|
||||||
kwargs["initial"] = self.placeholder
|
kwargs["default"] = self.placeholder
|
||||||
kwargs["required"] = False
|
kwargs["required"] = False
|
||||||
kwargs["label"] = ""
|
kwargs["label"] = ""
|
||||||
if self.type == FieldTypes.SEPARATOR:
|
if self.type == FieldTypes.SEPARATOR:
|
||||||
kwargs["required"] = False
|
kwargs["required"] = False
|
||||||
kwargs["label"] = ""
|
kwargs["label"] = ""
|
||||||
|
if default:
|
||||||
|
kwargs["default"] = default
|
||||||
return field_class(**kwargs)
|
return field_class(**kwargs)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from django.http import HttpRequest, HttpResponse
|
|||||||
from django.http.request import QueryDict
|
from django.http.request import QueryDict
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from rest_framework.fields import BooleanField, CharField, IntegerField
|
from rest_framework.fields import BooleanField, CharField, ChoiceField, IntegerField
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ class StagePromptSerializer(PassiveSerializer):
|
|||||||
|
|
||||||
field_key = CharField()
|
field_key = CharField()
|
||||||
label = CharField(allow_blank=True)
|
label = CharField(allow_blank=True)
|
||||||
type = CharField()
|
type = ChoiceField(choices=FieldTypes.choices)
|
||||||
required = BooleanField()
|
required = BooleanField()
|
||||||
placeholder = CharField(allow_blank=True)
|
placeholder = CharField(allow_blank=True)
|
||||||
order = IntegerField()
|
order = IntegerField()
|
||||||
@ -65,7 +65,8 @@ class PromptChallengeResponse(ChallengeResponse):
|
|||||||
fields = list(self.stage.fields.all())
|
fields = list(self.stage.fields.all())
|
||||||
for field in fields:
|
for field in fields:
|
||||||
field: Prompt
|
field: Prompt
|
||||||
self.fields[field.field_key] = field.field
|
current = plan.context.get(PLAN_CONTEXT_PROMPT, {}).get(field.field_key)
|
||||||
|
self.fields[field.field_key] = field.field(current)
|
||||||
# Special handling for fields with username type
|
# Special handling for fields with username type
|
||||||
# these check for existing users with the same username
|
# these check for existing users with the same username
|
||||||
if field.type == FieldTypes.USERNAME:
|
if field.type == FieldTypes.USERNAME:
|
||||||
@ -96,10 +97,11 @@ class PromptChallengeResponse(ChallengeResponse):
|
|||||||
# Check if we have any static or hidden fields, and ensure they
|
# Check if we have any static or hidden fields, and ensure they
|
||||||
# still have the same value
|
# still have the same value
|
||||||
static_hidden_fields: QuerySet[Prompt] = self.stage.fields.filter(
|
static_hidden_fields: QuerySet[Prompt] = self.stage.fields.filter(
|
||||||
type__in=[FieldTypes.HIDDEN, FieldTypes.STATIC]
|
type__in=[FieldTypes.HIDDEN, FieldTypes.STATIC, FieldTypes.TEXT_READ_ONLY]
|
||||||
)
|
)
|
||||||
for static_hidden in static_hidden_fields:
|
for static_hidden in static_hidden_fields:
|
||||||
attrs[static_hidden.field_key] = static_hidden.placeholder
|
field = self.fields[static_hidden.field_key]
|
||||||
|
attrs[static_hidden.field_key] = field.default
|
||||||
|
|
||||||
# Check if we have two password fields, and make sure they are the same
|
# Check if we have two password fields, and make sure they are the same
|
||||||
password_fields: QuerySet[Prompt] = self.stage.fields.filter(type=FieldTypes.PASSWORD)
|
password_fields: QuerySet[Prompt] = self.stage.fields.filter(type=FieldTypes.PASSWORD)
|
||||||
@ -163,10 +165,17 @@ class PromptStageView(ChallengeStageView):
|
|||||||
|
|
||||||
def get_challenge(self, *args, **kwargs) -> Challenge:
|
def get_challenge(self, *args, **kwargs) -> Challenge:
|
||||||
fields = list(self.executor.current_stage.fields.all().order_by("order"))
|
fields = list(self.executor.current_stage.fields.all().order_by("order"))
|
||||||
|
serializers = []
|
||||||
|
context_prompt = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
|
||||||
|
for field in fields:
|
||||||
|
data = StagePromptSerializer(field).data
|
||||||
|
if field.field_key in context_prompt:
|
||||||
|
data["placeholder"] = context_prompt.get(field.field_key)
|
||||||
|
serializers.append(data)
|
||||||
challenge = PromptChallenge(
|
challenge = PromptChallenge(
|
||||||
data={
|
data={
|
||||||
"type": ChallengeTypes.NATIVE.value,
|
"type": ChallengeTypes.NATIVE.value,
|
||||||
"fields": [StagePromptSerializer(field).data for field in fields],
|
"fields": serializers,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return challenge
|
return challenge
|
||||||
|
|||||||
@ -21,31 +21,32 @@ var running = true
|
|||||||
func main() {
|
func main() {
|
||||||
log.SetLevel(log.DebugLevel)
|
log.SetLevel(log.DebugLevel)
|
||||||
log.SetFormatter(&log.JSONFormatter{})
|
log.SetFormatter(&log.JSONFormatter{})
|
||||||
|
l := log.WithField("logger", "authentik.root")
|
||||||
config.DefaultConfig()
|
config.DefaultConfig()
|
||||||
err := config.LoadConfig("./authentik/lib/default.yml")
|
err := config.LoadConfig("./authentik/lib/default.yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Warning("failed to load default config")
|
l.WithError(err).Warning("failed to load default config")
|
||||||
}
|
}
|
||||||
err = config.LoadConfig("./local.env.yml")
|
err = config.LoadConfig("./local.env.yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Debug("no local config to load")
|
l.WithError(err).Debug("no local config to load")
|
||||||
}
|
}
|
||||||
err = config.FromEnv()
|
err = config.FromEnv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Debug("failed to environment variables")
|
l.WithError(err).Debug("failed to environment variables")
|
||||||
}
|
}
|
||||||
config.ConfigureLogger()
|
config.ConfigureLogger()
|
||||||
|
|
||||||
if config.G.ErrorReporting.Enabled {
|
if config.G.ErrorReporting.Enabled {
|
||||||
err := sentry.Init(sentry.ClientOptions{
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
Dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
|
Dsn: config.G.ErrorReporting.DSN,
|
||||||
AttachStacktrace: true,
|
AttachStacktrace: true,
|
||||||
TracesSampleRate: 0.6,
|
TracesSampleRate: 0.6,
|
||||||
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
|
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
|
||||||
Environment: config.G.ErrorReporting.Environment,
|
Environment: config.G.ErrorReporting.Environment,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Warning("failed to init sentry")
|
l.WithError(err).Warning("failed to init sentry")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,31 +57,31 @@ func main() {
|
|||||||
|
|
||||||
g := gounicorn.NewGoUnicorn()
|
g := gounicorn.NewGoUnicorn()
|
||||||
ws := web.NewWebServer(g)
|
ws := web.NewWebServer(g)
|
||||||
defer g.Kill()
|
g.HealthyCallback = func() {
|
||||||
defer ws.Shutdown()
|
if !config.G.Web.DisableEmbeddedOutpost {
|
||||||
|
go attemptProxyStart(ws, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
go web.RunMetricsServer()
|
go web.RunMetricsServer()
|
||||||
for {
|
for {
|
||||||
go attemptStartBackend(g)
|
go attemptStartBackend(g)
|
||||||
ws.Start()
|
ws.Start()
|
||||||
if !config.G.Web.DisableEmbeddedOutpost {
|
|
||||||
go attemptProxyStart(ws, u)
|
|
||||||
}
|
|
||||||
|
|
||||||
<-ex
|
<-ex
|
||||||
running = false
|
running = false
|
||||||
log.WithField("logger", "authentik").Info("shutting down webserver")
|
l.WithField("logger", "authentik").Info("shutting down gunicorn")
|
||||||
|
go g.Kill()
|
||||||
|
l.WithField("logger", "authentik").Info("shutting down webserver")
|
||||||
go ws.Shutdown()
|
go ws.Shutdown()
|
||||||
log.WithField("logger", "authentik").Info("killing gunicorn")
|
|
||||||
g.Kill()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func attemptStartBackend(g *gounicorn.GoUnicorn) {
|
func attemptStartBackend(g *gounicorn.GoUnicorn) {
|
||||||
for {
|
for {
|
||||||
err := g.Start()
|
|
||||||
if !running {
|
if !running {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
err := g.Start()
|
||||||
log.WithField("logger", "authentik.router").WithError(err).Warning("gunicorn process died, restarting")
|
log.WithField("logger", "authentik.router").WithError(err).Warning("gunicorn process died, restarting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,8 +89,6 @@ func attemptStartBackend(g *gounicorn.GoUnicorn) {
|
|||||||
func attemptProxyStart(ws *web.WebServer, u *url.URL) {
|
func attemptProxyStart(ws *web.WebServer, u *url.URL) {
|
||||||
maxTries := 100
|
maxTries := 100
|
||||||
attempt := 0
|
attempt := 0
|
||||||
// Sleep to wait for the app server to start
|
|
||||||
time.Sleep(30 * time.Second)
|
|
||||||
for {
|
for {
|
||||||
log.WithField("logger", "authentik").Debug("attempting to init outpost")
|
log.WithField("logger", "authentik").Debug("attempting to init outpost")
|
||||||
ac := ak.NewAPIController(*u, config.G.SecretKey)
|
ac := ak.NewAPIController(*u, config.G.SecretKey)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ services:
|
|||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
server:
|
server:
|
||||||
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.10.1-rc2}
|
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.10.3}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
@ -38,7 +38,7 @@ services:
|
|||||||
- "0.0.0.0:9000:9000"
|
- "0.0.0.0:9000:9000"
|
||||||
- "0.0.0.0:9443:9443"
|
- "0.0.0.0:9443:9443"
|
||||||
worker:
|
worker:
|
||||||
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.10.1-rc2}
|
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.10.3}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: worker
|
command: worker
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
13
go.mod
13
go.mod
@ -9,12 +9,8 @@ require (
|
|||||||
github.com/garyburd/redigo v1.6.2 // indirect
|
github.com/garyburd/redigo v1.6.2 // indirect
|
||||||
github.com/getsentry/sentry-go v0.11.0
|
github.com/getsentry/sentry-go v0.11.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.1
|
github.com/go-ldap/ldap/v3 v3.4.1
|
||||||
github.com/go-openapi/analysis v0.20.1 // indirect
|
github.com/go-openapi/runtime v0.21.0
|
||||||
github.com/go-openapi/errors v0.20.0 // indirect
|
github.com/go-openapi/strfmt v0.21.0
|
||||||
github.com/go-openapi/runtime v0.20.0
|
|
||||||
github.com/go-openapi/strfmt v0.20.3
|
|
||||||
github.com/go-openapi/swag v0.19.15 // indirect
|
|
||||||
github.com/go-openapi/validate v0.20.2 // indirect
|
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
@ -26,15 +22,14 @@ require (
|
|||||||
github.com/imdario/mergo v0.3.12
|
github.com/imdario/mergo v0.3.12
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
|
||||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3
|
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba
|
||||||
github.com/pires/go-proxyproto v0.6.1
|
github.com/pires/go-proxyproto v0.6.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
|
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
|
||||||
github.com/prometheus/client_golang v1.11.0
|
github.com/prometheus/client_golang v1.11.0
|
||||||
github.com/recws-org/recws v1.3.1
|
github.com/recws-org/recws v1.3.1
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.8.1
|
||||||
go.mongodb.org/mongo-driver v1.5.2 // indirect
|
goauthentik.io/api v0.2021102.6
|
||||||
goauthentik.io/api v0.2021101.2
|
|
||||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
|
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
|
||||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
|
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
|
||||||
|
|||||||
44
go.sum
44
go.sum
@ -150,8 +150,8 @@ github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpX
|
|||||||
github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
||||||
github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
||||||
github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
||||||
github.com/go-openapi/errors v0.20.0 h1:Sxpo9PjEHDzhs3FbnGNonvDgWcMW2U7wGTcDDSFSceM=
|
github.com/go-openapi/errors v0.20.1 h1:j23mMDtRxMwIobkpId7sWh7Ddcx4ivaoqUbfXx5P+a8=
|
||||||
github.com/go-openapi/errors v0.20.0/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
github.com/go-openapi/errors v0.20.1/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
|
||||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||||
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||||
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
|
||||||
@ -162,8 +162,9 @@ github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3Hfo
|
|||||||
github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||||
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
|
||||||
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
|
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
|
||||||
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
|
|
||||||
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||||
|
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||||
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||||
github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||||
github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||||
@ -173,16 +174,17 @@ github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2e
|
|||||||
github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc=
|
github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc=
|
||||||
github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc=
|
github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc=
|
||||||
github.com/go-openapi/loads v0.20.0/go.mod h1:2LhKquiE513rN5xC6Aan6lYOSddlL8Mp20AW9kpviM4=
|
github.com/go-openapi/loads v0.20.0/go.mod h1:2LhKquiE513rN5xC6Aan6lYOSddlL8Mp20AW9kpviM4=
|
||||||
github.com/go-openapi/loads v0.20.2 h1:z5p5Xf5wujMxS1y8aP+vxwW5qYT2zdJBbXKmQUG3lcc=
|
|
||||||
github.com/go-openapi/loads v0.20.2/go.mod h1:hTVUotJ+UonAMMZsvakEgmWKgtulweO9vYP2bQYKA/o=
|
github.com/go-openapi/loads v0.20.2/go.mod h1:hTVUotJ+UonAMMZsvakEgmWKgtulweO9vYP2bQYKA/o=
|
||||||
|
github.com/go-openapi/loads v0.21.0 h1:jYtUO4wwP7psAweisP/MDoOpdzsYEESdoPcsWjHDR68=
|
||||||
|
github.com/go-openapi/loads v0.21.0/go.mod h1:rHYve9nZrQ4CJhyeIIFJINGCg1tQpx2yJrrNo8sf1ws=
|
||||||
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
|
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
|
||||||
github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
|
github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
|
||||||
github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
|
github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
|
||||||
github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo=
|
github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo=
|
||||||
github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98=
|
github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98=
|
||||||
github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
|
github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
|
||||||
github.com/go-openapi/runtime v0.20.0 h1:DEV4oYH28MqakaabtbxH0cjvlzFegi/15kfUVCfiZW0=
|
github.com/go-openapi/runtime v0.21.0 h1:giZ8eT26R+/rx6RX2MkYjZPY8vPYVKDhP/mOazrQHzM=
|
||||||
github.com/go-openapi/runtime v0.20.0/go.mod h1:2WnLRxMiOUWNN0UZskSkxW0+WXdfB1KmqRKCFH+ZWYk=
|
github.com/go-openapi/runtime v0.21.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs=
|
||||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||||
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||||
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
|
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
|
||||||
@ -192,8 +194,9 @@ github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHK
|
|||||||
github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU=
|
github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU=
|
||||||
github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU=
|
github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU=
|
||||||
github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ=
|
github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ=
|
||||||
github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ=
|
|
||||||
github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg=
|
github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg=
|
||||||
|
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||||
|
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||||
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||||
github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||||
github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
|
github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
|
||||||
@ -203,8 +206,9 @@ github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk
|
|||||||
github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
|
github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
|
||||||
github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc=
|
github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc=
|
||||||
github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc=
|
github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc=
|
||||||
github.com/go-openapi/strfmt v0.20.3 h1:YVG4ZgPZ00km/lRHrIf7c6cKL5/4FAUtG2T9RxWAgDY=
|
github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk=
|
||||||
github.com/go-openapi/strfmt v0.20.3/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk=
|
github.com/go-openapi/strfmt v0.21.0 h1:hX2qEZKmYks+t0hKeb4VTJpUm2UYsdL3+DCid5swxIs=
|
||||||
|
github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg=
|
||||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||||
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||||
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||||
@ -223,8 +227,8 @@ github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbN
|
|||||||
github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4=
|
github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4=
|
||||||
github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI=
|
github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI=
|
||||||
github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0=
|
github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0=
|
||||||
github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3hTT4ts=
|
github.com/go-openapi/validate v0.20.3 h1:GZPPhhKSZrE8HjB4eEkoYAZmoWA4+tCemSgINH1/vKw=
|
||||||
github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0=
|
github.com/go-openapi/validate v0.20.3/go.mod h1:goDdqVGiigM3jChcrYJxD2joalke3ZXeftD16byIjA4=
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
@ -375,6 +379,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||||
|
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
@ -431,8 +436,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
|
|||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s=
|
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s=
|
||||||
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA=
|
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA=
|
||||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 h1:NNis9uuNpG5h97Dvxxo53Scg02qBg+3Nfabg6zjFGu8=
|
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba h1:DO8NFYdcRv1dnyAINJIBm6Bw2XibtLvQniNFGzf2W8E=
|
||||||
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3/go.mod h1:YtrVB1/v9Td9SyjXpjYVmbdKgj9B0nPTBsdGUxy0i8U=
|
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba/go.mod h1:4S0XndRL8HNOaQBfdViJ2F/GPCgL524xlXRuXFH12/U=
|
||||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@ -547,15 +552,15 @@ go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4S
|
|||||||
go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
|
go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
|
||||||
go.mongodb.org/mongo-driver v1.4.6/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
|
go.mongodb.org/mongo-driver v1.4.6/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
|
||||||
go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=
|
go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=
|
||||||
go.mongodb.org/mongo-driver v1.5.2 h1:AsxOLoJTgP6YNM0fXWw4OjdluYmWzQYp+lFJL7xu9fU=
|
go.mongodb.org/mongo-driver v1.7.3 h1:G4l/eYY9VrQAK/AUgkV0koQKzQnyddnWxrd/Etf0jIs=
|
||||||
go.mongodb.org/mongo-driver v1.5.2/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=
|
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
goauthentik.io/api v0.2021101.2 h1:MEmrcCmR/fWfxwVVlWPuVt4S3tigyv3OugNndLcFL3Y=
|
goauthentik.io/api v0.2021102.6 h1:jDah4AH28snsmFl9RwRMKrQ1ayW4zXrQWinryQdlJwA=
|
||||||
goauthentik.io/api v0.2021101.2/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
|
goauthentik.io/api v0.2021102.6/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
@ -644,6 +649,7 @@ golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
||||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
@ -709,6 +715,7 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
|
||||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@ -720,8 +727,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
|||||||
@ -26,6 +26,7 @@ func DefaultConfig() {
|
|||||||
LogLevel: "info",
|
LogLevel: "info",
|
||||||
ErrorReporting: ErrorReportingConfig{
|
ErrorReporting: ErrorReportingConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
|
DSN: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,4 +42,5 @@ type ErrorReportingConfig struct {
|
|||||||
Enabled bool `yaml:"enabled" env:"AUTHENTIK_ERROR_REPORTING__ENABLED"`
|
Enabled bool `yaml:"enabled" env:"AUTHENTIK_ERROR_REPORTING__ENABLED"`
|
||||||
Environment string `yaml:"environment" env:"AUTHENTIK_ERROR_REPORTING__ENVIRONMENT"`
|
Environment string `yaml:"environment" env:"AUTHENTIK_ERROR_REPORTING__ENVIRONMENT"`
|
||||||
SendPII bool `yaml:"send_pii" env:"AUTHENTIK_ERROR_REPORTING__SEND_PII"`
|
SendPII bool `yaml:"send_pii" env:"AUTHENTIK_ERROR_REPORTING__SEND_PII"`
|
||||||
|
DSN string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,4 +17,4 @@ func OutpostUserAgent() string {
|
|||||||
return fmt.Sprintf("authentik-outpost@%s (build=%s)", VERSION, BUILD())
|
return fmt.Sprintf("authentik-outpost@%s (build=%s)", VERSION, BUILD())
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION = "2021.10.1-rc2"
|
const VERSION = "2021.10.3"
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@ -11,6 +13,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type GoUnicorn struct {
|
type GoUnicorn struct {
|
||||||
|
HealthyCallback func()
|
||||||
|
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
p *exec.Cmd
|
p *exec.Cmd
|
||||||
started bool
|
started bool
|
||||||
@ -25,6 +29,7 @@ func NewGoUnicorn() *GoUnicorn {
|
|||||||
started: false,
|
started: false,
|
||||||
killed: false,
|
killed: false,
|
||||||
alive: false,
|
alive: false,
|
||||||
|
HealthyCallback: func() {},
|
||||||
}
|
}
|
||||||
g.initCmd()
|
g.initCmd()
|
||||||
return g
|
return g
|
||||||
@ -46,7 +51,7 @@ func (g *GoUnicorn) IsRunning() bool {
|
|||||||
|
|
||||||
func (g *GoUnicorn) Start() error {
|
func (g *GoUnicorn) Start() error {
|
||||||
if g.killed {
|
if g.killed {
|
||||||
g.log.Debug("Not restarting gunicorn since we're killed")
|
g.log.Debug("Not restarting gunicorn since we're shutdown")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if g.started {
|
if g.started {
|
||||||
@ -76,6 +81,7 @@ func (g *GoUnicorn) healthcheck() {
|
|||||||
for range time.Tick(time.Second) {
|
for range time.Tick(time.Second) {
|
||||||
if check() {
|
if check() {
|
||||||
g.log.Info("backend is alive, backing off with healthchecks")
|
g.log.Info("backend is alive, backing off with healthchecks")
|
||||||
|
g.HealthyCallback()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
g.log.Debug("backend not alive yet")
|
g.log.Debug("backend not alive yet")
|
||||||
@ -87,8 +93,15 @@ func (g *GoUnicorn) healthcheck() {
|
|||||||
|
|
||||||
func (g *GoUnicorn) Kill() {
|
func (g *GoUnicorn) Kill() {
|
||||||
g.killed = true
|
g.killed = true
|
||||||
err := g.p.Process.Kill()
|
var err error
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
g.log.WithField("method", "kill").Warning("stopping gunicorn")
|
||||||
|
err = g.p.Process.Kill()
|
||||||
|
} else {
|
||||||
|
g.log.WithField("method", "sigterm").Warning("stopping gunicorn")
|
||||||
|
err = syscall.Kill(g.p.Process.Pid, syscall.SIGTERM)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.log.WithError(err).Warning("failed to kill gunicorn")
|
g.log.WithError(err).Warning("failed to stop gunicorn")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@ type APIController struct {
|
|||||||
logger *log.Entry
|
logger *log.Entry
|
||||||
|
|
||||||
reloadOffset time.Duration
|
reloadOffset time.Duration
|
||||||
|
lastWsReconnect time.Time
|
||||||
|
|
||||||
wsConn *recws.RecConn
|
wsConn *recws.RecConn
|
||||||
instanceUUID uuid.UUID
|
instanceUUID uuid.UUID
|
||||||
@ -142,6 +143,10 @@ func (a *APIController) StartBackgorundTasks() error {
|
|||||||
"build": constants.BUILD(),
|
"build": constants.BUILD(),
|
||||||
}).SetToCurrentTime()
|
}).SetToCurrentTime()
|
||||||
}
|
}
|
||||||
|
go func() {
|
||||||
|
a.logger.Debug("Starting WS reconnector...")
|
||||||
|
a.startWSReConnector()
|
||||||
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
a.logger.Debug("Starting WS Handler...")
|
a.logger.Debug("Starting WS Handler...")
|
||||||
a.startWSHandler()
|
a.startWSHandler()
|
||||||
|
|||||||
@ -56,6 +56,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithError(err).Warning("Failed to hello to authentik")
|
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithError(err).Warning("Failed to hello to authentik")
|
||||||
}
|
}
|
||||||
|
ac.lastWsReconnect = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown Gracefully stops all workers, disconnects from websocket
|
// Shutdown Gracefully stops all workers, disconnects from websocket
|
||||||
@ -69,6 +70,20 @@ func (ac *APIController) Shutdown() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ac *APIController) startWSReConnector() {
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
if ac.wsConn.IsConnected() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if time.Since(ac.lastWsReconnect).Seconds() > 30 {
|
||||||
|
ac.wsConn.CloseAndReconnect()
|
||||||
|
ac.logger.Info("Reconnecting websocket")
|
||||||
|
ac.lastWsReconnect = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (ac *APIController) startWSHandler() {
|
func (ac *APIController) startWSHandler() {
|
||||||
logger := ac.logger.WithField("loop", "ws-handler")
|
logger := ac.logger.WithField("loop", "ws-handler")
|
||||||
for {
|
for {
|
||||||
@ -80,8 +95,7 @@ func (ac *APIController) startWSHandler() {
|
|||||||
"outpost_type": ac.Server.Type(),
|
"outpost_type": ac.Server.Type(),
|
||||||
"uuid": ac.instanceUUID.String(),
|
"uuid": ac.instanceUUID.String(),
|
||||||
}).Set(0)
|
}).Set(0)
|
||||||
logger.WithError(err).Warning("ws write error, reconnecting")
|
logger.WithError(err).Warning("ws read error")
|
||||||
ac.wsConn.CloseAndReconnect()
|
|
||||||
time.Sleep(time.Second * 5)
|
time.Sleep(time.Second * 5)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -126,8 +140,7 @@ func (ac *APIController) startWSHealth() {
|
|||||||
err := ac.wsConn.WriteJSON(aliveMsg)
|
err := ac.wsConn.WriteJSON(aliveMsg)
|
||||||
ac.logger.WithField("loop", "ws-health").Trace("hello'd")
|
ac.logger.WithField("loop", "ws-health").Trace("hello'd")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error, reconnecting")
|
ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error")
|
||||||
ac.wsConn.CloseAndReconnect()
|
|
||||||
time.Sleep(time.Second * 5)
|
time.Sleep(time.Second * 5)
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -18,7 +17,6 @@ import (
|
|||||||
"goauthentik.io/api"
|
"goauthentik.io/api"
|
||||||
"goauthentik.io/internal/constants"
|
"goauthentik.io/internal/constants"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
"goauthentik.io/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type StageComponent string
|
type StageComponent string
|
||||||
@ -103,8 +101,8 @@ type ChallengeInt interface {
|
|||||||
GetResponseErrors() map[string][]api.ErrorDetail
|
GetResponseErrors() map[string][]api.ErrorDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fe *FlowExecutor) DelegateClientIP(a net.Addr) {
|
func (fe *FlowExecutor) DelegateClientIP(a string) {
|
||||||
fe.cip = utils.GetIP(a)
|
fe.cip = a
|
||||||
fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, fe.cip)
|
fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, fe.cip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,44 +1,18 @@
|
|||||||
package ldap
|
package ldap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/nmcclain/ldap"
|
"github.com/nmcclain/ldap"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
log "github.com/sirupsen/logrus"
|
"goauthentik.io/internal/outpost/ldap/bind"
|
||||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||||
"goauthentik.io/internal/utils"
|
"goauthentik.io/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BindRequest struct {
|
|
||||||
BindDN string
|
|
||||||
BindPW string
|
|
||||||
id string
|
|
||||||
conn net.Conn
|
|
||||||
log *log.Entry
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
|
func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
|
||||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
|
req, span := bind.NewRequest(bindDN, bindPW, conn)
|
||||||
sentry.TransactionName("authentik.providers.ldap.bind"))
|
|
||||||
rid := uuid.New().String()
|
|
||||||
span.SetTag("request_uid", rid)
|
|
||||||
span.SetTag("user.username", bindDN)
|
|
||||||
|
|
||||||
bindDN = strings.ToLower(bindDN)
|
|
||||||
req := BindRequest{
|
|
||||||
BindDN: bindDN,
|
|
||||||
BindPW: bindPW,
|
|
||||||
conn: conn,
|
|
||||||
log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())),
|
|
||||||
id: rid,
|
|
||||||
ctx: span.Context(),
|
|
||||||
}
|
|
||||||
defer func() {
|
defer func() {
|
||||||
span.Finish()
|
span.Finish()
|
||||||
metrics.Requests.With(prometheus.Labels{
|
metrics.Requests.With(prometheus.Labels{
|
||||||
@ -46,19 +20,19 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
|
|||||||
"type": "bind",
|
"type": "bind",
|
||||||
"filter": "",
|
"filter": "",
|
||||||
"dn": req.BindDN,
|
"dn": req.BindDN,
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
"client": req.RemoteAddr(),
|
||||||
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
|
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
|
||||||
req.log.WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Bind request")
|
req.Log().WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Bind request")
|
||||||
}()
|
}()
|
||||||
for _, instance := range ls.providers {
|
for _, instance := range ls.providers {
|
||||||
username, err := instance.getUsername(bindDN)
|
username, err := instance.binder.GetUsername(bindDN)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return instance.Bind(username, req)
|
return instance.binder.Bind(username, req)
|
||||||
} else {
|
} else {
|
||||||
req.log.WithError(err).Debug("Username not for instance")
|
req.Log().WithError(err).Debug("Username not for instance")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req.log.WithField("request", "bind").Warning("No provider found for request")
|
req.Log().WithField("request", "bind").Warning("No provider found for request")
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
metrics.RequestsRejected.With(prometheus.Labels{
|
||||||
"outpost_name": ls.ac.Outpost.Name,
|
"outpost_name": ls.ac.Outpost.Name,
|
||||||
"type": "bind",
|
"type": "bind",
|
||||||
@ -68,10 +42,3 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
|
|||||||
}).Inc()
|
}).Inc()
|
||||||
return ldap.LDAPResultOperationsError, nil
|
return ldap.LDAPResultOperationsError, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ls *LDAPServer) TimerFlowCacheExpiry() {
|
|
||||||
for _, p := range ls.providers {
|
|
||||||
ls.log.WithField("flow", p.flowSlug).Debug("Pre-heating flow cache")
|
|
||||||
p.TimerFlowCacheExpiry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
9
internal/outpost/ldap/bind/binder.go
Normal file
9
internal/outpost/ldap/bind/binder.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import "github.com/nmcclain/ldap"
|
||||||
|
|
||||||
|
type Binder interface {
|
||||||
|
GetUsername(string) (string, error)
|
||||||
|
Bind(username string, req *Request) (ldap.LDAPResultCode, error)
|
||||||
|
TimerFlowCacheExpiry()
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package ldap
|
package direct
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -12,14 +12,30 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"goauthentik.io/api"
|
"goauthentik.io/api"
|
||||||
"goauthentik.io/internal/outpost"
|
"goauthentik.io/internal/outpost"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/bind"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/flags"
|
||||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||||
"goauthentik.io/internal/utils"
|
"goauthentik.io/internal/outpost/ldap/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ContextUserKey = "ak_user"
|
const ContextUserKey = "ak_user"
|
||||||
|
|
||||||
func (pi *ProviderInstance) getUsername(dn string) (string, error) {
|
type DirectBinder struct {
|
||||||
if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(pi.BaseDN)) {
|
si server.LDAPServerInstance
|
||||||
|
log *log.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDirectBinder(si server.LDAPServerInstance) *DirectBinder {
|
||||||
|
db := &DirectBinder{
|
||||||
|
si: si,
|
||||||
|
log: log.WithField("logger", "authentik.outpost.ldap.binder.direct"),
|
||||||
|
}
|
||||||
|
db.log.Info("initialised direct binder")
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DirectBinder) GetUsername(dn string) (string, error) {
|
||||||
|
if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(db.si.GetBaseDN())) {
|
||||||
return "", errors.New("invalid base DN")
|
return "", errors.New("invalid base DN")
|
||||||
}
|
}
|
||||||
dns, err := goldap.ParseDN(dn)
|
dns, err := goldap.ParseDN(dn)
|
||||||
@ -36,13 +52,13 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) {
|
|||||||
return "", errors.New("failed to find cn")
|
return "", errors.New("failed to find cn")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPResultCode, error) {
|
func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResultCode, error) {
|
||||||
fe := outpost.NewFlowExecutor(req.ctx, pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
fe := outpost.NewFlowExecutor(req.Context(), db.si.GetFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{
|
||||||
"bindDN": req.BindDN,
|
"bindDN": req.BindDN,
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
"client": req.RemoteAddr(),
|
||||||
"requestId": req.id,
|
"requestId": req.ID(),
|
||||||
})
|
})
|
||||||
fe.DelegateClientIP(req.conn.RemoteAddr())
|
fe.DelegateClientIP(req.RemoteAddr())
|
||||||
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
|
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
|
||||||
|
|
||||||
fe.Answers[outpost.StageIdentification] = username
|
fe.Answers[outpost.StageIdentification] = username
|
||||||
@ -51,83 +67,82 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
|
|||||||
passed, err := fe.Execute()
|
passed, err := fe.Execute()
|
||||||
if !passed {
|
if !passed {
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
metrics.RequestsRejected.With(prometheus.Labels{
|
||||||
"outpost_name": pi.outpostName,
|
"outpost_name": db.si.GetOutpostName(),
|
||||||
"type": "bind",
|
"type": "bind",
|
||||||
"reason": "invalid_credentials",
|
"reason": "invalid_credentials",
|
||||||
"dn": req.BindDN,
|
"dn": req.BindDN,
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
"client": req.RemoteAddr(),
|
||||||
}).Inc()
|
}).Inc()
|
||||||
return ldap.LDAPResultInvalidCredentials, nil
|
return ldap.LDAPResultInvalidCredentials, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
metrics.RequestsRejected.With(prometheus.Labels{
|
||||||
"outpost_name": pi.outpostName,
|
"outpost_name": db.si.GetOutpostName(),
|
||||||
"type": "bind",
|
"type": "bind",
|
||||||
"reason": "flow_error",
|
"reason": "flow_error",
|
||||||
"dn": req.BindDN,
|
"dn": req.BindDN,
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
"client": req.RemoteAddr(),
|
||||||
}).Inc()
|
}).Inc()
|
||||||
req.log.WithError(err).Warning("failed to execute flow")
|
req.Log().WithError(err).Warning("failed to execute flow")
|
||||||
return ldap.LDAPResultOperationsError, nil
|
return ldap.LDAPResultOperationsError, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
access, err := fe.CheckApplicationAccess(pi.appSlug)
|
access, err := fe.CheckApplicationAccess(db.si.GetAppSlug())
|
||||||
if !access {
|
if !access {
|
||||||
req.log.Info("Access denied for user")
|
req.Log().Info("Access denied for user")
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
metrics.RequestsRejected.With(prometheus.Labels{
|
||||||
"outpost_name": pi.outpostName,
|
"outpost_name": db.si.GetOutpostName(),
|
||||||
"type": "bind",
|
"type": "bind",
|
||||||
"reason": "access_denied",
|
"reason": "access_denied",
|
||||||
"dn": req.BindDN,
|
"dn": req.BindDN,
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
"client": req.RemoteAddr(),
|
||||||
}).Inc()
|
}).Inc()
|
||||||
return ldap.LDAPResultInsufficientAccessRights, nil
|
return ldap.LDAPResultInsufficientAccessRights, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
metrics.RequestsRejected.With(prometheus.Labels{
|
||||||
"outpost_name": pi.outpostName,
|
"outpost_name": db.si.GetOutpostName(),
|
||||||
"type": "bind",
|
"type": "bind",
|
||||||
"reason": "access_check_fail",
|
"reason": "access_check_fail",
|
||||||
"dn": req.BindDN,
|
"dn": req.BindDN,
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
"client": req.RemoteAddr(),
|
||||||
}).Inc()
|
}).Inc()
|
||||||
req.log.WithError(err).Warning("failed to check access")
|
req.Log().WithError(err).Warning("failed to check access")
|
||||||
return ldap.LDAPResultOperationsError, nil
|
return ldap.LDAPResultOperationsError, nil
|
||||||
}
|
}
|
||||||
req.log.Info("User has access")
|
req.Log().Info("User has access")
|
||||||
uisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.bind.user_info")
|
uisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.bind.user_info")
|
||||||
// Get user info to store in context
|
// Get user info to store in context
|
||||||
userInfo, _, err := fe.ApiClient().CoreApi.CoreUsersMeRetrieve(context.Background()).Execute()
|
userInfo, _, err := fe.ApiClient().CoreApi.CoreUsersMeRetrieve(context.Background()).Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
metrics.RequestsRejected.With(prometheus.Labels{
|
||||||
"outpost_name": pi.outpostName,
|
"outpost_name": db.si.GetOutpostName(),
|
||||||
"type": "bind",
|
"type": "bind",
|
||||||
"reason": "user_info_fail",
|
"reason": "user_info_fail",
|
||||||
"dn": req.BindDN,
|
"dn": req.BindDN,
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
"client": req.RemoteAddr(),
|
||||||
}).Inc()
|
}).Inc()
|
||||||
req.log.WithError(err).Warning("failed to get user info")
|
req.Log().WithError(err).Warning("failed to get user info")
|
||||||
return ldap.LDAPResultOperationsError, nil
|
return ldap.LDAPResultOperationsError, nil
|
||||||
}
|
}
|
||||||
pi.boundUsersMutex.Lock()
|
cs := db.SearchAccessCheck(userInfo.User)
|
||||||
cs := pi.SearchAccessCheck(userInfo.User)
|
flags := flags.UserFlags{
|
||||||
pi.boundUsers[req.BindDN] = UserFlags{
|
|
||||||
UserPk: userInfo.User.Pk,
|
UserPk: userInfo.User.Pk,
|
||||||
CanSearch: cs != nil,
|
CanSearch: cs != nil,
|
||||||
}
|
}
|
||||||
if pi.boundUsers[req.BindDN].CanSearch {
|
db.si.SetFlags(req.BindDN, flags)
|
||||||
req.log.WithField("group", cs).Info("Allowed access to search")
|
if flags.CanSearch {
|
||||||
|
req.Log().WithField("group", cs).Info("Allowed access to search")
|
||||||
}
|
}
|
||||||
uisp.Finish()
|
uisp.Finish()
|
||||||
defer pi.boundUsersMutex.Unlock()
|
|
||||||
return ldap.LDAPResultSuccess, nil
|
return ldap.LDAPResultSuccess, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchAccessCheck Check if the current user is allowed to search
|
// SearchAccessCheck Check if the current user is allowed to search
|
||||||
func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string {
|
func (db *DirectBinder) SearchAccessCheck(user api.UserSelf) *string {
|
||||||
for _, group := range user.Groups {
|
for _, group := range user.Groups {
|
||||||
for _, allowedGroup := range pi.searchAllowedGroups {
|
for _, allowedGroup := range db.si.GetSearchAllowedGroups() {
|
||||||
pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
|
db.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
|
||||||
if group.Pk == allowedGroup.String() {
|
if group.Pk == allowedGroup.String() {
|
||||||
return &group.Name
|
return &group.Name
|
||||||
}
|
}
|
||||||
@ -136,13 +151,13 @@ func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pi *ProviderInstance) TimerFlowCacheExpiry() {
|
func (db *DirectBinder) TimerFlowCacheExpiry() {
|
||||||
fe := outpost.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{})
|
fe := outpost.NewFlowExecutor(context.Background(), db.si.GetFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{})
|
||||||
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
|
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
|
||||||
fe.Params.Add("goauthentik.io/outpost/ldap-warmup", "true")
|
fe.Params.Add("goauthentik.io/outpost/ldap-warmup", "true")
|
||||||
|
|
||||||
err := fe.WarmUp()
|
err := fe.WarmUp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pi.log.WithError(err).Warning("failed to warm up flow cache")
|
db.log.WithError(err).Warning("failed to warm up flow cache")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
55
internal/outpost/ldap/bind/request.go
Normal file
55
internal/outpost/ldap/bind/request.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/getsentry/sentry-go"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
BindDN string
|
||||||
|
BindPW string
|
||||||
|
id string
|
||||||
|
conn net.Conn
|
||||||
|
log *log.Entry
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequest(bindDN string, bindPW string, conn net.Conn) (*Request, *sentry.Span) {
|
||||||
|
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
|
||||||
|
sentry.TransactionName("authentik.providers.ldap.bind"))
|
||||||
|
rid := uuid.New().String()
|
||||||
|
span.SetTag("request_uid", rid)
|
||||||
|
span.SetTag("user.username", bindDN)
|
||||||
|
|
||||||
|
bindDN = strings.ToLower(bindDN)
|
||||||
|
return &Request{
|
||||||
|
BindDN: bindDN,
|
||||||
|
BindPW: bindPW,
|
||||||
|
conn: conn,
|
||||||
|
log: log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())),
|
||||||
|
id: rid,
|
||||||
|
ctx: span.Context(),
|
||||||
|
}, span
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) Context() context.Context {
|
||||||
|
return r.ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) Log() *log.Entry {
|
||||||
|
return r.log
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) RemoteAddr() string {
|
||||||
|
return utils.GetIP(r.conn.RemoteAddr())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) ID() string {
|
||||||
|
return r.id
|
||||||
|
}
|
||||||
@ -1,32 +0,0 @@
|
|||||||
package ldap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (ls *LDAPServer) Close(boundDN string, conn net.Conn) error {
|
|
||||||
for _, p := range ls.providers {
|
|
||||||
p.delayDeleteUserInfo(boundDN)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pi *ProviderInstance) delayDeleteUserInfo(dn string) {
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
quit := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
pi.boundUsersMutex.Lock()
|
|
||||||
delete(pi.boundUsers, dn)
|
|
||||||
pi.boundUsersMutex.Unlock()
|
|
||||||
close(quit)
|
|
||||||
case <-quit:
|
|
||||||
ticker.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
21
internal/outpost/ldap/constants/constants.go
Normal file
21
internal/outpost/ldap/constants/constants.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
OCGroup = "group"
|
||||||
|
OCGroupOfUniqueNames = "groupOfUniqueNames"
|
||||||
|
OCAKGroup = "goauthentik.io/ldap/group"
|
||||||
|
OCAKVirtualGroup = "goauthentik.io/ldap/virtual-group"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OCUser = "user"
|
||||||
|
OCOrgPerson = "organizationalPerson"
|
||||||
|
OCInetOrgPerson = "inetOrgPerson"
|
||||||
|
OCAKUser = "goauthentik.io/ldap/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OUUsers = "users"
|
||||||
|
OUGroups = "groups"
|
||||||
|
OUVirtualGroups = "virtual-groups"
|
||||||
|
)
|
||||||
33
internal/outpost/ldap/entries.go
Normal file
33
internal/outpost/ldap/entries.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nmcclain/ldap"
|
||||||
|
"goauthentik.io/api"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/constants"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||||
|
dn := pi.GetUserDN(u.Username)
|
||||||
|
attrs := utils.AKAttrsToLDAP(u.Attributes)
|
||||||
|
|
||||||
|
attrs = utils.EnsureAttributes(attrs, map[string][]string{
|
||||||
|
"memberOf": pi.GroupsForUser(u),
|
||||||
|
// Old fields for backwards compatibility
|
||||||
|
"accountStatus": {utils.BoolToString(*u.IsActive)},
|
||||||
|
"superuser": {utils.BoolToString(u.IsSuperuser)},
|
||||||
|
// End old fields
|
||||||
|
"goauthentik.io/ldap/active": {utils.BoolToString(*u.IsActive)},
|
||||||
|
"goauthentik.io/ldap/superuser": {utils.BoolToString(u.IsSuperuser)},
|
||||||
|
"cn": {u.Username},
|
||||||
|
"sAMAccountName": {u.Username},
|
||||||
|
"uid": {u.Uid},
|
||||||
|
"name": {u.Name},
|
||||||
|
"displayName": {u.Name},
|
||||||
|
"mail": {*u.Email},
|
||||||
|
"objectClass": {constants.OCUser, constants.OCOrgPerson, constants.OCInetOrgPerson, constants.OCAKUser},
|
||||||
|
"uidNumber": {pi.GetUidNumber(u)},
|
||||||
|
"gidNumber": {pi.GetUidNumber(u)},
|
||||||
|
})
|
||||||
|
return &ldap.Entry{DN: dn, Attributes: attrs}
|
||||||
|
}
|
||||||
9
internal/outpost/ldap/flags/flags.go
Normal file
9
internal/outpost/ldap/flags/flags.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package flags
|
||||||
|
|
||||||
|
import "goauthentik.io/api"
|
||||||
|
|
||||||
|
type UserFlags struct {
|
||||||
|
UserInfo *api.User
|
||||||
|
UserPk int32
|
||||||
|
CanSearch bool
|
||||||
|
}
|
||||||
66
internal/outpost/ldap/group/group.go
Normal file
66
internal/outpost/ldap/group/group.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package group
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nmcclain/ldap"
|
||||||
|
"goauthentik.io/api"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/constants"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/server"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LDAPGroup struct {
|
||||||
|
DN string
|
||||||
|
CN string
|
||||||
|
Uid string
|
||||||
|
GidNumber string
|
||||||
|
Member []string
|
||||||
|
IsSuperuser bool
|
||||||
|
IsVirtualGroup bool
|
||||||
|
AKAttributes interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lg *LDAPGroup) Entry() *ldap.Entry {
|
||||||
|
attrs := utils.AKAttrsToLDAP(lg.AKAttributes)
|
||||||
|
|
||||||
|
objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCAKGroup}
|
||||||
|
if lg.IsVirtualGroup {
|
||||||
|
objectClass = append(objectClass, constants.OCAKVirtualGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs = utils.EnsureAttributes(attrs, map[string][]string{
|
||||||
|
"objectClass": objectClass,
|
||||||
|
"member": lg.Member,
|
||||||
|
"goauthentik.io/ldap/superuser": {utils.BoolToString(lg.IsSuperuser)},
|
||||||
|
"cn": {lg.CN},
|
||||||
|
"uid": {lg.Uid},
|
||||||
|
"sAMAccountName": {lg.CN},
|
||||||
|
"gidNumber": {lg.GidNumber},
|
||||||
|
})
|
||||||
|
return &ldap.Entry{DN: lg.DN, Attributes: attrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromAPIGroup(g api.Group, si server.LDAPServerInstance) *LDAPGroup {
|
||||||
|
return &LDAPGroup{
|
||||||
|
DN: si.GetGroupDN(g.Name),
|
||||||
|
CN: g.Name,
|
||||||
|
Uid: string(g.Pk),
|
||||||
|
GidNumber: si.GetGidNumber(g),
|
||||||
|
Member: si.UsersForGroup(g),
|
||||||
|
IsVirtualGroup: false,
|
||||||
|
IsSuperuser: *g.IsSuperuser,
|
||||||
|
AKAttributes: g.Attributes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromAPIUser(u api.User, si server.LDAPServerInstance) *LDAPGroup {
|
||||||
|
return &LDAPGroup{
|
||||||
|
DN: si.GetVirtualGroupDN(u.Username),
|
||||||
|
CN: u.Username,
|
||||||
|
Uid: u.Uid,
|
||||||
|
GidNumber: si.GetUidNumber(u),
|
||||||
|
Member: []string{si.GetUserDN(u.Username)},
|
||||||
|
IsVirtualGroup: true,
|
||||||
|
IsSuperuser: false,
|
||||||
|
AKAttributes: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
4
internal/outpost/ldap/handler/handler.go
Normal file
4
internal/outpost/ldap/handler/handler.go
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
}
|
||||||
83
internal/outpost/ldap/instance.go
Normal file
83
internal/outpost/ldap/instance.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/api"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/bind"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/flags"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/search"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProviderInstance struct {
|
||||||
|
BaseDN string
|
||||||
|
UserDN string
|
||||||
|
VirtualGroupDN string
|
||||||
|
GroupDN string
|
||||||
|
|
||||||
|
searcher search.Searcher
|
||||||
|
binder bind.Binder
|
||||||
|
|
||||||
|
appSlug string
|
||||||
|
flowSlug string
|
||||||
|
s *LDAPServer
|
||||||
|
log *log.Entry
|
||||||
|
|
||||||
|
tlsServerName *string
|
||||||
|
cert *tls.Certificate
|
||||||
|
outpostName string
|
||||||
|
searchAllowedGroups []*strfmt.UUID
|
||||||
|
boundUsersMutex sync.RWMutex
|
||||||
|
boundUsers map[string]flags.UserFlags
|
||||||
|
|
||||||
|
uidStartNumber int32
|
||||||
|
gidStartNumber int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetAPIClient() *api.APIClient {
|
||||||
|
return pi.s.ac.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetBaseDN() string {
|
||||||
|
return pi.BaseDN
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetBaseGroupDN() string {
|
||||||
|
return pi.GroupDN
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetBaseUserDN() string {
|
||||||
|
return pi.UserDN
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetOutpostName() string {
|
||||||
|
return pi.outpostName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetFlags(dn string) (flags.UserFlags, bool) {
|
||||||
|
pi.boundUsersMutex.RLock()
|
||||||
|
flags, ok := pi.boundUsers[dn]
|
||||||
|
pi.boundUsersMutex.RUnlock()
|
||||||
|
return flags, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) SetFlags(dn string, flag flags.UserFlags) {
|
||||||
|
pi.boundUsersMutex.Lock()
|
||||||
|
pi.boundUsers[dn] = flag
|
||||||
|
pi.boundUsersMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetAppSlug() string {
|
||||||
|
return pi.appSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetFlowSlug() string {
|
||||||
|
return pi.flowSlug
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID {
|
||||||
|
return pi.searchAllowedGroups
|
||||||
|
}
|
||||||
@ -1,244 +0,0 @@
|
|||||||
package ldap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/nmcclain/ldap"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
"goauthentik.io/api"
|
|
||||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
|
||||||
"goauthentik.io/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (pi *ProviderInstance) SearchMe(req SearchRequest, f UserFlags) (ldap.ServerSearchResult, error) {
|
|
||||||
if f.UserInfo == nil {
|
|
||||||
u, _, err := pi.s.ac.Client.CoreApi.CoreUsersRetrieve(req.ctx, f.UserPk).Execute()
|
|
||||||
if err != nil {
|
|
||||||
req.log.WithError(err).Warning("Failed to get user info")
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("failed to get userinfo")
|
|
||||||
}
|
|
||||||
f.UserInfo = &u
|
|
||||||
}
|
|
||||||
entries := make([]*ldap.Entry, 1)
|
|
||||||
entries[0] = pi.UserEntry(*f.UserInfo)
|
|
||||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult, error) {
|
|
||||||
accsp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.check_access")
|
|
||||||
baseDN := strings.ToLower("," + pi.BaseDN)
|
|
||||||
|
|
||||||
entries := []*ldap.Entry{}
|
|
||||||
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
|
|
||||||
if err != nil {
|
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
|
||||||
"outpost_name": pi.outpostName,
|
|
||||||
"type": "search",
|
|
||||||
"reason": "filter_parse_fail",
|
|
||||||
"dn": req.BindDN,
|
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
|
||||||
}).Inc()
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
|
||||||
}
|
|
||||||
if len(req.BindDN) < 1 {
|
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
|
||||||
"outpost_name": pi.outpostName,
|
|
||||||
"type": "search",
|
|
||||||
"reason": "empty_bind_dn",
|
|
||||||
"dn": req.BindDN,
|
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
|
||||||
}).Inc()
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(req.BindDN, baseDN) {
|
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
|
||||||
"outpost_name": pi.outpostName,
|
|
||||||
"type": "search",
|
|
||||||
"reason": "invalid_bind_dn",
|
|
||||||
"dn": req.BindDN,
|
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
|
||||||
}).Inc()
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, pi.BaseDN)
|
|
||||||
}
|
|
||||||
|
|
||||||
pi.boundUsersMutex.RLock()
|
|
||||||
flags, ok := pi.boundUsers[req.BindDN]
|
|
||||||
pi.boundUsersMutex.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
pi.log.Debug("User info not cached")
|
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
|
||||||
"outpost_name": pi.outpostName,
|
|
||||||
"type": "search",
|
|
||||||
"reason": "user_info_not_cached",
|
|
||||||
"dn": req.BindDN,
|
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
|
||||||
}).Inc()
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.SearchRequest.Scope == ldap.ScopeBaseObject {
|
|
||||||
pi.log.Debug("base scope, showing domain info")
|
|
||||||
return pi.SearchBase(req, flags.CanSearch)
|
|
||||||
}
|
|
||||||
if !flags.CanSearch {
|
|
||||||
pi.log.Debug("User can't search, showing info about user")
|
|
||||||
return pi.SearchMe(req, flags)
|
|
||||||
}
|
|
||||||
accsp.Finish()
|
|
||||||
|
|
||||||
parsedFilter, err := ldap.CompileFilter(req.Filter)
|
|
||||||
if err != nil {
|
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
|
||||||
"outpost_name": pi.outpostName,
|
|
||||||
"type": "search",
|
|
||||||
"reason": "filter_parse_fail",
|
|
||||||
"dn": req.BindDN,
|
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
|
||||||
}).Inc()
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a custom client to set additional headers
|
|
||||||
c := api.NewAPIClient(pi.s.ac.Client.GetConfig())
|
|
||||||
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
|
|
||||||
|
|
||||||
switch filterEntity {
|
|
||||||
default:
|
|
||||||
metrics.RequestsRejected.With(prometheus.Labels{
|
|
||||||
"outpost_name": pi.outpostName,
|
|
||||||
"type": "search",
|
|
||||||
"reason": "unhandled_filter_type",
|
|
||||||
"dn": req.BindDN,
|
|
||||||
"client": utils.GetIP(req.conn.RemoteAddr()),
|
|
||||||
}).Inc()
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, req.Filter)
|
|
||||||
case "groupOfUniqueNames":
|
|
||||||
fallthrough
|
|
||||||
case "goauthentik.io/ldap/group":
|
|
||||||
fallthrough
|
|
||||||
case "goauthentik.io/ldap/virtual-group":
|
|
||||||
fallthrough
|
|
||||||
case GroupObjectClass:
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
wg.Add(2)
|
|
||||||
|
|
||||||
gEntries := make([]*ldap.Entry, 0)
|
|
||||||
uEntries := make([]*ldap.Entry, 0)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
gapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_group")
|
|
||||||
searchReq, skip := parseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter, false)
|
|
||||||
if skip {
|
|
||||||
pi.log.Trace("Skip backend request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
groups, _, err := searchReq.Execute()
|
|
||||||
gapisp.Finish()
|
|
||||||
if err != nil {
|
|
||||||
req.log.WithError(err).Warning("failed to get groups")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pi.log.WithField("count", len(groups.Results)).Trace("Got results from API")
|
|
||||||
|
|
||||||
for _, g := range groups.Results {
|
|
||||||
gEntries = append(gEntries, pi.GroupEntry(pi.APIGroupToLDAPGroup(g)))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
uapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_user")
|
|
||||||
searchReq, skip := parseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
|
|
||||||
if skip {
|
|
||||||
pi.log.Trace("Skip backend request")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
users, _, err := searchReq.Execute()
|
|
||||||
uapisp.Finish()
|
|
||||||
if err != nil {
|
|
||||||
req.log.WithError(err).Warning("failed to get users")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, u := range users.Results {
|
|
||||||
uEntries = append(uEntries, pi.GroupEntry(pi.APIUserToLDAPGroup(u)))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
wg.Wait()
|
|
||||||
entries = append(gEntries, uEntries...)
|
|
||||||
case "":
|
|
||||||
fallthrough
|
|
||||||
case "organizationalPerson":
|
|
||||||
fallthrough
|
|
||||||
case "inetOrgPerson":
|
|
||||||
fallthrough
|
|
||||||
case "goauthentik.io/ldap/user":
|
|
||||||
fallthrough
|
|
||||||
case UserObjectClass:
|
|
||||||
uapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_user")
|
|
||||||
searchReq, skip := parseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
|
|
||||||
if skip {
|
|
||||||
pi.log.Trace("Skip backend request")
|
|
||||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
|
||||||
}
|
|
||||||
users, _, err := searchReq.Execute()
|
|
||||||
uapisp.Finish()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
|
||||||
}
|
|
||||||
for _, u := range users.Results {
|
|
||||||
entries = append(entries, pi.UserEntry(u))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
|
||||||
dn := pi.GetUserDN(u.Username)
|
|
||||||
attrs := AKAttrsToLDAP(u.Attributes)
|
|
||||||
|
|
||||||
attrs = pi.ensureAttributes(attrs, map[string][]string{
|
|
||||||
"memberOf": pi.GroupsForUser(u),
|
|
||||||
// Old fields for backwards compatibility
|
|
||||||
"accountStatus": {BoolToString(*u.IsActive)},
|
|
||||||
"superuser": {BoolToString(u.IsSuperuser)},
|
|
||||||
"goauthentik.io/ldap/active": {BoolToString(*u.IsActive)},
|
|
||||||
"goauthentik.io/ldap/superuser": {BoolToString(u.IsSuperuser)},
|
|
||||||
"cn": {u.Username},
|
|
||||||
"sAMAccountName": {u.Username},
|
|
||||||
"uid": {u.Uid},
|
|
||||||
"name": {u.Name},
|
|
||||||
"displayName": {u.Name},
|
|
||||||
"mail": {*u.Email},
|
|
||||||
"objectClass": {UserObjectClass, "organizationalPerson", "inetOrgPerson", "goauthentik.io/ldap/user"},
|
|
||||||
"uidNumber": {pi.GetUidNumber(u)},
|
|
||||||
"gidNumber": {pi.GetUidNumber(u)},
|
|
||||||
})
|
|
||||||
return &ldap.Entry{DN: dn, Attributes: attrs}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pi *ProviderInstance) GroupEntry(g LDAPGroup) *ldap.Entry {
|
|
||||||
attrs := AKAttrsToLDAP(g.akAttributes)
|
|
||||||
|
|
||||||
objectClass := []string{GroupObjectClass, "groupOfUniqueNames", "goauthentik.io/ldap/group"}
|
|
||||||
if g.isVirtualGroup {
|
|
||||||
objectClass = append(objectClass, "goauthentik.io/ldap/virtual-group")
|
|
||||||
}
|
|
||||||
|
|
||||||
attrs = pi.ensureAttributes(attrs, map[string][]string{
|
|
||||||
"objectClass": objectClass,
|
|
||||||
"member": g.member,
|
|
||||||
"goauthentik.io/ldap/superuser": {BoolToString(g.isSuperuser)},
|
|
||||||
"cn": {g.cn},
|
|
||||||
"uid": {g.uid},
|
|
||||||
"sAMAccountName": {g.cn},
|
|
||||||
"gidNumber": {g.gidNumber},
|
|
||||||
})
|
|
||||||
return &ldap.Entry{DN: g.dn, Attributes: attrs}
|
|
||||||
}
|
|
||||||
@ -2,50 +2,18 @@ package ldap
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/pires/go-proxyproto"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"goauthentik.io/api"
|
|
||||||
"goauthentik.io/internal/crypto"
|
"goauthentik.io/internal/crypto"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
|
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||||
|
|
||||||
"github.com/nmcclain/ldap"
|
"github.com/nmcclain/ldap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const GroupObjectClass = "group"
|
|
||||||
const UserObjectClass = "user"
|
|
||||||
|
|
||||||
type ProviderInstance struct {
|
|
||||||
BaseDN string
|
|
||||||
|
|
||||||
UserDN string
|
|
||||||
|
|
||||||
VirtualGroupDN string
|
|
||||||
GroupDN string
|
|
||||||
|
|
||||||
appSlug string
|
|
||||||
flowSlug string
|
|
||||||
s *LDAPServer
|
|
||||||
log *log.Entry
|
|
||||||
|
|
||||||
tlsServerName *string
|
|
||||||
cert *tls.Certificate
|
|
||||||
outpostName string
|
|
||||||
searchAllowedGroups []*strfmt.UUID
|
|
||||||
boundUsersMutex sync.RWMutex
|
|
||||||
boundUsers map[string]UserFlags
|
|
||||||
|
|
||||||
uidStartNumber int32
|
|
||||||
gidStartNumber int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserFlags struct {
|
|
||||||
UserInfo *api.User
|
|
||||||
UserPk int32
|
|
||||||
CanSearch bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type LDAPServer struct {
|
type LDAPServer struct {
|
||||||
s *ldap.Server
|
s *ldap.Server
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
@ -55,17 +23,6 @@ type LDAPServer struct {
|
|||||||
providers []*ProviderInstance
|
providers []*ProviderInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
type LDAPGroup struct {
|
|
||||||
dn string
|
|
||||||
cn string
|
|
||||||
uid string
|
|
||||||
gidNumber string
|
|
||||||
member []string
|
|
||||||
isSuperuser bool
|
|
||||||
isVirtualGroup bool
|
|
||||||
akAttributes interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(ac *ak.APIController) *LDAPServer {
|
func NewServer(ac *ak.APIController) *LDAPServer {
|
||||||
s := ldap.NewServer()
|
s := ldap.NewServer()
|
||||||
s.EnforceLDAP = true
|
s.EnforceLDAP = true
|
||||||
@ -83,10 +40,60 @@ func NewServer(ac *ak.APIController) *LDAPServer {
|
|||||||
ls.defaultCert = &defaultCert
|
ls.defaultCert = &defaultCert
|
||||||
s.BindFunc("", ls)
|
s.BindFunc("", ls)
|
||||||
s.SearchFunc("", ls)
|
s.SearchFunc("", ls)
|
||||||
s.CloseFunc("", ls)
|
|
||||||
return ls
|
return ls
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ls *LDAPServer) Type() string {
|
func (ls *LDAPServer) Type() string {
|
||||||
return "ldap"
|
return "ldap"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ls *LDAPServer) StartLDAPServer() error {
|
||||||
|
listen := "0.0.0.0:3389"
|
||||||
|
|
||||||
|
ln, err := net.Listen("tcp", listen)
|
||||||
|
if err != nil {
|
||||||
|
ls.log.WithField("listen", listen).WithError(err).Fatalf("FATAL: listen failed")
|
||||||
|
}
|
||||||
|
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) Start() error {
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(3)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
metrics.RunServer()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := ls.StartLDAPServer()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := ls.StartLDAPTLSServer()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LDAPServer) TimerFlowCacheExpiry() {
|
||||||
|
for _, p := range ls.providers {
|
||||||
|
ls.log.WithField("flow", p.flowSlug).Debug("Pre-heating flow cache")
|
||||||
|
p.binder.TimerFlowCacheExpiry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
55
internal/outpost/ldap/ldap_tls.go
Normal file
55
internal/outpost/ldap/ldap_tls.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package ldap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/pires/go-proxyproto"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.WithField("listen", listen).WithError(err).Fatalf("FATAL: listen failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user