Compare commits
192 Commits
version-20
...
version/20
Author | SHA1 | Date | |
---|---|---|---|
49dfb4756e | |||
814758e2aa | |||
5c42dac5e2 | |||
88603fa4f7 | |||
0232c4e162 | |||
11753c1fe1 | |||
f5cc6c67ec | |||
8b8ed3527a | |||
1aa0274e7c | |||
ecd33ca0c1 | |||
e93be0de9a | |||
a5adc4f8ed | |||
a6baed9753 | |||
ceaf832e63 | |||
a6b0b14685 | |||
f679250edd | |||
acc4de2235 | |||
56a8276dbf | |||
6dfe6edbef | |||
6af4bd0d9a | |||
7ee7f6bd6a | |||
f8b8334010 | |||
d4b65dc4b4 | |||
e4bbd3b1c0 | |||
87de5e625d | |||
efbe51673e | |||
a95bea53ea | |||
6021fc0f52 | |||
1415b68ff4 | |||
be6853ac52 | |||
7fd6be5abb | |||
91d6f572a5 | |||
016a9ce34e | |||
8adb95af7f | |||
1dc54775d8 | |||
370ef716b5 | |||
16e56ad9ca | |||
b5b5a9eed3 | |||
8b22e7bcc3 | |||
d48b5b9511 | |||
0eccaa3f1e | |||
67d550a80d | |||
ebb5711c32 | |||
79ec872232 | |||
4284e14ff7 | |||
92a09779d0 | |||
14c621631d | |||
c55f503b9b | |||
a908cad976 | |||
c2586557d8 | |||
01c80a82e2 | |||
0d47654651 | |||
1183095833 | |||
c281b11bdc | |||
61fe45a58c | |||
d43aab479c | |||
7f8383427a | |||
a06d6cf33d | |||
5b7cb205c9 | |||
293a932d20 | |||
fff901ff03 | |||
f47c936295 | |||
88d5aec618 | |||
96ae68cf09 | |||
63b3434b6f | |||
947ecec02b | |||
1c2b452406 | |||
47777529ac | |||
949095c376 | |||
4b112c2799 | |||
291a2516b1 | |||
4dcfd021e2 | |||
ca50848db3 | |||
0bb3e3c558 | |||
e4b25809ab | |||
7bf932f8e2 | |||
99d04528b0 | |||
e48d172036 | |||
c2388137a8 | |||
650e2cbc38 | |||
b32800ea71 | |||
e1c0c0b20c | |||
fe39e39dcd | |||
883f213b03 | |||
538996f617 | |||
2f4c92deb9 | |||
ef335ec083 | |||
07b09df3fe | |||
e70e031a1f | |||
c7ba183dc0 | |||
3ed23a37ea | |||
3d724db0e3 | |||
2997542114 | |||
84b18fff96 | |||
1dce408c72 | |||
e5ff47bf14 | |||
b53bf331c3 | |||
90e9a8b34c | |||
845f842783 | |||
7397849c60 | |||
6dd46b5fc5 | |||
89ca79ed10 | |||
713bef895c | |||
925115e9ce | |||
42f5cf8c93 | |||
82cc1d536a | |||
08af2fd46b | |||
70e3b27a4d | |||
6a411d7960 | |||
33567b56d7 | |||
0c1954aeb7 | |||
f4a6c70e98 | |||
5f198e7fe4 | |||
d172d32817 | |||
af3fb5c2cd | |||
885efb526e | |||
3bfb8b2cb2 | |||
9fc5ff4b77 | |||
dd8b579dd6 | |||
e12cbd8711 | |||
62d35f8f8c | |||
49be504c13 | |||
edad55e51d | |||
38086fa8bb | |||
c4f9a3e9a7 | |||
930df791bd | |||
9a6086634c | |||
b68e65355a | |||
72d33a91dd | |||
7067e3d69a | |||
4db370d24e | |||
41e7b9b73f | |||
7f47f93e4e | |||
89abd44b76 | |||
14c7d8c4f4 | |||
525976a81b | |||
64a2126ea4 | |||
994c5882ab | |||
ad64d51e85 | |||
a184a7518a | |||
943fd80920 | |||
01bb18b8c4 | |||
94baaaa5a5 | |||
40b164ce94 | |||
1d7c7801e7 | |||
0db0a12ef3 | |||
8008aba450 | |||
eaeab27004 | |||
111fbf119b | |||
300ad88447 | |||
92cc0c9c64 | |||
18ff803370 | |||
819af78e2b | |||
6338785ce1 | |||
973e151dff | |||
fae6d83f27 | |||
ed84fe0b8d | |||
1ee603403e | |||
7db7b7cc4d | |||
68a98cd86c | |||
e758db5727 | |||
4d7d700afa | |||
f9a5add01d | |||
2986b56389 | |||
58f79b525d | |||
0a1c0dae05 | |||
e18ef8dab6 | |||
3cacc59bec | |||
4eea46d399 | |||
11e25617bd | |||
4817126811 | |||
0181361efa | |||
8ff8e1d5f7 | |||
19d5902a92 | |||
71dffb21a9 | |||
bd283c506d | |||
ef564e5f1a | |||
2543224c7c | |||
077eee9310 | |||
d894eeaa67 | |||
452bfb39bf | |||
6b6702521f | |||
c07b8d95d0 | |||
bf347730b3 | |||
ececfc3a30 | |||
b76546de0c | |||
424d490a60 | |||
127dd85214 | |||
10570ac7f8 | |||
dc5667b0b8 | |||
ec9cacb610 | |||
0027dbc0e5 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.12.5
|
||||
current_version = 2022.1.4
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
|
26
.github/workflows/ci-main.yml
vendored
26
.github/workflows/ci-main.yml
vendored
@ -40,7 +40,7 @@ jobs:
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.cache/pypoetry/virtualenvs
|
||||
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
|
||||
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
|
||||
@ -56,7 +56,7 @@ jobs:
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.cache/pypoetry/virtualenvs
|
||||
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
|
||||
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
|
||||
@ -79,7 +79,7 @@ jobs:
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.cache/pypoetry/virtualenvs
|
||||
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
|
||||
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
|
||||
- name: checkout stable
|
||||
run: |
|
||||
# Copy current, latest config to local
|
||||
@ -95,11 +95,7 @@ jobs:
|
||||
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
|
||||
run: |
|
||||
scripts/ci_prepare.sh
|
||||
# Sync anyways since stable will have different dependencies
|
||||
# TODO: Remove after next stable release
|
||||
if [[ -f "Pipfile.lock" ]]; then
|
||||
pipenv install --dev
|
||||
fi
|
||||
# install anyways since stable will have different dependencies
|
||||
poetry install
|
||||
- name: run migrations to stable
|
||||
run: poetry run python -m lifecycle.migrate
|
||||
@ -108,13 +104,7 @@ jobs:
|
||||
set -x
|
||||
git fetch
|
||||
git reset --hard HEAD
|
||||
# TODO: Remove after next stable release
|
||||
rm -f poetry.lock
|
||||
git checkout $GITHUB_SHA
|
||||
# TODO: Remove after next stable release
|
||||
if [[ -f "Pipfile.lock" ]]; then
|
||||
pipenv install --dev
|
||||
fi
|
||||
poetry install
|
||||
- name: prepare
|
||||
env:
|
||||
@ -131,7 +121,7 @@ jobs:
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.cache/pypoetry/virtualenvs
|
||||
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
|
||||
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
|
||||
@ -158,7 +148,7 @@ jobs:
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.cache/pypoetry/virtualenvs
|
||||
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
|
||||
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
|
||||
@ -195,7 +185,7 @@ jobs:
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.cache/pypoetry/virtualenvs
|
||||
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
|
||||
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
|
||||
@ -240,7 +230,7 @@ jobs:
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.cache/pypoetry/virtualenvs
|
||||
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
|
||||
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
|
||||
|
18
.github/workflows/ci-outpost.yml
vendored
18
.github/workflows/ci-outpost.yml
vendored
@ -28,11 +28,27 @@ jobs:
|
||||
--rm \
|
||||
-v $(pwd):/app \
|
||||
-w /app \
|
||||
golangci/golangci-lint:v1.39.0 \
|
||||
golangci/golangci-lint:v1.43 \
|
||||
golangci-lint run -v --timeout 200s
|
||||
test-unittest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "^1.17"
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get github.com/axw/gocov/gocov
|
||||
go get github.com/AlekSi/gocov-xml
|
||||
go get github.com/jstemmer/go-junit-report
|
||||
- name: Go unittests
|
||||
run: |
|
||||
go test -timeout 0 -v -race -coverprofile=coverage.out -covermode=atomic -cover ./... | go-junit-report > junit.xml
|
||||
ci-outpost-mark:
|
||||
needs:
|
||||
- lint-golint
|
||||
- test-unittest
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo mark
|
||||
|
14
.github/workflows/release-publish.yml
vendored
14
.github/workflows/release-publish.yml
vendored
@ -30,14 +30,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.12.5,
|
||||
beryju/authentik:2022.1.4,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.12.5,
|
||||
ghcr.io/goauthentik/server:2022.1.4,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.12.5', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2022.1.4', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
@ -78,14 +78,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-${{ matrix.type }}:2021.12.5,
|
||||
beryju/authentik-${{ matrix.type }}:2022.1.4,
|
||||
beryju/authentik-${{ matrix.type }}:latest,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:2021.12.5,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:2022.1.4,
|
||||
ghcr.io/goauthentik/${{ matrix.type }}:latest
|
||||
file: ${{ matrix.type }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.12.5', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2022.1.4', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-${{ matrix.type }}:latest
|
||||
docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable
|
||||
@ -170,7 +170,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.12.5
|
||||
version: authentik@2022.1.4
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: '~/static/dist'
|
||||
|
2
.github/workflows/translation-compile.yml
vendored
2
.github/workflows/translation-compile.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
uses: actions/cache@v2.1.7
|
||||
with:
|
||||
path: ~/.cache/pypoetry/virtualenvs
|
||||
key: ${{ runner.os }}-poetry-cache-v3-${{ hashFiles('**/poetry.lock') }}
|
||||
key: ${{ runner.os }}-poetry-cache-v2-${{ hashFiles('**/poetry.lock') }}
|
||||
- name: prepare
|
||||
env:
|
||||
INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }}
|
||||
|
@ -1 +0,0 @@
|
||||
3.9.7
|
@ -16,7 +16,7 @@ ENV NODE_ENV=production
|
||||
RUN cd /work/web && npm i && npm run build
|
||||
|
||||
# Stage 3: Build go proxy
|
||||
FROM docker.io/golang:1.17.5-bullseye AS builder
|
||||
FROM docker.io/golang:1.17.6-bullseye AS builder
|
||||
|
||||
WORKDIR /work
|
||||
|
||||
@ -32,7 +32,7 @@ COPY ./go.sum /work/go.sum
|
||||
RUN go build -o /work/authentik ./cmd/server/main.go
|
||||
|
||||
# Stage 4: Run
|
||||
FROM docker.io/python:3.10.1-slim-bullseye
|
||||
FROM docker.io/python:3.10.2-slim-bullseye
|
||||
|
||||
LABEL org.opencontainers.image.url https://goauthentik.io
|
||||
LABEL org.opencontainers.image.description goauthentik.io Main server image, see https://goauthentik.io for more info.
|
||||
|
3
Makefile
3
Makefile
@ -15,6 +15,9 @@ test-e2e-provider:
|
||||
test-e2e-rest:
|
||||
coverage run manage.py test tests/e2e/test_flows* tests/e2e/test_source*
|
||||
|
||||
test-go:
|
||||
go test -timeout 0 -v -race -cover ./...
|
||||
|
||||
test:
|
||||
coverage run manage.py test authentik
|
||||
coverage html
|
||||
|
@ -57,4 +57,4 @@ DigitalOcean provides development and testing resources for authentik.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Netlify hosts the [goauthentik.io](goauthentik.io) site.
|
||||
Netlify hosts the [goauthentik.io](https://goauthentik.io) site.
|
||||
|
@ -1,3 +1,19 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.12.5"
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2022.1.4"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
def get_build_hash(fallback: Optional[str] = None) -> str:
|
||||
"""Get build hash"""
|
||||
return environ.get(ENV_GIT_HASH_KEY, fallback if fallback else "")
|
||||
|
||||
|
||||
def get_full_version() -> str:
|
||||
"""Get full version, with build hash appended"""
|
||||
version = __version__
|
||||
if (build_hash := get_build_hash()) != "":
|
||||
version += "." + build_hash
|
||||
return version
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""authentik administration overview"""
|
||||
from os import environ
|
||||
|
||||
from django.core.cache import cache
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from packaging.version import parse
|
||||
@ -10,7 +8,7 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||
from authentik.core.api.utils import PassiveSerializer
|
||||
|
||||
@ -25,7 +23,7 @@ class VersionSerializer(PassiveSerializer):
|
||||
|
||||
def get_build_hash(self, _) -> str:
|
||||
"""Get build hash, if version is not latest or released"""
|
||||
return environ.get(ENV_GIT_HASH_KEY, "")
|
||||
return get_build_hash()
|
||||
|
||||
def get_version_current(self, _) -> str:
|
||||
"""Get current version"""
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""authentik admin app config"""
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
@ -13,3 +15,4 @@ class AuthentikAdminConfig(AppConfig):
|
||||
from authentik.admin.tasks import clear_update_notifications
|
||||
|
||||
clear_update_notifications.delay()
|
||||
import_module("authentik.admin.signals")
|
||||
|
23
authentik/admin/signals.py
Normal file
23
authentik/admin/signals.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""admin signals"""
|
||||
from django.dispatch import receiver
|
||||
|
||||
from authentik.admin.api.tasks import TaskInfo
|
||||
from authentik.admin.api.workers import GAUGE_WORKERS
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.root.monitoring import monitoring_set
|
||||
|
||||
|
||||
@receiver(monitoring_set)
|
||||
# pylint: disable=unused-argument
|
||||
def monitoring_set_workers(sender, **kwargs):
|
||||
"""Set worker gauge"""
|
||||
count = len(CELERY_APP.control.ping(timeout=0.5))
|
||||
GAUGE_WORKERS.set(count)
|
||||
|
||||
|
||||
@receiver(monitoring_set)
|
||||
# pylint: disable=unused-argument
|
||||
def monitoring_set_tasks(sender, **kwargs):
|
||||
"""Set task gauges"""
|
||||
for task in TaskInfo.all().values():
|
||||
task.set_prom_metrics()
|
@ -1,6 +1,5 @@
|
||||
"""authentik admin tasks"""
|
||||
import re
|
||||
from os import environ
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.core.validators import URLValidator
|
||||
@ -9,7 +8,7 @@ from prometheus_client import Info
|
||||
from requests import RequestException
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.events.models import Event, EventAction, Notification
|
||||
from authentik.events.monitored_tasks import (
|
||||
MonitoredTask,
|
||||
@ -36,7 +35,7 @@ def _set_prom_info():
|
||||
{
|
||||
"version": __version__,
|
||||
"latest": cache.get(VERSION_CACHE_KEY, ""),
|
||||
"build_hash": environ.get(ENV_GIT_HASH_KEY, ""),
|
||||
"build_hash": get_build_hash(),
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -30,7 +30,7 @@ function getCookie(name) {
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
const rapidocEl = document.querySelector('rapi-doc');
|
||||
rapidocEl.addEventListener('before-try', (e) => {
|
||||
e.detail.request.headers.append('X-CSRFToken', getCookie("authentik_csrf"));
|
||||
e.detail.request.headers.append('X-authentik-CSRF', getCookie("authentik_csrf"));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -4,7 +4,5 @@ from django.urls import include, path
|
||||
from authentik.api.v3.urls import urlpatterns as v3_urls
|
||||
|
||||
urlpatterns = [
|
||||
# TODO: Remove in 2022.1
|
||||
path("v2beta/", include(v3_urls)),
|
||||
path("v3/", include(v3_urls)),
|
||||
]
|
||||
|
@ -80,7 +80,7 @@ class ConfigView(APIView):
|
||||
config = ConfigSerializer(
|
||||
{
|
||||
"error_reporting": {
|
||||
"enabled": CONFIG.y("error_reporting.enabled"),
|
||||
"enabled": CONFIG.y("error_reporting.enabled") and not settings.DEBUG,
|
||||
"environment": CONFIG.y("error_reporting.environment"),
|
||||
"send_pii": CONFIG.y("error_reporting.send_pii"),
|
||||
"traces_sample_rate": float(CONFIG.y("error_reporting.sample_rate", 0.4)),
|
||||
|
@ -1,10 +1,9 @@
|
||||
"""Tokens API Viewset"""
|
||||
from typing import Any
|
||||
|
||||
from django.http.response import Http404
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from guardian.shortcuts import assign_perm, get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
@ -96,10 +95,12 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
def perform_create(self, serializer: TokenSerializer):
|
||||
if not self.request.user.is_superuser:
|
||||
return serializer.save(
|
||||
instance = serializer.save(
|
||||
user=self.request.user,
|
||||
expiring=self.request.user.attributes.get(USER_ATTRIBUTE_TOKEN_EXPIRING, True),
|
||||
)
|
||||
assign_perm("authentik_core.view_token_key", self.request.user, instance)
|
||||
return instance
|
||||
return super().perform_create(serializer)
|
||||
|
||||
@permission_required("authentik_core.view_token_key")
|
||||
@ -114,7 +115,5 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
def view_key(self, request: Request, identifier: str) -> Response:
|
||||
"""Return token key and log access"""
|
||||
token: Token = self.get_object()
|
||||
if token.is_expired:
|
||||
raise Http404
|
||||
Event.new(EventAction.SECRET_VIEW, secret=token).from_http(request) # noqa # nosec
|
||||
return Response(TokenViewSerializer({"key": token.key}).data)
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""authentik core signals"""
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.apps import apps
|
||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||
from django.contrib.sessions.backends.cache import KEY_PREFIX
|
||||
from django.core.cache import cache
|
||||
@ -11,6 +12,8 @@ from django.dispatch import receiver
|
||||
from django.http.request import HttpRequest
|
||||
from prometheus_client import Gauge
|
||||
|
||||
from authentik.root.monitoring import monitoring_set
|
||||
|
||||
# Arguments: user: User, password: str
|
||||
password_changed = Signal()
|
||||
|
||||
@ -20,6 +23,17 @@ if TYPE_CHECKING:
|
||||
from authentik.core.models import AuthenticatedSession, User
|
||||
|
||||
|
||||
@receiver(monitoring_set)
|
||||
# pylint: disable=unused-argument
|
||||
def monitoring_set_models(sender, **kwargs):
|
||||
"""set models gauges"""
|
||||
for model in apps.get_models():
|
||||
GAUGE_MODELS.labels(
|
||||
model_name=model._meta.model_name,
|
||||
app=model._meta.app_label,
|
||||
).set(model.objects.count())
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def post_save_application(sender: type[Model], instance, created: bool, **_):
|
||||
@ -27,11 +41,6 @@ def post_save_application(sender: type[Model], instance, created: bool, **_):
|
||||
from authentik.core.api.applications import user_app_cache_key
|
||||
from authentik.core.models import Application
|
||||
|
||||
GAUGE_MODELS.labels(
|
||||
model_name=sender._meta.model_name,
|
||||
app=sender._meta.app_label,
|
||||
).set(sender.objects.count())
|
||||
|
||||
if sender != Application:
|
||||
return
|
||||
if not created: # pragma: no cover
|
||||
|
@ -30,6 +30,7 @@ class TestTokenAPI(APITestCase):
|
||||
self.assertEqual(token.user, self.user)
|
||||
self.assertEqual(token.intent, TokenIntents.INTENT_API)
|
||||
self.assertEqual(token.expiring, True)
|
||||
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
|
||||
|
||||
def test_token_create_invalid(self):
|
||||
"""Test token creation endpoint (invalid data)"""
|
||||
|
@ -17,6 +17,7 @@ from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ModelSerializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.api.decorators import permission_required
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
@ -26,6 +27,8 @@ from authentik.crypto.managed import MANAGED_KEY
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class CertificateKeyPairSerializer(ModelSerializer):
|
||||
"""CertificateKeyPair Serializer"""
|
||||
@ -76,8 +79,11 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
def validate_certificate_data(self, value: str) -> str:
|
||||
"""Verify that input is a valid PEM x509 Certificate"""
|
||||
try:
|
||||
load_pem_x509_certificate(value.encode("utf-8"), default_backend())
|
||||
except ValueError:
|
||||
# Cast to string to fully load and parse certificate
|
||||
# Prevents issues like https://github.com/goauthentik/authentik/issues/2082
|
||||
str(load_pem_x509_certificate(value.encode("utf-8"), default_backend()))
|
||||
except ValueError as exc:
|
||||
LOGGER.warning("Failed to load certificate", exc=exc)
|
||||
raise ValidationError("Unable to load certificate.")
|
||||
return value
|
||||
|
||||
@ -86,12 +92,17 @@ class CertificateKeyPairSerializer(ModelSerializer):
|
||||
# Since this field is optional, data can be empty.
|
||||
if value != "":
|
||||
try:
|
||||
load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in value.split("\n")])),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
# Cast to string to fully load and parse certificate
|
||||
# Prevents issues like https://github.com/goauthentik/authentik/issues/2082
|
||||
str(
|
||||
load_pem_private_key(
|
||||
str.encode("\n".join([x.strip() for x in value.split("\n")])),
|
||||
password=None,
|
||||
backend=default_backend(),
|
||||
)
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
except (ValueError, TypeError) as exc:
|
||||
LOGGER.warning("Failed to load private key", exc=exc)
|
||||
raise ValidationError("Unable to load private key (possibly encrypted?).")
|
||||
return value
|
||||
|
||||
|
@ -4,7 +4,7 @@ from typing import Any, Optional
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from prometheus_client import Histogram
|
||||
from prometheus_client import Gauge, Histogram
|
||||
from sentry_sdk.hub import Hub
|
||||
from sentry_sdk.tracing import Span
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
@ -16,7 +16,6 @@ from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||
from authentik.flows.models import Flow, FlowStageBinding, Stage
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
from authentik.root.monitoring import UpdatingGauge
|
||||
|
||||
LOGGER = get_logger()
|
||||
PLAN_CONTEXT_PENDING_USER = "pending_user"
|
||||
@ -27,10 +26,9 @@ PLAN_CONTEXT_SOURCE = "source"
|
||||
# Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan
|
||||
# was restored.
|
||||
PLAN_CONTEXT_IS_RESTORED = "is_restored"
|
||||
GAUGE_FLOWS_CACHED = UpdatingGauge(
|
||||
GAUGE_FLOWS_CACHED = Gauge(
|
||||
"authentik_flows_cached",
|
||||
"Cached flows",
|
||||
update_func=lambda: len(cache.keys("flow_*") or []),
|
||||
)
|
||||
HIST_FLOWS_PLAN_TIME = Histogram(
|
||||
"authentik_flows_plan_time",
|
||||
@ -171,7 +169,6 @@ class FlowPlanner:
|
||||
)
|
||||
plan = self._build_plan(user, request, default_context)
|
||||
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
|
||||
GAUGE_FLOWS_CACHED.update()
|
||||
if not plan.bindings and not self.allow_empty_flows:
|
||||
raise EmptyFlowException()
|
||||
return plan
|
||||
|
@ -4,6 +4,9 @@ from django.db.models.signals import post_save, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.flows.planner import GAUGE_FLOWS_CACHED
|
||||
from authentik.root.monitoring import monitoring_set
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@ -14,6 +17,13 @@ def delete_cache_prefix(prefix: str) -> int:
|
||||
return len(keys)
|
||||
|
||||
|
||||
@receiver(monitoring_set)
|
||||
# pylint: disable=unused-argument
|
||||
def monitoring_set_flows(sender, **kwargs):
|
||||
"""set flow gauges"""
|
||||
GAUGE_FLOWS_CACHED.set(len(cache.keys("flow_*") or []))
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
@receiver(pre_delete)
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -118,9 +118,12 @@ class ChallengeStageView(StageView):
|
||||
"""Allow usage of placeholder in flow title."""
|
||||
if not self.executor.plan:
|
||||
return self.executor.flow.title
|
||||
return self.executor.flow.title % {
|
||||
"app": self.executor.plan.context.get(PLAN_CONTEXT_APPLICATION, "")
|
||||
}
|
||||
try:
|
||||
return self.executor.flow.title % {
|
||||
"app": self.executor.plan.context.get(PLAN_CONTEXT_APPLICATION, "")
|
||||
}
|
||||
except ValueError:
|
||||
return self.executor.flow.title
|
||||
|
||||
def _get_challenge(self, *args, **kwargs) -> Challenge:
|
||||
with Hub.current.start_span(
|
||||
|
@ -6,7 +6,7 @@ postgresql:
|
||||
port: 5432
|
||||
password: 'env://POSTGRES_PASSWORD'
|
||||
backup:
|
||||
enabled: true
|
||||
enabled: false
|
||||
s3_backup:
|
||||
access_key: ""
|
||||
secret_key: ""
|
||||
|
@ -32,6 +32,7 @@ class BaseEvaluator:
|
||||
self._globals = {
|
||||
"regex_match": BaseEvaluator.expr_regex_match,
|
||||
"regex_replace": BaseEvaluator.expr_regex_replace,
|
||||
"list_flatten": BaseEvaluator.expr_flatten,
|
||||
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
||||
"ak_user_by": BaseEvaluator.expr_user_by,
|
||||
"ak_logger": get_logger(),
|
||||
@ -40,6 +41,15 @@ class BaseEvaluator:
|
||||
self._context = {}
|
||||
self._filename = "BaseEvalautor"
|
||||
|
||||
@staticmethod
|
||||
def expr_flatten(value: list[Any] | Any) -> Optional[Any]:
|
||||
"""Flatten `value` if its a list"""
|
||||
if isinstance(value, list):
|
||||
if len(value) < 1:
|
||||
return None
|
||||
return value[0]
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def expr_regex_match(value: Any, regex: str) -> bool:
|
||||
"""Expression Filter to run re.search"""
|
||||
|
@ -111,6 +111,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
|
||||
"django_redis.cache",
|
||||
"celery.backends.redis",
|
||||
"celery.worker",
|
||||
"paramiko.transport",
|
||||
]:
|
||||
return None
|
||||
LOGGER.debug("sending event to sentry", exc=exc_value, source_logger=event.get("logger", None))
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""http helpers"""
|
||||
from os import environ
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.http import HttpRequest
|
||||
@ -7,7 +6,7 @@ from requests.sessions import Session
|
||||
from sentry_sdk.hub import Hub
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik import get_full_version
|
||||
|
||||
OUTPOST_REMOTE_IP_HEADER = "HTTP_X_AUTHENTIK_REMOTE_IP"
|
||||
OUTPOST_TOKEN_HEADER = "HTTP_X_AUTHENTIK_OUTPOST_TOKEN" # nosec
|
||||
@ -75,8 +74,7 @@ def get_client_ip(request: Optional[HttpRequest]) -> str:
|
||||
|
||||
def authentik_user_agent() -> str:
|
||||
"""Get a common user agent"""
|
||||
build = environ.get(ENV_GIT_HASH_KEY, "tagged")
|
||||
return f"authentik@{__version__} (build={build})"
|
||||
return f"authentik@{get_full_version()}"
|
||||
|
||||
|
||||
def get_http_session() -> Session:
|
||||
|
@ -58,4 +58,6 @@ def get_env() -> str:
|
||||
return "compose"
|
||||
if CONFIG.y_bool("debug"):
|
||||
return "dev"
|
||||
if "AK_APPLIANCE" in os.environ:
|
||||
return os.environ["AK_APPLIANCE"]
|
||||
return "custom"
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""Outpost API Views"""
|
||||
from os import environ
|
||||
|
||||
from dacite.core import from_dict
|
||||
from dacite.exceptions import DaciteError
|
||||
from django_filters.filters import ModelMultipleChoiceFilter
|
||||
@ -14,7 +12,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.serializers import JSONField, ModelSerializer, ValidationError
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY
|
||||
from authentik import get_build_hash
|
||||
from authentik.core.api.providers import ProviderSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import PassiveSerializer, is_dict
|
||||
@ -154,7 +152,7 @@ class OutpostViewSet(UsedByMixin, ModelViewSet):
|
||||
"version_should": state.version_should,
|
||||
"version_outdated": state.version_outdated,
|
||||
"build_hash": state.build_hash,
|
||||
"build_hash_should": environ.get(ENV_GIT_HASH_KEY, ""),
|
||||
"build_hash_should": get_build_hash(),
|
||||
}
|
||||
)
|
||||
return Response(OutpostHealthSerializer(states, many=True).data)
|
||||
|
@ -1,12 +1,11 @@
|
||||
"""Base Controller"""
|
||||
from dataclasses import dataclass
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
from structlog.stdlib import get_logger
|
||||
from structlog.testing import capture_logs
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.outposts.models import (
|
||||
@ -102,5 +101,5 @@ class BaseController:
|
||||
return image_name_template % {
|
||||
"type": self.outpost.type,
|
||||
"version": __version__,
|
||||
"build_hash": environ.get(ENV_GIT_HASH_KEY, ""),
|
||||
"build_hash": get_build_hash(),
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ from docker import DockerClient as UpstreamDockerClient
|
||||
from docker.errors import DockerException, NotFound
|
||||
from docker.models.containers import Container
|
||||
from docker.utils.utils import kwargs_from_env
|
||||
from paramiko.ssh_exception import SSHException
|
||||
from structlog.stdlib import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
@ -49,10 +50,13 @@ class DockerClient(UpstreamDockerClient, BaseClient):
|
||||
authentication_kp=connection.tls_authentication,
|
||||
)
|
||||
tls_config = self.tls.write()
|
||||
super().__init__(
|
||||
base_url=connection.url,
|
||||
tls=tls_config,
|
||||
)
|
||||
try:
|
||||
super().__init__(
|
||||
base_url=connection.url,
|
||||
tls=tls_config,
|
||||
)
|
||||
except SSHException as exc:
|
||||
raise ServiceConnectionInvalid from exc
|
||||
self.logger = get_logger()
|
||||
# Ensure the client actually works
|
||||
self.containers.list()
|
||||
@ -102,9 +106,12 @@ class DockerController(BaseController):
|
||||
).lower()
|
||||
|
||||
def _get_labels(self) -> dict[str, str]:
|
||||
return {
|
||||
labels = {
|
||||
"io.goauthentik.outpost-uuid": self.outpost.pk.hex,
|
||||
}
|
||||
if self.outpost.config.docker_labels:
|
||||
labels.update(self.outpost.config.docker_labels)
|
||||
return labels
|
||||
|
||||
def _get_env(self) -> dict[str, str]:
|
||||
return {
|
||||
|
@ -18,6 +18,7 @@ from kubernetes.client import (
|
||||
V1SecretKeySelector,
|
||||
)
|
||||
|
||||
from authentik import __version__, get_full_version
|
||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
|
||||
@ -52,15 +53,18 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
raise NeedsUpdate()
|
||||
super().reconcile(current, reference)
|
||||
|
||||
def get_pod_meta(self) -> dict[str, str]:
|
||||
def get_pod_meta(self, **kwargs) -> dict[str, str]:
|
||||
"""Get common object metadata"""
|
||||
return {
|
||||
"app.kubernetes.io/name": "authentik-outpost",
|
||||
"app.kubernetes.io/managed-by": "goauthentik.io",
|
||||
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
|
||||
"goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
|
||||
"goauthentik.io/outpost-type": str(self.controller.outpost.type),
|
||||
}
|
||||
kwargs.update(
|
||||
{
|
||||
"app.kubernetes.io/name": f"authentik-outpost-{self.outpost.type}",
|
||||
"app.kubernetes.io/managed-by": "goauthentik.io",
|
||||
"goauthentik.io/outpost-uuid": self.controller.outpost.uuid.hex,
|
||||
"goauthentik.io/outpost-name": slugify(self.controller.outpost.name),
|
||||
"goauthentik.io/outpost-type": str(self.controller.outpost.type),
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_reference_object(self) -> V1Deployment:
|
||||
"""Get deployment object for outpost"""
|
||||
@ -77,13 +81,24 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
|
||||
meta = self.get_object_meta(name=self.name)
|
||||
image_name = self.controller.get_container_image()
|
||||
image_pull_secrets = self.outpost.config.kubernetes_image_pull_secrets
|
||||
version = get_full_version()
|
||||
return V1Deployment(
|
||||
metadata=meta,
|
||||
spec=V1DeploymentSpec(
|
||||
replicas=self.outpost.config.kubernetes_replicas,
|
||||
selector=V1LabelSelector(match_labels=self.get_pod_meta()),
|
||||
template=V1PodTemplateSpec(
|
||||
metadata=V1ObjectMeta(labels=self.get_pod_meta()),
|
||||
metadata=V1ObjectMeta(
|
||||
labels=self.get_pod_meta(
|
||||
**{
|
||||
# Support istio-specific labels, but also use the standard k8s
|
||||
# recommendations
|
||||
"app.kubernetes.io/version": version,
|
||||
"app": "authentik-outpost",
|
||||
"version": version,
|
||||
}
|
||||
)
|
||||
),
|
||||
spec=V1PodSpec(
|
||||
image_pull_secrets=[
|
||||
V1ObjectReference(name=secret) for secret in image_pull_secrets
|
||||
|
@ -6,6 +6,7 @@ from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
|
||||
from authentik.outposts.controllers.base import FIELD_MANAGER
|
||||
from authentik.outposts.controllers.k8s.base import KubernetesObjectReconciler
|
||||
from authentik.outposts.controllers.k8s.deployment import DeploymentReconciler
|
||||
from authentik.outposts.controllers.k8s.triggers import NeedsUpdate
|
||||
from authentik.outposts.controllers.k8s.utils import compare_ports
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -25,6 +26,8 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
|
||||
# after an authentik update. However the ports might have also changed during
|
||||
# the update, so this causes the service to be re-created with higher
|
||||
# priority than being updated.
|
||||
if current.spec.selector != reference.spec.selector:
|
||||
raise NeedsUpdate()
|
||||
super().reconcile(current, reference)
|
||||
|
||||
def get_reference_object(self) -> V1Service:
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Outpost models"""
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import datetime
|
||||
from os import environ
|
||||
from typing import Iterable, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
@ -17,7 +16,7 @@ from model_utils.managers import InheritanceManager
|
||||
from packaging.version import LegacyVersion, Version, parse
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik import __version__, get_build_hash
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP,
|
||||
USER_ATTRIBUTE_SA,
|
||||
@ -61,6 +60,7 @@ class OutpostConfig:
|
||||
|
||||
docker_network: Optional[str] = field(default=None)
|
||||
docker_map_ports: bool = field(default=True)
|
||||
docker_labels: Optional[dict[str, str]] = field(default=None)
|
||||
|
||||
container_image: Optional[str] = field(default=None)
|
||||
|
||||
@ -414,7 +414,7 @@ class OutpostState:
|
||||
"""Check if outpost version matches our version"""
|
||||
if not self.version:
|
||||
return False
|
||||
if self.build_hash != environ.get(ENV_GIT_HASH_KEY, ""):
|
||||
if self.build_hash != get_build_hash():
|
||||
return False
|
||||
return parse(self.version) < OUR_VERSION
|
||||
|
||||
|
@ -77,8 +77,12 @@ def outpost_service_connection_state(connection_pk: Any):
|
||||
cls = DockerClient
|
||||
if isinstance(connection, KubernetesServiceConnection):
|
||||
cls = KubernetesClient
|
||||
with cls(connection) as client:
|
||||
state = client.fetch_state()
|
||||
try:
|
||||
with cls(connection) as client:
|
||||
state = client.fetch_state()
|
||||
except ServiceConnectionInvalid as exc:
|
||||
LOGGER.warning("Failed to get client status", exc=exc)
|
||||
return
|
||||
cache.set(connection.state_key, state, timeout=None)
|
||||
|
||||
|
||||
|
@ -5,7 +5,7 @@ from typing import Iterator, Optional
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpRequest
|
||||
from prometheus_client import Histogram
|
||||
from prometheus_client import Gauge, Histogram
|
||||
from sentry_sdk.hub import Hub
|
||||
from sentry_sdk.tracing import Span
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
@ -14,13 +14,11 @@ from authentik.core.models import User
|
||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel, PolicyEngineMode
|
||||
from authentik.policies.process import PolicyProcess, cache_key
|
||||
from authentik.policies.types import PolicyRequest, PolicyResult
|
||||
from authentik.root.monitoring import UpdatingGauge
|
||||
|
||||
CURRENT_PROCESS = current_process()
|
||||
GAUGE_POLICIES_CACHED = UpdatingGauge(
|
||||
GAUGE_POLICIES_CACHED = Gauge(
|
||||
"authentik_policies_cached",
|
||||
"Cached Policies",
|
||||
update_func=lambda: len(cache.keys("policy_*") or []),
|
||||
)
|
||||
HIST_POLICIES_BUILD_TIME = Histogram(
|
||||
"authentik_policies_build_time",
|
||||
|
@ -45,7 +45,7 @@ class HaveIBeenPwendPolicy(Policy):
|
||||
fields=request.context.keys(),
|
||||
)
|
||||
return PolicyResult(False, _("Password not set in context"))
|
||||
password = request.context[self.password_field]
|
||||
password = str(request.context[self.password_field])
|
||||
|
||||
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec
|
||||
url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}"
|
||||
|
@ -5,10 +5,19 @@ from django.dispatch import receiver
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.api.applications import user_app_cache_key
|
||||
from authentik.policies.engine import GAUGE_POLICIES_CACHED
|
||||
from authentik.root.monitoring import monitoring_set
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@receiver(monitoring_set)
|
||||
# pylint: disable=unused-argument
|
||||
def monitoring_set_policies(sender, **kwargs):
|
||||
"""set policy gauges"""
|
||||
GAUGE_POLICIES_CACHED.set(len(cache.keys("policy_*") or []))
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
# pylint: disable=unused-argument
|
||||
def invalidate_policy_cache(sender, instance, **_):
|
||||
|
@ -99,7 +99,7 @@ class OAuthAuthorizationParams:
|
||||
# and POST request.
|
||||
query_dict = request.POST if request.method == "POST" else request.GET
|
||||
state = query_dict.get("state")
|
||||
redirect_uri = query_dict.get("redirect_uri", "")
|
||||
redirect_uri = query_dict.get("redirect_uri", "").lower()
|
||||
|
||||
response_type = query_dict.get("response_type", "")
|
||||
grant_type = None
|
||||
@ -156,13 +156,20 @@ class OAuthAuthorizationParams:
|
||||
if not self.redirect_uri:
|
||||
LOGGER.warning("Missing redirect uri.")
|
||||
raise RedirectUriError("", allowed_redirect_urls)
|
||||
if len(allowed_redirect_urls) < 1:
|
||||
|
||||
if self.provider.redirect_uris == "":
|
||||
LOGGER.info("Setting redirect for blank redirect_uris", redirect=self.redirect_uri)
|
||||
self.provider.redirect_uris = self.redirect_uri
|
||||
self.provider.save()
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
|
||||
if self.provider.redirect_uris == "*":
|
||||
LOGGER.warning(
|
||||
"Provider has no allowed redirect_uri set, allowing all.",
|
||||
allow=self.redirect_uri.lower(),
|
||||
"Provider has wildcard allowed redirect_uri set, allowing all.",
|
||||
allow=self.redirect_uri,
|
||||
)
|
||||
return
|
||||
if self.redirect_uri.lower() not in [x.lower() for x in allowed_redirect_urls]:
|
||||
if self.redirect_uri not in [x.lower() for x in allowed_redirect_urls]:
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri",
|
||||
redirect_uri=self.redirect_uri,
|
||||
|
@ -66,7 +66,7 @@ class TokenParams:
|
||||
provider=provider,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
redirect_uri=request.POST.get("redirect_uri", ""),
|
||||
redirect_uri=request.POST.get("redirect_uri", "").lower(),
|
||||
grant_type=request.POST.get("grant_type", ""),
|
||||
state=request.POST.get("state", ""),
|
||||
scope=request.POST.get("scope", "").split(),
|
||||
@ -123,21 +123,23 @@ class TokenParams:
|
||||
LOGGER.warning("Invalid grant type", grant_type=self.grant_type)
|
||||
raise TokenError("unsupported_grant_type")
|
||||
|
||||
def __post_init_code(self, raw_code):
|
||||
def __post_init_code(self, raw_code: str):
|
||||
if not raw_code:
|
||||
LOGGER.warning("Missing authorization code")
|
||||
raise TokenError("invalid_grant")
|
||||
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
if len(allowed_redirect_urls) < 1:
|
||||
if self.provider.redirect_uris == "*":
|
||||
LOGGER.warning(
|
||||
"Provider has no allowed redirect_uri set, allowing all.",
|
||||
allow=self.redirect_uri.lower(),
|
||||
"Provider has wildcard allowed redirect_uri set, allowing all.",
|
||||
redirect=self.redirect_uri,
|
||||
)
|
||||
elif self.redirect_uri.lower() not in [x.lower() for x in allowed_redirect_urls]:
|
||||
# At this point, no provider should have a blank redirect_uri, in case they do
|
||||
# this will check an empty array and raise an error
|
||||
elif self.redirect_uri not in [x.lower() for x in allowed_redirect_urls]:
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri",
|
||||
uri=self.redirect_uri,
|
||||
redirect=self.redirect_uri,
|
||||
expected=self.provider.redirect_uris.split(),
|
||||
)
|
||||
raise TokenError("invalid_client")
|
||||
|
@ -23,10 +23,12 @@ class ProxyDockerController(DockerController):
|
||||
proxy_provider: ProxyProvider
|
||||
external_host_name = urlparse(proxy_provider.external_host)
|
||||
hosts.append(f"`{external_host_name.netloc}`")
|
||||
traefik_name = f"ak-outpost-{self.outpost.pk.hex}"
|
||||
traefik_name = self.name
|
||||
labels = super()._get_labels()
|
||||
labels["traefik.enable"] = "true"
|
||||
labels[f"traefik.http.routers.{traefik_name}-router.rule"] = f"Host({','.join(hosts)})"
|
||||
labels[
|
||||
f"traefik.http.routers.{traefik_name}-router.rule"
|
||||
] = f"Host({','.join(hosts)}) && PathPrefix(`/akprox`)"
|
||||
labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true"
|
||||
labels[f"traefik.http.routers.{traefik_name}-router.service"] = f"{traefik_name}-service"
|
||||
labels[
|
||||
|
@ -121,13 +121,6 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
|
||||
forwardAuth=TraefikMiddlewareSpecForwardAuth(
|
||||
address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
|
||||
authResponseHeaders=[
|
||||
# Legacy headers, remove after 2022.1
|
||||
"X-Auth-Username",
|
||||
"X-Auth-Groups",
|
||||
"X-Forwarded-Email",
|
||||
"X-Forwarded-Preferred-Username",
|
||||
"X-Forwarded-User",
|
||||
# New headers, unique prefix
|
||||
"X-authentik-username",
|
||||
"X-authentik-groups",
|
||||
"X-authentik-email",
|
||||
|
@ -1,37 +1,17 @@
|
||||
"""Metrics view"""
|
||||
from base64 import b64encode
|
||||
from typing import Callable
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connections
|
||||
from django.db.utils import OperationalError
|
||||
from django.dispatch import Signal
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.views import View
|
||||
from django_prometheus.exports import ExportToDjangoView
|
||||
from django_redis import get_redis_connection
|
||||
from prometheus_client import Gauge
|
||||
from redis.exceptions import RedisError
|
||||
|
||||
from authentik.admin.api.workers import GAUGE_WORKERS
|
||||
from authentik.events.monitored_tasks import TaskInfo
|
||||
from authentik.root.celery import CELERY_APP
|
||||
|
||||
|
||||
class UpdatingGauge(Gauge):
|
||||
"""Gauge which fetches its own value from an update function.
|
||||
|
||||
Update function is called on instantiate"""
|
||||
|
||||
def __init__(self, *args, update_func: Callable, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._update_func = update_func
|
||||
self.update()
|
||||
|
||||
def update(self):
|
||||
"""Set value from update function"""
|
||||
val = self._update_func()
|
||||
if val:
|
||||
self.set(val)
|
||||
monitoring_set = Signal()
|
||||
|
||||
|
||||
class MetricsView(View):
|
||||
@ -49,11 +29,7 @@ class MetricsView(View):
|
||||
response["WWW-Authenticate"] = 'Basic realm="authentik-monitoring"'
|
||||
return response
|
||||
|
||||
count = len(CELERY_APP.control.ping(timeout=0.5))
|
||||
GAUGE_WORKERS.set(count)
|
||||
|
||||
for task in TaskInfo.all().values():
|
||||
task.set_prom_metrics()
|
||||
monitoring_set.send_robust(self)
|
||||
|
||||
return ExportToDjangoView(request)
|
||||
|
||||
|
@ -1,14 +1,4 @@
|
||||
"""
|
||||
Django settings for authentik project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.1.3.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.1/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.1/ref/settings/
|
||||
"""
|
||||
"""root settings for authentik"""
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
@ -18,24 +8,22 @@ from hashlib import sha512
|
||||
from json import dumps
|
||||
from tempfile import gettempdir
|
||||
from time import time
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import structlog
|
||||
from celery.schedules import crontab
|
||||
from sentry_sdk import init as sentry_init
|
||||
from sentry_sdk.api import set_tag
|
||||
from sentry_sdk.integrations.boto3 import Boto3Integration
|
||||
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||
from sentry_sdk.integrations.django import DjangoIntegration
|
||||
from sentry_sdk.integrations.redis import RedisIntegration
|
||||
from sentry_sdk.integrations.threading import ThreadingIntegration
|
||||
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||
from authentik import ENV_GIT_HASH_KEY, __version__, get_build_hash
|
||||
from authentik.core.middleware import structlog_add_request_id
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.logging import add_process_id
|
||||
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
|
||||
|
||||
@ -75,6 +63,7 @@ AUTH_USER_MODEL = "authentik_core.User"
|
||||
|
||||
_cookie_suffix = "_debug" if DEBUG else ""
|
||||
CSRF_COOKIE_NAME = "authentik_csrf"
|
||||
CSRF_HEADER_NAME = "HTTP_X_AUTHENTIK_CSRF"
|
||||
LANGUAGE_COOKIE_NAME = f"authentik_language{_cookie_suffix}"
|
||||
SESSION_COOKIE_NAME = f"authentik_session{_cookie_suffix}"
|
||||
SESSION_COOKIE_DOMAIN = CONFIG.y("cookie_domain", None)
|
||||
@ -164,9 +153,6 @@ SPECTACULAR_SETTINGS = {
|
||||
{
|
||||
"url": "/api/v3/",
|
||||
},
|
||||
{
|
||||
"url": "/api/v2beta/",
|
||||
},
|
||||
],
|
||||
"CONTACT": {
|
||||
"email": "hello@beryju.org",
|
||||
@ -222,7 +208,7 @@ if CONFIG.y_bool("redis.tls", False):
|
||||
REDIS_CELERY_TLS_REQUIREMENTS = f"?ssl_cert_reqs={CONFIG.y('redis.tls_reqs')}"
|
||||
_redis_url = (
|
||||
f"{REDIS_PROTOCOL_PREFIX}:"
|
||||
f"{quote(CONFIG.y('redis.password'))}@{quote(CONFIG.y('redis.host'))}:"
|
||||
f"{quote_plus(CONFIG.y('redis.password'))}@{quote_plus(CONFIG.y('redis.host'))}:"
|
||||
f"{int(CONFIG.y('redis.port'))}"
|
||||
)
|
||||
|
||||
@ -349,6 +335,7 @@ LOCALE_PATHS = ["./locale"]
|
||||
# Celery settings
|
||||
# Add a 10 minute timeout to all Celery tasks.
|
||||
CELERY_TASK_SOFT_TIME_LIMIT = 600
|
||||
CELERY_WORKER_MAX_TASKS_PER_CHILD = 50
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"clean_expired_models": {
|
||||
"task": "authentik.core.tasks.clean_expired_models",
|
||||
@ -357,7 +344,7 @@ CELERY_BEAT_SCHEDULE = {
|
||||
},
|
||||
"db_backup": {
|
||||
"task": "authentik.core.tasks.backup_database",
|
||||
"schedule": crontab(hour="*/24"),
|
||||
"schedule": crontab(hour="*/24", minute=0),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
}
|
||||
@ -378,7 +365,7 @@ DBBACKUP_CONNECTOR_MAPPING = {
|
||||
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
|
||||
}
|
||||
DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp" # nosec
|
||||
DBBACKUP_CLEANUP_KEEP = 30
|
||||
DBBACKUP_CLEANUP_KEEP = 10
|
||||
if CONFIG.y("postgresql.s3_backup.bucket", "") != "":
|
||||
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
|
||||
DBBACKUP_STORAGE_OPTIONS = {
|
||||
@ -398,10 +385,6 @@ if CONFIG.y("postgresql.s3_backup.bucket", "") != "":
|
||||
|
||||
# Sentry integration
|
||||
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)
|
||||
@ -413,7 +396,6 @@ if _ERROR_REPORTING:
|
||||
DjangoIntegration(transaction_style="function_name"),
|
||||
CeleryIntegration(),
|
||||
RedisIntegration(),
|
||||
Boto3Integration(),
|
||||
ThreadingIntegration(propagate_hub=True),
|
||||
],
|
||||
before_send=before_send,
|
||||
@ -422,35 +404,14 @@ if _ERROR_REPORTING:
|
||||
environment=CONFIG.y("error_reporting.environment", "customer"),
|
||||
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
|
||||
)
|
||||
set_tag("authentik.build_hash", build_hash)
|
||||
set_tag("authentik.build_hash", get_build_hash("tagged"))
|
||||
set_tag("authentik.env", env)
|
||||
set_tag("authentik.component", "backend")
|
||||
set_tag("authentik.uuid", sha512(SECRET_KEY.encode("ascii")).hexdigest()[:16])
|
||||
set_tag("authentik.uuid", sha512(str(SECRET_KEY).encode("ascii")).hexdigest()[:16])
|
||||
j_print(
|
||||
"Error reporting is enabled",
|
||||
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:
|
||||
try:
|
||||
get_http_session().post(
|
||||
"https://goauthentik.io/api/event",
|
||||
json={
|
||||
"domain": "authentik",
|
||||
"name": "pageview",
|
||||
"referrer": f"{__version__} ({build_hash})",
|
||||
"url": f"http://localhost/{env}?utm_source={__version__}&utm_medium={env}",
|
||||
},
|
||||
headers={
|
||||
"User-Agent": sha512(SECRET_KEY.encode("ascii")).hexdigest()[:16],
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
# pylint: disable=bare-except
|
||||
except: # nosec
|
||||
pass
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.1/howto/static-files/
|
||||
|
@ -35,21 +35,21 @@ class LDAPProviderManager(ObjectManager):
|
||||
"goauthentik.io/sources/ldap/ms-userprincipalname",
|
||||
name="authentik default Active Directory Mapping: userPrincipalName",
|
||||
object_field="attributes.upn",
|
||||
expression="return ldap.get('userPrincipalName')",
|
||||
expression="return list_flatten(ldap.get('userPrincipalName'))",
|
||||
),
|
||||
EnsureExists(
|
||||
LDAPPropertyMapping,
|
||||
"goauthentik.io/sources/ldap/ms-givenName",
|
||||
name="authentik default Active Directory Mapping: givenName",
|
||||
object_field="attributes.givenName",
|
||||
expression="return ldap.get('givenName')",
|
||||
expression="return list_flatten(ldap.get('givenName'))",
|
||||
),
|
||||
EnsureExists(
|
||||
LDAPPropertyMapping,
|
||||
"goauthentik.io/sources/ldap/ms-sn",
|
||||
name="authentik default Active Directory Mapping: sn",
|
||||
object_field="attributes.sn",
|
||||
expression="return ldap.get('sn')",
|
||||
expression="return list_flatten(ldap.get('sn'))",
|
||||
),
|
||||
# OpenLDAP specific mappings
|
||||
EnsureExists(
|
||||
|
@ -18,7 +18,12 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer):
|
||||
class Meta:
|
||||
|
||||
model = AuthenticateWebAuthnStage
|
||||
fields = StageSerializer.Meta.fields + ["configure_flow", "user_verification"]
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"configure_flow",
|
||||
"user_verification",
|
||||
"authenticator_attachment",
|
||||
"resident_key_requirement",
|
||||
]
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):
|
||||
|
@ -0,0 +1,37 @@
|
||||
# Generated by Django 4.0.1 on 2022-01-12 21:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
(
|
||||
"authentik_stages_authenticator_webauthn",
|
||||
"0005_authenticatewebauthnstage_user_verification",
|
||||
),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="authenticatewebauthnstage",
|
||||
name="authenticator_attachment",
|
||||
field=models.TextField(
|
||||
choices=[("platform", "Platform"), ("cross-platform", "Cross Platform")],
|
||||
default=None,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="authenticatewebauthnstage",
|
||||
name="resident_key_requirement",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("discouraged", "Discouraged"),
|
||||
("preferred", "Preferred"),
|
||||
("required", "Required"),
|
||||
],
|
||||
default="preferred",
|
||||
),
|
||||
),
|
||||
]
|
@ -31,6 +31,40 @@ class UserVerification(models.TextChoices):
|
||||
DISCOURAGED = "discouraged"
|
||||
|
||||
|
||||
class ResidentKeyRequirement(models.TextChoices):
|
||||
"""The Relying Party's preference for the authenticator to create a dedicated "client-side"
|
||||
credential for it. Requiring an authenticator to store a dedicated credential should not be
|
||||
done lightly due to the limited storage capacity of some types of authenticators.
|
||||
|
||||
Members:
|
||||
`DISCOURAGED`: The authenticator should not create a dedicated credential
|
||||
`PREFERRED`: The authenticator can create and store a dedicated credential, but if it
|
||||
doesn't that's alright too
|
||||
`REQUIRED`: The authenticator MUST create a dedicated credential. If it cannot, the RP
|
||||
is prepared for an error to occur.
|
||||
|
||||
https://www.w3.org/TR/webauthn-2/#enum-residentKeyRequirement
|
||||
"""
|
||||
|
||||
DISCOURAGED = "discouraged"
|
||||
PREFERRED = "preferred"
|
||||
REQUIRED = "required"
|
||||
|
||||
|
||||
class AuthenticatorAttachment(models.TextChoices):
|
||||
"""How an authenticator is connected to the client/browser.
|
||||
|
||||
Members:
|
||||
`PLATFORM`: A non-removable authenticator, like TouchID or Windows Hello
|
||||
`CROSS_PLATFORM`: A "roaming" authenticator, like a YubiKey
|
||||
|
||||
https://www.w3.org/TR/webauthn-2/#enumdef-authenticatorattachment
|
||||
"""
|
||||
|
||||
PLATFORM = "platform"
|
||||
CROSS_PLATFORM = "cross-platform"
|
||||
|
||||
|
||||
class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
|
||||
"""WebAuthn stage"""
|
||||
|
||||
@ -38,6 +72,13 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
|
||||
choices=UserVerification.choices,
|
||||
default=UserVerification.PREFERRED,
|
||||
)
|
||||
resident_key_requirement = models.TextField(
|
||||
choices=ResidentKeyRequirement.choices,
|
||||
default=ResidentKeyRequirement.PREFERRED,
|
||||
)
|
||||
authenticator_attachment = models.TextField(
|
||||
choices=AuthenticatorAttachment.choices, default=None, null=True
|
||||
)
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
|
@ -13,7 +13,6 @@ from webauthn.helpers.structs import (
|
||||
AuthenticatorSelectionCriteria,
|
||||
PublicKeyCredentialCreationOptions,
|
||||
RegistrationCredential,
|
||||
ResidentKeyRequirement,
|
||||
)
|
||||
from webauthn.registration.verify_registration_response import VerifiedRegistration
|
||||
|
||||
@ -85,6 +84,12 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||
stage: AuthenticateWebAuthnStage = self.executor.current_stage
|
||||
user = self.get_pending_user()
|
||||
|
||||
# library accepts none so we store null in the database, but if there is a value
|
||||
# set, cast it to string to ensure it's not a django class
|
||||
authenticator_attachment = stage.authenticator_attachment
|
||||
if authenticator_attachment:
|
||||
authenticator_attachment = str(authenticator_attachment)
|
||||
|
||||
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
|
||||
rp_id=get_rp_id(self.request),
|
||||
rp_name=self.request.tenant.branding_title,
|
||||
@ -92,8 +97,9 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
|
||||
user_name=user.username,
|
||||
user_display_name=user.name,
|
||||
authenticator_selection=AuthenticatorSelectionCriteria(
|
||||
resident_key=ResidentKeyRequirement.PREFERRED,
|
||||
resident_key=str(stage.resident_key_requirement),
|
||||
user_verification=str(stage.user_verification),
|
||||
authenticator_attachment=authenticator_attachment,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/debug"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/ldap"
|
||||
)
|
||||
@ -27,6 +28,7 @@ func main() {
|
||||
log.FieldKeyTime: "timestamp",
|
||||
},
|
||||
})
|
||||
go debug.EnableDebugServer()
|
||||
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
|
||||
if !found {
|
||||
fmt.Println("env AUTHENTIK_HOST not set!")
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/debug"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/proxyv2"
|
||||
)
|
||||
@ -32,6 +33,7 @@ func main() {
|
||||
log.FieldKeyTime: "timestamp",
|
||||
},
|
||||
})
|
||||
go debug.EnableDebugServer()
|
||||
akURL, found := os.LookupEnv("AUTHENTIK_HOST")
|
||||
if !found {
|
||||
fmt.Println("env AUTHENTIK_HOST not set!")
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"goauthentik.io/internal/common"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/constants"
|
||||
"goauthentik.io/internal/debug"
|
||||
"goauthentik.io/internal/gounicorn"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/proxyv2"
|
||||
@ -28,6 +29,7 @@ func main() {
|
||||
log.FieldKeyTime: "timestamp",
|
||||
},
|
||||
})
|
||||
go debug.EnableDebugServer()
|
||||
l := log.WithField("logger", "authentik.root")
|
||||
config.DefaultConfig()
|
||||
err := config.LoadConfig("./authentik/lib/default.yml")
|
||||
|
@ -17,7 +17,7 @@ services:
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.12.5}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -35,10 +35,10 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "0.0.0.0:9000:9000"
|
||||
- "0.0.0.0:9443:9443"
|
||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.12.5}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
11
go.mod
11
go.mod
@ -9,10 +9,9 @@ require (
|
||||
github.com/garyburd/redigo v1.6.2 // indirect
|
||||
github.com/getsentry/sentry-go v0.12.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.1
|
||||
github.com/go-openapi/runtime v0.21.0
|
||||
github.com/go-openapi/runtime v0.22.0
|
||||
github.com/go-openapi/strfmt v0.21.1
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.0
|
||||
@ -26,10 +25,12 @@ require (
|
||||
github.com/pires/go-proxyproto v0.6.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
goauthentik.io/api v0.2021124.9
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558
|
||||
github.com/stretchr/testify v1.7.0
|
||||
goauthentik.io/api v0.2021125.1
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b
|
||||
|
32
go.sum
32
go.sum
@ -70,8 +70,9 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
@ -182,8 +183,8 @@ github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29g
|
||||
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.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
|
||||
github.com/go-openapi/runtime v0.21.0 h1:giZ8eT26R+/rx6RX2MkYjZPY8vPYVKDhP/mOazrQHzM=
|
||||
github.com/go-openapi/runtime v0.21.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs=
|
||||
github.com/go-openapi/runtime v0.22.0 h1:vY2D0u807kkcwidaj0YJuq4zyAWQnjLNDpJcVBrUFNs=
|
||||
github.com/go-openapi/runtime v0.22.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.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
|
||||
@ -360,6 +361,7 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
@ -427,6 +429,7 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
@ -467,8 +470,9 @@ github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac/go.mod h1:hoL
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@ -476,13 +480,17 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
@ -562,8 +570,8 @@ 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.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
goauthentik.io/api v0.2021124.9 h1:l6kzTggi8lt+Vmm5js7oxp9U/bXoOEIl4565bUeepuM=
|
||||
goauthentik.io/api v0.2021124.9/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
|
||||
goauthentik.io/api v0.2021125.1 h1:Ja00N1D1wjqFiR90JV8nDayhmm39uLudzsJhwCjoQJ4=
|
||||
goauthentik.io/api v0.2021125.1/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-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
@ -655,6 +663,7 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211008194852-3b03d305991f h1:1scJEYZBaF48BaG6tYbtxmLcXqwYGSfGcMoStTqkkIw=
|
||||
golang.org/x/net v0.0.0-20211008194852-3b03d305991f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@ -663,8 +672,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558 h1:D7nTwh4J0i+5mW4Zjzn5omvlr6YBcWywE6KOcatyNxY=
|
||||
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@ -731,8 +740,9 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac h1:oN6lz7iLW/YC7un8pq+9bOLyXrprv2+DKfkJY+2LJJw=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -5,16 +5,24 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func BUILD() string {
|
||||
func BUILD(def string) string {
|
||||
build := os.Getenv("GIT_BUILD_HASH")
|
||||
if build == "" {
|
||||
return "tagged"
|
||||
return def
|
||||
}
|
||||
return build
|
||||
}
|
||||
|
||||
func OutpostUserAgent() string {
|
||||
return fmt.Sprintf("authentik-outpost@%s (build=%s)", VERSION, BUILD())
|
||||
func FullVersion() string {
|
||||
ver := VERSION
|
||||
if b := BUILD(""); b != "" {
|
||||
ver = fmt.Sprintf("%s.%s", ver, b)
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
const VERSION = "2021.12.5"
|
||||
func OutpostUserAgent() string {
|
||||
return fmt.Sprintf("authentik-outpost@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2022.1.4"
|
||||
|
24
internal/debug/debug.go
Normal file
24
internal/debug/debug.go
Normal file
@ -0,0 +1,24 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func EnableDebugServer() {
|
||||
l := log.WithField("logger", "authentik.go_debugger")
|
||||
if deb := os.Getenv("AUTHENTIK_DEBUG"); strings.ToLower(deb) != "true" {
|
||||
l.Info("not enabling debug server, set `AUTHENTIK_DEBUG` to `true` to enable it.")
|
||||
}
|
||||
h := http.NewServeMux()
|
||||
h.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
h.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
h.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
h.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
h.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
l.Println(http.ListenAndServe("0.0.0.0:9900", nil))
|
||||
}
|
@ -171,7 +171,7 @@ func (a *APIController) StartBackgorundTasks() error {
|
||||
"outpost_type": a.Server.Type(),
|
||||
"uuid": a.instanceUUID.String(),
|
||||
"version": constants.VERSION,
|
||||
"build": constants.BUILD(),
|
||||
"build": constants.BUILD("tagged"),
|
||||
}).Set(1)
|
||||
go func() {
|
||||
a.logger.Debug("Starting WS Handler...")
|
||||
|
@ -51,7 +51,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
|
||||
Instruction: WebsocketInstructionHello,
|
||||
Args: map[string]interface{}{
|
||||
"version": constants.VERSION,
|
||||
"buildHash": constants.BUILD(),
|
||||
"buildHash": constants.BUILD("tagged"),
|
||||
"uuid": ac.instanceUUID.String(),
|
||||
},
|
||||
}
|
||||
@ -151,7 +151,7 @@ func (ac *APIController) startWSHandler() {
|
||||
"outpost_type": ac.Server.Type(),
|
||||
"uuid": ac.instanceUUID.String(),
|
||||
"version": constants.VERSION,
|
||||
"build": constants.BUILD(),
|
||||
"build": constants.BUILD("tagged"),
|
||||
}).SetToCurrentTime()
|
||||
}
|
||||
}
|
||||
@ -165,7 +165,7 @@ func (ac *APIController) startWSHealth() {
|
||||
Instruction: WebsocketInstructionHello,
|
||||
Args: map[string]interface{}{
|
||||
"version": constants.VERSION,
|
||||
"buildHash": constants.BUILD(),
|
||||
"buildHash": constants.BUILD("tagged"),
|
||||
"uuid": ac.instanceUUID.String(),
|
||||
},
|
||||
}
|
||||
@ -205,7 +205,7 @@ func (ac *APIController) startIntervalUpdater() {
|
||||
"outpost_type": ac.Server.Type(),
|
||||
"uuid": ac.instanceUUID.String(),
|
||||
"version": constants.VERSION,
|
||||
"build": constants.BUILD(),
|
||||
"build": constants.BUILD("tagged"),
|
||||
}).SetToCurrentTime()
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ func doGlobalSetup(outpost api.Outpost, globalConfig api.Config) {
|
||||
} else {
|
||||
l.Debug("Managed outpost, not setting global log level")
|
||||
}
|
||||
l.WithField("hash", constants.BUILD()).WithField("version", constants.VERSION).Info("Starting authentik outpost")
|
||||
l.WithField("hash", constants.BUILD("tagged")).WithField("version", constants.VERSION).Info("Starting authentik outpost")
|
||||
|
||||
if globalConfig.ErrorReporting.Enabled {
|
||||
dsn := "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
|
||||
|
66
internal/outpost/ak/test.go
Normal file
66
internal/outpost/ak/test.go
Normal file
@ -0,0 +1,66 @@
|
||||
package ak
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/securecookie"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
|
||||
func TestSecret() string {
|
||||
return base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32))
|
||||
}
|
||||
|
||||
func MockConfig() api.Config {
|
||||
return *api.NewConfig(
|
||||
*api.NewErrorReportingConfig(false, "test", false, 0.0),
|
||||
[]api.CapabilitiesEnum{},
|
||||
100,
|
||||
100,
|
||||
100,
|
||||
100,
|
||||
)
|
||||
}
|
||||
|
||||
func MockAK(outpost api.Outpost, globalConfig api.Config) *APIController {
|
||||
config := api.NewConfiguration()
|
||||
config.HTTPClient = &http.Client{
|
||||
Transport: GetTLSTransport(),
|
||||
}
|
||||
token := TestSecret()
|
||||
config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
// create the API client, with the transport
|
||||
apiClient := api.NewAPIClient(config)
|
||||
|
||||
log := log.WithField("logger", "authentik.outpost.ak-api-controller")
|
||||
|
||||
log.WithField("name", outpost.Name).Debug("Fetched outpost configuration")
|
||||
|
||||
log.Debug("Fetched global configuration")
|
||||
|
||||
// doGlobalSetup is called by the OnRefresh handler, which ticks on start
|
||||
// doGlobalSetup(outpost, akConfig)
|
||||
|
||||
ac := &APIController{
|
||||
Client: apiClient,
|
||||
GlobalConfig: globalConfig,
|
||||
|
||||
token: token,
|
||||
logger: log,
|
||||
|
||||
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
|
||||
instanceUUID: uuid.New(),
|
||||
Outpost: outpost,
|
||||
wsBackoffMultiplier: 1,
|
||||
refreshHandlers: make([]func(), 0),
|
||||
}
|
||||
ac.logger.WithField("offset", ac.reloadOffset.String()).Debug("HA Reload offset")
|
||||
return ac
|
||||
}
|
@ -16,6 +16,7 @@ import (
|
||||
"goauthentik.io/internal/outpost/ldap/flags"
|
||||
"goauthentik.io/internal/outpost/ldap/metrics"
|
||||
"goauthentik.io/internal/outpost/ldap/server"
|
||||
"goauthentik.io/internal/outpost/ldap/utils"
|
||||
)
|
||||
|
||||
const ContextUserKey = "ak_user"
|
||||
@ -35,7 +36,7 @@ func NewDirectBinder(si server.LDAPServerInstance) *DirectBinder {
|
||||
}
|
||||
|
||||
func (db *DirectBinder) GetUsername(dn string) (string, error) {
|
||||
if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(db.si.GetBaseDN())) {
|
||||
if !utils.HasSuffixNoCase(dn, db.si.GetBaseDN()) {
|
||||
return "", errors.New("invalid base DN")
|
||||
}
|
||||
dns, err := goldap.ParseDN(dn)
|
||||
|
@ -12,11 +12,7 @@ func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
|
||||
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
|
||||
"memberOf": pi.GroupsForUser(u),
|
||||
"goauthentik.io/ldap/active": {utils.BoolToString(*u.IsActive)},
|
||||
"goauthentik.io/ldap/superuser": {utils.BoolToString(u.IsSuperuser)},
|
||||
"cn": {u.Username},
|
||||
|
@ -124,7 +124,7 @@ func (pi *ProviderInstance) GetBaseEntry() *ldap.Entry {
|
||||
},
|
||||
{
|
||||
Name: "vendorVersion",
|
||||
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s (build %s)", constants.VERSION, constants.BUILD())},
|
||||
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s", constants.FullVersion())},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -140,26 +140,26 @@ func (pi *ProviderInstance) GetNeededObjects(scope int, baseDN string, filterOC
|
||||
// If our requested base DN doesn't match any of the container DNs, then
|
||||
// we're probably loading a user or group. If it does, then make sure our
|
||||
// scope will eventually take us to users or groups.
|
||||
if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.UserDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetUserOCs()) {
|
||||
if (strings.EqualFold(baseDN, pi.BaseDN) || utils.HasSuffixNoCase(baseDN, pi.UserDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetUserOCs()) {
|
||||
if baseDN != pi.UserDN && baseDN != pi.BaseDN ||
|
||||
baseDN == pi.BaseDN && scope > 1 ||
|
||||
baseDN == pi.UserDN && scope > 0 {
|
||||
strings.EqualFold(baseDN, pi.BaseDN) && scope > 1 ||
|
||||
strings.EqualFold(baseDN, pi.UserDN) && scope > 0 {
|
||||
needUsers = true
|
||||
}
|
||||
}
|
||||
|
||||
if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.GroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetGroupOCs()) {
|
||||
if (strings.EqualFold(baseDN, pi.BaseDN) || utils.HasSuffixNoCase(baseDN, pi.GroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetGroupOCs()) {
|
||||
if baseDN != pi.GroupDN && baseDN != pi.BaseDN ||
|
||||
baseDN == pi.BaseDN && scope > 1 ||
|
||||
baseDN == pi.GroupDN && scope > 0 {
|
||||
strings.EqualFold(baseDN, pi.BaseDN) && scope > 1 ||
|
||||
strings.EqualFold(baseDN, pi.GroupDN) && scope > 0 {
|
||||
needGroups = true
|
||||
}
|
||||
}
|
||||
|
||||
if (baseDN == pi.BaseDN || strings.HasSuffix(baseDN, pi.VirtualGroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetVirtualGroupOCs()) {
|
||||
if (strings.EqualFold(baseDN, pi.BaseDN) || utils.HasSuffixNoCase(baseDN, pi.VirtualGroupDN)) && utils.IncludeObjectClass(filterOC, ldapConstants.GetVirtualGroupOCs()) {
|
||||
if baseDN != pi.VirtualGroupDN && baseDN != pi.BaseDN ||
|
||||
baseDN == pi.BaseDN && scope > 1 ||
|
||||
baseDN == pi.VirtualGroupDN && scope > 0 {
|
||||
strings.EqualFold(baseDN, pi.BaseDN) && scope > 1 ||
|
||||
strings.EqualFold(baseDN, pi.VirtualGroupDN) && scope > 0 {
|
||||
needUsers = true
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func (ds *DirectSearcher) SearchBase(req *search.Request, authz bool) (ldap.Serv
|
||||
},
|
||||
{
|
||||
Name: "vendorVersion",
|
||||
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s (build %s)", constants.VERSION, constants.BUILD())},
|
||||
Values: []string{fmt.Sprintf("authentik LDAP Outpost Version %s", constants.FullVersion())},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -36,7 +36,7 @@ func NewDirectSearcher(si server.LDAPServerInstance) *DirectSearcher {
|
||||
|
||||
func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
|
||||
baseDN := strings.ToLower(ds.si.GetBaseDN())
|
||||
baseDN := ds.si.GetBaseDN()
|
||||
|
||||
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
|
||||
if err != nil {
|
||||
@ -59,7 +59,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
|
||||
}
|
||||
if !strings.HasSuffix(req.BindDN, ","+baseDN) {
|
||||
if !utils.HasSuffixNoCase(req.BindDN, ","+baseDN) {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ds.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
@ -105,7 +105,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
scope := req.SearchRequest.Scope
|
||||
needUsers, needGroups := ds.si.GetNeededObjects(scope, req.BaseDN, filterOC)
|
||||
|
||||
if scope >= 0 && req.BaseDN == baseDN {
|
||||
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
||||
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
|
||||
entries = append(entries, ds.si.GetBaseEntry())
|
||||
}
|
||||
@ -209,8 +209,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err
|
||||
}
|
||||
|
||||
if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseUserDN())) {
|
||||
singleu := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseUserDN())
|
||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseUserDN())) {
|
||||
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseUserDN())
|
||||
|
||||
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
||||
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseUserDN(), constants.OUUsers))
|
||||
@ -220,7 +220,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) {
|
||||
for _, u := range *users {
|
||||
entry := ds.si.UserEntry(u)
|
||||
if req.BaseDN == entry.DN || !singleu {
|
||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
@ -229,8 +229,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
scope += 1 // Return the scope to what it was before we descended
|
||||
}
|
||||
|
||||
if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseGroupDN())) {
|
||||
singleg := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseGroupDN())
|
||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseGroupDN())) {
|
||||
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseGroupDN())
|
||||
|
||||
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
||||
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseGroupDN(), constants.OUGroups))
|
||||
@ -240,7 +240,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) {
|
||||
for _, g := range *groups {
|
||||
entry := group.FromAPIGroup(g, ds.si).Entry()
|
||||
if req.BaseDN == entry.DN || !singleg {
|
||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singleg {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
@ -249,8 +249,8 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
scope += 1 // Return the scope to what it was before we descended
|
||||
}
|
||||
|
||||
if scope >= 0 && (req.BaseDN == ds.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) {
|
||||
singlevg := strings.HasSuffix(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN())
|
||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ds.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ds.si.GetBaseVirtualGroupDN())) {
|
||||
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ds.si.GetBaseVirtualGroupDN())
|
||||
|
||||
if !singlevg || utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
||||
entries = append(entries, utils.GetContainerEntry(filterOC, ds.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
|
||||
@ -260,7 +260,7 @@ func (ds *DirectSearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) {
|
||||
for _, u := range *users {
|
||||
entry := group.FromAPIUser(u, ds.si).Entry()
|
||||
if req.BaseDN == entry.DN || !singlevg {
|
||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ func NewMemorySearcher(si server.LDAPServerInstance) *MemorySearcher {
|
||||
|
||||
func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, error) {
|
||||
accsp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.search.check_access")
|
||||
baseDN := strings.ToLower(ms.si.GetBaseDN())
|
||||
baseDN := ms.si.GetBaseDN()
|
||||
|
||||
filterOC, err := ldap.GetFilterObjectClass(req.Filter)
|
||||
if err != nil {
|
||||
@ -62,7 +62,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
}).Inc()
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
|
||||
}
|
||||
if !strings.HasSuffix(req.BindDN, ","+baseDN) {
|
||||
if !utils.HasSuffixNoCase(req.BindDN, ","+baseDN) {
|
||||
metrics.RequestsRejected.With(prometheus.Labels{
|
||||
"outpost_name": ms.si.GetOutpostName(),
|
||||
"type": "search",
|
||||
@ -92,7 +92,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
scope := req.SearchRequest.Scope
|
||||
needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, filterOC)
|
||||
|
||||
if scope >= 0 && req.BaseDN == baseDN {
|
||||
if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) {
|
||||
if utils.IncludeObjectClass(filterOC, constants.GetDomainOCs()) {
|
||||
entries = append(entries, ms.si.GetBaseEntry())
|
||||
}
|
||||
@ -139,7 +139,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
// as a member.
|
||||
for _, u := range g.UsersObj {
|
||||
if flags.UserPk == u.Pk {
|
||||
// TODO: Is there a better way to clone this object?
|
||||
//TODO: Is there a better way to clone this object?
|
||||
fg := api.NewGroup(g.Pk, g.Name, g.Parent, g.ParentName, []int32{flags.UserPk}, []api.GroupMember{u})
|
||||
fg.SetAttributes(*g.Attributes)
|
||||
fg.SetIsSuperuser(*g.IsSuperuser)
|
||||
@ -155,8 +155,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, err
|
||||
}
|
||||
|
||||
if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseUserDN())) {
|
||||
singleu := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseUserDN())
|
||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseUserDN())) {
|
||||
singleu := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseUserDN())
|
||||
|
||||
if !singleu && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
||||
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseUserDN(), constants.OUUsers))
|
||||
@ -166,7 +166,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetUserOCs()) {
|
||||
for _, u := range *users {
|
||||
entry := ms.si.UserEntry(u)
|
||||
if req.BaseDN == entry.DN || !singleu {
|
||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singleu {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
@ -175,8 +175,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
scope += 1 // Return the scope to what it was before we descended
|
||||
}
|
||||
|
||||
if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseGroupDN())) {
|
||||
singleg := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseGroupDN())
|
||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseGroupDN())) {
|
||||
singleg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseGroupDN())
|
||||
|
||||
if !singleg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
||||
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseGroupDN(), constants.OUGroups))
|
||||
@ -185,7 +185,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
|
||||
if scope >= 0 && groups != nil && utils.IncludeObjectClass(filterOC, constants.GetGroupOCs()) {
|
||||
for _, g := range groups {
|
||||
if req.BaseDN == g.DN || !singleg {
|
||||
if strings.EqualFold(req.BaseDN, g.DN) || !singleg {
|
||||
entries = append(entries, g.Entry())
|
||||
}
|
||||
}
|
||||
@ -194,8 +194,8 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
scope += 1 // Return the scope to what it was before we descended
|
||||
}
|
||||
|
||||
if scope >= 0 && (req.BaseDN == ms.si.GetBaseDN() || strings.HasSuffix(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) {
|
||||
singlevg := strings.HasSuffix(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN())
|
||||
if scope >= 0 && (strings.EqualFold(req.BaseDN, ms.si.GetBaseDN()) || utils.HasSuffixNoCase(req.BaseDN, ms.si.GetBaseVirtualGroupDN())) {
|
||||
singlevg := utils.HasSuffixNoCase(req.BaseDN, ","+ms.si.GetBaseVirtualGroupDN())
|
||||
|
||||
if !singlevg && utils.IncludeObjectClass(filterOC, constants.GetContainerOCs()) {
|
||||
entries = append(entries, utils.GetContainerEntry(filterOC, ms.si.GetBaseVirtualGroupDN(), constants.OUVirtualGroups))
|
||||
@ -205,7 +205,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult,
|
||||
if scope >= 0 && users != nil && utils.IncludeObjectClass(filterOC, constants.GetVirtualGroupOCs()) {
|
||||
for _, u := range *users {
|
||||
entry := group.FromAPIUser(u, ms.si).Entry()
|
||||
if req.BaseDN == entry.DN || !singlevg {
|
||||
if strings.EqualFold(req.BaseDN, entry.DN) || !singlevg {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ type Request struct {
|
||||
func NewRequest(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (*Request, *sentry.Span) {
|
||||
rid := uuid.New().String()
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
searchReq.BaseDN = strings.ToLower(searchReq.BaseDN)
|
||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search"))
|
||||
span.Description = fmt.Sprintf("%s (%s)", searchReq.BaseDN, ldap.ScopeMap[searchReq.Scope])
|
||||
span.SetTag("request_uid", rid)
|
||||
|
@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/nmcclain/ldap"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -117,3 +118,7 @@ func GetContainerEntry(filterOC string, dn string, ou string) *ldap.Entry {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func HasSuffixNoCase(s1 string, s2 string) bool {
|
||||
return strings.HasSuffix(strings.ToLower(s1), strings.ToLower(s2))
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
ber "github.com/nmcclain/asn1-ber"
|
||||
"github.com/nmcclain/ldap"
|
||||
@ -41,7 +43,7 @@ func parseFilterForGroupSingle(req api.ApiCoreGroupsListRequest, f *ber.Packet)
|
||||
// Switch on type of the value, then check the key
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
switch k {
|
||||
switch strings.ToLower(k.(string)) {
|
||||
case "cn":
|
||||
return req.Name(vv), false
|
||||
case "member":
|
||||
@ -54,14 +56,14 @@ func parseFilterForGroupSingle(req api.ApiCoreGroupsListRequest, f *ber.Packet)
|
||||
username := userDN.RDNs[0].Attributes[0].Value
|
||||
// If the DN's first ou is virtual-groups, ignore this filter
|
||||
if len(userDN.RDNs) > 1 {
|
||||
if userDN.RDNs[1].Attributes[0].Value == constants.OUVirtualGroups || userDN.RDNs[1].Attributes[0].Value == constants.OUGroups {
|
||||
if strings.EqualFold(userDN.RDNs[1].Attributes[0].Value, constants.OUVirtualGroups) || strings.EqualFold(userDN.RDNs[1].Attributes[0].Value, constants.OUGroups) {
|
||||
// Since we know we're not filtering anything, skip this request
|
||||
return req, true
|
||||
}
|
||||
}
|
||||
return req.MembersByUsername([]string{username}), false
|
||||
}
|
||||
// TODO: Support int
|
||||
//TODO: Support int
|
||||
default:
|
||||
return req, false
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ func parseFilterForUserSingle(req api.ApiCoreUsersListRequest, f *ber.Packet) (a
|
||||
}
|
||||
return req.GroupsByName([]string{name}), false
|
||||
}
|
||||
// TODO: Support int
|
||||
//TODO: Support int
|
||||
default:
|
||||
return req, false
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ type Application struct {
|
||||
|
||||
log *log.Entry
|
||||
mux *mux.Router
|
||||
ak *ak.APIController
|
||||
|
||||
errorTemplates *template.Template
|
||||
}
|
||||
@ -84,7 +85,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
||||
mux := mux.NewRouter()
|
||||
a := &Application{
|
||||
Host: externalHost.Host,
|
||||
log: log.WithField("logger", "authentik.outpost.proxy.bundle").WithField("provider", p.Name),
|
||||
log: muxLogger,
|
||||
outpostName: ak.Outpost.Name,
|
||||
endpint: endpoint,
|
||||
oauthConfig: oauth2Config,
|
||||
@ -93,6 +94,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
||||
httpClient: c,
|
||||
mux: mux,
|
||||
errorTemplates: templates.GetTemplates(),
|
||||
ak: ak,
|
||||
}
|
||||
a.sessions = a.getStore(p)
|
||||
mux.Use(web.NewLoggingHandler(muxLogger, func(l *log.Entry, r *http.Request) *log.Entry {
|
||||
@ -172,7 +174,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
|
||||
for _, regex := range strings.Split(*p.SkipPathRegex, "\n") {
|
||||
re, err := regexp.Compile(regex)
|
||||
if err != nil {
|
||||
// TODO: maybe create event for this?
|
||||
//TODO: maybe create event for this?
|
||||
a.log.WithError(err).Warning("failed to compile SkipPathRegex")
|
||||
continue
|
||||
} else {
|
||||
@ -187,12 +189,16 @@ func (a *Application) Mode() api.ProxyMode {
|
||||
return *a.proxyConfig.Mode
|
||||
}
|
||||
|
||||
func (a *Application) ProxyConfig() api.ProxyOutpostConfig {
|
||||
return a.proxyConfig
|
||||
}
|
||||
|
||||
func (a *Application) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
a.mux.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
func (a *Application) handleSignOut(rw http.ResponseWriter, r *http.Request) {
|
||||
// TODO: Token revocation
|
||||
//TODO: Token revocation
|
||||
s, err := a.sessions.Get(r, constants.SeesionName)
|
||||
if err != nil {
|
||||
http.Redirect(rw, r, a.endpint.EndSessionEndpoint, http.StatusFound)
|
||||
|
@ -1,7 +1,8 @@
|
||||
package application
|
||||
|
||||
type ProxyClaims struct {
|
||||
UserAttributes map[string]interface{} `json:"user_attributes"`
|
||||
UserAttributes map[string]interface{} `json:"user_attributes"`
|
||||
BackendOverride string `json:"backend_override"`
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
|
@ -13,15 +13,18 @@ import (
|
||||
type OIDCEndpoint struct {
|
||||
oauth2.Endpoint
|
||||
EndSessionEndpoint string
|
||||
JwksUri string
|
||||
}
|
||||
|
||||
func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoint {
|
||||
authUrl := p.OidcConfiguration.AuthorizationEndpoint
|
||||
endUrl := p.OidcConfiguration.EndSessionEndpoint
|
||||
jwksUrl := p.OidcConfiguration.JwksUri
|
||||
if browserHost, found := os.LookupEnv("AUTHENTIK_HOST_BROWSER"); found && browserHost != "" {
|
||||
host := os.Getenv("AUTHENTIK_HOST")
|
||||
authUrl = strings.ReplaceAll(authUrl, host, browserHost)
|
||||
endUrl = strings.ReplaceAll(endUrl, host, browserHost)
|
||||
jwksUrl = strings.ReplaceAll(jwksUrl, host, browserHost)
|
||||
}
|
||||
ep := OIDCEndpoint{
|
||||
Endpoint: oauth2.Endpoint{
|
||||
@ -30,6 +33,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoin
|
||||
AuthStyle: oauth2.AuthStyleInParams,
|
||||
},
|
||||
EndSessionEndpoint: endUrl,
|
||||
JwksUri: jwksUrl,
|
||||
}
|
||||
authU, err := url.Parse(authUrl)
|
||||
if err != nil {
|
||||
@ -39,6 +43,10 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoin
|
||||
if err != nil {
|
||||
return ep
|
||||
}
|
||||
jwksU, err := url.Parse(jwksUrl)
|
||||
if err != nil {
|
||||
return ep
|
||||
}
|
||||
if authU.Host != "localhost:8000" {
|
||||
return ep
|
||||
}
|
||||
@ -54,7 +62,10 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string) OIDCEndpoin
|
||||
authU.Scheme = aku.Scheme
|
||||
endU.Host = aku.Host
|
||||
endU.Scheme = aku.Scheme
|
||||
jwksU.Host = aku.Host
|
||||
jwksU.Scheme = aku.Scheme
|
||||
ep.AuthURL = authU.String()
|
||||
ep.EndSessionEndpoint = endU.String()
|
||||
ep.JwksUri = jwksU.String()
|
||||
return ep
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -14,14 +16,6 @@ import (
|
||||
func (a *Application) addHeaders(headers http.Header, c *Claims) {
|
||||
// https://goauthentik.io/docs/providers/proxy/proxy
|
||||
|
||||
// Legacy headers, remove after 2022.1
|
||||
headers.Set("X-Auth-Username", c.PreferredUsername)
|
||||
headers.Set("X-Auth-Groups", strings.Join(c.Groups, "|"))
|
||||
headers.Set("X-Forwarded-Email", c.Email)
|
||||
headers.Set("X-Forwarded-Preferred-Username", c.PreferredUsername)
|
||||
headers.Set("X-Forwarded-User", c.Sub)
|
||||
|
||||
// New headers, unique prefix
|
||||
headers.Set("X-authentik-username", c.PreferredUsername)
|
||||
headers.Set("X-authentik-groups", strings.Join(c.Groups, "|"))
|
||||
headers.Set("X-authentik-email", c.Email)
|
||||
@ -30,7 +24,7 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
|
||||
headers.Set("X-authentik-jwt", c.RawToken)
|
||||
|
||||
// System headers
|
||||
headers.Set("X-authentik-meta-jwks", a.proxyConfig.OidcConfiguration.JwksUri)
|
||||
headers.Set("X-authentik-meta-jwks", a.endpint.JwksUri)
|
||||
headers.Set("X-authentik-meta-outpost", a.outpostName)
|
||||
headers.Set("X-authentik-meta-provider", a.proxyConfig.Name)
|
||||
headers.Set("X-authentik-meta-app", a.proxyConfig.AssignedApplicationSlug)
|
||||
@ -65,7 +59,8 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) getTraefikForwardUrl(r *http.Request) *url.URL {
|
||||
// getTraefikForwardUrl See https://doc.traefik.io/traefik/middlewares/forwardauth/
|
||||
func (a *Application) getTraefikForwardUrl(r *http.Request) (*url.URL, error) {
|
||||
u, err := url.Parse(fmt.Sprintf(
|
||||
"%s://%s%s",
|
||||
r.Header.Get("X-Forwarded-Proto"),
|
||||
@ -73,33 +68,63 @@ func (a *Application) getTraefikForwardUrl(r *http.Request) *url.URL {
|
||||
r.Header.Get("X-Forwarded-Uri"),
|
||||
))
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("Failed to parse URL from traefik")
|
||||
return r.URL
|
||||
return nil, err
|
||||
}
|
||||
a.log.WithField("url", u.String()).Trace("traefik forwarded url")
|
||||
return u
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (a *Application) IsAllowlisted(r *http.Request) bool {
|
||||
url := r.URL
|
||||
// In Forward auth mode, we can't directly match against the requested URL
|
||||
// Since that would be /akprox/auth/...
|
||||
if a.Mode() == api.PROXYMODE_FORWARD_SINGLE || a.Mode() == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
// For traefik, we can get the Upstream URL from headers
|
||||
// For nginx we can attempt to as well, but it's not guaranteed to work.
|
||||
if strings.HasPrefix(r.URL.Path, "/akprox/auth") {
|
||||
url = a.getTraefikForwardUrl(r)
|
||||
// getNginxForwardUrl See https://github.com/kubernetes/ingress-nginx/blob/main/rootfs/etc/nginx/template/nginx.tmpl
|
||||
func (a *Application) getNginxForwardUrl(r *http.Request) (*url.URL, error) {
|
||||
ou := r.Header.Get("X-Original-URI")
|
||||
if ou != "" {
|
||||
// Turn this full URL into a relative URL
|
||||
u := &url.URL{
|
||||
Host: "",
|
||||
Scheme: "",
|
||||
Path: ou,
|
||||
}
|
||||
a.log.WithField("url", u.String()).Info("building forward URL from X-Original-URI")
|
||||
return u, nil
|
||||
}
|
||||
for _, u := range a.UnauthenticatedRegex {
|
||||
h := r.Header.Get("X-Original-URL")
|
||||
if len(h) < 1 {
|
||||
return nil, errors.New("no forward URL found")
|
||||
}
|
||||
u, err := url.Parse(h)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to parse URL from nginx")
|
||||
return nil, err
|
||||
}
|
||||
a.log.WithField("url", u.String()).Trace("nginx forwarded url")
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (a *Application) ReportMisconfiguration(r *http.Request, msg string, fields map[string]interface{}) {
|
||||
fields["message"] = msg
|
||||
a.log.WithFields(fields).Error("Reporting configuration error")
|
||||
req := api.EventRequest{
|
||||
Action: api.EVENTACTIONS_CONFIGURATION_ERROR,
|
||||
App: "authentik.providers.proxy", // must match python apps.py name
|
||||
ClientIp: *api.NewNullableString(api.PtrString(r.RemoteAddr)),
|
||||
Context: &fields,
|
||||
}
|
||||
_, _, err := a.ak.Client.EventsApi.EventsEventsCreate(context.Background()).EventRequest(req).Execute()
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to report configuration error")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) IsAllowlisted(u *url.URL) bool {
|
||||
for _, ur := range a.UnauthenticatedRegex {
|
||||
var testString string
|
||||
if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE {
|
||||
testString = url.Path
|
||||
testString = u.Path
|
||||
} else {
|
||||
testString = url.String()
|
||||
testString = u.String()
|
||||
}
|
||||
a.log.WithField("regex", u.String()).WithField("url", testString).Trace("Matching URL against allow list")
|
||||
if u.MatchString(testString) {
|
||||
if ur.MatchString(testString) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
@ -24,39 +25,52 @@ func (a *Application) configureForward() error {
|
||||
}
|
||||
|
||||
func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Request) {
|
||||
a.log.WithField("header", r.Header).Trace("tracing headers for debug")
|
||||
// First check if we've got everything we need
|
||||
fwd, err := a.getTraefikForwardUrl(r)
|
||||
if err != nil {
|
||||
a.ReportMisconfiguration(r, fmt.Sprintf("Outpost %s (Provider %s) failed to detect a forward URL from Traefik", a.outpostName, a.proxyConfig.Name), map[string]interface{}{
|
||||
"provider": a.proxyConfig.Name,
|
||||
"outpost": a.outpostName,
|
||||
"url": r.URL.String(),
|
||||
"headers": cleanseHeaders(r.Header),
|
||||
})
|
||||
http.Error(rw, "configuration error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := a.getClaims(r)
|
||||
if claims != nil && err == nil {
|
||||
a.addHeaders(rw.Header(), claims)
|
||||
rw.Header().Set("User-Agent", r.Header.Get("User-Agent"))
|
||||
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
|
||||
return
|
||||
} else if claims == nil && a.IsAllowlisted(r) {
|
||||
} else if claims == nil && a.IsAllowlisted(fwd) {
|
||||
a.log.Trace("path can be accessed without authentication")
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.Header.Get("X-Forwarded-Uri"), "/akprox") {
|
||||
a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access")
|
||||
return
|
||||
}
|
||||
host := ""
|
||||
s, _ := a.sessions.Get(r, constants.SeesionName)
|
||||
// Optional suffix, which is appended to the URL
|
||||
if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_SINGLE {
|
||||
host = web.GetHost(r)
|
||||
} else if *a.proxyConfig.Mode == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
eh, _ := url.Parse(a.proxyConfig.ExternalHost)
|
||||
host = eh.Host
|
||||
eh, err := url.Parse(a.proxyConfig.ExternalHost)
|
||||
if err != nil {
|
||||
a.log.WithField("host", a.proxyConfig.ExternalHost).WithError(err).Warning("invalid external_host")
|
||||
} else {
|
||||
host = eh.Host
|
||||
}
|
||||
}
|
||||
// set the redirect flag to the current URL we have, since we redirect
|
||||
// to a (possibly) different domain, but we want to be redirected back
|
||||
// to the application
|
||||
// see https://doc.traefik.io/traefik/middlewares/forwardauth/
|
||||
// X-Forwarded-Uri is only the path, so we need to build the entire URL
|
||||
s.Values[constants.SessionRedirect] = a.getTraefikForwardUrl(r).String()
|
||||
if r.Header.Get("X-Forwarded-Uri") == "/akprox/start" {
|
||||
a.log.Info("Detected potential redirect loop")
|
||||
if val, ok := s.Values[constants.SessionLoopDetection]; !ok {
|
||||
s.Values[constants.SessionLoopDetection] = 1
|
||||
} else {
|
||||
s.Values[constants.SessionLoopDetection] = val.(int) + 1
|
||||
}
|
||||
}
|
||||
s.Values[constants.SessionRedirect] = fwd.String()
|
||||
err = s.Save(r, rw)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to save session before redirect")
|
||||
@ -72,6 +86,19 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request) {
|
||||
a.log.WithField("header", r.Header).Trace("tracing headers for debug")
|
||||
fwd, err := a.getNginxForwardUrl(r)
|
||||
if err != nil {
|
||||
a.ReportMisconfiguration(r, fmt.Sprintf("Outpost %s (Provider %s) failed to detect a forward URL from nginx", a.outpostName, a.proxyConfig.Name), map[string]interface{}{
|
||||
"provider": a.proxyConfig.Name,
|
||||
"outpost": a.outpostName,
|
||||
"url": r.URL.String(),
|
||||
"headers": cleanseHeaders(r.Header),
|
||||
})
|
||||
http.Error(rw, "configuration error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := a.getClaims(r)
|
||||
if claims != nil && err == nil {
|
||||
a.addHeaders(rw.Header(), claims)
|
||||
@ -79,9 +106,23 @@ func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request
|
||||
rw.WriteHeader(200)
|
||||
a.log.WithField("headers", rw.Header()).Trace("headers written to forward_auth")
|
||||
return
|
||||
} else if claims == nil && a.IsAllowlisted(r) {
|
||||
} else if claims == nil && a.IsAllowlisted(fwd) {
|
||||
a.log.Trace("path can be accessed without authentication")
|
||||
return
|
||||
}
|
||||
|
||||
s, _ := a.sessions.Get(r, constants.SeesionName)
|
||||
s.Values[constants.SessionRedirect] = fwd.String()
|
||||
err = s.Save(r, rw)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to save session before redirect")
|
||||
}
|
||||
|
||||
if fwd.String() != r.URL.String() {
|
||||
if strings.HasPrefix(fwd.Path, "/akprox") {
|
||||
a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access")
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Error(rw, "unauthorized request", http.StatusUnauthorized)
|
||||
}
|
||||
|
134
internal/outpost/proxyv2/application/mode_forward_nginx_test.go
Normal file
134
internal/outpost/proxyv2/application/mode_forward_nginx_test.go
Normal file
@ -0,0 +1,134 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
)
|
||||
|
||||
func TestForwardHandleNginx_Single_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
}
|
||||
|
||||
func TestForwardHandleNginx_Single_Skip(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URL", "http://test.goauthentik.io/skip")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestForwardHandleNginx_Single_Headers(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
assert.Equal(t, rr.Code, http.StatusUnauthorized)
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
|
||||
}
|
||||
|
||||
func TestForwardHandleNginx_Single_URI(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "https://foo.bar/akprox/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URI", "/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
assert.Equal(t, rr.Code, http.StatusUnauthorized)
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "/app", s.Values[constants.SessionRedirect])
|
||||
}
|
||||
|
||||
func TestForwardHandleNginx_Single_Claims(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URI", "/")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
s.Values[constants.SessionClaims] = Claims{
|
||||
Sub: "foo",
|
||||
Proxy: ProxyClaims{
|
||||
UserAttributes: map[string]interface{}{
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"additionalHeaders": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := a.sessions.Save(req, rr, s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
h := rr.Result().Header
|
||||
|
||||
assert.Equal(t, []string{"Basic Zm9vOmJhcg=="}, h["Authorization"])
|
||||
assert.Equal(t, []string{"bar"}, h["Foo"])
|
||||
assert.Equal(t, []string{""}, h["User-Agent"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Email"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Groups"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Jwt"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Meta-App"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Jwks"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Outpost"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Name"])
|
||||
assert.Equal(t, []string{"foo"}, h["X-Authentik-Uid"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Username"])
|
||||
}
|
||||
|
||||
func TestForwardHandleNginx_Domain_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
}
|
||||
|
||||
func TestForwardHandleNginx_Domain_Header(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil)
|
||||
req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleNginx(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, rr.Code)
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
)
|
||||
|
||||
func TestForwardHandleTraefik_Single_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleTraefik(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
}
|
||||
|
||||
func TestForwardHandleTraefik_Single_Skip(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
|
||||
req.Header.Set("X-Forwarded-Uri", "/skip")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleTraefik(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
}
|
||||
|
||||
func TestForwardHandleTraefik_Single_Headers(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
|
||||
req.Header.Set("X-Forwarded-Uri", "/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleTraefik(rr, req)
|
||||
|
||||
assert.Equal(t, rr.Code, http.StatusTemporaryRedirect)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, loc.String(), "http://test.goauthentik.io/akprox/start")
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
|
||||
}
|
||||
|
||||
func TestForwardHandleTraefik_Single_Claims(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
|
||||
req.Header.Set("X-Forwarded-Uri", "/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleTraefik(rr, req)
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
s.Values[constants.SessionClaims] = Claims{
|
||||
Sub: "foo",
|
||||
Proxy: ProxyClaims{
|
||||
UserAttributes: map[string]interface{}{
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"additionalHeaders": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := a.sessions.Save(req, rr, s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
a.forwardHandleTraefik(rr, req)
|
||||
|
||||
h := rr.Result().Header
|
||||
|
||||
assert.Equal(t, []string{"Basic Zm9vOmJhcg=="}, h["Authorization"])
|
||||
assert.Equal(t, []string{"bar"}, h["Foo"])
|
||||
assert.Equal(t, []string{""}, h["User-Agent"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Email"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Groups"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Jwt"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Meta-App"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Jwks"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Meta-Outpost"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Name"])
|
||||
assert.Equal(t, []string{"foo"}, h["X-Authentik-Uid"])
|
||||
assert.Equal(t, []string{""}, h["X-Authentik-Username"])
|
||||
}
|
||||
|
||||
func TestForwardHandleTraefik_Domain_Blank(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleTraefik(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code)
|
||||
}
|
||||
|
||||
func TestForwardHandleTraefik_Domain_Header(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil)
|
||||
req.Header.Set("X-Forwarded-Proto", "http")
|
||||
req.Header.Set("X-Forwarded-Host", "test.goauthentik.io")
|
||||
req.Header.Set("X-Forwarded-Uri", "/app")
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.forwardHandleTraefik(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusTemporaryRedirect, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "http://auth.test.goauthentik.io/akprox/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect])
|
||||
}
|
@ -35,7 +35,7 @@ func (a *Application) configureProxy() error {
|
||||
rp.ModifyResponse = a.proxyModifyResponse
|
||||
a.mux.PathPrefix("/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
claims, err := a.getClaims(r)
|
||||
if claims == nil && a.IsAllowlisted(r) {
|
||||
if claims == nil && a.IsAllowlisted(r.URL) {
|
||||
a.log.Trace("path can be accessed without authentication")
|
||||
} else if claims == nil && err != nil {
|
||||
a.redirectToStart(rw, r)
|
||||
@ -60,7 +60,7 @@ func (a *Application) configureProxy() error {
|
||||
}
|
||||
metrics.UpstreamTiming.With(prometheus.Labels{
|
||||
"outpost_name": a.outpostName,
|
||||
"upstream_host": u.String(),
|
||||
"upstream_host": r.URL.Host,
|
||||
"scheme": r.URL.Scheme,
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
@ -71,13 +71,24 @@ func (a *Application) configureProxy() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Application) proxyModifyRequest(u *url.URL) func(req *http.Request) {
|
||||
return func(req *http.Request) {
|
||||
req.URL.Scheme = u.Scheme
|
||||
req.URL.Host = u.Host
|
||||
func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) {
|
||||
return func(r *http.Request) {
|
||||
claims, _ := a.getClaims(r)
|
||||
if claims.Proxy.BackendOverride != "" {
|
||||
u, err := url.Parse(claims.Proxy.BackendOverride)
|
||||
if err != nil {
|
||||
a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override")
|
||||
}
|
||||
r.URL.Scheme = u.Scheme
|
||||
r.URL.Host = u.Host
|
||||
} else {
|
||||
r.URL.Scheme = ou.Scheme
|
||||
r.URL.Host = ou.Host
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) proxyModifyResponse(res *http.Response) error {
|
||||
res.Header.Set("X-Powered-By", "authentik_proxy2")
|
||||
return nil
|
||||
}
|
||||
|
@ -25,13 +25,6 @@ func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to save session")
|
||||
}
|
||||
if loop, ok := s.Values[constants.SessionLoopDetection]; ok {
|
||||
if loop.(int) > 10 {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
a.ErrorPage(rw, r, "Detected redirect loop, make sure /akprox is accessible without authentication.")
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Redirect(rw, r, a.oauthConfig.AuthCodeURL(newState), http.StatusFound)
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
rs.SetMaxLength(math.MaxInt64)
|
||||
rs.SetMaxLength(math.MaxInt)
|
||||
if p.TokenValidity.IsSet() {
|
||||
t := p.TokenValidity.Get()
|
||||
// Add one to the validity to ensure we don't have a session with indefinite length
|
||||
@ -39,7 +39,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store {
|
||||
// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
|
||||
|
||||
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
|
||||
cs.MaxLength(math.MaxInt64)
|
||||
cs.MaxLength(math.MaxInt)
|
||||
if p.TokenValidity.IsSet() {
|
||||
t := p.TokenValidity.Get()
|
||||
// Add one to the validity to ensure we don't have a session with indefinite length
|
||||
|
40
internal/outpost/proxyv2/application/test.go
Normal file
40
internal/outpost/proxyv2/application/test.go
Normal file
@ -0,0 +1,40 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/quasoft/memstore"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
)
|
||||
|
||||
func newTestApplication() *Application {
|
||||
a, _ := NewApplication(
|
||||
api.ProxyOutpostConfig{
|
||||
Name: ak.TestSecret(),
|
||||
ClientId: api.PtrString(ak.TestSecret()),
|
||||
ClientSecret: api.PtrString(ak.TestSecret()),
|
||||
CookieSecret: api.PtrString(ak.TestSecret()),
|
||||
CookieDomain: api.PtrString(""),
|
||||
Mode: api.PROXYMODE_FORWARD_SINGLE.Ptr(),
|
||||
SkipPathRegex: api.PtrString("/skip.*"),
|
||||
BasicAuthEnabled: api.PtrBool(true),
|
||||
BasicAuthUserAttribute: api.PtrString("username"),
|
||||
BasicAuthPasswordAttribute: api.PtrString("password"),
|
||||
},
|
||||
http.DefaultClient,
|
||||
nil,
|
||||
ak.MockAK(
|
||||
api.Outpost{
|
||||
Config: map[string]interface{}{
|
||||
"authentik_host": ak.TestSecret(),
|
||||
},
|
||||
},
|
||||
ak.MockConfig(),
|
||||
),
|
||||
)
|
||||
a.sessions = memstore.NewMemStore(
|
||||
[]byte(ak.TestSecret()),
|
||||
)
|
||||
return a
|
||||
}
|
@ -6,7 +6,9 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
)
|
||||
|
||||
@ -20,6 +22,26 @@ func urlJoin(originalUrl string, newPath string) string {
|
||||
}
|
||||
|
||||
func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) {
|
||||
s, err := a.sessions.Get(r, constants.SeesionName)
|
||||
if err == nil {
|
||||
a.log.WithError(err).Warning("failed to decode session")
|
||||
}
|
||||
redirectUrl := urlJoin(a.proxyConfig.ExternalHost, r.URL.Path)
|
||||
if a.Mode() == api.PROXYMODE_FORWARD_DOMAIN {
|
||||
dom := strings.TrimPrefix(*a.proxyConfig.CookieDomain, ".")
|
||||
// In forward_domain we only check that the current URL's host
|
||||
// ends with the cookie domain (remove the leading period if set)
|
||||
if !strings.HasSuffix(r.URL.Hostname(), dom) {
|
||||
a.log.WithField("url", r.URL.String()).WithField("cd", dom).Warning("Invalid redirect found")
|
||||
redirectUrl = a.proxyConfig.ExternalHost
|
||||
}
|
||||
}
|
||||
s.Values[constants.SessionRedirect] = redirectUrl
|
||||
err = s.Save(r, rw)
|
||||
if err != nil {
|
||||
a.log.WithError(err).Warning("failed to save session before redirect")
|
||||
}
|
||||
|
||||
authUrl := urlJoin(a.proxyConfig.ExternalHost, "/akprox/start")
|
||||
http.Redirect(rw, r, authUrl, http.StatusFound)
|
||||
}
|
||||
@ -65,3 +87,13 @@ func contains(s []string, e string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func cleanseHeaders(headers http.Header) map[string]string {
|
||||
h := make(map[string]string)
|
||||
for hk, hv := range headers {
|
||||
if len(hv) > 0 {
|
||||
h[hk] = hv[0]
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
81
internal/outpost/proxyv2/application/utils_test.go
Normal file
81
internal/outpost/proxyv2/application/utils_test.go
Normal file
@ -0,0 +1,81 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/proxyv2/constants"
|
||||
)
|
||||
|
||||
func TestRedirectToStart_Proxy(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_PROXY.Ptr()
|
||||
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.redirectToStart(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect])
|
||||
}
|
||||
|
||||
func TestRedirectToStart_Forward(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_SINGLE.Ptr()
|
||||
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.redirectToStart(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect])
|
||||
}
|
||||
|
||||
func TestRedirectToStart_Forward_Domain_Invalid(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("foo")
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.redirectToStart(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect])
|
||||
}
|
||||
|
||||
func TestRedirectToStart_Forward_Domain(t *testing.T) {
|
||||
a := newTestApplication()
|
||||
a.proxyConfig.CookieDomain = api.PtrString("goauthentik.io")
|
||||
a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr()
|
||||
a.proxyConfig.ExternalHost = "https://test.goauthentik.io"
|
||||
req, _ := http.NewRequest("GET", "/foo/bar/baz", nil)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
a.redirectToStart(rr, req)
|
||||
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
loc, _ := rr.Result().Location()
|
||||
assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String())
|
||||
|
||||
s, _ := a.sessions.Get(req, constants.SeesionName)
|
||||
assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect])
|
||||
}
|
@ -6,4 +6,3 @@ const SessionOAuthState = "oauth_state"
|
||||
const SessionClaims = "claims"
|
||||
|
||||
const SessionRedirect = "redirect"
|
||||
const SessionLoopDetection = "loop_detection"
|
||||
|
@ -8,6 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/proxyv2/application"
|
||||
"goauthentik.io/internal/outpost/proxyv2/metrics"
|
||||
"goauthentik.io/internal/utils/web"
|
||||
staticWeb "goauthentik.io/web"
|
||||
@ -43,6 +45,50 @@ func (ps *ProxyServer) HandleStatic(rw http.ResponseWriter, r *http.Request) {
|
||||
}).Observe(float64(after))
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) lookupApp(r *http.Request) (*application.Application, string) {
|
||||
host := web.GetHost(r)
|
||||
// Try to find application by directly looking up host first (proxy, forward_auth_single)
|
||||
a, ok := ps.apps[host]
|
||||
if ok {
|
||||
ps.log.WithField("host", host).WithField("app", a.ProxyConfig().Name).Debug("Found app based direct host match")
|
||||
return a, host
|
||||
}
|
||||
// For forward_auth_domain, we don't have a direct app to domain relationship
|
||||
// Check through all apps, and check how much of their cookie domain matches the host
|
||||
// Return the application that has the longest match
|
||||
var longestMatch *application.Application
|
||||
longestMatchLength := 0
|
||||
for _, app := range ps.apps {
|
||||
if app.Mode() != api.PROXYMODE_FORWARD_DOMAIN {
|
||||
continue
|
||||
}
|
||||
// Check if the cookie domain has a leading period for a wildcard
|
||||
// This will decrease the weight of a wildcard domain, but a request to example.com
|
||||
// with the cookie domain set to example.com will still be routed correctly.
|
||||
cd := strings.TrimPrefix(*app.ProxyConfig().CookieDomain, ".")
|
||||
if !strings.HasSuffix(host, cd) {
|
||||
continue
|
||||
}
|
||||
if len(cd) < longestMatchLength {
|
||||
continue
|
||||
}
|
||||
longestMatch = app
|
||||
longestMatchLength = len(cd)
|
||||
// Also for forward_auth_domain, we need to respond on the external domain
|
||||
if app.ProxyConfig().ExternalHost == host {
|
||||
ps.log.WithField("host", host).WithField("app", app.ProxyConfig().Name).Debug("Found app based on external_host")
|
||||
return app, host
|
||||
}
|
||||
}
|
||||
// Check if our longes match is 0, in which case we didn't match, so we
|
||||
// manually return no app
|
||||
if longestMatchLength == 0 {
|
||||
return nil, host
|
||||
}
|
||||
ps.log.WithField("host", host).WithField("app", longestMatch.ProxyConfig().Name).Debug("Found app based on cookie domain")
|
||||
return longestMatch, host
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/akprox/static") {
|
||||
ps.HandleStatic(rw, r)
|
||||
@ -52,9 +98,8 @@ func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) {
|
||||
ps.HandlePing(rw, r)
|
||||
return
|
||||
}
|
||||
host := web.GetHost(r)
|
||||
a, ok := ps.apps[host]
|
||||
if !ok {
|
||||
a, host := ps.lookupApp(r)
|
||||
if a == nil {
|
||||
// If we only have one handler, host name switching doesn't matter
|
||||
if len(ps.apps) == 1 {
|
||||
ps.log.WithField("host", host).Trace("passing to single app mux")
|
||||
|
@ -46,7 +46,7 @@ func NewProxyServer(ac *ak.APIController, portOffset int) *ProxyServer {
|
||||
rootMux.Use(func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
h.ServeHTTP(rw, r)
|
||||
rw.Header().Set("Server", "authentik_proxy2")
|
||||
rw.Header().Set("X-Powered-By", "authentik_proxy2")
|
||||
})
|
||||
})
|
||||
|
||||
@ -70,11 +70,12 @@ func NewProxyServer(ac *ak.APIController, portOffset int) *ProxyServer {
|
||||
return s
|
||||
}
|
||||
|
||||
func (ps *ProxyServer) HandleHost(host string, rw http.ResponseWriter, r *http.Request) bool {
|
||||
if app, ok := ps.apps[host]; ok {
|
||||
if app.Mode() == api.PROXYMODE_PROXY {
|
||||
func (ps *ProxyServer) HandleHost(rw http.ResponseWriter, r *http.Request) bool {
|
||||
a, host := ps.lookupApp(r)
|
||||
if a != nil {
|
||||
if a.Mode() == api.PROXYMODE_PROXY {
|
||||
ps.log.WithField("host", host).Trace("routing to proxy outpost")
|
||||
app.ServeHTTP(rw, r)
|
||||
a.ServeHTTP(rw, r)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -90,7 +91,7 @@ func (ps *ProxyServer) TimerFlowCacheExpiry() {}
|
||||
func (ps *ProxyServer) GetCertificate(serverName string) *tls.Certificate {
|
||||
app, ok := ps.apps[serverName]
|
||||
if !ok {
|
||||
ps.log.WithField("server-name", serverName).Debug("app does not exist")
|
||||
ps.log.WithField("server-name", serverName).Debug("failed to get certificate for ServerName")
|
||||
return nil
|
||||
}
|
||||
if app.Cert == nil {
|
||||
@ -150,17 +151,14 @@ func (ps *ProxyServer) Start() error {
|
||||
wg.Add(3)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ps.log.Debug("Starting HTTP Server...")
|
||||
ps.ServeHTTP()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ps.log.Debug("Starting HTTPs Server...")
|
||||
ps.ServeHTTPS()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ps.log.Debug("Starting Metrics Server...")
|
||||
metrics.RunServer()
|
||||
}()
|
||||
return nil
|
||||
|
@ -99,14 +99,14 @@ func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
h.handler.ServeHTTP(responseLogger, req)
|
||||
duration := float64(time.Since(t)) / float64(time.Millisecond)
|
||||
h.afterHandler(h.logger.WithFields(log.Fields{
|
||||
"remote": req.RemoteAddr,
|
||||
"host": GetHost(req),
|
||||
"request_protocol": req.Proto,
|
||||
"runtime": fmt.Sprintf("%0.3f", duration),
|
||||
"method": req.Method,
|
||||
"size": responseLogger.Size(),
|
||||
"status": responseLogger.Status(),
|
||||
"upstream": responseLogger.upstream,
|
||||
"request_useragent": req.UserAgent(),
|
||||
"remote": req.RemoteAddr,
|
||||
"host": GetHost(req),
|
||||
"runtime": fmt.Sprintf("%0.3f", duration),
|
||||
"method": req.Method,
|
||||
"scheme": req.URL.Scheme,
|
||||
"size": responseLogger.Size(),
|
||||
"status": responseLogger.Status(),
|
||||
"upstream": responseLogger.upstream,
|
||||
"user_agent": req.UserAgent(),
|
||||
}), req).Info(url.RequestURI())
|
||||
}
|
||||
|
@ -47,10 +47,9 @@ func (ws *WebServer) configureProxy() {
|
||||
ws.proxyErrorHandler(rw, r, fmt.Errorf("authentik core not running yet"))
|
||||
return
|
||||
}
|
||||
host := web.GetHost(r)
|
||||
before := time.Now()
|
||||
if ws.ProxyServer != nil {
|
||||
if ws.ProxyServer.HandleHost(host, rw, r) {
|
||||
if ws.ProxyServer.HandleHost(rw, r) {
|
||||
Requests.With(prometheus.Labels{
|
||||
"dest": "embedded_outpost",
|
||||
}).Observe(float64(time.Since(before)))
|
||||
@ -60,7 +59,7 @@ func (ws *WebServer) configureProxy() {
|
||||
Requests.With(prometheus.Labels{
|
||||
"dest": "py",
|
||||
}).Observe(float64(time.Since(before)))
|
||||
ws.log.WithField("host", host).Trace("routing to application server")
|
||||
ws.log.WithField("host", web.GetHost(r)).Trace("routing to application server")
|
||||
rp.ServeHTTP(rw, r)
|
||||
})
|
||||
}
|
||||
@ -75,6 +74,6 @@ func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request
|
||||
}
|
||||
|
||||
func (ws *WebServer) proxyModifyResponse(r *http.Response) error {
|
||||
r.Header.Set("server", "authentik")
|
||||
r.Header.Set("X-Powered-By", "authentik")
|
||||
return nil
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Stage 1: Build
|
||||
FROM docker.io/golang:1.17.5-bullseye AS builder
|
||||
FROM docker.io/golang:1.17.6-bullseye AS builder
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
|
@ -61,11 +61,15 @@ MODE_FILE="/tmp/authentik-mode"
|
||||
if [[ "$1" == "server" ]]; then
|
||||
wait_for_db
|
||||
echo "server" > $MODE_FILE
|
||||
# We only set prometheus_multiproc_dir for serer, as with the worker it just fills up the disk
|
||||
export prometheus_multiproc_dir=/dev/shm/
|
||||
# We only set PROMETHEUS_MULTIPROC_DIR for serer, as with the worker it just fills up the disk
|
||||
# as one file is created per process
|
||||
#
|
||||
# Set to TMPDIR instead hardcoded path so this can be used outside docker too
|
||||
export PROMETHEUS_MULTIPROC_DIR=$TMPDIR
|
||||
python -m lifecycle.migrate
|
||||
/authentik-proxy
|
||||
elif [[ "$1" == "worker" ]]; then
|
||||
wait_for_db
|
||||
echo "worker" > $MODE_FILE
|
||||
check_if_root "celery -A authentik.root.celery worker -Ofair --max-tasks-per-child=1 --autoscale 3,1 -E -B -s /tmp/celerybeat-schedule -Q authentik,authentik_scheduled,authentik_events"
|
||||
elif [[ "$1" == "flower" ]]; then
|
||||
|
@ -1,13 +1,18 @@
|
||||
"""Gunicorn config"""
|
||||
import os
|
||||
import pwd
|
||||
from hashlib import sha512
|
||||
from multiprocessing import cpu_count
|
||||
|
||||
import structlog
|
||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||
|
||||
from authentik import get_full_version
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.lib.utils.reflection import get_env
|
||||
|
||||
bind = "127.0.0.1:8000"
|
||||
reload = True
|
||||
|
||||
try:
|
||||
pwd.getpwnam("authentik")
|
||||
@ -23,6 +28,9 @@ if os.path.exists("/dev/shm"): # nosec
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
|
||||
|
||||
max_requests = 1000
|
||||
max_requests_jitter = 50
|
||||
|
||||
logconfig_dict = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
@ -60,3 +68,38 @@ else:
|
||||
|
||||
workers = int(os.environ.get("WORKERS", default_workers))
|
||||
threads = int(os.environ.get("THREADS", 4))
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def worker_exit(server, worker):
|
||||
"""Remove pid dbs when worker is shutdown"""
|
||||
from prometheus_client import multiprocess
|
||||
|
||||
multiprocess.mark_process_dead(worker.pid)
|
||||
|
||||
|
||||
if not CONFIG.y_bool("disable_startup_analytics", False):
|
||||
env = get_env()
|
||||
should_send = env not in ["dev", "ci"]
|
||||
if should_send:
|
||||
try:
|
||||
get_http_session().post(
|
||||
"https://goauthentik.io/api/event",
|
||||
json={
|
||||
"domain": "authentik",
|
||||
"name": "pageview",
|
||||
"referrer": get_full_version(),
|
||||
"url": (
|
||||
f"http://localhost/{env}?utm_source={get_full_version()}&utm_medium={env}"
|
||||
),
|
||||
},
|
||||
headers={
|
||||
"User-Agent": sha512(str(CONFIG.y("secret_key")).encode("ascii")).hexdigest()[
|
||||
:16
|
||||
],
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout=5,
|
||||
)
|
||||
# pylint: disable=bare-except
|
||||
except: # nosec
|
||||
pass
|
||||
|
@ -2,6 +2,7 @@
|
||||
"""This file needs to be run from the root of the project to correctly
|
||||
import authentik. This is done by the dockerfile."""
|
||||
from json import dumps
|
||||
from sys import exit as sysexit
|
||||
from sys import stderr
|
||||
from time import sleep, time
|
||||
|
||||
@ -25,6 +26,16 @@ def j_print(event: str, log_level: str = "info", **kwargs):
|
||||
print(dumps(data), file=stderr)
|
||||
|
||||
|
||||
j_print("Starting authentik bootstrap")
|
||||
|
||||
# Sanity check, ensure SECRET_KEY is set before we even check for database connectivity
|
||||
if CONFIG.y("secret_key") is None or len(CONFIG.y("secret_key")) == 0:
|
||||
j_print("----------------------------------------------------------------------")
|
||||
j_print("Secret key missing, check https://goauthentik.io/docs/installation/.")
|
||||
j_print("----------------------------------------------------------------------")
|
||||
sysexit(1)
|
||||
|
||||
|
||||
while True:
|
||||
try:
|
||||
conn = connect(
|
||||
@ -38,7 +49,9 @@ while True:
|
||||
break
|
||||
except OperationalError as exc:
|
||||
sleep(1)
|
||||
j_print(f"PostgreSQL Connection failed, retrying... ({exc})")
|
||||
j_print(f"PostgreSQL connection failed, retrying... ({exc})")
|
||||
finally:
|
||||
j_print("PostgreSQL connection successful")
|
||||
|
||||
REDIS_PROTOCOL_PREFIX = "redis://"
|
||||
if CONFIG.y_bool("redis.tls", False):
|
||||
@ -56,3 +69,7 @@ while True:
|
||||
except RedisError as exc:
|
||||
sleep(1)
|
||||
j_print(f"Redis Connection failed, retrying... ({exc})", redis_url=REDIS_URL)
|
||||
finally:
|
||||
j_print("Redis Connection successful")
|
||||
|
||||
j_print("Finished authentik bootstrap")
|
||||
|
BIN
locale/es/LC_MESSAGES/django.mo
Normal file
BIN
locale/es/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
1831
locale/es/LC_MESSAGES/django.po
Normal file
1831
locale/es/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
738
poetry.lock
generated
738
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,7 @@ ENV NODE_ENV=production
|
||||
RUN cd /static && npm i && npm run build-proxy
|
||||
|
||||
# Stage 2: Build
|
||||
FROM docker.io/golang:1.17.5-bullseye AS builder
|
||||
FROM docker.io/golang:1.17.6-bullseye AS builder
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
|
@ -14,7 +14,7 @@ pythonPlatform = "Linux"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py39']
|
||||
target-version = ['py310']
|
||||
exclude = 'node_modules'
|
||||
|
||||
[tool.isort]
|
||||
@ -92,7 +92,7 @@ addopts = "-p no:celery --junitxml=unittest.xml"
|
||||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2021.12.5"
|
||||
version = "2022.1.4"
|
||||
description = ""
|
||||
authors = ["Jens Langhammer <jens.langhammer@beryju.org>"]
|
||||
|
||||
@ -135,7 +135,7 @@ pyjwt = "*"
|
||||
python = "^3.10"
|
||||
pyyaml = "*"
|
||||
requests-oauthlib = "*"
|
||||
sentry-sdk = { git = 'https://github.com/beryju/sentry-python.git', rev = '379aee28b15d3b87b381317746c4efd24b3d7bc3' }
|
||||
sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
|
48
schema.yml
48
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2021.12.5
|
||||
version: 2022.1.4
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@beryju.org
|
||||
@ -15270,6 +15270,14 @@ paths:
|
||||
operationId: stages_authenticator_webauthn_list
|
||||
description: AuthenticateWebAuthnStage Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: authenticator_attachment
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
enum:
|
||||
- cross-platform
|
||||
- platform
|
||||
- in: query
|
||||
name: configure_flow
|
||||
schema:
|
||||
@ -15297,6 +15305,14 @@ paths:
|
||||
description: Number of results to return per page.
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: resident_key_requirement
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- discouraged
|
||||
- preferred
|
||||
- required
|
||||
- name: search
|
||||
required: false
|
||||
in: query
|
||||
@ -19174,6 +19190,12 @@ components:
|
||||
If empty, user will not be able to configure this stage.
|
||||
user_verification:
|
||||
$ref: '#/components/schemas/UserVerificationEnum'
|
||||
authenticator_attachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
|
||||
nullable: true
|
||||
resident_key_requirement:
|
||||
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
|
||||
required:
|
||||
- component
|
||||
- meta_model_name
|
||||
@ -19200,6 +19222,12 @@ components:
|
||||
If empty, user will not be able to configure this stage.
|
||||
user_verification:
|
||||
$ref: '#/components/schemas/UserVerificationEnum'
|
||||
authenticator_attachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
|
||||
nullable: true
|
||||
resident_key_requirement:
|
||||
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
|
||||
required:
|
||||
- name
|
||||
AuthenticatedSession:
|
||||
@ -19288,6 +19316,11 @@ components:
|
||||
- last_used
|
||||
- user
|
||||
- user_agent
|
||||
AuthenticatorAttachmentEnum:
|
||||
enum:
|
||||
- platform
|
||||
- cross-platform
|
||||
type: string
|
||||
AuthenticatorDuoChallenge:
|
||||
type: object
|
||||
description: Duo Challenge
|
||||
@ -26519,6 +26552,12 @@ components:
|
||||
If empty, user will not be able to configure this stage.
|
||||
user_verification:
|
||||
$ref: '#/components/schemas/UserVerificationEnum'
|
||||
authenticator_attachment:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticatorAttachmentEnum'
|
||||
nullable: true
|
||||
resident_key_requirement:
|
||||
$ref: '#/components/schemas/ResidentKeyRequirementEnum'
|
||||
PatchedAuthenticatorDuoStageRequest:
|
||||
type: object
|
||||
description: AuthenticatorDuoStage Serializer
|
||||
@ -29376,6 +29415,12 @@ components:
|
||||
type: integer
|
||||
maximum: 2147483647
|
||||
minimum: -2147483648
|
||||
ResidentKeyRequirementEnum:
|
||||
enum:
|
||||
- discouraged
|
||||
- preferred
|
||||
- required
|
||||
type: string
|
||||
SAMLMetadata:
|
||||
type: object
|
||||
description: SAML Provider Metadata serializer
|
||||
@ -31331,4 +31376,3 @@ components:
|
||||
name: Authorization
|
||||
servers:
|
||||
- url: /api/v3/
|
||||
- url: /api/v2beta/
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user