Merge branch 'master' into version-2021.12

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	Dockerfile
This commit is contained in:
Jens Langhammer 2021-12-13 10:54:21 +01:00
commit 5914bbf173
60 changed files with 3819 additions and 4530 deletions

View file

@ -114,16 +114,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js environment
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Build web api client and web ui
- name: Get static files from docker image
run: |
export NODE_ENV=production
cd web
npm i
npm run build
docker pull ghcr.io/goauthentik/server:latest
container=$(docker container create ghcr.io/goauthentik/server:latest)
docker cp ${container}:web/ .
- name: Create a Sentry.io release
uses: getsentry/action-release@v1
if: ${{ github.event_name == 'release' }}

View file

@ -28,7 +28,7 @@ ENV NODE_ENV=production
RUN cd /work/web && npm i && npm run build
# Stage 4: Build go proxy
FROM docker.io/golang:1.17.4-bullseye AS builder
FROM docker.io/golang:1.17.5-bullseye AS builder
WORKDIR /work
@ -64,8 +64,8 @@ RUN apt-get update && \
apt-get clean && \
rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/ && \
adduser --system --no-create-home --uid 1000 --group --home /authentik authentik && \
mkdir /backups /certs && \
chown authentik:authentik /backups /certs
mkdir -p /backups /certs /media && \
chown authentik:authentik /backups /certs /media
COPY ./authentik/ /authentik
COPY ./pyproject.toml /

View file

@ -32,14 +32,15 @@ geoip2 = "*"
gunicorn = "*"
kubernetes = "==v19.15.0"
ldap3 = "*"
lxml = "*"
# 4.6.5 and later remove `lxml-version.h` which is required by xmlsec
lxml = "==4.6.4"
packaging = "*"
psycopg2-binary = "*"
pycryptodome = "*"
pyjwt = "*"
pyyaml = "*"
requests-oauthlib = "*"
sentry-sdk = "*"
sentry-sdk = { git = 'https://github.com/BeryJu/sentry-python.git', ref = 'bba7d80c05bc6845aa333ebbd87e3b76747ed355' }
service_identity = "*"
structlog = "*"
swagger-spec-validator = "*"
@ -50,6 +51,7 @@ uvicorn = {extras = ["standard"],version = "*"}
webauthn = "*"
xmlsec = "*"
flower = "*"
wsproto = "*"
[dev-packages]
bandit = "*"

94
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "6f7f70af39ff6e2d95dd62e225a52c5846957ebac5218aa2b49417fb7f9d8cf1"
"sha256": "f826456d2aa4f1379f61f3c1f76ddd2db7af3395a272d6b56fd5402c4aa3ce2f"
},
"pipfile-spec": 6,
"requires": {},
@ -109,11 +109,11 @@
},
"amqp": {
"hashes": [
"sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2",
"sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"
"sha256:4d9cb6b5d69183ba279e97382ff68a071864c25b561d206dab73499d3ed26d1c",
"sha256:d757b78fd7d3c6bb60e3ee811b68145583643747ed3ec253329f086aa3a72a5d"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.6"
"version": "==5.0.7"
},
"asgiref": {
"hashes": [
@ -169,19 +169,19 @@
},
"boto3": {
"hashes": [
"sha256:4cdaca9699a266936c04543700a5c6cdf52b563d33703812459cfe42c4c63ace",
"sha256:c8fbacb7d4e90a17cfe2a68e728c574e5b4a97ab66de417a44c373f7236366a1"
"sha256:76b3ee0d1dd860c9218bc864cd29f1ee986f6e1e75e8669725dd3c411039379e",
"sha256:c39cb6ed376ba1d4689ac8f6759a2b2d8a0b0424dbec0cd3af1558079bcf06e8"
],
"index": "pypi",
"version": "==1.20.22"
"version": "==1.20.23"
},
"botocore": {
"hashes": [
"sha256:6738e87baa48e4befc447dded787fcadb87a23efb3a5ee6bfe78177bc9630516",
"sha256:e66e4905dc048d5109df2cc6db55772b9111bfab8907557fa610f9241dca8752"
"sha256:640b62110aa6d1c25553eceafb5bcd89aedeb84b191598d1f6492ad24374d285",
"sha256:7459766c4594f3b8877e8013f93f0dc6c6486acbeb7d9c9ae488396529cc2e84"
],
"markers": "python_version >= '3.6'",
"version": "==1.23.22"
"version": "==1.23.23"
},
"cachetools": {
"hashes": [
@ -191,6 +191,14 @@
"markers": "python_version ~= '3.5'",
"version": "==4.2.4"
},
"cattrs": {
"hashes": [
"sha256:1ef33f089e0a494e8d1b487508356f055c865b1955b125c00c991a4358543c80",
"sha256:8eca49962b1bfc09c24d442aa55688be88efe5c24aeef89d3be135614b95c678"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==1.9.0"
},
"cbor2": {
"hashes": [
"sha256:0153635a78e62d70f26f5b3469cb8de822420eda69c996304fb3d0dc1a53d7f3",
@ -317,7 +325,7 @@
"sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
"sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
],
"markers": "python_version < '4' and python_full_version >= '3.6.2'",
"markers": "python_version < '4.0' and python_full_version >= '3.6.2'",
"version": "==0.3.0"
},
"click-plugins": {
@ -1150,34 +1158,6 @@
"index": "pypi",
"version": "==3.12.0"
},
"pydantic": {
"hashes": [
"sha256:021ea0e4133e8c824775a0cfe098677acf6fa5a3cbf9206a376eed3fc09302cd",
"sha256:05ddfd37c1720c392f4e0d43c484217b7521558302e7069ce8d318438d297739",
"sha256:05ef5246a7ffd2ce12a619cbb29f3307b7c4509307b1b49f456657b43529dc6f",
"sha256:10e5622224245941efc193ad1d159887872776df7a8fd592ed746aa25d071840",
"sha256:18b5ea242dd3e62dbf89b2b0ec9ba6c7b5abaf6af85b95a97b00279f65845a23",
"sha256:234a6c19f1c14e25e362cb05c68afb7f183eb931dd3cd4605eafff055ebbf287",
"sha256:244ad78eeb388a43b0c927e74d3af78008e944074b7d0f4f696ddd5b2af43c62",
"sha256:26464e57ccaafe72b7ad156fdaa4e9b9ef051f69e175dbbb463283000c05ab7b",
"sha256:41b542c0b3c42dc17da70554bc6f38cbc30d7066d2c2815a94499b5684582ecb",
"sha256:4a03cbbe743e9c7247ceae6f0d8898f7a64bb65800a45cbdc52d65e370570820",
"sha256:4be75bebf676a5f0f87937c6ddb061fa39cbea067240d98e298508c1bda6f3f3",
"sha256:54cd5121383f4a461ff7644c7ca20c0419d58052db70d8791eacbbe31528916b",
"sha256:589eb6cd6361e8ac341db97602eb7f354551482368a37f4fd086c0733548308e",
"sha256:8621559dcf5afacf0069ed194278f35c255dc1a1385c28b32dd6c110fd6531b3",
"sha256:8b223557f9510cf0bfd8b01316bf6dd281cf41826607eada99662f5e4963f316",
"sha256:99a9fc39470010c45c161a1dc584997f1feb13f689ecf645f59bb4ba623e586b",
"sha256:a7c6002203fe2c5a1b5cbb141bb85060cbff88c2d78eccbc72d97eb7022c43e4",
"sha256:a83db7205f60c6a86f2c44a61791d993dff4b73135df1973ecd9eed5ea0bda20",
"sha256:ac8eed4ca3bd3aadc58a13c2aa93cd8a884bcf21cb019f8cfecaae3b6ce3746e",
"sha256:e710876437bc07bd414ff453ac8ec63d219e7690128d925c6e82889d674bb505",
"sha256:ea5cb40a3b23b3265f6325727ddfc45141b08ed665458be8c6285e7b85bd73a1",
"sha256:fec866a0b59f372b7e776f2d7308511784dace622e0992a0b59ea3ccee0ae833"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==1.8.2"
},
"pyjwt": {
"hashes": [
"sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41",
@ -1191,6 +1171,7 @@
"sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3",
"sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==21.0.0"
},
"pyparsing": {
@ -1331,12 +1312,12 @@
"version": "==0.5.0"
},
"sentry-sdk": {
"git": "https://github.com/BeryJu/sentry-python.git",
"hashes": [
"sha256:0db297ab32e095705c20f742c3a5dac62fe15c4318681884053d0898e5abb2f6",
"sha256:789a11a87ca02491896e121efdd64e8fd93327b69e8f2f7d42f03e2569648e88"
],
"index": "pypi",
"version": "==1.5.0"
"ref": "bba7d80c05bc6845aa333ebbd87e3b76747ed355"
},
"service-identity": {
"hashes": [
@ -1348,11 +1329,11 @@
},
"setuptools": {
"hashes": [
"sha256:6d10741ff20b89cd8c6a536ee9dc90d3002dec0226c78fb98605bfb9ef8a7adf",
"sha256:d144f85102f999444d06f9c0e8c737fd0194f10f2f7e5fdb77573f6e2fa4fad0"
"sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373",
"sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e"
],
"markers": "python_version >= '3.6'",
"version": "==59.5.0"
"version": "==59.6.0"
},
"six": {
"hashes": [
@ -1540,11 +1521,11 @@
},
"webauthn": {
"hashes": [
"sha256:1a24696c67f8f3eb09267b10b3bfa4aade86bf08e2c41333d8c00f199c528b97",
"sha256:523e6e488a4d5e8c4fe183dde154a7cdfc9dad62e6e30e97c1d52e4a4ae6390e"
"sha256:681b71d803c085d0911e29f2e2bbb0e49feda1ecdaa3982bae3d63b3377efdda",
"sha256:ecae0f99a6dc1a6d53e72f4be323cf58b418c02fe66dffae752eedc1d2ee6a70"
],
"index": "pypi",
"version": "==1.1.0"
"version": "==1.2.0"
},
"websocket-client": {
"hashes": [
@ -1607,6 +1588,14 @@
],
"version": "==10.1"
},
"wsproto": {
"hashes": [
"sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38",
"sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f"
],
"index": "pypi",
"version": "==1.0.0"
},
"xmlsec": {
"hashes": [
"sha256:135724cdce60e6bbd072fca6f09a21f72e2cecc59eebb4eed7740c316ecabc7b",
@ -2029,7 +2018,7 @@
"sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7",
"sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"
],
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
"markers": "python_version < '4' and python_full_version >= '3.6.1'",
"version": "==5.10.1"
},
"lazy-object-proxy": {
@ -2165,6 +2154,7 @@
"sha256:5e2d8c5e46d0d865ae933bef5230090bdaf5506281e9eec60fa250ee80600cb3",
"sha256:8935bd4920ab9abfebb07c41a4f58296407ed77f04bd1a92914044b848ba1ed6"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==21.0.0"
},
"pyparsing": {
@ -2342,11 +2332,11 @@
},
"setuptools": {
"hashes": [
"sha256:6d10741ff20b89cd8c6a536ee9dc90d3002dec0226c78fb98605bfb9ef8a7adf",
"sha256:d144f85102f999444d06f9c0e8c737fd0194f10f2f7e5fdb77573f6e2fa4fad0"
"sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373",
"sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e"
],
"markers": "python_version >= '3.6'",
"version": "==59.5.0"
"version": "==59.6.0"
},
"six": {
"hashes": [
@ -2500,7 +2490,7 @@
"sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38",
"sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f"
],
"markers": "python_full_version >= '3.6.1'",
"index": "pypi",
"version": "==1.0.0"
},
"zipp": {

View file

@ -1,9 +1,11 @@
"""Groups API Viewset"""
from json import loads
from django.db.models.query import QuerySet
from django_filters.filters import ModelMultipleChoiceFilter
from django_filters.filters import CharFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet
from rest_framework.fields import CharField, JSONField
from rest_framework.serializers import ListSerializer, ModelSerializer
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
@ -62,6 +64,13 @@ class GroupSerializer(ModelSerializer):
class GroupFilter(FilterSet):
"""Filter for groups"""
attributes = CharFilter(
field_name="attributes",
lookup_expr="",
label="Attributes",
method="filter_attributes",
)
members_by_username = ModelMultipleChoiceFilter(
field_name="users__username",
to_field_name="username",
@ -72,10 +81,28 @@ class GroupFilter(FilterSet):
queryset=User.objects.all(),
)
# pylint: disable=unused-argument
def filter_attributes(self, queryset, name, value):
"""Filter attributes by query args"""
try:
value = loads(value)
except ValueError:
raise ValidationError(detail="filter: failed to parse JSON")
if not isinstance(value, dict):
raise ValidationError(detail="filter: value must be key:value mapping")
qs = {}
for key, _value in value.items():
qs[f"attributes__{key}"] = _value
try:
_ = len(queryset.filter(**qs))
return queryset.filter(**qs)
except ValueError:
return queryset
class Meta:
model = Group
fields = ["name", "is_superuser", "members_by_pk", "members_by_username"]
fields = ["name", "is_superuser", "members_by_pk", "attributes", "members_by_username"]
class GroupViewSet(UsedByMixin, ModelViewSet):

View file

@ -233,7 +233,11 @@ class UsersFilter(FilterSet):
qs = {}
for key, _value in value.items():
qs[f"attributes__{key}"] = _value
return queryset.filter(**qs)
try:
_ = len(queryset.filter(**qs))
return queryset.filter(**qs)
except ValueError:
return queryset
class Meta:
model = User

View file

@ -285,7 +285,7 @@ class FlowToken(Token):
return loads(b64decode(self._plan.encode())) # nosec
def __str__(self) -> str:
return f"Flow Token {super.__str__()}"
return f"Flow Token {super().__str__()}"
class Meta:

View file

@ -126,7 +126,7 @@ class FlowPlanner:
) -> FlowPlan:
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
and return ordered list"""
with Hub.current.start_span(op="flow.planner.plan") as span:
with Hub.current.start_span(op="flow.planner.plan", description=self.flow.slug) as span:
span: Span
span.set_data("flow", self.flow)
span.set_data("request", request)
@ -181,7 +181,8 @@ class FlowPlanner:
"""Build flow plan by checking each stage in their respective
order and checking the applied policies"""
with Hub.current.start_span(
op="flow.planner.build_plan"
op="flow.planner.build_plan",
description=self.flow.slug,
) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time():
span: Span
span.set_data("flow", self.flow)

View file

@ -19,6 +19,8 @@ from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer,
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from sentry_sdk import capture_exception
from sentry_sdk.api import set_tag
from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.models import USER_ATTRIBUTE_DEBUG
@ -126,6 +128,7 @@ class FlowExecutorView(APIView):
super().setup(request, flow_slug=flow_slug)
self.flow = get_object_or_404(Flow.objects.select_related(), slug=flow_slug)
self._logger = get_logger().bind(flow_slug=flow_slug)
set_tag("authentik.flow", self.flow.slug)
def handle_invalid_flow(self, exc: BaseException) -> HttpResponse:
"""When a flow is non-applicable check if user is on the correct domain"""
@ -156,74 +159,80 @@ class FlowExecutorView(APIView):
# pylint: disable=unused-argument, too-many-return-statements
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
get_params = QueryDict(request.GET.get("query", ""))
if QS_KEY_TOKEN in get_params:
plan = self._check_flow_token(get_params)
if plan:
self.request.session[SESSION_KEY_PLAN] = plan
# Early check if there's an active Plan for the current session
if SESSION_KEY_PLAN in self.request.session:
self.plan = self.request.session[SESSION_KEY_PLAN]
if self.plan.flow_pk != self.flow.pk.hex:
self._logger.warning(
"f(exec): Found existing plan for other flow, deleting plan",
)
# Existing plan is deleted from session and instance
self.plan = None
self.cancel()
self._logger.debug("f(exec): Continuing existing plan")
with Hub.current.start_span(
op="flow.executor.dispatch", description=self.flow.slug
) as span:
span.set_data("authentik Flow", self.flow.slug)
get_params = QueryDict(request.GET.get("query", ""))
if QS_KEY_TOKEN in get_params:
plan = self._check_flow_token(get_params)
if plan:
self.request.session[SESSION_KEY_PLAN] = plan
# Early check if there's an active Plan for the current session
if SESSION_KEY_PLAN in self.request.session:
self.plan = self.request.session[SESSION_KEY_PLAN]
if self.plan.flow_pk != self.flow.pk.hex:
self._logger.warning(
"f(exec): Found existing plan for other flow, deleting plan",
)
# Existing plan is deleted from session and instance
self.plan = None
self.cancel()
self._logger.debug("f(exec): Continuing existing plan")
# Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan:
request.session[SESSION_KEY_HISTORY] = []
self._logger.debug("f(exec): No active Plan found, initiating planner")
# Don't check session again as we've either already loaded the plan or we need to plan
if not self.plan:
request.session[SESSION_KEY_HISTORY] = []
self._logger.debug("f(exec): No active Plan found, initiating planner")
try:
self.plan = self._initiate_plan()
except FlowNonApplicableException as exc:
self._logger.warning("f(exec): Flow not applicable to current user", exc=exc)
return to_stage_response(self.request, self.handle_invalid_flow(exc))
except EmptyFlowException as exc:
self._logger.warning("f(exec): Flow is empty", exc=exc)
# To match behaviour with loading an empty flow plan from cache,
# we don't show an error message here, but rather call _flow_done()
return self._flow_done()
# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params
# We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
try:
self.plan = self._initiate_plan()
except FlowNonApplicableException as exc:
self._logger.warning("f(exec): Flow not applicable to current user", exc=exc)
return to_stage_response(self.request, self.handle_invalid_flow(exc))
except EmptyFlowException as exc:
self._logger.warning("f(exec): Flow is empty", exc=exc)
# To match behaviour with loading an empty flow plan from cache,
# we don't show an error message here, but rather call _flow_done()
# This is the first time we actually access any attribute on the selected plan
# if the cached plan is from an older version, it might have different attributes
# in which case we just delete the plan and invalidate everything
next_binding = self.plan.next(self.request)
except Exception as exc: # pylint: disable=broad-except
self._logger.warning(
"f(exec): found incompatible flow plan, invalidating run", exc=exc
)
keys = cache.keys("flow_*")
cache.delete_many(keys)
return self.stage_invalid()
if not next_binding:
self._logger.debug("f(exec): no more stages, flow is done.")
return self._flow_done()
# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params
# We don't save the Plan after getting the next stage
# as it hasn't been successfully passed yet
try:
# This is the first time we actually access any attribute on the selected plan
# if the cached plan is from an older version, it might have different attributes
# in which case we just delete the plan and invalidate everything
next_binding = self.plan.next(self.request)
except Exception as exc: # pylint: disable=broad-except
self._logger.warning("f(exec): found incompatible flow plan, invalidating run", exc=exc)
keys = cache.keys("flow_*")
cache.delete_many(keys)
return self.stage_invalid()
if not next_binding:
self._logger.debug("f(exec): no more stages, flow is done.")
return self._flow_done()
self.current_binding = next_binding
self.current_stage = next_binding.stage
self._logger.debug(
"f(exec): Current stage",
current_stage=self.current_stage,
flow_slug=self.flow.slug,
)
try:
stage_cls = self.current_stage.type
except NotImplementedError as exc:
self._logger.debug("Error getting stage type", exc=exc)
return self.stage_invalid()
self.current_stage_view = stage_cls(self)
self.current_stage_view.args = self.args
self.current_stage_view.kwargs = self.kwargs
self.current_stage_view.request = request
try:
return super().dispatch(request)
except InvalidStageError as exc:
return self.stage_invalid(str(exc))
self.current_binding = next_binding
self.current_stage = next_binding.stage
self._logger.debug(
"f(exec): Current stage",
current_stage=self.current_stage,
flow_slug=self.flow.slug,
)
try:
stage_cls = self.current_stage.type
except NotImplementedError as exc:
self._logger.debug("Error getting stage type", exc=exc)
return self.stage_invalid()
self.current_stage_view = stage_cls(self)
self.current_stage_view.args = self.args
self.current_stage_view.kwargs = self.kwargs
self.current_stage_view.request = request
try:
return super().dispatch(request)
except InvalidStageError as exc:
return self.stage_invalid(str(exc))
def handle_exception(self, exc: Exception) -> HttpResponse:
"""Handle exception in stage execution"""
@ -265,8 +274,15 @@ class FlowExecutorView(APIView):
stage=self.current_stage,
)
try:
stage_response = self.current_stage_view.get(request, *args, **kwargs)
return to_stage_response(request, stage_response)
with Hub.current.start_span(
op="flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
span.set_data("Method", "GET")
span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.get(request, *args, **kwargs)
return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except
return self.handle_exception(exc)
@ -302,8 +318,15 @@ class FlowExecutorView(APIView):
stage=self.current_stage,
)
try:
stage_response = self.current_stage_view.post(request, *args, **kwargs)
return to_stage_response(request, stage_response)
with Hub.current.start_span(
op="flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
span.set_data("Method", "POST")
span.set_data("authentik Stage", self.current_stage_view)
span.set_data("authentik Flow", self.flow.slug)
stage_response = self.current_stage_view.post(request, *args, **kwargs)
return to_stage_response(request, stage_response)
except Exception as exc: # pylint: disable=broad-except
return self.handle_exception(exc)

View file

@ -87,9 +87,7 @@ class FlowInspectorView(APIView):
@extend_schema(
responses={
200: FlowInspectionSerializer(),
400: OpenApiResponse(
description="No flow plan in session."
), # This error can be raised by the email stage
400: OpenApiResponse(description="No flow plan in session."),
},
request=OpenApiTypes.NONE,
operation_id="flows_inspector_get",
@ -106,7 +104,10 @@ class FlowInspectorView(APIView):
if SESSION_KEY_PLAN in request.session:
current_plan: FlowPlan = request.session[SESSION_KEY_PLAN]
else:
current_plan = request.session[SESSION_KEY_HISTORY][-1]
try:
current_plan = request.session[SESSION_KEY_HISTORY][-1]
except KeyError:
return Response(status=400)
is_completed = True
current_serializer = FlowInspectorPlanSerializer(
instance=current_plan, context={"request": request}

View file

@ -20,7 +20,6 @@ web:
listen: 0.0.0.0:9000
listen_tls: 0.0.0.0:9443
listen_metrics: 0.0.0.0:9300
load_local_files: false
outpost_port_offset: 0
redis:

View file

@ -4,6 +4,7 @@ from typing import Any, Optional
from django.http import HttpRequest
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__
@ -52,6 +53,12 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
fake_ip=fake_ip,
)
return None
# Update sentry scope to include correct IP
user = Hub.current.scope._user
if not user:
user = {}
user["ip_address"] = fake_ip
Hub.current.scope.set_user(user)
return fake_ip

View file

@ -9,7 +9,7 @@ from dacite import from_dict
from dacite.data import Data
from guardian.shortcuts import get_objects_for_user
from prometheus_client import Gauge
from structlog.stdlib import get_logger
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.channels import AuthJsonConsumer
from authentik.outposts.models import OUTPOST_HELLO_INTERVAL, Outpost, OutpostState
@ -23,8 +23,6 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge(
["outpost", "uid", "version"],
)
LOGGER = get_logger()
class WebsocketMessageInstruction(IntEnum):
"""Commands which can be triggered over Websocket"""
@ -51,6 +49,7 @@ class OutpostConsumer(AuthJsonConsumer):
"""Handler for Outposts that connect over websockets for health checks and live updates"""
outpost: Optional[Outpost] = None
logger: BoundLogger
last_uid: Optional[str] = None
@ -59,11 +58,20 @@ class OutpostConsumer(AuthJsonConsumer):
def connect(self):
super().connect()
uuid = self.scope["url_route"]["kwargs"]["pk"]
outpost = get_objects_for_user(self.user, "authentik_outposts.view_outpost").filter(pk=uuid)
if not outpost.exists():
outpost = (
get_objects_for_user(self.user, "authentik_outposts.view_outpost")
.filter(pk=uuid)
.first()
)
if not outpost:
raise DenyConnection()
self.accept()
self.outpost = outpost.first()
self.logger = get_logger().bind(outpost=outpost)
try:
self.accept()
except RuntimeError as exc:
self.logger.warning("runtime error during accept", exc=exc)
raise DenyConnection()
self.outpost = outpost
self.last_uid = self.channel_name
# pylint: disable=unused-argument
@ -78,9 +86,8 @@ class OutpostConsumer(AuthJsonConsumer):
uid=self.last_uid,
expected=self.outpost.config.kubernetes_replicas,
).dec()
LOGGER.debug(
self.logger.debug(
"removed outpost instance from cache",
outpost=self.outpost,
instance_uuid=self.last_uid,
)
@ -103,9 +110,8 @@ class OutpostConsumer(AuthJsonConsumer):
uid=self.last_uid,
expected=self.outpost.config.kubernetes_replicas,
).inc()
LOGGER.debug(
self.logger.debug(
"added outpost instance to cache",
outpost=self.outpost,
instance_uuid=self.last_uid,
)
self.first_msg = True

View file

@ -24,6 +24,8 @@ class DockerController(BaseController):
def __init__(self, outpost: Outpost, connection: DockerServiceConnection) -> None:
super().__init__(outpost, connection)
if outpost.managed == MANAGED_OUTPOST:
return
try:
self.client = connection.client()
except ServiceConnectionInvalid as exc:
@ -225,12 +227,14 @@ class DockerController(BaseController):
raise ControllerException(str(exc)) from exc
def down(self):
if self.outpost.managed != MANAGED_OUTPOST:
if self.outpost.managed == MANAGED_OUTPOST:
return
try:
container, _ = self._get_container()
if container.status == "running":
self.logger.info("Stopping container.")
container.kill()
self.logger.info("Removing container.")
container.remove(force=True)
except DockerException as exc:
raise ControllerException(str(exc)) from exc

View file

@ -401,6 +401,7 @@ class Outpost(ManagedModel):
user = users.first()
user.attributes[USER_ATTRIBUTE_SA] = True
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
user.name = f"Outpost {self.name} Service-Account"
user.save()
if should_create_user:
self.build_user_permissions(user)

View file

@ -105,9 +105,12 @@ def outpost_controller(
logs = []
if from_cache:
outpost: Outpost = cache.get(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
LOGGER.debug("Getting outpost from cache to delete")
else:
outpost: Outpost = Outpost.objects.filter(pk=outpost_pk).first()
LOGGER.debug("Getting outpost from DB")
if not outpost:
LOGGER.warning("No outpost")
return
self.set_uid(slugify(outpost.name))
try:

View file

@ -90,7 +90,8 @@ class PolicyEngine:
def build(self) -> "PolicyEngine":
"""Build wrapper which monitors performance"""
with Hub.current.start_span(
op="policy.engine.build"
op="policy.engine.build",
description=self.__pbm,
) as span, HIST_POLICIES_BUILD_TIME.labels(
object_name=self.__pbm,
object_type=f"{self.__pbm._meta.app_label}.{self.__pbm._meta.model_name}",

View file

@ -23,6 +23,6 @@ def invalidate_policy_cache(sender, instance, **_):
total += len(keys)
cache.delete_many(keys)
LOGGER.debug("Invalidating policy cache", policy=instance, keys=total)
# Also delete user application cache
keys = cache.keys(user_app_cache_key("*")) or []
cache.delete_many(keys)
# Also delete user application cache
keys = cache.keys(user_app_cache_key("*")) or []
cache.delete_many(keys)

View file

@ -37,7 +37,7 @@ def config_loggers(*args, **kwargs):
def after_task_publish_hook(sender=None, headers=None, body=None, **kwargs):
"""Log task_id after it was published"""
info = headers if "task" in headers else body
LOGGER.debug("Task published", task_id=info.get("id", ""), task_name=info.get("task", ""))
LOGGER.info("Task published", task_id=info.get("id", ""), task_name=info.get("task", ""))
# pylint: disable=unused-argument
@ -48,14 +48,14 @@ def task_prerun_hook(task_id: str, task, *args, **kwargs):
LOCAL.authentik_task = {
"request_id": request_id,
}
LOGGER.debug("Task started", task_id=task_id, task_name=task.__name__)
LOGGER.info("Task started", task_id=task_id, task_name=task.__name__)
# pylint: disable=unused-argument
@task_postrun.connect
def task_postrun_hook(task_id, task, *args, retval=None, state=None, **kwargs):
"""Log task_id on worker"""
LOGGER.debug("Task finished", task_id=task_id, task_name=task.__name__, state=state)
LOGGER.info("Task finished", task_id=task_id, task_name=task.__name__, state=state)
if not hasattr(LOCAL, "authentik_task"):
return
for key in list(LOCAL.authentik_task.keys()):

View file

@ -24,6 +24,7 @@ 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
@ -231,6 +232,7 @@ CACHES = {
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
}
}
DJANGO_REDIS_SCAN_ITERSIZE = 1000
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
@ -421,6 +423,7 @@ if _ERROR_REPORTING:
DjangoIntegration(transaction_style="function_name"),
CeleryIntegration(),
RedisIntegration(),
Boto3Integration(),
],
before_send=before_send,
release=f"authentik@{__version__}",

View file

@ -30,7 +30,7 @@ class PytestTestRunner: # pragma: no cover
CONFIG.y_set("authentik.geoip", "tests/GeoLite2-City-Test.mmdb")
CONFIG.y_set(
"outposts.container_image_base",
f"goauthentik.io/dev-%(type)s:{get_docker_tag()}",
f"ghcr.io/goauthentik/dev-%(type)s:{get_docker_tag()}",
)
@classmethod

View file

@ -53,9 +53,6 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
def validate_response(self, response: dict) -> dict:
"""Validate webauthn challenge response"""
# pylint: disable=no-name-in-module
from pydantic.error_wrappers import ValidationError as PydanticValidationError
challenge = self.request.session["challenge"]
try:
@ -65,7 +62,7 @@ class AuthenticatorWebAuthnChallengeResponse(ChallengeResponse):
expected_rp_id=get_rp_id(self.request),
expected_origin=get_origin(self.request),
)
except (InvalidRegistrationResponse, PydanticValidationError) as exc:
except InvalidRegistrationResponse as exc:
LOGGER.warning("registration failed", exc=exc)
raise ValidationError(f"Registration failed. Error: {exc}")

3
go.mod
View file

@ -27,9 +27,8 @@ require (
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/recws-org/recws v1.3.1
github.com/sirupsen/logrus v1.8.1
goauthentik.io/api v0.2021104.10
goauthentik.io/api v0.2021104.11
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558

7
go.sum
View file

@ -356,7 +356,6 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -481,8 +480,6 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
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/recws-org/recws v1.3.1 h1:vtRhYpgNPBs3iFyu/+zxBqNzLYgID7UPC5siThkvbs0=
github.com/recws-org/recws v1.3.1/go.mod h1:gRH/uJLMsO7lbcecAB1Im1Zc6eKxs93ftGR0R39QeYA=
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=
@ -561,8 +558,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.2021104.10 h1:5A2KLhwe5uSkPPiZDg8td3OLFKxcODSMqkyvRSavcUM=
goauthentik.io/api v0.2021104.10/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
goauthentik.io/api v0.2021104.11 h1:LqT0LM0e/RRrxPuo6Xl5uz3PCR5ytuE+YlNlfW9w0yU=
goauthentik.io/api v0.2021104.11/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=

View file

@ -16,9 +16,8 @@ func DefaultConfig() {
G = Config{
Debug: false,
Web: WebConfig{
Listen: "localhost:9000",
ListenTLS: "localhost:9443",
LoadLocalFiles: false,
Listen: "localhost:9000",
ListenTLS: "localhost:9443",
},
Paths: PathsConfig{
Media: "./media",

View file

@ -30,7 +30,6 @@ type WebConfig struct {
Listen string `yaml:"listen"`
ListenTLS string `yaml:"listen_tls"`
ListenMetrics string `yaml:"listen_metrics"`
LoadLocalFiles bool `yaml:"load_local_files" env:"AUTHENTIK_WEB_LOAD_LOCAL_FILES"`
DisableEmbeddedOutpost bool `yaml:"disable_embedded_outpost" env:"AUTHENTIK_WEB__DISABLE_EMBEDDED_OUTPOST"`
}

View file

@ -11,10 +11,10 @@ import (
"syscall"
"time"
"github.com/go-openapi/strfmt"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus"
"github.com/recws-org/recws"
"goauthentik.io/api"
"goauthentik.io/internal/constants"
@ -35,20 +35,25 @@ type APIController struct {
logger *log.Entry
reloadOffset time.Duration
lastWsReconnect time.Time
reloadOffset time.Duration
wsConn *websocket.Conn
lastWsReconnect time.Time
wsIsReconnecting bool
wsBackoffMultiplier int
wsConn *recws.RecConn
instanceUUID uuid.UUID
}
// NewAPIController initialise new API Controller instance from URL and API token
func NewAPIController(akURL url.URL, token string) *APIController {
rsp := sentry.StartSpan(context.TODO(), "authentik.outposts.init")
config := api.NewConfiguration()
config.Host = akURL.Host
config.Scheme = akURL.Scheme
config.HTTPClient = &http.Client{
Transport: NewUserAgentTransport(constants.OutpostUserAgent(), NewTracingTransport(context.TODO(), GetTLSTransport())),
Transport: NewUserAgentTransport(constants.OutpostUserAgent(), NewTracingTransport(rsp.Context(), GetTLSTransport())),
}
config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
@ -85,12 +90,16 @@ func NewAPIController(akURL url.URL, token string) *APIController {
token: token,
logger: log,
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
instanceUUID: uuid.New(),
Outpost: outpost,
reloadOffset: time.Duration(rand.Intn(10)) * time.Second,
instanceUUID: uuid.New(),
Outpost: outpost,
wsBackoffMultiplier: 1,
}
ac.logger.WithField("offset", ac.reloadOffset.String()).Debug("HA Reload offset")
ac.initWS(akURL, strfmt.UUID(outpost.Pk))
err = ac.initWS(akURL, outpost.Pk)
if err != nil {
go ac.reconnectWS()
}
ac.configureRefreshSignal()
return ac
}
@ -148,10 +157,6 @@ func (a *APIController) StartBackgorundTasks() error {
"version": constants.VERSION,
"build": constants.BUILD(),
}).Set(1)
go func() {
a.logger.Debug("Starting WS re-connector...")
a.startWSReConnector()
}()
go func() {
a.logger.Debug("Starting WS Handler...")
a.startWSHandler()

View file

@ -6,18 +6,17 @@ import (
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/go-openapi/strfmt"
"github.com/gorilla/websocket"
"github.com/prometheus/client_golang/prometheus"
"github.com/recws-org/recws"
"goauthentik.io/internal/constants"
)
func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
pathTemplate := "%s://%s/ws/outpost/%s/"
func (ac *APIController) initWS(akURL url.URL, outpostUUID string) error {
pathTemplate := "%s://%s/ws/outpost/%s/?%s"
scheme := strings.ReplaceAll(akURL.Scheme, "http", "ws")
authHeader := fmt.Sprintf("Bearer %s", ac.token)
@ -32,15 +31,19 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
value = "false"
}
ws := &recws.RecConn{
NonVerbose: true,
dialer := websocket.Dialer{
Proxy: http.ProxyFromEnvironment,
HandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: strings.ToLower(value) == "true",
},
}
ws.Dial(fmt.Sprintf(pathTemplate, scheme, akURL.Host, outpostUUID.String()), header)
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithField("outpost", outpostUUID.String()).Debug("Connecting to authentik")
ws, _, err := dialer.Dial(fmt.Sprintf(pathTemplate, scheme, akURL.Host, outpostUUID, akURL.Query().Encode()), header)
if err != nil {
ac.logger.WithError(err).Warning("failed to connect websocket")
return err
}
ac.wsConn = ws
// Send hello message with our version
@ -52,11 +55,14 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
"uuid": ac.instanceUUID.String(),
},
}
err := ws.WriteJSON(msg)
err = ws.WriteJSON(msg)
if err != nil {
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithError(err).Warning("Failed to hello to authentik")
return err
}
ac.lastWsReconnect = time.Now()
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithField("outpost", outpostUUID).Debug("Successfully connected websocket")
return nil
}
// Shutdown Gracefully stops all workers, disconnects from websocket
@ -65,21 +71,43 @@ func (ac *APIController) Shutdown() {
// waiting (with timeout) for the server to close the connection.
err := ac.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
ac.logger.Println("write close:", err)
ac.logger.WithError(err).Warning("failed to write close message")
return
}
err = ac.wsConn.Close()
if err != nil {
ac.logger.WithError(err).Warning("failed to close websocket")
}
ac.logger.Info("finished shutdown")
}
func (ac *APIController) startWSReConnector() {
func (ac *APIController) reconnectWS() {
if ac.wsIsReconnecting {
return
}
ac.wsIsReconnecting = true
u := url.URL{
Host: ac.Client.GetConfig().Host,
Scheme: ac.Client.GetConfig().Scheme,
}
attempt := 1
for {
time.Sleep(time.Second * 5)
if ac.wsConn.IsConnected() {
continue
}
if time.Since(ac.lastWsReconnect).Seconds() > 30 {
ac.wsConn.CloseAndReconnect()
ac.logger.Info("Reconnecting websocket")
ac.lastWsReconnect = time.Now()
q := u.Query()
q.Set("attempt", strconv.Itoa(attempt))
u.RawQuery = q.Encode()
err := ac.initWS(u, ac.Outpost.Pk)
attempt += 1
if err != nil {
ac.logger.Infof("waiting %d seconds to reconnect", ac.wsBackoffMultiplier)
time.Sleep(time.Duration(ac.wsBackoffMultiplier) * time.Second)
ac.wsBackoffMultiplier = ac.wsBackoffMultiplier * 2
// Limit to 300 seconds (5m)
if ac.wsBackoffMultiplier >= 300 {
ac.wsBackoffMultiplier = 300
}
} else {
ac.wsIsReconnecting = false
return
}
}
}
@ -88,6 +116,11 @@ func (ac *APIController) startWSHandler() {
logger := ac.logger.WithField("loop", "ws-handler")
for {
var wsMsg websocketMessage
if ac.wsConn == nil {
go ac.reconnectWS()
time.Sleep(time.Second * 5)
continue
}
err := ac.wsConn.ReadJSON(&wsMsg)
if err != nil {
ConnectionStatus.With(prometheus.Labels{
@ -96,6 +129,7 @@ func (ac *APIController) startWSHandler() {
"uuid": ac.instanceUUID.String(),
}).Set(0)
logger.WithError(err).Warning("ws read error")
go ac.reconnectWS()
time.Sleep(time.Second * 5)
continue
}
@ -126,9 +160,6 @@ func (ac *APIController) startWSHandler() {
func (ac *APIController) startWSHealth() {
ticker := time.NewTicker(time.Second * 10)
for ; true; <-ticker.C {
if !ac.wsConn.IsConnected() {
continue
}
aliveMsg := websocketMessage{
Instruction: WebsocketInstructionHello,
Args: map[string]interface{}{
@ -137,10 +168,16 @@ func (ac *APIController) startWSHealth() {
"uuid": ac.instanceUUID.String(),
},
}
if ac.wsConn == nil {
go ac.reconnectWS()
time.Sleep(time.Second * 5)
continue
}
err := ac.wsConn.WriteJSON(aliveMsg)
ac.logger.WithField("loop", "ws-health").Trace("hello'd")
if err != nil {
ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error")
go ac.reconnectWS()
time.Sleep(time.Second * 5)
continue
} else {

View file

@ -73,7 +73,7 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
config.Scheme = refConfig.Scheme
config.HTTPClient = &http.Client{
Jar: jar,
Transport: ak.NewUserAgentTransport(constants.OutpostUserAgent(), ak.NewTracingTransport(ctx, ak.GetTLSTransport())),
Transport: ak.NewUserAgentTransport(constants.OutpostUserAgent(), ak.NewTracingTransport(rsp.Context(), ak.GetTLSTransport())),
}
token := strings.Split(refConfig.DefaultHeader["Authorization"], " ")[1]
config.AddDefaultHeader(HeaderAuthentikOutpostToken, token)

View file

@ -52,7 +52,7 @@ func (ls *LDAPServer) StartLDAPServer() error {
ln, err := net.Listen("tcp", listen)
if err != nil {
ls.log.WithField("listen", listen).WithError(err).Fatalf("FATAL: listen failed")
ls.log.WithField("listen", listen).WithError(err).Fatalf("listen failed")
}
proxyListener := &proxyproto.Listener{Listener: ln}
defer proxyListener.Close()

View file

@ -37,7 +37,7 @@ func (ls *LDAPServer) StartLDAPTLSServer() error {
ln, err := net.Listen("tcp", listen)
if err != nil {
ls.log.WithField("listen", listen).WithError(err).Fatalf("FATAL: listen failed")
ls.log.WithField("listen", listen).WithError(err).Fatalf("listen failed")
}
proxyListener := &proxyproto.Listener{Listener: ln}

View file

@ -3,6 +3,7 @@ package ldap
import (
"errors"
"net"
"strings"
"github.com/getsentry/sentry-go"
goldap "github.com/go-ldap/ldap/v3"
@ -41,13 +42,13 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
if searchReq.BaseDN == "" {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil
}
bd, err := goldap.ParseDN(searchReq.BaseDN)
bd, err := goldap.ParseDN(strings.ToLower(searchReq.BaseDN))
if err != nil {
req.Log().WithError(err).Info("failed to parse basedn")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, errors.New("invalid DN")
}
for _, provider := range ls.providers {
providerBase, _ := goldap.ParseDN(provider.BaseDN)
providerBase, _ := goldap.ParseDN(strings.ToLower(provider.BaseDN))
if providerBase.AncestorOf(bd) || providerBase.Equal(bd) {
return provider.searcher.Search(req)
}

View file

@ -13,4 +13,6 @@ type Claims struct {
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
Groups []string `json:"groups"`
RawToken string
}

View file

@ -25,6 +25,7 @@ func (a *Application) addHeaders(headers http.Header, c *Claims) {
headers.Set("X-authentik-email", c.Email)
headers.Set("X-authentik-name", c.Name)
headers.Set("X-authentik-uid", c.Sub)
headers.Set("X-authentik-jwt", c.RawToken)
// System headers
headers.Set("X-authentik-meta-jwks", a.proxyConfig.OidcConfiguration.JwksUri)

View file

@ -8,6 +8,7 @@ import (
"net/url"
"time"
"github.com/getsentry/sentry-go"
"github.com/prometheus/client_golang/prometheus"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/proxyv2/metrics"
@ -28,7 +29,8 @@ func (a *Application) configureProxy() error {
return err
}
rp := &httputil.ReverseProxy{Director: a.proxyModifyRequest(u)}
rp.Transport = ak.NewTracingTransport(context.TODO(), a.getUpstreamTransport())
rsp := sentry.StartSpan(context.TODO(), "authentik.outposts.proxy.application_transport")
rp.Transport = ak.NewTracingTransport(rsp.Context(), a.getUpstreamTransport())
rp.ErrorHandler = a.newProxyErrorHandler(templates.GetTemplates())
rp.ModifyResponse = a.proxyModifyResponse
a.mux.PathPrefix("/").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {

View file

@ -45,5 +45,6 @@ func (a *Application) redeemCallback(r *http.Request, shouldState string) (*Clai
if err := idToken.Claims(&claims); err != nil {
return nil, err
}
claims.RawToken = rawIDToken
return claims, nil
}

View file

@ -2,6 +2,7 @@ package application
import (
"fmt"
"os"
"strconv"
"github.com/gorilla/sessions"
@ -26,14 +27,14 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store {
a.log.Info("using redis session backend")
store = rs
} else {
cs := sessions.NewCookieStore([]byte(*p.CookieSecret))
cs := sessions.NewFilesystemStore(os.TempDir(), []byte(*p.CookieSecret))
cs.Options.Domain = *p.CookieDomain
if p.TokenValidity.IsSet() {
t := p.TokenValidity.Get()
// Add one to the validity to ensure we don't have a session with indefinite length
cs.Options.MaxAge = int(*t) + 1
}
a.log.Info("using cookie session backend")
a.log.Info("using filesystem session backend")
store = cs
}
return store

View file

@ -103,7 +103,7 @@ func (ps *ProxyServer) ServeHTTP() {
listenAddress := fmt.Sprintf(ps.Listen, 9000+ps.PortOffset)
listener, err := net.Listen("tcp", listenAddress)
if err != nil {
ps.log.Fatalf("FATAL: listen (%s) failed - %s", listenAddress, err)
ps.log.WithField("listen", listenAddress).WithError(err).Fatalf("listen failed")
}
proxyListener := &proxyproto.Listener{Listener: listener}
defer proxyListener.Close()

View file

@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"github.com/getsentry/sentry-go"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/proxyv2/application"
@ -20,9 +21,10 @@ func (ps *ProxyServer) Refresh() error {
}
apps := make(map[string]*application.Application)
for _, provider := range providers.Results {
rsp := sentry.StartSpan(context.Background(), "authentik.outposts.proxy.application_ss")
ua := fmt.Sprintf(" (provider=%s)", provider.Name)
hc := &http.Client{
Transport: ak.NewUserAgentTransport(constants.OutpostUserAgent()+ua, ak.NewTracingTransport(context.TODO(), ak.GetTLSTransport())),
Transport: ak.NewUserAgentTransport(constants.OutpostUserAgent()+ua, ak.NewTracingTransport(rsp.Context(), ak.GetTLSTransport())),
}
a, err := application.NewApplication(provider, hc, ps.cryptoStore, ps.akAPI)
if err != nil {

View file

@ -1,5 +1,5 @@
# Stage 1: Build
FROM docker.io/golang:1.17.4-bullseye AS builder
FROM docker.io/golang:1.17.5-bullseye AS builder
WORKDIR /go/src/goauthentik.io

View file

@ -16,7 +16,7 @@ try:
except KeyError:
pass
worker_class = "uvicorn.workers.UvicornWorker"
worker_class = "lifecycle.worker.DjangoUvicornWorker"
# Docker containers don't have /tmp as tmpfs
if os.path.exists("/dev/shm"): # nosec
worker_tmp_dir = "/dev/shm" # nosec

13
lifecycle/worker.py Normal file
View file

@ -0,0 +1,13 @@
"""Uvicorn worker"""
from uvicorn.workers import UvicornWorker
class DjangoUvicornWorker(UvicornWorker):
"""Custom configured Uvicorn Worker without lifespan"""
CONFIG_KWARGS = {
"loop": "uvloop",
"http": "httptools",
"lifespan": "off",
"ws": "wsproto",
}

View file

@ -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.4-bullseye AS builder
FROM docker.io/golang:1.17.5-bullseye AS builder
WORKDIR /go/src/goauthentik.io

View file

@ -2058,6 +2058,11 @@ paths:
operationId: core_groups_list
description: Group Viewset
parameters:
- in: query
name: attributes
schema:
type: string
description: Attributes
- in: query
name: is_superuser
schema:
@ -11846,6 +11851,7 @@ paths:
- ml
- mn
- mr
- ms
- my
- nb
- ne

View file

@ -34,7 +34,7 @@ class TestProviderLDAP(SeleniumTestCase):
"""Start ldap container based on outpost created"""
client: DockerClient = from_env()
container = client.containers.run(
image=self.get_container_image("goauthentik.io/dev-ldap"),
image=self.get_container_image("ghcr.io/goauthentik/dev-ldap"),
detach=True,
network_mode="host",
auto_remove=True,
@ -183,7 +183,7 @@ class TestProviderLDAP(SeleniumTestCase):
User.objects.filter(username="akadmin").delete()
_connection.search(
"ou=users,dc=ldap,dc=goauthentik,dc=io",
"ou=Users,DC=ldaP,dc=goauthentik,dc=io",
"(objectClass=user)",
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
@ -203,8 +203,8 @@ class TestProviderLDAP(SeleniumTestCase):
"cn": [o_user.username],
"sAMAccountName": [o_user.username],
"uid": [o_user.uid],
"name": [""],
"displayName": [""],
"name": [o_user.name],
"displayName": [o_user.name],
"mail": [""],
"objectClass": [
"user",
@ -230,8 +230,8 @@ class TestProviderLDAP(SeleniumTestCase):
"cn": [embedded_account.username],
"sAMAccountName": [embedded_account.username],
"uid": [embedded_account.uid],
"name": [""],
"displayName": [""],
"name": [embedded_account.name],
"displayName": [embedded_account.name],
"mail": [""],
"objectClass": [
"user",

View file

@ -42,7 +42,7 @@ class TestProviderProxy(SeleniumTestCase):
"""Start proxy container based on outpost created"""
client: DockerClient = from_env()
container = client.containers.run(
image=self.get_container_image("goauthentik.io/dev-proxy"),
image=self.get_container_image("ghcr.io/goauthentik/dev-proxy"),
detach=True,
network_mode="host",
environment={
@ -158,8 +158,7 @@ class TestProviderProxyConnect(ChannelsLiveServerTestCase):
sleep(0.5)
state = outpost.state
self.assertEqual(len(state), 1)
self.assertEqual(state[0].version, __version__)
self.assertTrue(len(state) >= 1)
# Make sure to delete the outpost to remove the container
outpost.delete()

View file

@ -93,10 +93,10 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def output_container_logs(self, container: Optional[Container] = None):
"""Output the container logs to our STDOUT"""
_container = container or self.container
self.logger.debug("--------container logs", container=_container.image.tags[0])
print(f"--------container logs {_container.image.tags[0]}")
for log in _container.logs().decode().split("\n"):
self.logger.debug(log, container=_container.image.tags[0])
self.logger.debug("--------end container logs", container=_container.image.tags[0])
print(log)
print(f"--------end container logs {_container.image.tags[0]}")
def get_container_specs(self) -> Optional[dict[str, Any]]:
"""Optionally get container specs which will launched on setup, wait for the container to

View file

@ -116,5 +116,5 @@ class OutpostDockerTests(ChannelsLiveServerTestCase):
self.assertEqual(compose["version"], "3.5")
self.assertEqual(
compose["services"]["authentik_proxy"]["image"],
f"goauthentik.io/dev-proxy:{get_docker_tag()}",
f"ghcr.io/goauthentik/dev-proxy:{get_docker_tag()}",
)

View file

@ -116,5 +116,5 @@ class TestProxyDocker(ChannelsLiveServerTestCase):
self.assertEqual(compose["version"], "3.5")
self.assertEqual(
compose["services"]["authentik_proxy"]["image"],
f"goauthentik.io/dev-proxy:{get_docker_tag()}",
f"ghcr.io/goauthentik/dev-proxy:{get_docker_tag()}",
)

211
web/package-lock.json generated
View file

@ -15,7 +15,7 @@
"@babel/preset-env": "^7.16.4",
"@babel/preset-typescript": "^7.16.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@goauthentik/api": "^2021.10.4-1638781871",
"@goauthentik/api": "^2021.10.4-1639076050",
"@jackfranklin/rollup-plugin-markdown": "^0.3.0",
"@lingui/cli": "^3.13.0",
"@lingui/core": "^3.13.0",
@ -29,8 +29,8 @@
"@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-replace": "^3.0.0",
"@rollup/plugin-typescript": "^8.3.0",
"@sentry/browser": "^6.16.0",
"@sentry/tracing": "^6.16.0",
"@sentry/browser": "^6.16.1",
"@sentry/tracing": "^6.16.1",
"@squoosh/cli": "^0.7.2",
"@trivago/prettier-plugin-sort-imports": "^3.1.1",
"@types/chart.js": "^2.9.34",
@ -44,7 +44,7 @@
"chart.js": "^3.6.2",
"chartjs-adapter-moment": "^1.0.0",
"codemirror": "^5.64.0",
"construct-style-sheets-polyfill": "^2.4.16",
"construct-style-sheets-polyfill": "^3.0.5",
"eslint": "^8.4.1",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.4",
@ -55,7 +55,7 @@
"moment": "^2.29.1",
"prettier": "^2.5.1",
"rapidoc": "^9.1.3",
"rollup": "^2.60.2",
"rollup": "^2.61.1",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-cssimport": "^1.0.2",
"rollup-plugin-minify-html-literals": "^1.2.6",
@ -63,7 +63,7 @@
"rollup-plugin-terser": "^7.0.2",
"ts-lit-plugin": "^1.2.1",
"tslib": "^2.3.1",
"typescript": "^4.5.2",
"typescript": "^4.5.3",
"webcomponent-qr-code": "^1.0.5",
"yaml": "^1.10.2"
}
@ -1708,9 +1708,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2021.10.4-1638781871",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638781871.tgz",
"integrity": "sha512-QI/pqVVCt/W+iXZGXXipAYX39CBpuUEPDxFKPCiQyU+G0+rLYt1T1umBjTIlIEXRKC8xKSInLkDS/GEz32kXNA=="
"version": "2021.10.4-1639076050",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1639076050.tgz",
"integrity": "sha512-Ud8hYHeaz9BCpJwYlOGce41MqhHeHiGVh7nlsWdUIOGCyrraJD3lFaPBgnG1l0M2RpxUeHz2owSUetS23X164g=="
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.9.2",
@ -2370,13 +2370,13 @@
}
},
"node_modules/@sentry/browser": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.16.0.tgz",
"integrity": "sha512-rpFrS/DPKH9NAWfEhrgpVmqJtfUIGvl9y6KQv0QsNv7X0ZISNtsoHIUe2jVrbjysjWXrJCryCxcSxNgqsa4Www==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.16.1.tgz",
"integrity": "sha512-F2I5RL7RTLQF9CccMrqt73GRdK3FdqaChED3RulGQX5lH6U3exHGFxwyZxSrY4x6FedfBFYlfXWWCJXpLnFkow==",
"dependencies": {
"@sentry/core": "6.16.0",
"@sentry/types": "6.16.0",
"@sentry/utils": "6.16.0",
"@sentry/core": "6.16.1",
"@sentry/types": "6.16.1",
"@sentry/utils": "6.16.1",
"tslib": "^1.9.3"
},
"engines": {
@ -2389,14 +2389,14 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/core": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.16.0.tgz",
"integrity": "sha512-XqIlMjefuJmwQSAzv9J1PtV6+sXiz1dgBbtRr6e+QGIYZ+BDkuyDQv/HsGPfxxMHxgJBxBzi71FFLjEJsF6CBg==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.16.1.tgz",
"integrity": "sha512-UFI0264CPUc5cR1zJH+S2UPOANpm6dLJOnsvnIGTjsrwzR0h8Hdl6rC2R/GPq+WNbnipo9hkiIwDlqbqvIU5vw==",
"dependencies": {
"@sentry/hub": "6.16.0",
"@sentry/minimal": "6.16.0",
"@sentry/types": "6.16.0",
"@sentry/utils": "6.16.0",
"@sentry/hub": "6.16.1",
"@sentry/minimal": "6.16.1",
"@sentry/types": "6.16.1",
"@sentry/utils": "6.16.1",
"tslib": "^1.9.3"
},
"engines": {
@ -2409,12 +2409,12 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/hub": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.16.0.tgz",
"integrity": "sha512-NBkcgGjnYsoXyIJwi2TGCxGnxbDJc/t++0ukFoBRy6RL/pw2YnryCu8PWNFsDkZdlb1zt5SIC6Kui+q1ViNS/A==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.16.1.tgz",
"integrity": "sha512-4PGtg6AfpqMkreTpL7ymDeQ/U1uXv03bKUuFdtsSTn/FRf9TLS4JB0KuTZCxfp1IRgAA+iFg6B784dDkT8R9eg==",
"dependencies": {
"@sentry/types": "6.16.0",
"@sentry/utils": "6.16.0",
"@sentry/types": "6.16.1",
"@sentry/utils": "6.16.1",
"tslib": "^1.9.3"
},
"engines": {
@ -2427,12 +2427,12 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/minimal": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.16.0.tgz",
"integrity": "sha512-9/h0J9BDDY5W/dKILGEq3ewECspNoxcXuly/WOWQdt2SQpIcoh8l/dF8iTXle+icndin0EiMEyHOzaCPWG24oQ==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.16.1.tgz",
"integrity": "sha512-dq+mI1EQIvUM+zJtGCVgH3/B3Sbx4hKlGf2Usovm9KoqWYA+QpfVBholYDe/H2RXgO7LFEefDLvOdHDkqeJoyA==",
"dependencies": {
"@sentry/hub": "6.16.0",
"@sentry/types": "6.16.0",
"@sentry/hub": "6.16.1",
"@sentry/types": "6.16.1",
"tslib": "^1.9.3"
},
"engines": {
@ -2445,14 +2445,14 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/tracing": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.16.0.tgz",
"integrity": "sha512-vTTjGnLc9fa3jM0RKkEgOLW23CiPb1Kh6bkHbUw68d3DVz6o0Tj2SqzW+Y+LaIwlFjhrozf+YV/KS9vj4BhHTw==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.16.1.tgz",
"integrity": "sha512-MPSbqXX59P+OEeST+U2V/8Hu/8QjpTUxTNeNyTHWIbbchdcMMjDbXTS3etCgajZR6Ro+DHElOz5cdSxH6IBGlA==",
"dependencies": {
"@sentry/hub": "6.16.0",
"@sentry/minimal": "6.16.0",
"@sentry/types": "6.16.0",
"@sentry/utils": "6.16.0",
"@sentry/hub": "6.16.1",
"@sentry/minimal": "6.16.1",
"@sentry/types": "6.16.1",
"@sentry/utils": "6.16.1",
"tslib": "^1.9.3"
},
"engines": {
@ -2465,19 +2465,19 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/@sentry/types": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.16.0.tgz",
"integrity": "sha512-ZgIyLYlQS4SPi+d68XD8n9FzoObrNQLWxBuMYMnG3uJSuFeYAJrVYkDRtW4OW0D3awuajYGiHJZC2O5qTRGflA==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.16.1.tgz",
"integrity": "sha512-Wh354g30UsJ5kYJbercektGX4ZMc9MHU++1NjeN2bTMnbofEcpUDWIiKeulZEY65IC1iU+1zRQQgtYO+/hgCUQ==",
"engines": {
"node": ">=6"
}
},
"node_modules/@sentry/utils": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.16.0.tgz",
"integrity": "sha512-FJl1AyUVAIzxfEXufWsgX7KxIvOrQawxhAhLXO4vU5xrFrJOteicxAIFJO+GG0QDELgr9siP0Qgeb8LoINWcrw==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.16.1.tgz",
"integrity": "sha512-7ngq/i4R8JZitJo9Sl8PDnjSbDehOxgr1vsoMmerIsyRZ651C/8B+jVkMhaAPgSdyJ0AlE3O7DKKTP1FXFw9qw==",
"dependencies": {
"@sentry/types": "6.16.0",
"@sentry/types": "6.16.1",
"tslib": "^1.9.3"
},
"engines": {
@ -3750,9 +3750,12 @@
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"node_modules/construct-style-sheets-polyfill": {
"version": "2.4.17",
"resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-2.4.17.tgz",
"integrity": "sha512-rKtZGWWAVFE6HgdBuuui1emic/t8aAKQbePQ7Je6ird8nZYSd3mqqBX9IvFn2CMtnbh7mQTk/vxc9mfaLl7cGQ=="
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-3.0.5.tgz",
"integrity": "sha512-prLKSx9gYwtmqWtq+pZxppU1SaH9o4ug7JIc0I/1zMV2bFE3GvRtQaMTIpotlhw33XjtC7rGQFOZJsOFnlAAhQ==",
"engines": {
"npm": ">=7"
}
},
"node_modules/convert-source-map": {
"version": "1.8.0",
@ -7188,9 +7191,9 @@
}
},
"node_modules/rollup": {
"version": "2.60.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.2.tgz",
"integrity": "sha512-1Bgjpq61sPjgoZzuiDSGvbI1tD91giZABgjCQBKM5aYLnzjq52GoDuWVwT/cm/MCxCMPU8gqQvkj8doQ5C8Oqw==",
"version": "2.61.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.61.1.tgz",
"integrity": "sha512-BbTXlEvB8d+XFbK/7E5doIcRtxWPRiqr0eb5vQ0+2paMM04Ye4PZY5nHOQef2ix24l/L0SpLd5hwcH15QHPdvA==",
"bin": {
"rollup": "dist/bin/rollup"
},
@ -8328,9 +8331,9 @@
}
},
"node_modules/typescript": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.3.tgz",
"integrity": "sha512-eVYaEHALSt+s9LbvgEv4Ef+Tdq7hBiIZgii12xXJnukryt3pMgJf6aKhoCZ3FWQsu6sydEnkg11fYXLzhLBjeQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -9895,9 +9898,9 @@
"integrity": "sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg=="
},
"@goauthentik/api": {
"version": "2021.10.4-1638781871",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1638781871.tgz",
"integrity": "sha512-QI/pqVVCt/W+iXZGXXipAYX39CBpuUEPDxFKPCiQyU+G0+rLYt1T1umBjTIlIEXRKC8xKSInLkDS/GEz32kXNA=="
"version": "2021.10.4-1639076050",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2021.10.4-1639076050.tgz",
"integrity": "sha512-Ud8hYHeaz9BCpJwYlOGce41MqhHeHiGVh7nlsWdUIOGCyrraJD3lFaPBgnG1l0M2RpxUeHz2owSUetS23X164g=="
},
"@humanwhocodes/config-array": {
"version": "0.9.2",
@ -10415,13 +10418,13 @@
}
},
"@sentry/browser": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.16.0.tgz",
"integrity": "sha512-rpFrS/DPKH9NAWfEhrgpVmqJtfUIGvl9y6KQv0QsNv7X0ZISNtsoHIUe2jVrbjysjWXrJCryCxcSxNgqsa4Www==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.16.1.tgz",
"integrity": "sha512-F2I5RL7RTLQF9CccMrqt73GRdK3FdqaChED3RulGQX5lH6U3exHGFxwyZxSrY4x6FedfBFYlfXWWCJXpLnFkow==",
"requires": {
"@sentry/core": "6.16.0",
"@sentry/types": "6.16.0",
"@sentry/utils": "6.16.0",
"@sentry/core": "6.16.1",
"@sentry/types": "6.16.1",
"@sentry/utils": "6.16.1",
"tslib": "^1.9.3"
},
"dependencies": {
@ -10433,14 +10436,14 @@
}
},
"@sentry/core": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.16.0.tgz",
"integrity": "sha512-XqIlMjefuJmwQSAzv9J1PtV6+sXiz1dgBbtRr6e+QGIYZ+BDkuyDQv/HsGPfxxMHxgJBxBzi71FFLjEJsF6CBg==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.16.1.tgz",
"integrity": "sha512-UFI0264CPUc5cR1zJH+S2UPOANpm6dLJOnsvnIGTjsrwzR0h8Hdl6rC2R/GPq+WNbnipo9hkiIwDlqbqvIU5vw==",
"requires": {
"@sentry/hub": "6.16.0",
"@sentry/minimal": "6.16.0",
"@sentry/types": "6.16.0",
"@sentry/utils": "6.16.0",
"@sentry/hub": "6.16.1",
"@sentry/minimal": "6.16.1",
"@sentry/types": "6.16.1",
"@sentry/utils": "6.16.1",
"tslib": "^1.9.3"
},
"dependencies": {
@ -10452,12 +10455,12 @@
}
},
"@sentry/hub": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.16.0.tgz",
"integrity": "sha512-NBkcgGjnYsoXyIJwi2TGCxGnxbDJc/t++0ukFoBRy6RL/pw2YnryCu8PWNFsDkZdlb1zt5SIC6Kui+q1ViNS/A==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.16.1.tgz",
"integrity": "sha512-4PGtg6AfpqMkreTpL7ymDeQ/U1uXv03bKUuFdtsSTn/FRf9TLS4JB0KuTZCxfp1IRgAA+iFg6B784dDkT8R9eg==",
"requires": {
"@sentry/types": "6.16.0",
"@sentry/utils": "6.16.0",
"@sentry/types": "6.16.1",
"@sentry/utils": "6.16.1",
"tslib": "^1.9.3"
},
"dependencies": {
@ -10469,12 +10472,12 @@
}
},
"@sentry/minimal": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.16.0.tgz",
"integrity": "sha512-9/h0J9BDDY5W/dKILGEq3ewECspNoxcXuly/WOWQdt2SQpIcoh8l/dF8iTXle+icndin0EiMEyHOzaCPWG24oQ==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.16.1.tgz",
"integrity": "sha512-dq+mI1EQIvUM+zJtGCVgH3/B3Sbx4hKlGf2Usovm9KoqWYA+QpfVBholYDe/H2RXgO7LFEefDLvOdHDkqeJoyA==",
"requires": {
"@sentry/hub": "6.16.0",
"@sentry/types": "6.16.0",
"@sentry/hub": "6.16.1",
"@sentry/types": "6.16.1",
"tslib": "^1.9.3"
},
"dependencies": {
@ -10486,14 +10489,14 @@
}
},
"@sentry/tracing": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.16.0.tgz",
"integrity": "sha512-vTTjGnLc9fa3jM0RKkEgOLW23CiPb1Kh6bkHbUw68d3DVz6o0Tj2SqzW+Y+LaIwlFjhrozf+YV/KS9vj4BhHTw==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.16.1.tgz",
"integrity": "sha512-MPSbqXX59P+OEeST+U2V/8Hu/8QjpTUxTNeNyTHWIbbchdcMMjDbXTS3etCgajZR6Ro+DHElOz5cdSxH6IBGlA==",
"requires": {
"@sentry/hub": "6.16.0",
"@sentry/minimal": "6.16.0",
"@sentry/types": "6.16.0",
"@sentry/utils": "6.16.0",
"@sentry/hub": "6.16.1",
"@sentry/minimal": "6.16.1",
"@sentry/types": "6.16.1",
"@sentry/utils": "6.16.1",
"tslib": "^1.9.3"
},
"dependencies": {
@ -10505,16 +10508,16 @@
}
},
"@sentry/types": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.16.0.tgz",
"integrity": "sha512-ZgIyLYlQS4SPi+d68XD8n9FzoObrNQLWxBuMYMnG3uJSuFeYAJrVYkDRtW4OW0D3awuajYGiHJZC2O5qTRGflA=="
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.16.1.tgz",
"integrity": "sha512-Wh354g30UsJ5kYJbercektGX4ZMc9MHU++1NjeN2bTMnbofEcpUDWIiKeulZEY65IC1iU+1zRQQgtYO+/hgCUQ=="
},
"@sentry/utils": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.16.0.tgz",
"integrity": "sha512-FJl1AyUVAIzxfEXufWsgX7KxIvOrQawxhAhLXO4vU5xrFrJOteicxAIFJO+GG0QDELgr9siP0Qgeb8LoINWcrw==",
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.16.1.tgz",
"integrity": "sha512-7ngq/i4R8JZitJo9Sl8PDnjSbDehOxgr1vsoMmerIsyRZ651C/8B+jVkMhaAPgSdyJ0AlE3O7DKKTP1FXFw9qw==",
"requires": {
"@sentry/types": "6.16.0",
"@sentry/types": "6.16.1",
"tslib": "^1.9.3"
},
"dependencies": {
@ -11457,9 +11460,9 @@
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"construct-style-sheets-polyfill": {
"version": "2.4.17",
"resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-2.4.17.tgz",
"integrity": "sha512-rKtZGWWAVFE6HgdBuuui1emic/t8aAKQbePQ7Je6ird8nZYSd3mqqBX9IvFn2CMtnbh7mQTk/vxc9mfaLl7cGQ=="
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-3.0.5.tgz",
"integrity": "sha512-prLKSx9gYwtmqWtq+pZxppU1SaH9o4ug7JIc0I/1zMV2bFE3GvRtQaMTIpotlhw33XjtC7rGQFOZJsOFnlAAhQ=="
},
"convert-source-map": {
"version": "1.8.0",
@ -14025,9 +14028,9 @@
}
},
"rollup": {
"version": "2.60.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.60.2.tgz",
"integrity": "sha512-1Bgjpq61sPjgoZzuiDSGvbI1tD91giZABgjCQBKM5aYLnzjq52GoDuWVwT/cm/MCxCMPU8gqQvkj8doQ5C8Oqw==",
"version": "2.61.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.61.1.tgz",
"integrity": "sha512-BbTXlEvB8d+XFbK/7E5doIcRtxWPRiqr0eb5vQ0+2paMM04Ye4PZY5nHOQef2ix24l/L0SpLd5hwcH15QHPdvA==",
"requires": {
"fsevents": "~2.3.2"
}
@ -14913,9 +14916,9 @@
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="
},
"typescript": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw=="
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.3.tgz",
"integrity": "sha512-eVYaEHALSt+s9LbvgEv4Ef+Tdq7hBiIZgii12xXJnukryt3pMgJf6aKhoCZ3FWQsu6sydEnkg11fYXLzhLBjeQ=="
},
"uglify-js": {
"version": "3.14.1",

View file

@ -51,7 +51,7 @@
"@babel/preset-env": "^7.16.4",
"@babel/preset-typescript": "^7.16.0",
"@fortawesome/fontawesome-free": "^5.15.4",
"@goauthentik/api": "^2021.10.4-1638781871",
"@goauthentik/api": "^2021.10.4-1639076050",
"@jackfranklin/rollup-plugin-markdown": "^0.3.0",
"@lingui/cli": "^3.13.0",
"@lingui/core": "^3.13.0",
@ -65,8 +65,8 @@
"@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-replace": "^3.0.0",
"@rollup/plugin-typescript": "^8.3.0",
"@sentry/browser": "^6.16.0",
"@sentry/tracing": "^6.16.0",
"@sentry/browser": "^6.16.1",
"@sentry/tracing": "^6.16.1",
"@squoosh/cli": "^0.7.2",
"@trivago/prettier-plugin-sort-imports": "^3.1.1",
"@types/chart.js": "^2.9.34",
@ -80,7 +80,7 @@
"chart.js": "^3.6.2",
"chartjs-adapter-moment": "^1.0.0",
"codemirror": "^5.64.0",
"construct-style-sheets-polyfill": "^2.4.16",
"construct-style-sheets-polyfill": "^3.0.5",
"eslint": "^8.4.1",
"eslint-config-google": "^0.14.0",
"eslint-plugin-custom-elements": "0.0.4",
@ -91,7 +91,7 @@
"moment": "^2.29.1",
"prettier": "^2.5.1",
"rapidoc": "^9.1.3",
"rollup": "^2.60.2",
"rollup": "^2.61.1",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-cssimport": "^1.0.2",
"rollup-plugin-minify-html-literals": "^1.2.6",
@ -99,7 +99,7 @@
"rollup-plugin-terser": "^7.0.2",
"ts-lit-plugin": "^1.2.1",
"tslib": "^2.3.1",
"typescript": "^4.5.2",
"typescript": "^4.5.3",
"webcomponent-qr-code": "^1.0.5",
"yaml": "^1.10.2"
}

View file

@ -338,6 +338,9 @@ html > form > input {
.pf-c-notification-drawer__list-item {
background-color: var(--ak-dark-background-light-ish);
color: var(--ak-dark-foreground);
--pf-c-notification-drawer__list-item--BorderBottomColor: var(
--ak-dark-background-lighter
) !important;
}
/* data list */
.pf-c-data-list__item {

View file

@ -62,7 +62,7 @@ export class UserListPage extends TablePage<User> {
search: this.search || "",
attributes: this.hideServiceAccounts
? JSON.stringify({
"goauthentik.io/user/service-account__isnull": "true",
"goauthentik.io/user/service-account__isnull": true,
})
: undefined,
});

View file

@ -21,7 +21,6 @@ If you want to only make changes on the UI, you don't need a backend running fro
AUTHENTIK_TAG=gh-next
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=goauthentik.io/dev-%(type)s:gh-next
AUTHENTIK_LOG_LEVEL=debug
AUTHENTIK_WEB_LOAD_LOCAL_FILES=true
```
This will cause authentik to use the beta images.

View file

@ -7,31 +7,16 @@ services:
container_name: traefik
volumes:
- /var/run/docker.sock:/var/run/docker.sock
labels:
traefik.enable: true
traefik.http.routers.api.rule: Host(`traefik.example.com`)
traefik.http.routers.api.entrypoints: https
traefik.http.routers.api.service: api@internal
traefik.http.routers.api.tls: true
ports:
- 80:80
- 443:443
command:
- '--api'
- '--log=true'
- '--log.level=DEBUG'
- '--log.filepath=/var/log/traefik.log'
- '--providers.docker=true'
- '--providers.docker.exposedByDefault=false'
- '--entrypoints.http=true'
- '--entrypoints.http.address=:80'
- '--entrypoints.http.http.redirections.entrypoint.to=https'
- '--entrypoints.http.http.redirections.entrypoint.scheme=https'
- '--entrypoints.https=true'
- '--entrypoints.https.address=:443'
- "--entrypoints.web.address=:80"
authentik_proxy:
image: goauthentik.io/proxy:2021.5.1
authentik-proxy:
image: goauthentik.io/proxy:latest
ports:
- 9000:9000
- 9443:9443
@ -46,11 +31,10 @@ services:
traefik.enable: true
traefik.port: 9000
traefik.http.routers.authentik.rule: Host(`app.company`) && PathPrefix(`/akprox/`)
traefik.http.routers.authentik.entrypoints: https
traefik.http.routers.authentik.tls: true
traefik.http.middlewares.authentik.forwardauth.address: http://outpost.company:9000/akprox/auth/traefik
# `authentik-proxy` refers to the service name in the compose file.
traefik.http.middlewares.authentik.forwardauth.address: http://authentik-proxy:9000/akprox/auth/traefik
traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: true
traefik.http.middlewares.authentik.forwardauth.authResponseHeadersRegex: ^.*$
traefik.http.middlewares.authentik.forwardauth.authResponseHeadersRegex: ^.*$$
restart: unless-stopped
whoami:
@ -58,8 +42,6 @@ services:
labels:
traefik.enable: true
traefik.http.routers.whoami.rule: Host(`app.company`)
traefik.http.routers.whoami.entrypoints: https
traefik.http.routers.whoami.tls: true
traefik.http.routers.whoami.middlewares: authentik@docker
restart: unless-stopped
```

View file

@ -86,6 +86,23 @@ This release does not have any headline features, and mostly fixes bugs.
- web/flows: fix linting errors
- web/flows: Revise duo authenticator login prompt text (#1872)
## Fixed in 2021.12.1-rc3
- core: add FlowToken which saves the pickled flow plan, replace standard token in email stage to allow finishing flows in different sessions
- core: fix missing permission check for group creating when creating service account
- outposts/ldap: Fix search case sensitivity. (#1897)
- policies/expression: add ak_call_policy
- providers/saml: add ?force_binding to limit bindings for metadata endpoint
- root: add request_id to celery tasks, prefixed with "task-"
- sources/*: Allow creation of source connections via API
- stages/prompt: use policyenginemode all
- tests/e2e: add post binding test
- web: fix duplicate classes, make generic icon clickable
- web: fix text colour for bad request on light mode
- web/admin: show outpost warning on application page too
- web/elements: close dropdown when refresh event is dispatched
- web/user: allow custom font-awesome icons for applications
## Upgrading
This release does not introduce any new requirements.

View file

@ -0,0 +1,54 @@
---
title: FortiManager
---
## What is FortiManager
From https://www.fortinet.com/products/management/fortimanager
:::note
FortiManager supports network operations use cases for centralized management, best practices compliance, and workflow automation to provide better protection against breaches.
FortiManager is a paid enterprise product.
:::
## Preparation
The following placeholders will be used:
- `fgm.company` is the FQDN of the FortiManager install.
- `authentik.company` is the FQDN of the authentik install.
Create an application and Provider in authentik, note the slug, as this will be used later. Create a SAML provider with the following parameters:
Provider:
- ACS URL: `https://fgm.company/saml/?acs`
- Issuer: `https://authentik.company/application/saml/fgm/sso/binding/redirect/`
- Service Provider Binding: Post
You can of course use a custom signing certificate, and adjust durations.
Application:
- Launch URL: 'https://fgm.company/p/sso_sp/'
## FortiManager Configuration
Navigate to `https://fgm.company/p/app/#!/sys/sso_settings` and select SAML SSO settings to configure SAML.
Select 'Service Provider (SP)' under Single Sign-On Mode to enable SAML authentication.
Set the Field 'SP Address' to the FortiManager FQDN 'fgm.company'. (This gives you the URLs to configure in Authentik)
Set the Default Login Page to either 'Normal' or 'Single-Sign On'. (Normal allows both local and SAML authentication vs only SAML SSO)
FortiManager create a new user by default if one does not exist so you will need to set the Default Admin Profile to the permissions you want any new users to have. (We created a no_permissions profile to assign by default)
Set the Field 'IdP Type' to 'Custom'
Set the Field `IdP entity ID` to `https://authentik.company/application/saml/fgm/sso/binding/redirect/`.
Set the Field `IdP Login URL` to `https://authentik.company/application/saml/fgm/sso/binding/redirect/`.
Set the Field `IdP Logout URL` to `https://authentik.company/`
For the Field 'IdP Certificate" Import your Authentik cert. (Self Signed or real)

7394
website/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,11 +12,11 @@
"serve": "docusaurus serve"
},
"dependencies": {
"@docusaurus/plugin-client-redirects": "2.0.0-beta.9",
"@docusaurus/preset-classic": "2.0.0-beta.9",
"@docusaurus/plugin-client-redirects": "2.0.0-beta.13",
"@docusaurus/preset-classic": "2.0.0-beta.13",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.1.1",
"postcss": "^8.4.4",
"postcss": "^8.4.5",
"rapidoc": "^9.1.3",
"react": "^17.0.2",
"react-before-after-slider-component": "^1.1.1",

View file

@ -31,6 +31,7 @@ module.exports = {
"services/awx-tower/index",
"services/bookstack/index",
"services/budibase/index",
"services/fortimanager/index",
"services/gitea/index",
"services/gitlab/index",
"services/grafana/index",