Compare commits
18 Commits
trustchain
...
version-20
Author | SHA1 | Date |
---|---|---|
Jens Langhammer | 918355e1c9 | |
Jens Langhammer | c07a48a3ec | |
Jens Langhammer | e1bae1240f | |
risson | 37bd62d291 | |
Jens Langhammer | ac63db0136 | |
Jens Langhammer | 5cdf3a09a9 | |
Jens Langhammer | 3e17adf33f | |
Jens Langhammer | 8392916c84 | |
Jens Langhammer | 7e75a48fd0 | |
Jens Langhammer | d69d84e48c | |
Jens Langhammer | 78cc8fa498 | |
Jens Langhammer | 0fcdf5e968 | |
Jens Langhammer | f05997740f | |
Jens Langhammer | 1aff300171 | |
Jens Langhammer | ffb98eaa75 | |
Jens Langhammer | 5c1db432f0 | |
Jens Langhammer | 07fd4daa3e | |
Jens Langhammer | aa80babfff |
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 2023.4.0
|
||||
current_version = 2023.4.3
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
|
|
|
@ -10,6 +10,11 @@ jobs:
|
|||
name: Delete old unused container images
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Delete 'dev' containers older than a week
|
||||
uses: snok/container-retention-policy@v2
|
||||
with:
|
||||
|
@ -18,5 +23,5 @@ jobs:
|
|||
account-type: org
|
||||
org-name: goauthentik
|
||||
untagged-only: false
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
skip-tags: gh-next,gh-main
|
||||
|
|
|
@ -22,18 +22,23 @@ jobs:
|
|||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root server test-all
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- name: Extract version number
|
||||
id: get_version
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1.1.4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ steps.get_version.outputs.result }}
|
||||
|
|
|
@ -18,9 +18,14 @@ jobs:
|
|||
compile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- name: Setup authentik env
|
||||
uses: ./.github/actions/setup
|
||||
- name: run compile
|
||||
|
@ -29,7 +34,7 @@ jobs:
|
|||
uses: peter-evans/create-pull-request@v5
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: compile-backend-translation
|
||||
commit-message: "core: compile backend translations"
|
||||
title: "core: compile backend translations"
|
||||
|
|
|
@ -9,9 +9,14 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
- uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
node-version: '18'
|
||||
|
@ -33,7 +38,7 @@ jobs:
|
|||
- uses: peter-evans/create-pull-request@v5
|
||||
id: cpr
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
branch: update-web-api-client
|
||||
commit-message: "web: bump API Client version"
|
||||
title: "web: bump API Client version"
|
||||
|
@ -44,6 +49,6 @@ jobs:
|
|||
author: authentik bot <github-bot@goauthentik.io>
|
||||
- uses: peter-evans/enable-pull-request-automerge@v3
|
||||
with:
|
||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
merge-method: squash
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2023.4.0"
|
||||
__version__ = "2023.4.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
"""authentik administration overview"""
|
||||
import os
|
||||
import platform
|
||||
from datetime import datetime
|
||||
from sys import version as python_version
|
||||
|
@ -34,7 +33,6 @@ class RuntimeDict(TypedDict):
|
|||
class SystemSerializer(PassiveSerializer):
|
||||
"""Get system information."""
|
||||
|
||||
env = SerializerMethodField()
|
||||
http_headers = SerializerMethodField()
|
||||
http_host = SerializerMethodField()
|
||||
http_is_secure = SerializerMethodField()
|
||||
|
@ -43,10 +41,6 @@ class SystemSerializer(PassiveSerializer):
|
|||
server_time = SerializerMethodField()
|
||||
embedded_outpost_host = SerializerMethodField()
|
||||
|
||||
def get_env(self, request: Request) -> dict[str, str]:
|
||||
"""Get Environment"""
|
||||
return os.environ.copy()
|
||||
|
||||
def get_http_headers(self, request: Request) -> dict[str, str]:
|
||||
"""Get HTTP Request headers"""
|
||||
headers = {}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""API Authentication"""
|
||||
from hmac import compare_digest
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.conf import settings
|
||||
|
@ -78,7 +79,7 @@ def token_secret_key(value: str) -> Optional[User]:
|
|||
and return the service account for the managed outpost"""
|
||||
from authentik.outposts.apps import MANAGED_OUTPOST
|
||||
|
||||
if value != settings.SECRET_KEY:
|
||||
if not compare_digest(value, settings.SECRET_KEY):
|
||||
return None
|
||||
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
|
||||
if not outposts:
|
||||
|
|
|
@ -82,7 +82,10 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel):
|
|||
def retrieve_file(self) -> str:
|
||||
"""Get blueprint from path"""
|
||||
try:
|
||||
full_path = Path(CONFIG.y("blueprints_dir")).joinpath(Path(self.path))
|
||||
base = Path(CONFIG.y("blueprints_dir"))
|
||||
full_path = base.joinpath(Path(self.path)).resolve()
|
||||
if not str(full_path).startswith(str(base.resolve())):
|
||||
raise BlueprintRetrievalFailed("Invalid blueprint path")
|
||||
with full_path.open("r", encoding="utf-8") as _file:
|
||||
return _file.read()
|
||||
except (IOError, OSError) as exc:
|
||||
|
|
|
@ -1,34 +1,15 @@
|
|||
"""authentik managed models tests"""
|
||||
from typing import Callable, Type
|
||||
|
||||
from django.apps import apps
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.v1.importer import is_model_allowed
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.blueprints.models import BlueprintInstance, BlueprintRetrievalFailed
|
||||
from authentik.lib.generators import generate_id
|
||||
|
||||
|
||||
class TestModels(TestCase):
|
||||
"""Test Models"""
|
||||
|
||||
|
||||
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
|
||||
"""Test serializer"""
|
||||
|
||||
def tester(self: TestModels):
|
||||
if test_model._meta.abstract: # pragma: no cover
|
||||
return
|
||||
model_class = test_model()
|
||||
self.assertTrue(isinstance(model_class, SerializerModel))
|
||||
self.assertIsNotNone(model_class.serializer)
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
for app in apps.get_app_configs():
|
||||
if not app.label.startswith("authentik"):
|
||||
continue
|
||||
for model in app.get_models():
|
||||
if not is_model_allowed(model):
|
||||
continue
|
||||
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
|
||||
def test_retrieve_file(self):
|
||||
"""Test retrieve_file"""
|
||||
instance = BlueprintInstance.objects.create(name=generate_id(), path="../etc/hosts")
|
||||
with self.assertRaises(BlueprintRetrievalFailed):
|
||||
instance.retrieve()
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
"""authentik managed models tests"""
|
||||
from typing import Callable, Type
|
||||
|
||||
from django.apps import apps
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.v1.importer import is_model_allowed
|
||||
from authentik.lib.models import SerializerModel
|
||||
|
||||
|
||||
class TestModels(TestCase):
|
||||
"""Test Models"""
|
||||
|
||||
|
||||
def serializer_tester_factory(test_model: Type[SerializerModel]) -> Callable:
|
||||
"""Test serializer"""
|
||||
|
||||
def tester(self: TestModels):
|
||||
if test_model._meta.abstract: # pragma: no cover
|
||||
return
|
||||
model_class = test_model()
|
||||
self.assertTrue(isinstance(model_class, SerializerModel))
|
||||
self.assertIsNotNone(model_class.serializer)
|
||||
|
||||
return tester
|
||||
|
||||
|
||||
for app in apps.get_app_configs():
|
||||
if not app.label.startswith("authentik"):
|
||||
continue
|
||||
for model in app.get_models():
|
||||
if not is_model_allowed(model):
|
||||
continue
|
||||
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
|
|
@ -67,11 +67,12 @@ from authentik.core.models import (
|
|||
TokenIntents,
|
||||
User,
|
||||
)
|
||||
from authentik.events.models import EventAction
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.exceptions import FlowNonApplicableException
|
||||
from authentik.flows.models import FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||
from authentik.flows.views.executor import QS_KEY_TOKEN
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.stages.email.models import EmailStage
|
||||
from authentik.stages.email.tasks import send_mails
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
|
@ -543,6 +544,58 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
|||
send_mails(email_stage, message)
|
||||
return Response(status=204)
|
||||
|
||||
@permission_required("authentik_core.impersonate")
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses={
|
||||
"204": OpenApiResponse(description="Successfully started impersonation"),
|
||||
"401": OpenApiResponse(description="Access denied"),
|
||||
},
|
||||
)
|
||||
@action(detail=True, methods=["POST"])
|
||||
def impersonate(self, request: Request, pk: int) -> Response:
|
||||
"""Impersonate a user"""
|
||||
if not CONFIG.y_bool("impersonation"):
|
||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
||||
return Response(status=401)
|
||||
if not request.user.has_perm("impersonate"):
|
||||
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
|
||||
return Response(status=401)
|
||||
|
||||
user_to_be = self.get_object()
|
||||
|
||||
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||
|
||||
return Response(status=201)
|
||||
|
||||
@extend_schema(
|
||||
request=OpenApiTypes.NONE,
|
||||
responses={
|
||||
"204": OpenApiResponse(description="Successfully started impersonation"),
|
||||
},
|
||||
)
|
||||
@action(detail=False, methods=["GET"])
|
||||
def impersonate_end(self, request: Request) -> Response:
|
||||
"""End Impersonation a user"""
|
||||
if (
|
||||
SESSION_KEY_IMPERSONATE_USER not in request.session
|
||||
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||
):
|
||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||
return Response(status=204)
|
||||
|
||||
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
del request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
||||
|
||||
return Response(status=204)
|
||||
|
||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
||||
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
|
||||
for backend in list(self.filter_backends):
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
"""impersonation tests"""
|
||||
from json import loads
|
||||
|
||||
from django.test.testcases import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_admin_user
|
||||
|
||||
|
||||
class TestImpersonation(TestCase):
|
||||
class TestImpersonation(APITestCase):
|
||||
"""impersonation tests"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
|
@ -23,10 +23,10 @@ class TestImpersonation(TestCase):
|
|||
self.other_user.save()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.client.get(
|
||||
self.client.post(
|
||||
reverse(
|
||||
"authentik_core:impersonate-init",
|
||||
kwargs={"user_id": self.other_user.pk},
|
||||
"authentik_api:user-impersonate",
|
||||
kwargs={"pk": self.other_user.pk},
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -35,7 +35,7 @@ class TestImpersonation(TestCase):
|
|||
self.assertEqual(response_body["user"]["username"], self.other_user.username)
|
||||
self.assertEqual(response_body["original"]["username"], self.user.username)
|
||||
|
||||
self.client.get(reverse("authentik_core:impersonate-end"))
|
||||
self.client.get(reverse("authentik_api:user-impersonate-end"))
|
||||
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
response_body = loads(response.content.decode())
|
||||
|
@ -46,9 +46,7 @@ class TestImpersonation(TestCase):
|
|||
"""test impersonation without permissions"""
|
||||
self.client.force_login(self.other_user)
|
||||
|
||||
self.client.get(
|
||||
reverse("authentik_core:impersonate-init", kwargs={"user_id": self.user.pk})
|
||||
)
|
||||
self.client.get(reverse("authentik_api:user-impersonate", kwargs={"pk": self.user.pk}))
|
||||
|
||||
response = self.client.get(reverse("authentik_api:user-me"))
|
||||
response_body = loads(response.content.decode())
|
||||
|
@ -58,5 +56,5 @@ class TestImpersonation(TestCase):
|
|||
"""test un-impersonation without impersonating first"""
|
||||
self.client.force_login(self.other_user)
|
||||
|
||||
response = self.client.get(reverse("authentik_core:impersonate-end"))
|
||||
self.assertRedirects(response, reverse("authentik_core:if-user"))
|
||||
response = self.client.get(reverse("authentik_api:user-impersonate-end"))
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
|
|
@ -7,7 +7,7 @@ from django.urls import path
|
|||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from authentik.core.views import apps, impersonate
|
||||
from authentik.core.views import apps
|
||||
from authentik.core.views.debug import AccessDeniedView
|
||||
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
|
||||
from authentik.core.views.session import EndSessionView
|
||||
|
@ -28,17 +28,6 @@ urlpatterns = [
|
|||
apps.RedirectToAppLaunch.as_view(),
|
||||
name="application-launch",
|
||||
),
|
||||
# Impersonation
|
||||
path(
|
||||
"-/impersonation/<int:user_id>/",
|
||||
impersonate.ImpersonateInitView.as_view(),
|
||||
name="impersonate-init",
|
||||
),
|
||||
path(
|
||||
"-/impersonation/end/",
|
||||
impersonate.ImpersonateEndView.as_view(),
|
||||
name="impersonate-end",
|
||||
),
|
||||
# Interfaces
|
||||
path(
|
||||
"if/admin/",
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
"""authentik impersonation views"""
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.middleware import (
|
||||
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
|
||||
SESSION_KEY_IMPERSONATE_USER,
|
||||
)
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class ImpersonateInitView(View):
|
||||
"""Initiate Impersonation"""
|
||||
|
||||
def get(self, request: HttpRequest, user_id: int) -> HttpResponse:
|
||||
"""Impersonation handler, checks permissions"""
|
||||
if not CONFIG.y_bool("impersonation"):
|
||||
LOGGER.debug("User attempted to impersonate", user=request.user)
|
||||
return HttpResponse("Unauthorized", status=401)
|
||||
if not request.user.has_perm("impersonate"):
|
||||
LOGGER.debug("User attempted to impersonate without permissions", user=request.user)
|
||||
return HttpResponse("Unauthorized", status=401)
|
||||
|
||||
user_to_be = get_object_or_404(User, pk=user_id)
|
||||
|
||||
request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||
|
||||
return redirect("authentik_core:if-user")
|
||||
|
||||
|
||||
class ImpersonateEndView(View):
|
||||
"""End User impersonation"""
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""End Impersonation handler"""
|
||||
if (
|
||||
SESSION_KEY_IMPERSONATE_USER not in request.session
|
||||
or SESSION_KEY_IMPERSONATE_ORIGINAL_USER not in request.session
|
||||
):
|
||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||
return redirect("authentik_core:if-user")
|
||||
|
||||
original_user = request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
del request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||
del request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER]
|
||||
|
||||
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
||||
|
||||
return redirect("authentik_core:root-redirect")
|
|
@ -23,7 +23,8 @@ class DiagramElement:
|
|||
style: list[str] = field(default_factory=lambda: ["[", "]"])
|
||||
|
||||
def __str__(self) -> str:
|
||||
element = f'{self.identifier}{self.style[0]}"{self.description}"{self.style[1]}'
|
||||
description = self.description.replace('"', "#quot;")
|
||||
element = f'{self.identifier}{self.style[0]}"{description}"{self.style[1]}'
|
||||
if self.action is not None:
|
||||
if self.action != "":
|
||||
element = f"--{self.action}--> {element}"
|
||||
|
|
|
@ -204,12 +204,12 @@ class ChallengeStageView(StageView):
|
|||
for field, errors in response.errors.items():
|
||||
for error in errors:
|
||||
full_errors.setdefault(field, [])
|
||||
full_errors[field].append(
|
||||
{
|
||||
"string": str(error),
|
||||
"code": error.code,
|
||||
}
|
||||
)
|
||||
field_error = {
|
||||
"string": str(error),
|
||||
}
|
||||
if hasattr(error, "code"):
|
||||
field_error["code"] = error.code
|
||||
full_errors[field].append(field_error)
|
||||
challenge_response.initial_data["response_errors"] = full_errors
|
||||
if not challenge_response.is_valid():
|
||||
self.logger.error(
|
||||
|
|
|
@ -5,18 +5,25 @@ postgresql:
|
|||
name: authentik
|
||||
user: authentik
|
||||
port: 5432
|
||||
password: 'env://POSTGRES_PASSWORD'
|
||||
password: "env://POSTGRES_PASSWORD"
|
||||
use_pgbouncer: false
|
||||
|
||||
listen:
|
||||
listen_http: 0.0.0.0:9000
|
||||
listen_https: 0.0.0.0:9443
|
||||
listen_metrics: 0.0.0.0:9300
|
||||
trusted_proxy_cidrs:
|
||||
- 127.0.0.0/8
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- fe80::/10
|
||||
- ::1/128
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ''
|
||||
password: ""
|
||||
tls: false
|
||||
tls_reqs: "none"
|
||||
db: 0
|
||||
|
|
|
@ -16,10 +16,12 @@ LOGGER = get_logger()
|
|||
|
||||
def _get_client_ip_from_meta(meta: dict[str, Any]) -> str:
|
||||
"""Attempt to get the client's IP by checking common HTTP Headers.
|
||||
Returns none if no IP Could be found"""
|
||||
Returns none if no IP Could be found
|
||||
|
||||
No additional validation is done here as requests are expected to only arrive here
|
||||
via the go proxy, which deals with validating these headers for us"""
|
||||
headers = (
|
||||
"HTTP_X_FORWARDED_FOR",
|
||||
"HTTP_X_REAL_IP",
|
||||
"REMOTE_ADDR",
|
||||
)
|
||||
for _header in headers:
|
||||
|
|
|
@ -132,9 +132,9 @@ class TestPolicyProcess(TestCase):
|
|||
)
|
||||
binding = PolicyBinding(policy=policy, target=Application.objects.create(name="test"))
|
||||
|
||||
http_request = self.factory.get(reverse("authentik_core:impersonate-end"))
|
||||
http_request = self.factory.get(reverse("authentik_api:user-impersonate-end"))
|
||||
http_request.user = self.user
|
||||
http_request.resolver_match = resolve(reverse("authentik_core:impersonate-end"))
|
||||
http_request.resolver_match = resolve(reverse("authentik_api:user-impersonate-end"))
|
||||
|
||||
request = PolicyRequest(self.user)
|
||||
request.set_http_request(http_request)
|
||||
|
|
|
@ -63,8 +63,8 @@ def ldap_sync_password(sender, user: User, password: str, **_):
|
|||
if not sources.exists():
|
||||
return
|
||||
source = sources.first()
|
||||
changer = LDAPPasswordChanger(source)
|
||||
try:
|
||||
changer = LDAPPasswordChanger(source)
|
||||
changer.change_password(user, password)
|
||||
except LDAPOperationResult as exc:
|
||||
Event.new(
|
||||
|
|
|
@ -134,6 +134,12 @@ def validate_challenge_webauthn(data: dict, stage_view: StageView, user: User) -
|
|||
device = WebAuthnDevice.objects.filter(credential_id=credential_id).first()
|
||||
if not device:
|
||||
raise ValidationError("Invalid device")
|
||||
# We can only check the device's user if the user we're given isn't anonymous
|
||||
# as this validation is also used for password-less login where webauthn is the very first
|
||||
# step done by a user. Only if this validation happens at a later stage we can check
|
||||
# that the device belongs to the user
|
||||
if not user.is_anonymous and device.user != user:
|
||||
raise ValidationError("Invalid device")
|
||||
|
||||
stage: AuthenticatorValidateStage = stage_view.executor.current_stage
|
||||
|
||||
|
|
|
@ -37,9 +37,9 @@ from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_ME
|
|||
|
||||
COOKIE_NAME_MFA = "authentik_mfa"
|
||||
|
||||
SESSION_KEY_STAGES = "authentik/stages/authenticator_validate/stages"
|
||||
SESSION_KEY_SELECTED_STAGE = "authentik/stages/authenticator_validate/selected_stage"
|
||||
SESSION_KEY_DEVICE_CHALLENGES = "authentik/stages/authenticator_validate/device_challenges"
|
||||
PLAN_CONTEXT_STAGES = "goauthentik.io/stages/authenticator_validate/stages"
|
||||
PLAN_CONTEXT_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage"
|
||||
PLAN_CONTEXT_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges"
|
||||
|
||||
|
||||
class SelectableStageSerializer(PassiveSerializer):
|
||||
|
@ -73,8 +73,8 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||
component = CharField(default="ak-stage-authenticator-validate")
|
||||
|
||||
def _challenge_allowed(self, classes: list):
|
||||
device_challenges: list[dict] = self.stage.request.session.get(
|
||||
SESSION_KEY_DEVICE_CHALLENGES, []
|
||||
device_challenges: list[dict] = self.stage.executor.plan.context.get(
|
||||
PLAN_CONTEXT_DEVICE_CHALLENGES, []
|
||||
)
|
||||
if not any(x["device_class"] in classes for x in device_challenges):
|
||||
raise ValidationError("No compatible device class allowed")
|
||||
|
@ -104,7 +104,9 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||
"""Check which challenge the user has selected. Actual logic only used for SMS stage."""
|
||||
# First check if the challenge is valid
|
||||
allowed = False
|
||||
for device_challenge in self.stage.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, []):
|
||||
for device_challenge in self.stage.executor.plan.context.get(
|
||||
PLAN_CONTEXT_DEVICE_CHALLENGES, []
|
||||
):
|
||||
if device_challenge.get("device_class", "") == challenge.get(
|
||||
"device_class", ""
|
||||
) and device_challenge.get("device_uid", "") == challenge.get("device_uid", ""):
|
||||
|
@ -122,11 +124,11 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse):
|
|||
|
||||
def validate_selected_stage(self, stage_pk: str) -> str:
|
||||
"""Check that the selected stage is valid"""
|
||||
stages = self.stage.request.session.get(SESSION_KEY_STAGES, [])
|
||||
stages = self.stage.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
|
||||
if not any(str(stage.pk) == stage_pk for stage in stages):
|
||||
raise ValidationError("Selected stage is invalid")
|
||||
self.stage.logger.debug("Setting selected stage to ", stage=stage_pk)
|
||||
self.stage.request.session[SESSION_KEY_SELECTED_STAGE] = stage_pk
|
||||
self.stage.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = stage_pk
|
||||
return stage_pk
|
||||
|
||||
def validate(self, attrs: dict):
|
||||
|
@ -231,7 +233,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
else:
|
||||
self.logger.debug("No pending user, continuing")
|
||||
return self.executor.stage_ok()
|
||||
self.request.session[SESSION_KEY_DEVICE_CHALLENGES] = challenges
|
||||
self.executor.plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = challenges
|
||||
|
||||
# No allowed devices
|
||||
if len(challenges) < 1:
|
||||
|
@ -264,23 +266,23 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
if stage.configuration_stages.count() == 1:
|
||||
next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk)
|
||||
self.logger.debug("Single stage configured, auto-selecting", stage=next_stage)
|
||||
self.request.session[SESSION_KEY_SELECTED_STAGE] = next_stage
|
||||
self.executor.plan.context[PLAN_CONTEXT_SELECTED_STAGE] = next_stage
|
||||
# Because that normal execution only happens on post, we directly inject it here and
|
||||
# return it
|
||||
self.executor.plan.insert_stage(next_stage)
|
||||
return self.executor.stage_ok()
|
||||
stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses()
|
||||
self.request.session[SESSION_KEY_STAGES] = stages
|
||||
self.executor.plan.context[PLAN_CONTEXT_STAGES] = stages
|
||||
return super().get(self.request, *args, **kwargs)
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
res = super().post(request, *args, **kwargs)
|
||||
if (
|
||||
SESSION_KEY_SELECTED_STAGE in self.request.session
|
||||
PLAN_CONTEXT_SELECTED_STAGE in self.executor.plan.context
|
||||
and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE
|
||||
):
|
||||
self.logger.debug("Got selected stage in session, running that")
|
||||
stage_pk = self.request.session.get(SESSION_KEY_SELECTED_STAGE)
|
||||
self.logger.debug("Got selected stage in context, running that")
|
||||
stage_pk = self.executor.plan.context(PLAN_CONTEXT_SELECTED_STAGE)
|
||||
# Because the foreign key to stage.configuration_stage points to
|
||||
# a base stage class, we need to do another lookup
|
||||
stage = Stage.objects.get_subclass(pk=stage_pk)
|
||||
|
@ -291,8 +293,8 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
return res
|
||||
|
||||
def get_challenge(self) -> AuthenticatorValidationChallenge:
|
||||
challenges = self.request.session.get(SESSION_KEY_DEVICE_CHALLENGES, [])
|
||||
stages = self.request.session.get(SESSION_KEY_STAGES, [])
|
||||
challenges = self.executor.plan.context.get(PLAN_CONTEXT_DEVICE_CHALLENGES, [])
|
||||
stages = self.executor.plan.context.get(PLAN_CONTEXT_STAGES, [])
|
||||
stage_challenges = []
|
||||
for stage in stages:
|
||||
serializer = SelectableStageSerializer(
|
||||
|
@ -307,6 +309,7 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
stage_challenges.append(serializer.data)
|
||||
return AuthenticatorValidationChallenge(
|
||||
data={
|
||||
"component": "ak-stage-authenticator-validate",
|
||||
"type": ChallengeTypes.NATIVE.value,
|
||||
"device_challenges": challenges,
|
||||
"configuration_stages": stage_challenges,
|
||||
|
@ -390,8 +393,3 @@ class AuthenticatorValidateStageView(ChallengeStageView):
|
|||
)
|
||||
)
|
||||
return self.set_valid_mfa_cookie(response.device)
|
||||
|
||||
def cleanup(self):
|
||||
self.request.session.pop(SESSION_KEY_STAGES, None)
|
||||
self.request.session.pop(SESSION_KEY_SELECTED_STAGE, None)
|
||||
self.request.session.pop(SESSION_KEY_DEVICE_CHALLENGES, None)
|
||||
|
|
|
@ -1,26 +1,19 @@
|
|||
"""Test validator stage"""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls.base import reverse
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding, NotConfiguredAction
|
||||
from authentik.flows.planner import FlowPlan
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN, FlowExecutorView
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_id, generate_key
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.stages.authenticator_duo.models import AuthenticatorDuoStage, DuoDevice
|
||||
from authentik.stages.authenticator_validate.api import AuthenticatorValidateStageSerializer
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||
from authentik.stages.authenticator_validate.stage import (
|
||||
SESSION_KEY_DEVICE_CHALLENGES,
|
||||
AuthenticatorValidationChallengeResponse,
|
||||
)
|
||||
from authentik.stages.authenticator_validate.stage import PLAN_CONTEXT_DEVICE_CHALLENGES
|
||||
from authentik.stages.identification.models import IdentificationStage, UserFields
|
||||
|
||||
|
||||
|
@ -86,12 +79,17 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
|||
|
||||
def test_validate_selected_challenge(self):
|
||||
"""Test validate_selected_challenge"""
|
||||
# Prepare request with session
|
||||
request = self.request_factory.get("/")
|
||||
flow = create_test_flow()
|
||||
stage = AuthenticatorValidateStage.objects.create(
|
||||
name=generate_id(),
|
||||
not_configured_action=NotConfiguredAction.CONFIGURE,
|
||||
device_classes=[DeviceClasses.STATIC, DeviceClasses.TOTP],
|
||||
)
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session[SESSION_KEY_DEVICE_CHALLENGES] = [
|
||||
session = self.client.session
|
||||
plan = FlowPlan(flow_pk=flow.pk.hex)
|
||||
plan.append_stage(stage)
|
||||
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
|
||||
{
|
||||
"device_class": "static",
|
||||
"device_uid": "1",
|
||||
|
@ -101,23 +99,43 @@ class AuthenticatorValidateStageTests(FlowTestCase):
|
|||
"device_uid": "2",
|
||||
},
|
||||
]
|
||||
request.session.save()
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
res = AuthenticatorValidationChallengeResponse()
|
||||
res.stage = StageView(FlowExecutorView())
|
||||
res.stage.request = request
|
||||
with self.assertRaises(ValidationError):
|
||||
res.validate_selected_challenge(
|
||||
{
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
data={
|
||||
"selected_challenge": {
|
||||
"device_class": "baz",
|
||||
"device_uid": "quox",
|
||||
"challenge": {},
|
||||
}
|
||||
)
|
||||
res.validate_selected_challenge(
|
||||
{
|
||||
"device_class": "static",
|
||||
"device_uid": "1",
|
||||
}
|
||||
},
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
flow,
|
||||
response_errors={
|
||||
"selected_challenge": [{"string": "invalid challenge selected", "code": "invalid"}]
|
||||
},
|
||||
component="ak-stage-authenticator-validate",
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
data={
|
||||
"selected_challenge": {
|
||||
"device_class": "static",
|
||||
"device_uid": "1",
|
||||
"challenge": {},
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
flow,
|
||||
response_errors={"non_field_errors": [{"string": "Empty response", "code": "invalid"}]},
|
||||
component="ak-stage-authenticator-validate",
|
||||
)
|
||||
|
||||
@patch(
|
||||
|
|
|
@ -22,7 +22,7 @@ from authentik.stages.authenticator_validate.challenge import (
|
|||
)
|
||||
from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage, DeviceClasses
|
||||
from authentik.stages.authenticator_validate.stage import (
|
||||
SESSION_KEY_DEVICE_CHALLENGES,
|
||||
PLAN_CONTEXT_DEVICE_CHALLENGES,
|
||||
AuthenticatorValidateStageView,
|
||||
)
|
||||
from authentik.stages.authenticator_webauthn.models import UserVerification, WebAuthnDevice
|
||||
|
@ -211,14 +211,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||
plan.append_stage(stage)
|
||||
plan.append_stage(UserLoginStage(name=generate_id()))
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = self.user
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session[SESSION_KEY_DEVICE_CHALLENGES] = [
|
||||
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
|
||||
{
|
||||
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
||||
"device_uid": device.pk,
|
||||
"challenge": {},
|
||||
}
|
||||
]
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
||||
)
|
||||
|
@ -283,14 +283,14 @@ class AuthenticatorValidateStageWebAuthnTests(FlowTestCase):
|
|||
plan = FlowPlan(flow_pk=flow.pk.hex)
|
||||
plan.append_stage(stage)
|
||||
plan.append_stage(UserLoginStage(name=generate_id()))
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session[SESSION_KEY_DEVICE_CHALLENGES] = [
|
||||
plan.context[PLAN_CONTEXT_DEVICE_CHALLENGES] = [
|
||||
{
|
||||
"device_class": device.__class__.__name__.lower().replace("device", ""),
|
||||
"device_uid": device.pk,
|
||||
"challenge": {},
|
||||
}
|
||||
]
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session[SESSION_KEY_WEBAUTHN_CHALLENGE] = base64url_to_bytes(
|
||||
"g98I51mQvZXo5lxLfhrD2zfolhZbLRyCgqkkYap1jwSaJ13BguoJWCF9_Lg3AgO4Wh-Bqa556JE20oKsYbl6RA"
|
||||
)
|
||||
|
|
|
@ -32,7 +32,7 @@ services:
|
|||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.3}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
|
@ -50,7 +50,7 @@ services:
|
|||
- "${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
||||
- "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.4.3}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
|
|
@ -38,13 +38,14 @@ type RedisConfig struct {
|
|||
}
|
||||
|
||||
type ListenConfig struct {
|
||||
HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
|
||||
HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
|
||||
LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
|
||||
LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
|
||||
Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
|
||||
Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
|
||||
Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
|
||||
HTTP string `yaml:"listen_http" env:"AUTHENTIK_LISTEN__HTTP"`
|
||||
HTTPS string `yaml:"listen_https" env:"AUTHENTIK_LISTEN__HTTPS"`
|
||||
LDAP string `yaml:"listen_ldap" env:"AUTHENTIK_LISTEN__LDAP"`
|
||||
LDAPS string `yaml:"listen_ldaps" env:"AUTHENTIK_LISTEN__LDAPS"`
|
||||
Radius string `yaml:"listen_radius" env:"AUTHENTIK_LISTEN__RADIUS"`
|
||||
Metrics string `yaml:"listen_metrics" env:"AUTHENTIK_LISTEN__METRICS"`
|
||||
Debug string `yaml:"listen_debug" env:"AUTHENTIK_LISTEN__DEBUG"`
|
||||
TrustedProxyCIDRs []string `yaml:"trusted_proxy_cidrs" env:"AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS"`
|
||||
}
|
||||
|
||||
type PathsConfig struct {
|
||||
|
|
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
|||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2023.4.0"
|
||||
const VERSION = "2023.4.3"
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/config"
|
||||
)
|
||||
|
||||
// ProxyHeaders Set proxy headers like X-Forwarded-For and such, but only if the direct connection
|
||||
// comes from a client that's in a list of trusted CIDRs
|
||||
func ProxyHeaders() func(http.Handler) http.Handler {
|
||||
nets := []*net.IPNet{}
|
||||
for _, rn := range config.Get().Listen.TrustedProxyCIDRs {
|
||||
_, cidr, err := net.ParseCIDR(rn)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
nets = append(nets, cidr)
|
||||
}
|
||||
ph := handlers.ProxyHeaders
|
||||
return func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err == nil {
|
||||
// remoteAddr will be nil if the IP cannot be parsed
|
||||
remoteAddr := net.ParseIP(host)
|
||||
for _, allowedCidr := range nets {
|
||||
if remoteAddr != nil && allowedCidr.Contains(remoteAddr) {
|
||||
log.WithField("remoteAddr", remoteAddr).WithField("cidr", allowedCidr.String()).Trace("Setting proxy headers")
|
||||
ph(h).ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
// Request is not directly coming from a CIDR we "trust"
|
||||
// so set XFF to the direct host IP
|
||||
r.Header.Set("X-Forwarded-For", host)
|
||||
h.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ type WebServer struct {
|
|||
func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
|
||||
l := log.WithField("logger", "authentik.router")
|
||||
mainHandler := mux.NewRouter()
|
||||
mainHandler.Use(handlers.ProxyHeaders)
|
||||
mainHandler.Use(web.ProxyHeaders())
|
||||
mainHandler.Use(handlers.CompressHandler)
|
||||
loggingHandler := mainHandler.NewRoute().Subrouter()
|
||||
loggingHandler.Use(web.NewLoggingHandler(l, nil))
|
||||
|
|
|
@ -105,7 +105,7 @@ filterwarnings = [
|
|||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2023.4.0"
|
||||
version = "2023.4.3"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
|
64
schema.yml
64
schema.yml
|
@ -1,7 +1,7 @@
|
|||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2023.4.0
|
||||
version: 2023.4.3
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
|
@ -4783,6 +4783,38 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/users/{id}/impersonate/:
|
||||
post:
|
||||
operationId: core_users_impersonate_create
|
||||
description: Impersonate a user
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
schema:
|
||||
type: integer
|
||||
description: A unique integer value identifying this User.
|
||||
required: true
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: Successfully started impersonation
|
||||
'401':
|
||||
description: Access denied
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/users/{id}/metrics/:
|
||||
get:
|
||||
operationId: core_users_metrics_retrieve
|
||||
|
@ -4962,6 +4994,29 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/users/impersonate_end/:
|
||||
get:
|
||||
operationId: core_users_impersonate_end_retrieve
|
||||
description: End Impersonation a user
|
||||
tags:
|
||||
- core
|
||||
security:
|
||||
- authentik: []
|
||||
responses:
|
||||
'204':
|
||||
description: Successfully started impersonation
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
description: ''
|
||||
'403':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
description: ''
|
||||
/core/users/me/:
|
||||
get:
|
||||
operationId: core_users_me_retrieve
|
||||
|
@ -40367,12 +40422,6 @@ components:
|
|||
type: object
|
||||
description: Get system information.
|
||||
properties:
|
||||
env:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Get Environment
|
||||
readOnly: true
|
||||
http_headers:
|
||||
type: object
|
||||
additionalProperties:
|
||||
|
@ -40426,7 +40475,6 @@ components:
|
|||
readOnly: true
|
||||
required:
|
||||
- embedded_outpost_host
|
||||
- env
|
||||
- http_headers
|
||||
- http_host
|
||||
- http_is_secure
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
"@codemirror/theme-one-dark": "^6.1.1",
|
||||
"@formatjs/intl-listformat": "^7.1.9",
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@goauthentik/api": "^2023.4.0-1681471246",
|
||||
"@goauthentik/api": "^2023.4.1-1687461872",
|
||||
"@hcaptcha/types": "^1.0.3",
|
||||
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
|
||||
"@lingui/cli": "^3.17.2",
|
||||
|
@ -2026,9 +2026,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2023.4.0-1681471246",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.4.0-1681471246.tgz",
|
||||
"integrity": "sha512-/P9CfSHM4qEe1eaphC5MTYb/4yVrXBqME2amrj9JtK8dItGM/qSGDMIS8v18zZUsbO5fM+RQ/AtL/Izj1COZWA=="
|
||||
"version": "2023.4.1-1687461872",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.4.1-1687461872.tgz",
|
||||
"integrity": "sha512-pY6Lcxyw6K04MAZPJz8JPeQkyR8lFcZPeC/twmSegzVQjrP5ygvpj1WSzJildOQoDAn93jP1BU7oRRPEITgdLg=="
|
||||
},
|
||||
"node_modules/@hcaptcha/types": {
|
||||
"version": "1.0.3",
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
"@codemirror/theme-one-dark": "^6.1.1",
|
||||
"@formatjs/intl-listformat": "^7.1.9",
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@goauthentik/api": "^2023.4.0-1681471246",
|
||||
"@goauthentik/api": "^2023.4.1-1687461872",
|
||||
"@hcaptcha/types": "^1.0.3",
|
||||
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
|
||||
"@lingui/cli": "^3.17.2",
|
||||
|
|
|
@ -31,7 +31,7 @@ import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
|||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { AdminApi, SessionUser, Version } from "@goauthentik/api";
|
||||
import { AdminApi, CoreApi, SessionUser, Version } from "@goauthentik/api";
|
||||
|
||||
autoDetectLanguage();
|
||||
|
||||
|
@ -175,10 +175,11 @@ export class AdminInterface extends Interface {
|
|||
${this.user?.original
|
||||
? html`<ak-sidebar-item
|
||||
?highlight=${true}
|
||||
?isAbsoluteLink=${true}
|
||||
path=${`/-/impersonation/end/?back=${encodeURIComponent(
|
||||
`${window.location.pathname}#${window.location.hash}`,
|
||||
)}`}
|
||||
@click=${() => {
|
||||
new CoreApi(DEFAULT_CONFIG).coreUsersImpersonateEndRetrieve().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span slot="label"
|
||||
>${t`You're currently impersonating ${this.user.user.username}. Click to stop.`}</span
|
||||
|
|
|
@ -115,9 +115,8 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
|
|||
`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="forUser">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||
const args: CoreUsersListRequest = {
|
||||
|
@ -144,7 +143,6 @@ export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
|
|||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
${this.result ? this.renderResult() : html``}
|
||||
</form>`;
|
||||
${this.result ? this.renderResult() : html``}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,9 +21,12 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
|||
});
|
||||
};
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Common Name`} name="commonName" ?required=${true}>
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${t`Common Name`}
|
||||
name="commonName"
|
||||
?required=${true}
|
||||
>
|
||||
<input type="text" class="pf-c-form-control" required />
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Subject-alt name`} name="subjectAltName">
|
||||
|
@ -38,7 +41,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
|
|||
?required=${true}
|
||||
>
|
||||
<input class="pf-c-form-control" type="number" value="365" />
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,15 +87,13 @@ export class FlowImportForm extends Form<Flow> {
|
|||
`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Flow`} name="flow">
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${t`Flow`} name="flow">
|
||||
<input type="file" value="" class="pf-c-form-control" />
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`.yaml files, which can be found on goauthentik.io and can be exported by authentik.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.result ? this.renderResult() : html``}
|
||||
</form>`;
|
||||
${this.result ? this.renderResult() : html``}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,41 +46,39 @@ export class RelatedGroupAdd extends Form<{ groups: string[] }> {
|
|||
return data;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Groups to add`} name="groups">
|
||||
<div class="pf-c-input-group">
|
||||
<ak-user-group-select-table
|
||||
.confirm=${(items: Group[]) => {
|
||||
this.groupsToAdd = items;
|
||||
this.requestUpdate();
|
||||
return Promise.resolve();
|
||||
}}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ak-user-group-select-table>
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group>
|
||||
${this.groupsToAdd.map((group) => {
|
||||
return html`<ak-chip
|
||||
.removable=${true}
|
||||
value=${ifDefined(group.pk)}
|
||||
@remove=${() => {
|
||||
const idx = this.groupsToAdd.indexOf(group);
|
||||
this.groupsToAdd.splice(idx, 1);
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>
|
||||
${group.name}
|
||||
</ak-chip>`;
|
||||
})}
|
||||
</ak-chip-group>
|
||||
</div>
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${t`Groups to add`} name="groups">
|
||||
<div class="pf-c-input-group">
|
||||
<ak-user-group-select-table
|
||||
.confirm=${(items: Group[]) => {
|
||||
this.groupsToAdd = items;
|
||||
this.requestUpdate();
|
||||
return Promise.resolve();
|
||||
}}
|
||||
>
|
||||
<button slot="trigger" class="pf-c-button pf-m-control" type="button">
|
||||
<i class="fas fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</ak-user-group-select-table>
|
||||
<div class="pf-c-form-control">
|
||||
<ak-chip-group>
|
||||
${this.groupsToAdd.map((group) => {
|
||||
return html`<ak-chip
|
||||
.removable=${true}
|
||||
value=${ifDefined(group.pk)}
|
||||
@remove=${() => {
|
||||
const idx = this.groupsToAdd.indexOf(group);
|
||||
this.groupsToAdd.splice(idx, 1);
|
||||
this.requestUpdate();
|
||||
}}
|
||||
>
|
||||
${group.name}
|
||||
</ak-chip>`;
|
||||
})}
|
||||
</ak-chip-group>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
</form> `;
|
||||
</div>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -116,9 +116,8 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
|
|||
`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||
const args: CoreUsersListRequest = {
|
||||
|
@ -155,7 +154,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
|
|||
${t`Set custom attributes using YAML or JSON.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
${this.result ? this.renderResult() : html``}
|
||||
</form>`;
|
||||
${this.result ? this.renderResult() : html``}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,9 +64,63 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
|
|||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
|
||||
renderExampleButtons(): TemplateResult {
|
||||
const header = html`<p>${t`Example context data`}</p>`;
|
||||
switch (this.mapping?.metaModelName) {
|
||||
case "authentik_sources_ldap.ldappropertymapping":
|
||||
return html`${header}${this.renderExampleLDAP()}`;
|
||||
default:
|
||||
return html``;
|
||||
}
|
||||
}
|
||||
|
||||
renderExampleLDAP(): TemplateResult {
|
||||
return html`
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
role="button"
|
||||
@click=${() => {
|
||||
this.request = {
|
||||
user: this.request?.user || 0,
|
||||
context: {
|
||||
ldap: {
|
||||
name: "test-user",
|
||||
objectSid: "S-1-5-21-2611707862-2219215769-354220275-1137",
|
||||
objectClass: "person",
|
||||
displayName: "authentik test user",
|
||||
sAMAccountName: "sAMAccountName",
|
||||
distinguishedName: "cn=user,ou=users,dc=goauthentik,dc=io",
|
||||
},
|
||||
},
|
||||
};
|
||||
}}
|
||||
>
|
||||
${t`Active Directory User`}
|
||||
</button>
|
||||
<button
|
||||
class="pf-c-button pf-m-secondary"
|
||||
role="button"
|
||||
@click=${() => {
|
||||
this.request = {
|
||||
user: this.request?.user || 0,
|
||||
context: {
|
||||
ldap: {
|
||||
name: "test-group",
|
||||
objectSid: "S-1-5-21-2611707862-2219215769-354220275-1137",
|
||||
objectClass: "group",
|
||||
distinguishedName: "cn=group,ou=groups,dc=goauthentik,dc=io",
|
||||
},
|
||||
},
|
||||
};
|
||||
}}
|
||||
>
|
||||
${t`Active Directory Group`}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${t`User`} ?required=${true} name="user">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<User[]> => {
|
||||
const args: CoreUsersListRequest = {
|
||||
|
@ -98,7 +152,6 @@ export class PolicyTestForm extends Form<PolicyTestRequest> {
|
|||
>>
|
||||
</ak-codemirror>
|
||||
</ak-form-element-horizontal>
|
||||
${this.result ? this.renderResult() : html``}
|
||||
</form>`;
|
||||
${this.result ? this.renderResult() : html``}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,9 +37,8 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
|||
});
|
||||
};
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
|
||||
<input type="text" class="pf-c-form-control" required />
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
|
@ -77,7 +76,6 @@ export class SAMLProviderImportForm extends Form<SAMLProvider> {
|
|||
|
||||
<ak-form-element-horizontal label=${t`Metadata`} name="metadata">
|
||||
<input type="file" value="" class="pf-c-form-control" />
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,9 +59,8 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
|
|||
return data;
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
${this.group?.isSuperuser ? html`` : html``}
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`${this.group?.isSuperuser ? html`` : html``}
|
||||
<ak-form-element-horizontal label=${t`Users to add`} name="users">
|
||||
<div class="pf-c-input-group">
|
||||
<ak-group-member-select-table
|
||||
|
@ -93,8 +92,7 @@ export class RelatedUserAdd extends Form<{ users: number[] }> {
|
|||
</ak-chip-group>
|
||||
</div>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
</form> `;
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,12 +191,20 @@ export class RelatedUserList extends Table<User> {
|
|||
</ak-forms-modal>
|
||||
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.Impersonate)
|
||||
? html`
|
||||
<a
|
||||
class="pf-c-button pf-m-tertiary"
|
||||
href="${`/-/impersonation/${item.pk}/`}"
|
||||
<ak-action-button
|
||||
class="pf-m-tertiary"
|
||||
.apiRequest=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersImpersonateCreate({
|
||||
id: item.pk,
|
||||
})
|
||||
.then(() => {
|
||||
window.location.href = "/";
|
||||
});
|
||||
}}
|
||||
>
|
||||
${t`Impersonate`}
|
||||
</a>
|
||||
</ak-action-button>
|
||||
`
|
||||
: html``}`,
|
||||
];
|
||||
|
|
|
@ -35,9 +35,8 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
|
|||
this.result = undefined;
|
||||
}
|
||||
|
||||
renderRequestForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name">
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="name">
|
||||
<input type="text" value="" class="pf-c-form-control" required />
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`User's primary identifier. 150 characters or fewer.`}
|
||||
|
@ -78,8 +77,7 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
|
|||
value="${dateTimeLocal(new Date(Date.now() + 1000 * 60 ** 2 * 24 * 360))}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
|
||||
renderResponseForm(): TemplateResult {
|
||||
|
@ -113,6 +111,6 @@ export class ServiceAccountForm extends Form<UserServiceAccountRequest> {
|
|||
if (this.result) {
|
||||
return this.renderResponseForm();
|
||||
}
|
||||
return this.renderRequestForm();
|
||||
return super.renderForm();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,12 +151,20 @@ export class UserListPage extends TablePage<User> {
|
|||
</ak-forms-modal>
|
||||
${rootInterface()?.config?.capabilities.includes(CapabilitiesEnum.Impersonate)
|
||||
? html`
|
||||
<a
|
||||
class="pf-c-button pf-m-tertiary"
|
||||
href="${`/-/impersonation/${item.pk}/`}"
|
||||
<ak-action-button
|
||||
class="pf-m-tertiary"
|
||||
.apiRequest=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersImpersonateCreate({
|
||||
id: item.pk,
|
||||
})
|
||||
.then(() => {
|
||||
window.location.href = "/";
|
||||
});
|
||||
}}
|
||||
>
|
||||
${t`Impersonate`}
|
||||
</a>
|
||||
</ak-action-button>
|
||||
`
|
||||
: html``}`,
|
||||
];
|
||||
|
|
|
@ -26,11 +26,13 @@ export class UserPasswordForm extends Form<UserPasswordSetRequest> {
|
|||
});
|
||||
};
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Password`} ?required=${true} name="password">
|
||||
<input type="password" value="" class="pf-c-form-control" required />
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${t`Password`}
|
||||
?required=${true}
|
||||
name="password"
|
||||
>
|
||||
<input type="password" value="" class="pf-c-form-control" required />
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,32 +32,34 @@ export class UserResetEmailForm extends Form<CoreUsersRecoveryEmailRetrieveReque
|
|||
return new CoreApi(DEFAULT_CONFIG).coreUsersRecoveryEmailRetrieve(data);
|
||||
};
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Email stage`} ?required=${true} name="emailStage">
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
|
||||
const args: StagesAllListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
|
||||
return stages.results;
|
||||
}}
|
||||
.groupBy=${(items: Stage[]) => {
|
||||
return groupBy(items, (stage) => stage.verboseNamePlural);
|
||||
}}
|
||||
.renderElement=${(stage: Stage): string => {
|
||||
return stage.name;
|
||||
}}
|
||||
.value=${(stage: Stage | undefined): string | undefined => {
|
||||
return stage?.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>
|
||||
</form>`;
|
||||
renderInlineForm(): TemplateResult {
|
||||
return html`<ak-form-element-horizontal
|
||||
label=${t`Email stage`}
|
||||
?required=${true}
|
||||
name="emailStage"
|
||||
>
|
||||
<ak-search-select
|
||||
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
|
||||
const args: StagesAllListRequest = {
|
||||
ordering: "name",
|
||||
};
|
||||
if (query !== undefined) {
|
||||
args.search = query;
|
||||
}
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesEmailList(args);
|
||||
return stages.results;
|
||||
}}
|
||||
.groupBy=${(items: Stage[]) => {
|
||||
return groupBy(items, (stage) => stage.verboseNamePlural);
|
||||
}}
|
||||
.renderElement=${(stage: Stage): string => {
|
||||
return stage.name;
|
||||
}}
|
||||
.value=${(stage: Stage | undefined): string | undefined => {
|
||||
return stage?.pk;
|
||||
}}
|
||||
>
|
||||
</ak-search-select>
|
||||
</ak-form-element-horizontal>`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -201,12 +201,20 @@ export class UserViewPage extends AKElement {
|
|||
)
|
||||
? html`
|
||||
<div class="pf-c-card__footer">
|
||||
<a
|
||||
class="pf-c-button pf-m-tertiary"
|
||||
href="${`/-/impersonation/${this.user?.pk}/`}"
|
||||
<ak-action-button
|
||||
class="pf-m-tertiary"
|
||||
.apiRequest=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersImpersonateCreate({
|
||||
id: this.user?.pk || 0,
|
||||
})
|
||||
.then(() => {
|
||||
window.location.href = "/";
|
||||
});
|
||||
}}
|
||||
>
|
||||
${t`Impersonate`}
|
||||
</a>
|
||||
</ak-action-button>
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
|
|
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
|||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2023.4.0";
|
||||
export const VERSION = "2023.4.3";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
|
|
@ -46,6 +46,7 @@ export class Diagram extends AKElement {
|
|||
flowchart: {
|
||||
curve: "linear",
|
||||
},
|
||||
htmlLabels: false,
|
||||
};
|
||||
mermaid.initialize(this.config);
|
||||
}
|
||||
|
|
|
@ -279,9 +279,23 @@ export abstract class Form<T> extends AKElement {
|
|||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
const inline = this.renderInlineForm();
|
||||
if (inline) {
|
||||
return html`<form class="pf-c-form pf-m-horizontal" @submit=${this.submit}>
|
||||
${inline}
|
||||
</form>`;
|
||||
}
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline form render callback when inheriting this class, should be overwritten
|
||||
* instead of `this.renderForm`
|
||||
*/
|
||||
renderInlineForm(): TemplateResult | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
renderNonFieldErrors(): TemplateResult {
|
||||
if (!this.nonFieldErrors) {
|
||||
return html``;
|
||||
|
|
|
@ -11,6 +11,7 @@ import { me } from "@goauthentik/common/users";
|
|||
import { first } from "@goauthentik/common/utils";
|
||||
import { WebsocketClient } from "@goauthentik/common/ws";
|
||||
import { Interface } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/buttons/ActionButton";
|
||||
import "@goauthentik/elements/messages/MessageContainer";
|
||||
import "@goauthentik/elements/notifications/APIDrawer";
|
||||
import "@goauthentik/elements/notifications/NotificationDrawer";
|
||||
|
@ -36,7 +37,7 @@ import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
|||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFDisplay from "@patternfly/patternfly/utilities/Display/display.css";
|
||||
|
||||
import { EventsApi, SessionUser } from "@goauthentik/api";
|
||||
import { CoreApi, EventsApi, SessionUser } from "@goauthentik/api";
|
||||
|
||||
autoDetectLanguage();
|
||||
|
||||
|
@ -234,18 +235,23 @@ export class UserInterface extends Interface {
|
|||
: html``}
|
||||
</div>
|
||||
${this.me.original
|
||||
? html`<div class="pf-c-page__header-tools">
|
||||
<div class="pf-c-page__header-tools-group">
|
||||
<a
|
||||
class="pf-c-button pf-m-warning pf-m-small"
|
||||
href=${`/-/impersonation/end/?back=${encodeURIComponent(
|
||||
`${window.location.pathname}#${window.location.hash}`,
|
||||
)}`}
|
||||
>
|
||||
${t`Stop impersonation`}
|
||||
</a>
|
||||
</div>
|
||||
</div>`
|
||||
? html`
|
||||
<div class="pf-c-page__header-tools">
|
||||
<div class="pf-c-page__header-tools-group">
|
||||
<ak-action-button
|
||||
class="pf-m-warning pf-m-small"
|
||||
.apiRequest=${() => {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersImpersonateEndRetrieve()
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}}
|
||||
>
|
||||
${t`Stop impersonation`}
|
||||
</ak-action-button>
|
||||
</div>
|
||||
</div>`
|
||||
: html``}
|
||||
<div class="pf-c-page__header-tools-group">
|
||||
<div class="pf-c-page__header-tools-item pf-m-hidden pf-m-visible-on-md">
|
||||
|
|
|
@ -57,6 +57,11 @@ kubectl exec -it deployment/authentik-worker -c authentik -- ak dump_config
|
|||
- `AUTHENTIK_LISTEN__LDAPS`: Listening address:port (e.g. `0.0.0.0:6636`) for LDAPS (LDAP outpost)
|
||||
- `AUTHENTIK_LISTEN__METRICS`: Listening address:port (e.g. `0.0.0.0:9300`) for Prometheus metrics (All)
|
||||
- `AUTHENTIK_LISTEN__DEBUG`: Listening address:port (e.g. `0.0.0.0:9900`) for Go Debugging metrics (All)
|
||||
- `AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS`: List of CIDRs that proxy headers should be accepted from (Server)
|
||||
|
||||
Defaults to `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fe80::/10`, `::1/128`.
|
||||
|
||||
Requests directly coming from one an address within a CIDR specified here are able to set proxy headers, such as `X-Forwarded-For`. Requests coming from other addresses will not be able to set these headers.
|
||||
|
||||
## authentik Settings
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# CVE-2023-36456
|
||||
|
||||
_Reported by [@thijsa](https://github.com/thijsa)_
|
||||
|
||||
## Lack of Proxy IP headers validation
|
||||
|
||||
### Summary
|
||||
|
||||
authentik does not verify the source of the X-Forwarded-For and X-Real-IP headers, both in the Python code and the go code.
|
||||
|
||||
### Impact
|
||||
|
||||
Only authentik setups that are directly accessible by users without a reverse proxy are susceptible to this. Possible spoofing of IP addresses in logs, downstream applications proxied by (built in) outpost, IP bypassing in custom flows if used.
|
||||
|
||||
### Details
|
||||
|
||||
This poses a possible security risk when you have flows or policies that check the user's IP address, e.g. when you want to ignore the user's 2 factor authentication when the user is connected to the company network.
|
||||
|
||||
Another security risk is that the IP addresses in the logfiles and user sessions is not reliable anymore, anybody can spoof this address and you cannot verify that the user has logged in from the IP address that is in their account's log.
|
||||
|
||||
And the third risk is that this header is passed on to the proxied application behind an outpost. The application may do any kind of verification, logging, blocking or rate limiting based on the IP address, and this IP address can be overridden by anybody that want to.
|
|
@ -322,10 +322,11 @@ module.exports = {
|
|||
},
|
||||
items: [
|
||||
"security/policy",
|
||||
"security/CVE-2023-36456",
|
||||
"security/CVE-2023-26481",
|
||||
"security/CVE-2022-23555",
|
||||
"security/CVE-2022-46145",
|
||||
"security/CVE-2022-46172",
|
||||
"security/CVE-2023-26481",
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
Reference in New Issue