Compare commits

..

30 Commits

Author SHA1 Message Date
63cfbb721c release: 2023.2.3 2023-03-02 20:17:51 +01:00
2b74a1f03b security: fix CVE-2023-26481 (#4832)
fix CVE-2023-26481

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-02 20:16:29 +01:00
093573f89a website: always show build version in version dropdown
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#3940
2023-02-16 14:39:17 +01:00
d842fc4958 release: 2023.2.2 2023-02-15 19:53:42 +01:00
19f5e6e07e website/docs: update events page
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:44:13 +01:00
acfa9c76d1 providers/ldap: check MFA password on password stage
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:27:08 +01:00
bff34cc5dc root: use channel send workaround for sync sending of websocket messages
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:08:01 +01:00
7f009f6d02 flows: include flow authentication requirement in diagram
closes #4533

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:04:45 +01:00
dfb9ae548c web/admin: fix error when creating new users
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#4685
2023-02-15 15:32:48 +01:00
7d6b573f8b website: migrate to mermaid charts, rework proxy page
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 12:14:17 +01:00
ade397fc24 web/user: revert truncate behaviour for application description
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 11:17:45 +01:00
d945d30cda providers/proxy: fix value is too long with filesystem sessions
closes #4693

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 10:50:01 +01:00
c8c401e2c5 lib: don't try to cache generated avatar with full user, only cache with name
closes #4690

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 10:49:13 +01:00
e4ca20bfc6 core: bump golang from 1.20.0-bullseye to 1.20.1-bullseye (#4691)
Bumps golang from 1.20.0-bullseye to 1.20.1-bullseye.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 10:46:02 +01:00
6347716815 core: bump goauthentik.io/api/v3 from 3.2023012.5 to 3.2023021.1 (#4692)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023012.5 to 3.2023021.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023012.5...v3.2023021.1)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 10:45:20 +01:00
859b6cc60e website: adjust padding on hero header
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 00:28:45 +01:00
06a1a7f076 ci: add time limits to ci jobs
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 00:28:36 +01:00
b6c120f555 providers/proxy: fix client credential flows not using http interceptor
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 00:22:56 +01:00
6cc363bc5d web: bump API Client version (#4689)
Signed-off-by: GitHub <noreply@github.com>
2023-02-14 20:35:22 +01:00
80de3ee853 release: 2023.2.1 2023-02-14 18:52:36 +01:00
c340830b37 website/docs: prepare 2023.2.1
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-14 18:19:02 +01:00
472b9ea866 web: bump API Client version (#4686)
Signed-off-by: GitHub <noreply@github.com>
2023-02-14 15:28:30 +00:00
4c5b07a091 web/admin: use full page size for modals
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-14 16:16:26 +01:00
fbd00bf5f8 web/admin: remove groups and users from users and group form to prevent accidental removal when updating
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-14 16:15:43 +01:00
a598276aa0 web/admin: improve action button spinner on ldap source page
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-14 16:07:31 +01:00
deb91bd12b sources/ldap: add LDAP Debug endpoint
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-14 16:06:54 +01:00
92b8cf1b64 web: bump API Client version (#4684)
Signed-off-by: GitHub <noreply@github.com>
2023-02-14 13:39:33 +00:00
58e001c3d5 internal: fix scheme not being forwarded correctly for host intercepted requests
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-14 14:31:04 +01:00
54ac920f81 Merge branch 'version-2023.2' 2023-02-14 13:58:03 +01:00
fc9ae9e938 website: include 2023.2 in sidebar
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-14 13:17:55 +01:00
57 changed files with 1681 additions and 327 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2023.2.0
current_version = 2023.2.3
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -80,6 +80,7 @@ jobs:
run: poetry run python -m lifecycle.migrate
test-unittest:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Setup authentik env
@ -94,6 +95,7 @@ jobs:
flags: unit
test-integration:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Setup authentik env
@ -111,6 +113,7 @@ jobs:
test-e2e:
name: test-e2e (${{ matrix.job.name }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:

View File

@ -31,7 +31,7 @@ RUN pip install --no-cache-dir poetry && \
poetry export -f requirements.txt --dev --output requirements-dev.txt
# Stage 4: Build go proxy
FROM docker.io/golang:1.20.0-bullseye AS go-builder
FROM docker.io/golang:1.20.1-bullseye AS go-builder
WORKDIR /work

View File

@ -2,7 +2,7 @@
from os import environ
from typing import Optional
__version__ = "2023.2.0"
__version__ = "2023.2.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -8,7 +8,7 @@ from rest_framework.serializers import CharField
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
from authentik.flows.models import Flow, FlowStageBinding
from authentik.flows.models import Flow, FlowAuthenticationRequirement, FlowStageBinding
@dataclass
@ -160,12 +160,37 @@ class FlowDiagram:
)
return stages + elements
def get_flow_auth_requirement(self) -> list[DiagramElement]:
"""Get flow authentication requirement"""
end_el = DiagramElement(
"done",
_("End of the flow"),
_("Requirement not fulfilled"),
style=["[[", "]]"],
)
elements = []
if self.flow.authentication == FlowAuthenticationRequirement.NONE:
return []
auth = DiagramElement(
"flow_auth_requirement",
_("Flow authentication requirement") + "\n" + self.flow.authentication,
)
elements.append(auth)
end_el.source = [auth]
elements.append(end_el)
elements.append(
DiagramElement("flow_start", "placeholder", _("Requirement fulfilled"), source=[auth])
)
return elements
def build(self) -> str:
"""Build flowchart"""
all_elements = [
"graph TD",
]
all_elements.extend(self.get_flow_auth_requirement())
pre_flow_policies_element = DiagramElement(
"flow_pre", _("Pre-flow policies"), style=["[[", "]]"]
)
@ -179,6 +204,7 @@ class FlowDiagram:
_("End of the flow"),
_("Policy denied"),
flow_policies,
style=["[[", "]]"],
)
)

View File

@ -162,7 +162,7 @@ class FlowExecutorView(APIView):
token.delete()
if not isinstance(plan, FlowPlan):
return None
plan.context[PLAN_CONTEXT_IS_RESTORED] = True
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
self._logger.debug("f(exec): restored flow plan from token", plan=plan)
return plan

View File

@ -86,7 +86,7 @@ def generate_colors(text: str) -> tuple[str, str]:
@cache
# pylint: disable=too-many-arguments,too-many-locals
def generate_avatar_from_name(
user: "User",
name: str,
length: int = 2,
size: int = 64,
rounded: bool = False,
@ -98,8 +98,6 @@ def generate_avatar_from_name(
Inspired from: https://github.com/LasseRafn/ui-avatars
"""
name = user.name if user.name != "" else "a k"
name_parts = name.split()
# Only abbreviate first and last name
if len(name_parts) > 2:
@ -152,7 +150,7 @@ def generate_avatar_from_name(
def avatar_mode_generated(user: "User", mode: str) -> Optional[str]:
"""Wrapper that converts generated avatar to base64 svg"""
svg = generate_avatar_from_name(user)
svg = generate_avatar_from_name(user.name if user.name != "" else "a k")
return f"data:image/svg+xml;base64,{b64encode(svg.encode('utf-8')).decode('utf-8')}"

View File

@ -7,7 +7,6 @@ from urllib.parse import urlparse
import yaml
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.core.cache import cache
from django.db import DatabaseError, InternalError, ProgrammingError
from django.db.models.base import Model
@ -43,6 +42,7 @@ from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesContro
from authentik.providers.proxy.controllers.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.root.celery import CELERY_APP
from authentik.root.messages.storage import closing_send
LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s"
@ -217,26 +217,23 @@ def outpost_post_save(model_class: str, model_pk: Any):
def outpost_send_update(model_instace: Model):
"""Send outpost update to all registered outposts, regardless to which authentik
instance they are connected"""
channel_layer = get_channel_layer()
if isinstance(model_instace, OutpostModel):
for outpost in model_instace.outpost_set.all():
_outpost_single_update(outpost, channel_layer)
_outpost_single_update(outpost)
elif isinstance(model_instace, Outpost):
_outpost_single_update(model_instace, channel_layer)
_outpost_single_update(model_instace)
def _outpost_single_update(outpost: Outpost, layer=None):
def _outpost_single_update(outpost: Outpost):
"""Update outpost instances connected to a single outpost"""
# Ensure token again, because this function is called when anything related to an
# OutpostModel is saved, so we can be sure permissions are right
_ = outpost.token
outpost.build_user_permissions(outpost.user)
if not layer: # pragma: no cover
layer = get_channel_layer()
for state in OutpostState.for_outpost(outpost):
for channel in state.channel_ids:
LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost)
async_to_sync(layer.send)(channel, {"type": "event.update"})
async_to_sync(closing_send)(channel, {"type": "event.update"})
@CELERY_APP.task()

View File

@ -1,6 +1,7 @@
"""Channels Messages storage"""
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from channels import DEFAULT_CHANNEL_LAYER
from channels.layers import channel_layers
from django.contrib.messages.storage.base import Message
from django.contrib.messages.storage.session import SessionStorage
from django.core.cache import cache
@ -10,13 +11,21 @@ SESSION_KEY = "_messages"
CACHE_PREFIX = "goauthentik.io/root/messages_"
async def closing_send(channel, message):
"""Wrapper around layer send that closes the connection"""
# See https://github.com/django/channels_redis/issues/332
# TODO: Remove this after channels_redis 4.1 is released
channel_layer = channel_layers.make_backend(DEFAULT_CHANNEL_LAYER)
await channel_layer.send(channel, message)
await channel_layer.close_pools()
class ChannelsStorage(SessionStorage):
"""Send contrib.messages over websocket"""
def __init__(self, request: HttpRequest) -> None:
# pyright: reportGeneralTypeIssues=false
super().__init__(request)
self.channel = get_channel_layer()
def _store(self, messages: list[Message], response, *args, **kwargs):
prefix = f"{CACHE_PREFIX}{self.request.session.session_key}_messages_"
@ -28,7 +37,7 @@ class ChannelsStorage(SessionStorage):
for key in keys:
uid = key.replace(prefix, "")
for message in messages:
async_to_sync(self.channel.send)(
async_to_sync(closing_send)(
uid,
{
"type": "event.update",

View File

@ -4,9 +4,10 @@ from typing import Any
from django_filters.filters import AllValuesMultipleFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, extend_schema_field
from drf_spectacular.utils import extend_schema, extend_schema_field, inline_serializer
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import DictField, ListField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
@ -104,11 +105,38 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
results = []
for sync_class in SYNC_CLASSES:
sync_name = sync_class.__name__.replace("LDAPSynchronizer", "").lower()
task = TaskInfo.by_name(f"ldap_sync/{source.slug}_{sync_name}")
task = TaskInfo.by_name(f"ldap_sync:{source.slug}:{sync_name}")
if task:
results.append(task)
return Response(TaskSerializer(results, many=True).data)
@extend_schema(
responses={
200: inline_serializer(
"LDAPDebugSerializer",
fields={
"user": ListField(child=DictField(), read_only=True),
"group": ListField(child=DictField(), read_only=True),
"membership": ListField(child=DictField(), read_only=True),
},
),
}
)
@action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[])
def debug(self, request: Request, slug: str) -> Response:
"""Get raw LDAP data to debug"""
source = self.get_object()
all_objects = {}
for sync_class in SYNC_CLASSES:
class_name = sync_class.__name__.replace("LDAPSynchronizer", "").lower()
all_objects.setdefault(class_name, [])
for obj in sync_class(source).get_objects(size_limit=10):
obj: dict
obj.pop("raw_attributes", None)
obj.pop("raw_dn", None)
all_objects[class_name].append(obj)
return Response(data=all_objects)
class LDAPPropertyMappingSerializer(PropertyMappingSerializer):
"""LDAP PropertyMapping Serializer"""

View File

@ -1,8 +1,9 @@
"""authentik LDAP Authentication Backend"""
from typing import Optional
import ldap3
from django.http import HttpRequest
from ldap3 import Connection
from ldap3.core.exceptions import LDAPException, LDAPInvalidCredentialsResult
from structlog.stdlib import get_logger
from authentik.core.auth import InbuiltBackend
@ -57,7 +58,7 @@ class LDAPBackend(InbuiltBackend):
# Try to bind as new user
LOGGER.debug("Attempting Binding as user", user=user)
try:
temp_connection = ldap3.Connection(
temp_connection = Connection(
source.server,
user=user.attributes.get(LDAP_DISTINGUISHED_NAME),
password=password,
@ -66,8 +67,8 @@ class LDAPBackend(InbuiltBackend):
)
temp_connection.bind()
return user
except ldap3.core.exceptions.LDAPInvalidCredentialsResult as exception:
except LDAPInvalidCredentialsResult as exception:
LOGGER.debug("LDAPInvalidCredentialsResult", user=user, error=exception)
except ldap3.core.exceptions.LDAPException as exception:
except LDAPException as exception:
LOGGER.warning(exception)
return None

View File

@ -3,7 +3,7 @@ from enum import IntFlag
from re import split
from typing import Optional
import ldap3
from ldap3 import BASE
from ldap3.core.exceptions import LDAPAttributeError
from structlog.stdlib import get_logger
@ -64,7 +64,7 @@ class LDAPPasswordChanger:
root_attrs = self._source.connection.extend.standard.paged_search(
search_base=root_dn,
search_filter="(objectClass=*)",
search_scope=ldap3.BASE,
search_scope=BASE,
attributes=["pwdProperties"],
)
root_attrs = list(root_attrs)[0]
@ -97,7 +97,7 @@ class LDAPPasswordChanger:
self._source.connection.extend.standard.paged_search(
search_base=user_dn,
search_filter=self._source.user_object_filter,
search_scope=ldap3.BASE,
search_scope=BASE,
attributes=["displayName", "sAMAccountName"],
)
)

View File

@ -1,5 +1,5 @@
"""Sync LDAP Users and groups into authentik"""
from typing import Any
from typing import Any, Generator
from django.db.models.base import Model
from django.db.models.query import QuerySet
@ -47,9 +47,16 @@ class BaseLDAPSynchronizer:
def message(self, *args, **kwargs):
"""Add message that is later added to the System Task and shown to the user"""
self._messages.append(" ".join(args))
formatted_message = " ".join(args)
if "dn" in kwargs:
formatted_message += f"; DN: {kwargs['dn']}"
self._messages.append(formatted_message)
self._logger.warning(*args, **kwargs)
def get_objects(self, **kwargs) -> Generator:
"""Get objects from LDAP, implemented in subclass"""
raise NotImplementedError()
def sync(self) -> int:
"""Sync function, implemented in subclass"""
raise NotImplementedError()

View File

@ -1,8 +1,9 @@
"""Sync LDAP Users and groups into authentik"""
import ldap3
import ldap3.core.exceptions
from typing import Generator
from django.core.exceptions import FieldError
from django.db.utils import IntegrityError
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
from authentik.core.models import Group
from authentik.events.models import Event, EventAction
@ -12,19 +13,24 @@ from authentik.sources.ldap.sync.base import LDAP_UNIQUENESS, BaseLDAPSynchroniz
class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
"""Sync LDAP Users and groups into authentik"""
def get_objects(self, **kwargs) -> Generator:
return self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
**kwargs,
)
def sync(self) -> int:
"""Iterate over all LDAP Groups and create authentik_core.Group instances"""
if not self._source.sync_groups:
self.message("Group syncing is disabled for this Source")
return -1
groups = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
)
group_count = 0
for group in groups:
for group in self.get_objects():
if "attributes" not in group:
continue
attributes = group.get("attributes", {})
group_dn = self._flatten(self._flatten(group.get("entryDN", group.get("dn"))))
if self._source.object_uniqueness_field not in attributes:

View File

@ -1,9 +1,8 @@
"""Sync LDAP Users and groups into authentik"""
from typing import Any, Optional
from typing import Any, Generator, Optional
import ldap3
import ldap3.core.exceptions
from django.db.models import Q
from ldap3 import SUBTREE
from authentik.core.models import Group, User
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
@ -20,23 +19,28 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
super().__init__(source)
self.group_cache: dict[str, Group] = {}
def sync(self) -> int:
"""Iterate over all Users and assign Groups using memberOf Field"""
if not self._source.sync_groups:
self.message("Group syncing is disabled for this Source")
return -1
groups = self._source.connection.extend.standard.paged_search(
def get_objects(self, **kwargs) -> Generator:
return self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter,
search_scope=ldap3.SUBTREE,
search_scope=SUBTREE,
attributes=[
self._source.group_membership_field,
self._source.object_uniqueness_field,
LDAP_DISTINGUISHED_NAME,
],
**kwargs,
)
def sync(self) -> int:
"""Iterate over all Users and assign Groups using memberOf Field"""
if not self._source.sync_groups:
self.message("Group syncing is disabled for this Source")
return -1
membership_count = 0
for group in groups:
for group in self.get_objects():
if "attributes" not in group:
continue
members = group.get("attributes", {}).get(self._source.group_membership_field, [])
ak_group = self.get_group(group)
if not ak_group:

View File

@ -1,8 +1,9 @@
"""Sync LDAP Users into authentik"""
import ldap3
import ldap3.core.exceptions
from typing import Generator
from django.core.exceptions import FieldError
from django.db.utils import IntegrityError
from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
from authentik.core.models import User
from authentik.events.models import Event, EventAction
@ -14,19 +15,24 @@ from authentik.sources.ldap.sync.vendor.ms_ad import MicrosoftActiveDirectory
class UserLDAPSynchronizer(BaseLDAPSynchronizer):
"""Sync LDAP Users into authentik"""
def get_objects(self, **kwargs) -> Generator:
return self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=self._source.user_object_filter,
search_scope=SUBTREE,
attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
**kwargs,
)
def sync(self) -> int:
"""Iterate over all LDAP Users and create authentik_core.User instances"""
if not self._source.sync_users:
self.message("User syncing is disabled for this Source")
return -1
users = self._source.connection.extend.standard.paged_search(
search_base=self.base_dn_users,
search_filter=self._source.user_object_filter,
search_scope=ldap3.SUBTREE,
attributes=[ldap3.ALL_ATTRIBUTES, ldap3.ALL_OPERATIONAL_ATTRIBUTES],
)
user_count = 0
for user in users:
for user in self.get_objects():
if "attributes" not in user:
continue
attributes = user.get("attributes", {})
user_dn = self._flatten(user.get("entryDN", user.get("dn")))
if self._source.object_uniqueness_field not in attributes:

View File

@ -1,6 +1,6 @@
"""FreeIPA specific"""
from datetime import datetime
from typing import Any
from typing import Any, Generator
from pytz import UTC
@ -11,6 +11,9 @@ from authentik.sources.ldap.sync.base import BaseLDAPSynchronizer
class FreeIPA(BaseLDAPSynchronizer):
"""FreeIPA-specific LDAP"""
def get_objects(self, **kwargs) -> Generator:
yield None
def sync(self, attributes: dict[str, Any], user: User, created: bool):
self.check_pwd_last_set(attributes, user, created)

View File

@ -1,7 +1,7 @@
"""Active Directory specific"""
from datetime import datetime
from enum import IntFlag
from typing import Any
from typing import Any, Generator
from pytz import UTC
@ -42,6 +42,9 @@ class UserAccountControl(IntFlag):
class MicrosoftActiveDirectory(BaseLDAPSynchronizer):
"""Microsoft-specific LDAP"""
def get_objects(self, **kwargs) -> Generator:
yield None
def sync(self, attributes: dict[str, Any], user: User, created: bool):
self.ms_check_pwd_last_set(attributes, user, created)
self.ms_check_uac(attributes, user)

View File

@ -44,7 +44,7 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
# to set the state with
return
sync = path_to_class(sync_class)
self.set_uid(f"{source.slug}_{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
self.set_uid(f"{source.slug}:{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
try:
sync_inst = sync(source)
count = sync_inst.sync()

View File

@ -15,7 +15,7 @@ from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTyp
from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_GET
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -103,12 +103,14 @@ class EmailStageView(ChallengeStageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if the user came back from the email link to verify
if QS_KEY_TOKEN in request.session.get(
SESSION_KEY_GET, {}
) and self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, False):
restore_token: FlowToken = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, None)
user = self.get_pending_user()
if restore_token:
if restore_token.user != user:
self.logger.warning("Flow token for non-matching user, denying request")
return self.executor.stage_invalid()
messages.success(request, _("Successfully verified Email."))
if self.executor.current_stage.activate_user_on_success:
user = self.get_pending_user()
user.is_active = True
user.save()
return self.executor.stage_ok()

View File

@ -7,10 +7,9 @@ from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
from django.urls import reverse
from django.utils.http import urlencode
from authentik.core.models import Token
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
@ -134,7 +133,7 @@ class TestEmailStage(FlowTestCase):
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
token: Token = Token.objects.get(user=self.user)
token: FlowToken = FlowToken.objects.get(user=self.user)
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
# Call the executor shell to preseed the session
@ -165,3 +164,43 @@ class TestEmailStage(FlowTestCase):
plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER], self.user)
self.assertTrue(plan.context[PLAN_CONTEXT_PENDING_USER].is_active)
def test_token_invalid_user(self):
"""Test with token with invalid user"""
# Make sure token exists
self.test_pending_user()
self.user.is_active = False
self.user.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
# Set flow token user to a different user
token: FlowToken = FlowToken.objects.get(user=self.user)
token.user = create_test_admin_user()
token.save()
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
# Call the executor shell to preseed the session
url = reverse(
"authentik_api:flow-executor",
kwargs={"flow_slug": self.flow.slug},
)
url_query = urlencode(
{
QS_KEY_TOKEN: token.key,
}
)
url += f"?query={url_query}"
self.client.get(url)
# Call the actual executor to get the JSON Response
response = self.client.get(
reverse(
"authentik_api:flow-executor",
kwargs={"flow_slug": self.flow.slug},
)
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(response, component="ak-stage-access-denied")

View File

@ -154,6 +154,7 @@ entries:
policy: !KeyOf default-recovery-skip-if-restored
target: !KeyOf flow-binding-email
order: 0
state: absent
model: authentik_policies.policybinding
attrs:
negate: false

View File

@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.2.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.2.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.2.0}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.2.3}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

@ -25,7 +25,7 @@ require (
github.com/prometheus/client_golang v1.14.0
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.1
goauthentik.io/api/v3 v3.2023012.5
goauthentik.io/api/v3 v3.2023021.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f

9
go.sum
View File

@ -380,10 +380,8 @@ go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZp
go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
goauthentik.io/api/v3 v3.2023012.4 h1:Vn85T5WQOEZfyimcAA6Anf4f+S68Lw3Ap5+v9d4+hZA=
goauthentik.io/api/v3 v3.2023012.4/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
goauthentik.io/api/v3 v3.2023012.5 h1:MRFw5LuRboeb3tD3gl1yRBuAwBLWfOdO9EGXcn/5yIk=
goauthentik.io/api/v3 v3.2023012.5/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
goauthentik.io/api/v3 v3.2023021.1 h1:UH9MiG9tRvbsV4UeCqvtjxxdl69Z3nIMsulwk9Sn1zM=
goauthentik.io/api/v3 v3.2023021.1/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
@ -528,8 +526,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -590,7 +586,6 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064 h1:BmCFkEH4nJrYcAc2L08yX5RhYGD4j58PTMkEUDkpz2I=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2023.2.0"
const VERSION = "2023.2.3"

View File

@ -25,6 +25,7 @@ func (fe *FlowExecutor) solveChallenge_Identification(challenge *api.ChallengeTy
}
func (fe *FlowExecutor) solveChallenge_Password(challenge *api.ChallengeTypes, req api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
fe.checkPasswordMFA()
r := api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword))
return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
}

View File

@ -36,7 +36,7 @@ func (a *Application) attemptBasicAuth(username, password string) *Claims {
return nil
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := a.httpClient.Do(req)
res, err := a.publicHostHTTPClient.Do(req)
if err != nil || res.StatusCode > 200 {
b, err := io.ReadAll(res.Body)
if err != nil {

View File

@ -39,7 +39,7 @@ func (a *Application) attemptBearerAuth(token string) *TokenIntrospectionRespons
return nil
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := a.httpClient.Do(req)
res, err := a.publicHostHTTPClient.Do(req)
if err != nil || res.StatusCode > 200 {
a.log.WithError(err).Warning("failed to send introspection request")
return nil

View File

@ -1,6 +1,8 @@
package codecs
import (
"math"
"github.com/gorilla/securecookie"
log "github.com/sirupsen/logrus"
)
@ -12,6 +14,7 @@ type Codec struct {
func New(maxAge int, hashKey, blockKey []byte) *Codec {
cookie := securecookie.New(hashKey, blockKey)
cookie.MaxAge(maxAge)
cookie.MaxLength(math.MaxInt)
return &Codec{
SecureCookie: cookie,
}

View File

@ -8,12 +8,14 @@ import (
)
type hostInterceptor struct {
inner http.RoundTripper
host string
inner http.RoundTripper
host string
scheme string
}
func (t hostInterceptor) RoundTrip(r *http.Request) (*http.Response, error) {
r.Host = t.host
r.Header.Set("X-Forwarded-Proto", t.scheme)
return t.inner.RoundTrip(r)
}
@ -24,8 +26,9 @@ func NewHostInterceptor(inner *http.Client, host string) *http.Client {
}
return &http.Client{
Transport: hostInterceptor{
inner: inner.Transport,
host: aku.Host,
inner: inner.Transport,
host: aku.Host,
scheme: aku.Scheme,
},
}
}

View File

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

View File

@ -7,7 +7,7 @@ ENV NODE_ENV=production
RUN cd /static && npm ci && npm run build-proxy
# Stage 2: Build
FROM docker.io/golang:1.20.0-bullseye AS builder
FROM docker.io/golang:1.20.1-bullseye AS builder
WORKDIR /go/src/goauthentik.io

View File

@ -1,8 +1,5 @@
[tool.pyright]
ignore = [
"**/migrations/**",
"**/node_modules/**"
]
ignore = ["**/migrations/**", "**/node_modules/**"]
reportMissingTypeStubs = false
strictParameterNoneValue = true
strictDictionaryInference = true
@ -63,14 +60,7 @@ exclude_lines = [
show_missing = true
[tool.pylint.basic]
good-names = [
"pk",
"id",
"i",
"j",
"k",
"_",
]
good-names = ["pk", "id", "i", "j", "k", "_"]
[tool.pylint.master]
disable = [
@ -85,6 +75,7 @@ disable = [
"protected-access",
"unused-argument",
"raise-missing-from",
"fixme",
# To preserve django's translation function we need to use %-formatting
"consider-using-f-string",
]
@ -114,13 +105,13 @@ filterwarnings = [
[tool.poetry]
name = "authentik"
version = "2023.2.0"
version = "2023.2.3"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]
[tool.poetry.dependencies]
celery = "*"
channels = {version = "*", extras = ["daphne"]}
channels = { version = "*", extras = ["daphne"] }
channels-redis = "*"
codespell = "*"
colorama = "*"
@ -147,7 +138,7 @@ gunicorn = "*"
kubernetes = "*"
ldap3 = "*"
lxml = "*"
opencontainers = {extras = ["reggie"],version = "*"}
opencontainers = { extras = ["reggie"], version = "*" }
packaging = "*"
paramiko = "*"
psycopg2-binary = "*"
@ -163,8 +154,8 @@ swagger-spec-validator = "*"
twilio = "*"
twisted = "*"
ua-parser = "*"
urllib3 = {extras = ["secure"],version = "*"}
uvicorn = {extras = ["standard"],version = "*"}
urllib3 = { extras = ["secure"], version = "*" }
uvicorn = { extras = ["standard"], version = "*" }
webauthn = "*"
wsproto = "*"
xmlsec = "*"
@ -176,7 +167,7 @@ bandit = "*"
black = "*"
bump2version = "*"
colorama = "*"
coverage = {extras = ["toml"],version = "*"}
coverage = { extras = ["toml"], version = "*" }
importlib-metadata = "*"
pylint = "*"
pylint-django = "*"

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2023.2.0
version: 2023.2.3
description: Making authentication simple.
contact:
email: hello@goauthentik.io
@ -16287,6 +16287,40 @@ paths:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/sources/ldap/{slug}/debug/:
get:
operationId: sources_ldap_debug_retrieve
description: Get raw LDAP data to debug
parameters:
- in: path
name: slug
schema:
type: string
description: Internal source name, used in URLs.
required: true
tags:
- sources
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/LDAPDebug'
description: ''
'400':
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationError'
description: ''
'403':
content:
application/json:
schema:
$ref: '#/components/schemas/GenericError'
description: ''
/sources/ldap/{slug}/sync_status/:
get:
operationId: sources_ldap_sync_status_list
@ -28618,6 +28652,31 @@ components:
- direct
- cached
type: string
LDAPDebug:
type: object
properties:
user:
type: array
items:
type: object
additionalProperties: {}
readOnly: true
group:
type: array
items:
type: object
additionalProperties: {}
readOnly: true
membership:
type: array
items:
type: object
additionalProperties: {}
readOnly: true
required:
- group
- membership
- user
LDAPOutpostConfig:
type: object
description: LDAPProvider Serializer

14
web/package-lock.json generated
View File

@ -22,7 +22,7 @@
"@codemirror/theme-one-dark": "^6.1.0",
"@formatjs/intl-listformat": "^7.1.7",
"@fortawesome/fontawesome-free": "^6.3.0",
"@goauthentik/api": "^2023.1.2-1675970414",
"@goauthentik/api": "^2023.2.1-1676400495",
"@hcaptcha/types": "^1.0.3",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@lingui/cli": "^3.17.1",
@ -1975,9 +1975,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2023.1.2-1675970414",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.1.2-1675970414.tgz",
"integrity": "sha512-TI6jccWKNb7f5vLMoa3qOUMp8PgJuGZTCG2N1hmyXcm8MYsU/9SfB86pzy661huMNeXhA0yzUYNsHt4awicnGw=="
"version": "2023.2.1-1676400495",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.2.1-1676400495.tgz",
"integrity": "sha512-NBP17SLYOE+AXIAyhQWdTNm6wkcruzGG1jlGaQQSGa8ShDl6g98bO7F+gLMIyOZTXeKx6+bLlKzsL+QKQ34AeA=="
},
"node_modules/@hcaptcha/types": {
"version": "1.0.3",
@ -11502,9 +11502,9 @@
"integrity": "sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA=="
},
"@goauthentik/api": {
"version": "2023.1.2-1675970414",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.1.2-1675970414.tgz",
"integrity": "sha512-TI6jccWKNb7f5vLMoa3qOUMp8PgJuGZTCG2N1hmyXcm8MYsU/9SfB86pzy661huMNeXhA0yzUYNsHt4awicnGw=="
"version": "2023.2.1-1676400495",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.2.1-1676400495.tgz",
"integrity": "sha512-NBP17SLYOE+AXIAyhQWdTNm6wkcruzGG1jlGaQQSGa8ShDl6g98bO7F+gLMIyOZTXeKx6+bLlKzsL+QKQ34AeA=="
},
"@hcaptcha/types": {
"version": "1.0.3",

View File

@ -66,7 +66,7 @@
"@codemirror/theme-one-dark": "^6.1.0",
"@formatjs/intl-listformat": "^7.1.7",
"@fortawesome/fontawesome-free": "^6.3.0",
"@goauthentik/api": "^2023.1.2-1675970414",
"@goauthentik/api": "^2023.2.1-1676400495",
"@hcaptcha/types": "^1.0.3",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@lingui/cli": "^3.17.1",

View File

@ -7,7 +7,6 @@ import "@goauthentik/elements/chips/ChipGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import "@goauthentik/elements/forms/SearchSelect";
import { UserOption } from "@goauthentik/elements/user/utils";
import YAML from "yaml";
import { t } from "@lingui/macro";
@ -15,9 +14,8 @@ import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { CoreApi, CoreGroupsListRequest, Group, User } from "@goauthentik/api";
import { CoreApi, CoreGroupsListRequest, Group } from "@goauthentik/api";
@customElement("ak-group-form")
export class GroupForm extends ModelForm<Group, string> {
@ -48,12 +46,12 @@ export class GroupForm extends ModelForm<Group, string> {
send = (data: Group): Promise<Group> => {
if (this.instance?.pk) {
return new CoreApi(DEFAULT_CONFIG).coreGroupsUpdate({
return new CoreApi(DEFAULT_CONFIG).coreGroupsPartialUpdate({
groupUuid: this.instance.pk,
groupRequest: data,
patchedGroupRequest: data,
});
} else {
data.users = Array.from(this.instance?.users || []);
data.users = [];
return new CoreApi(DEFAULT_CONFIG).coreGroupsCreate({
groupRequest: data,
});
@ -113,63 +111,6 @@ export class GroupForm extends ModelForm<Group, string> {
>
</ak-search-select>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Members`} name="users">
<div class="pf-c-input-group">
<ak-group-member-select-table
.confirm=${(items: User[]) => {
// Because the model only has the IDs, map the user list to IDs
const ids = items.map((u) => u.pk || 0);
if (!this.instance) this.instance = {} as Group;
this.instance.users = Array.from(this.instance?.users || []).concat(
ids,
);
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-group-member-select-table>
<div class="pf-c-form-control">
<ak-chip-group>
${until(
new CoreApi(DEFAULT_CONFIG)
.coreUsersList({
ordering: "username",
})
.then((users) => {
return users.results.map((user) => {
const selected = Array.from(
this.instance?.users || [],
).some((su) => {
return su == user.pk;
});
if (!selected) return;
return html`<ak-chip
.removable=${true}
value=${ifDefined(user.pk)}
@remove=${() => {
if (!this.instance) return;
const users = Array.from(
this.instance?.users || [],
);
const idx = users.indexOf(user.pk || 0);
users.splice(idx, 1);
this.instance.users = users;
this.requestUpdate();
}}
>
${UserOption(user)}
</ak-chip>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</ak-chip-group>
</div>
</div>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Attributes`} ?required=${true} name="attributes">
<ak-codemirror
mode="yaml"

View File

@ -170,7 +170,7 @@ export class LDAPSourceViewPage extends AKElement {
</div>
<div class="pf-c-card__footer">
<ak-action-button
class="pf-m-primary"
class="pf-m-secondary"
.apiRequest=${() => {
return new SourcesApi(DEFAULT_CONFIG)
.sourcesLdapPartialUpdate({

View File

@ -31,7 +31,7 @@ export class GroupSelectModal extends TableModal<Group> {
return new CoreApi(DEFAULT_CONFIG).coreGroupsList({
ordering: this.order,
page: page,
pageSize: (await uiConfig()).pagination.perPage / 2,
pageSize: (await uiConfig()).pagination.perPage,
search: this.search || "",
});
}

View File

@ -11,9 +11,8 @@ import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { until } from "lit/directives/until.js";
import { CoreApi, Group, User } from "@goauthentik/api";
import { CoreApi, User } from "@goauthentik/api";
@customElement("ak-user-form")
export class UserForm extends ModelForm<User, number> {
@ -44,11 +43,12 @@ export class UserForm extends ModelForm<User, number> {
send = (data: User): Promise<User> => {
if (this.instance?.pk) {
return new CoreApi(DEFAULT_CONFIG).coreUsersUpdate({
return new CoreApi(DEFAULT_CONFIG).coreUsersPartialUpdate({
id: this.instance.pk,
userRequest: data,
patchedUserRequest: data,
});
} else {
data.groups = [];
return new CoreApi(DEFAULT_CONFIG).coreUsersCreate({
userRequest: data,
});
@ -110,63 +110,6 @@ export class UserForm extends ModelForm<User, number> {
${t`Designates whether this user should be treated as active. Unselect this instead of deleting accounts.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Groups`} name="groups">
<div class="pf-c-input-group">
<ak-user-group-select-table
.confirm=${(items: Group[]) => {
// Because the model only has the IDs, map the group list to IDs
const ids = items.map((g) => g.pk);
if (!this.instance) this.instance = {} as User;
this.instance.groups = Array.from(this.instance?.groups || []).concat(
ids,
);
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>
${until(
new CoreApi(DEFAULT_CONFIG)
.coreGroupsList({
ordering: "name",
})
.then((groups) => {
return groups.results.map((group) => {
const selected = Array.from(
this.instance?.groups || [],
).some((sg) => {
return sg == group.pk;
});
if (!selected) return;
return html`<ak-chip
.removable=${true}
value=${ifDefined(group.pk)}
@remove=${() => {
if (!this.instance) return;
const groups = Array.from(
this.instance?.groups || [],
);
const idx = groups.indexOf(group.pk);
groups.splice(idx, 1);
this.instance.groups = groups;
this.requestUpdate();
}}
>
${group.name}
</ak-chip>`;
});
}),
html`<option>${t`Loading...`}</option>`,
)}
</ak-chip-group>
</div>
</div>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Attributes`} ?required=${true} name="attributes">
<ak-codemirror
mode="yaml"

View File

@ -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.2.0";
export const VERSION = "2023.2.3";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -29,6 +29,20 @@ export function convertToTitle(text: string): string {
});
}
/**
* Truncate a string based on maximum word count
*/
export function truncateWords(string: string, length = 10): string {
string = string || "";
const array = string.trim().split(" ");
const ellipsis = array.length > length ? "..." : "";
return array.slice(0, length).join(" ") + ellipsis;
}
/**
* Truncate a string based on character count
*/
export function truncate(string: string, length = 10): string {
return string.length > length ? `${string.substring(0, length)}...` : string;
}

View File

@ -32,7 +32,7 @@ export class UserEvents extends Table<Event> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsList({
page: page,
ordering: this.order,
pageSize: (await uiConfig()).pagination.perPage / 2,
pageSize: (await uiConfig()).pagination.perPage,
username: this.targetUser,
});
}

View File

@ -1,6 +1,6 @@
import { uiConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users";
import { truncate } from "@goauthentik/common/utils";
import { truncateWords } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import { t } from "@lingui/macro";
@ -127,7 +127,7 @@ export class LibraryApplication extends AKElement {
</div>
</div>
<div class="pf-c-card__body">
${truncate(this.application.metaDescription || "", 35)}
${truncateWords(this.application.metaDescription || "", 35)}
</div>
</div>`;
}

View File

@ -4,11 +4,30 @@ title: Terminology
slug: /terminology
---
![](/img/authentik_objects.svg)
```mermaid
graph LR
source_ldap((LDAP Source)) <-->|Synchronizes| datasource_ldap["FreeIPA/
Active Directory"]
datasource_oauth1(Twtitter) --> source_oauth((OAuth/SAML\nSource))
datasource_oauth2(GitHub) --> source_oauth((OAuth/SAML\nSource))
source_oauth --> authentik_db(authentik Database)
source_ldap --> authentik_db(authentik Database)
### System tasks
app_sso(Gitlab) --> authentik_provider[Provider]
authentik_provider --> authentik_db
authentik_provider --> authentik_app["Application
(Stores permissions and UI details)"]
authentik_app --> authentik_policy_engine[Policy Engine]
authentik_policy_engine --> authentik_db
These are longer-running tasks which authentik runs in the background. This is used to sync LDAP sources, backup the database, and other various tasks.
app_ldap("Applications that only
support LDAP (e.g. pfSense)") --> authentik_outpost_ldap[LDAP Outpost]
app_proxy("Applications that don't
support any SSO (e.g. Plex)") --> authentik_outpost_proxy[Proxy Outpost]
authentik_outpost_ldap --> authentik_outposts[Outposts]
authentik_outpost_proxy --> authentik_outposts[Outposts]
authentik_outposts --> authentik_provider
```
### Application
@ -26,7 +45,7 @@ A Provider is a way for other applications to authenticate against authentik. Co
At a base level a policy is a yes/no gate. It will either evaluate to True or False depending on the Policy Kind and settings. For example, a "Group Membership Policy" evaluates to True if the user is member of the specified Group and False if not. This can be used to conditionally apply Stages, grant/deny access to various objects, and for other custom logic.
See [Policies](./policies/)
See [Policies](../policies/index.md)
### Flows & Stages
@ -34,16 +53,20 @@ Flows are an ordered sequence of stages. These flows can be used to define how a
A stage represents a single verification or logic step. They are used to authenticate users, enroll users, and more. These stages can optionally be applied to a flow via policies.
See [Flows](./flow/)
See [Flows](../flow/index.md)
### Property Mappings
Property Mappings allow you to make information available for external applications. For example, if you want to login to AWS with authentik, you'd use Property Mappings to set the user's roles in AWS based on their group memberships in authentik.
See [Property Mappings](./property-mappings/)
See [Property Mappings](../property-mappings/index.md)
### Outpost
An outpost is a separate component of authentik, which can be deployed anywhere, regardless of the authentik deployment. The outpost offers services that aren't implemented directly into the authentik core, e.g. Reverse Proxying.
See [Outposts](./outposts/)
See [Outposts](../outposts/index.mdx)
### System tasks
These are longer-running tasks which authentik runs in the background. This is used to sync LDAP sources, backup the database, and other various tasks.

View File

@ -4,30 +4,305 @@ title: Events
Events are authentik's built-in logging system. Whenever any of the following actions occur, an event is created:
- A user logs in/logs out (including the source, if available)
- A user fails to login
- A user sets their password
- A user views a token
- An invitation is used
- A user object is written to during a flow
- A user authorizes an application
- A user links a source to their account
- A user starts/ends impersonation, including the user that was impersonated
- A policy is executed (when a policy has "Execution Logging" enabled)
- A policy or property mapping causes an exception
- A configuration error occurs, for example during the authorization of an application
- Any objects is created/updated/deleted
- An update is available
Certain information is stripped from events, to ensure no passwords or other credentials are saved in the log.
If you want to forward these events to another application, simply forward the log output of all authentik containers. Every event creation is logged there.
If you want to forward these events to another application, forward the log output of all authentik containers. Every event creation is logged with the log level "info".
### `login`
A user logs in (including the source, if available)
<details><summary>Example</summary>
<p>
```json
{
"pk": "f00f54e7-2b38-421f-bc78-e61f950048d6",
"user": {
"pk": 1,
"email": "root@localhost",
"username": "akadmin"
},
"action": "login",
"app": "authentik.events.signals",
"context": {
"auth_method": "password",
"http_request": {
"args": {
"query": "next=%2F"
},
"path": "/api/v3/flows/executor/default-authentication-flow/",
"method": "GET"
},
"auth_method_args": {}
},
"client_ip": "::1",
"created": "2023-02-15T15:33:42.771091Z",
"expires": "2024-02-15T15:33:42.770425Z",
"tenant": {
"pk": "fcba828076b94dedb2d5a6b4c5556fa1",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `login_failed`
A failed login attempt
<details><summary>Example</summary>
<p>
```json
{
"pk": "2779b173-eb2a-4c2b-a1a4-8283eda308d7",
"user": {
"pk": 2,
"email": "",
"username": "AnonymousUser"
},
"action": "login_failed",
"app": "authentik.events.signals",
"context": {
"stage": {
"pk": "7e88f4a991c442c1a1335d80f0827d7f",
"app": "authentik_stages_password",
"name": "default-authentication-password",
"model_name": "passwordstage"
},
"password": "********************",
"username": "akadmin",
"http_request": {
"args": {
"query": "next=%2F"
},
"path": "/api/v3/flows/executor/default-authentication-flow/",
"method": "POST"
}
},
"client_ip": "::1",
"created": "2023-02-15T15:32:55.319608Z",
"expires": "2024-02-15T15:32:55.314581Z",
"tenant": {
"pk": "fcba828076b94dedb2d5a6b4c5556fa1",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `logout`
A user logs out.
<details><summary>Example</summary>
<p>
```json
{
"pk": "474ffb6b-77e3-401c-b681-7d618962440f",
"user": {
"pk": 1,
"email": "root@localhost",
"username": "akadmin"
},
"action": "logout",
"app": "authentik.events.signals",
"context": {
"http_request": {
"args": {
"query": ""
},
"path": "/api/v3/flows/executor/default-invalidation-flow/",
"method": "GET"
}
},
"client_ip": "::1",
"created": "2023-02-15T15:39:55.976243Z",
"expires": "2024-02-15T15:39:55.975535Z",
"tenant": {
"pk": "fcba828076b94dedb2d5a6b4c5556fa1",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `user_write`
A user is written to during a flow execution.
<details><summary>Example</summary>
<p>
```json
{
"pk": "d012e8af-cb94-4fa2-9e92-961e4eebc060",
"user": {
"pk": 1,
"email": "root@localhost",
"username": "akadmin"
},
"action": "user_write",
"app": "authentik.events.signals",
"context": {
"name": "authentik Default Admin",
"email": "root@localhost",
"created": false,
"username": "akadmin",
"attributes": {
"settings": {
"locale": ""
}
},
"http_request": {
"args": {
"query": ""
},
"path": "/api/v3/flows/executor/default-user-settings-flow/",
"method": "GET"
}
},
"client_ip": "::1",
"created": "2023-02-15T15:41:18.411017Z",
"expires": "2024-02-15T15:41:18.410276Z",
"tenant": {
"pk": "fcba828076b94dedb2d5a6b4c5556fa1",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `suspicious_request`
A suspicious request has been received (for example, a revoked token was used).
### `password_set`
A user sets their password.
### `secret_view`
A user views a token's/certificate's data.
### `secret_rotate`
A token was rotated automatically by authentik.
### `invitation_used`
An invitation is used.
### `authorize_application`
A user authorizes an application.
<details><summary>Example</summary>
<p>
```json
{
"pk": "f52f9eb9-dc2a-4f1e-afea-ad5af90bf680",
"user": {
"pk": 1,
"email": "root@localhost",
"username": "akadmin"
},
"action": "authorize_application",
"app": "authentik.providers.oauth2.views.authorize",
"context": {
"geo": {
"lat": 42.0,
"city": "placeholder",
"long": 42.0,
"country": "placeholder",
"continent": "placeholder"
},
"flow": "53287faa8a644b6cb124cb602a84282f",
"scopes": "ak_proxy profile openid email",
"http_request": {
"args": {
"query": "[...]"
},
"path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/",
"method": "GET"
},
"authorized_application": {
"pk": "bed6a2495fdc4b2e8c3f93cb2ed7e021",
"app": "authentik_core",
"name": "Alertmanager",
"model_name": "application"
}
},
"client_ip": "::1",
"created": "2023-02-15T10:02:48.615499Z",
"expires": "2023-04-26T10:02:48.612809Z",
"tenant": {
"pk": "10800be643d44842ab9d97cb5f898ce9",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `source_linked`
A user links a source to their account
### `impersonation_started` / `impersonation_ended`
A user starts/ends impersonation, including the user that was impersonated
### `policy_execution`
A policy is executed (when a policy has "Execution Logging" enabled).
### `policy_exception` / `property_mapping_exception`
A policy or property mapping causes an exception
### `system_task_exception`
An exception occurred in a system task.
### `system_exception`
A general exception in authentik occurred.
### `configuration_error`
A configuration error occurs, for example during the authorization of an application
### `model_created` / `model_updated` / `model_deleted`
Logged when any model is created/updated/deleted, including the user that sent the request.
### `email_sent`
An email has been sent. Included is the email that was sent.
### `update_available`
An update is available

View File

@ -99,7 +99,7 @@ This includes the following:
- `context['application']`: The application the user is in the process of authorizing. (Optional)
- `context['source']`: The source the user is authenticating/enrolling with. (Optional)
- `context['pending_user']`: The currently pending user, see [User](../user-group/user.md#object-attributes)
- `context['is_restored']`: Set to `True` when the flow plan has been restored from a flow token, for example the user clicked a link to a flow which was sent by an email stage. (Optional)
- `context['is_restored']`: Contains the flow token when the flow plan was restored from a link, for example the user clicked a link to a flow which was sent by an email stage. (Optional)
- `context['auth_method']`: Authentication method (this value is set by password stages) (Optional)
Depending on method, `context['auth_method_args']` is also set.

View File

@ -1,28 +1,94 @@
---
title: Overview
title: Proxy Provider
---
```mermaid
sequenceDiagram
participant u as User accesses service
participant rp as Reverse proxy
participant ak as authentik
participant s as Service
u->>rp: Initial request
rp->>ak: Checks authentication
alt User is authenticated
ak ->> rp: Successful response
rp ->> s: Initial request is forwarded
else User needs to be authenticated
ak ->> rp: Redirect to the login page
rp ->> u: Redirect is passed to enduser
end
```
## Headers
The proxy outpost sets the following user-specific headers:
- X-authentik-username: `akadmin`
### `X-authentik-username`
The username of the currently logged in user
Example value: `akadmin`
- X-authentik-groups: `foo|bar|baz`
The username of the currently logged in user
The groups the user is member of, separated by a pipe
### `X-authentik-groups`
- X-authentik-email: `root@localhost`
Example value: `foo|bar|baz`
The email address of the currently logged in user
The groups the user is member of, separated by a pipe
- X-authentik-name: `authentik Default Admin`
### `X-authentik-email`
Full name of the current user
Example value: `root@localhost`
- X-authentik-uid: `900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb`
The email address of the currently logged in user
The hashed identifier of the currently logged in user.
### `X-authentik-name`
Example value: `authentik Default Admin`
Full name of the current user
### `X-authentik-uid`
Example value: `900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb`
The hashed identifier of the currently logged in user.
Besides these user-specific headers, some application specific headers are also set:
### `X-authentik-meta-outpost`
Example value: `authentik Embedded Outpost`
The authentik outpost's name.
### `X-authentik-meta-provider`
Example value: `test`
The authentik provider's name.
### `X-authentik-meta-app`
Example value: `test`
The authentik application's slug.
### `X-authentik-meta-version`
Example value: `goauthentik.io/outpost/1.2.3`
The authentik outpost's version.
### `X-Forwarded-Host`
:::info
Only set in proxy mode
:::
The original Host header sent by the client. This is set as the `Host` header is set to the host of the configured backend.
### Additional headers
Additionally, you can set `additionalHeaders` attribute on groups or users to set additional headers:
@ -31,30 +97,6 @@ additionalHeaders:
X-test-header: test-value
```
Besides these user-specific headers, some application specific headers are also set:
- X-authentik-meta-outpost: `authentik Embedded Outpost`
The authentik outpost's name.
- X-authentik-meta-provider: `test`
The authentik provider's name.
- X-authentik-meta-app: `test`
The authentik application's slug.
- X-authentik-meta-version: `goauthentik.io/outpost/1.2.3`
The authentik outpost's version.
### Only in proxy mode
- X-Forwarded-Host:
The original Host header sent by the client. This is set as the `Host` header is set to the host of the configured backend.
## HTTPS
The outpost listens on both 9000 for HTTP and 9443 for HTTPS.

View File

@ -88,6 +88,14 @@ image:
- web/user: filter tokens by username
- web/user: refactor loading of data in userinterface
## Fixed in 2023.2.1
- internal: fix scheme not being forwarded correctly for host intercepted requests
- sources/ldap: add LDAP Debug endpoint
- web/admin: improve action button spinner on ldap source page
- web/admin: remove groups and users from users and group form to prevent accidental removal when updating
- web/admin: use full page size for modals
## API Changes
#### What's New

View File

@ -0,0 +1,27 @@
# CVE-2023-26481
_Reported by [@fuomag9](https://github.com/fuomag9)_
## Insufficient user check in FlowTokens by Email stage
### Summary
Due to an insufficient access check, a recovery flow link that is created by an admin (or sent via email by an admin) can be used to set the password for any arbitrary user.
### Patches
authentik 2022.12.3, 2023.1.3, 2023.2.3 fix this issue.
### Impact
This attack is only possible if a recovery flow exists, which has both an Identification and an Email stage bound to it. If the flow has policies on the identification stage to skip it when the flow is restored (by checking `request.context['is_restored']`), the flow is not affected by this. With this flow in place, an administrator must create a recovery Link or send a recovery URL to the attacker, who can, due to the improper validation of the token create, set the password for any account.
### Workaround
It is recommended to upgrade to the patched version of authentik. Regardless, for custom recovery flows it is recommended to add a policy that checks if the flow is restored, and skips the identification stage.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

View File

@ -48,15 +48,15 @@ module.exports = {
},
{
type: "dropdown",
label: `Version: latest`,
label: `Version: ${releases[0].replace(
/releases\/\d+\/v/,
""
)}`,
position: "right",
items: releases.map((release) => {
const subdomain = release
.replace(/releases\/\d+\/v/, "")
.replace(".", "-");
const label =
"Version: " +
release.replace(/releases\/\d+\/v/, "");
const version = release.replace(/releases\/\d+\/v/, "");
const subdomain = version.replace(".", "-");
const label = `Version: ${version}`;
return {
label: label,
href: `https://version-${subdomain}.goauthentik.io`,
@ -169,6 +169,10 @@ module.exports = {
},
],
],
markdown: {
mermaid: true,
},
themes: ["@docusaurus/theme-mermaid"],
scripts: [
{
src: "https://goauthentik.io/js/script.js",

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
"dependencies": {
"@docusaurus/plugin-client-redirects": "2.3.1",
"@docusaurus/preset-classic": "2.3.1",
"@docusaurus/theme-mermaid": "^2.3.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"disqus-react": "^1.1.5",

View File

@ -220,13 +220,14 @@ module.exports = {
description: "Release notes for recent authentik versions",
},
items: [
"releases/2023/v2023.2",
"releases/2023/v2023.1",
"releases/2022/v2022.12",
"releases/2022/v2022.11",
{
type: "category",
label: "Previous versions",
items: [
"releases/2022/v2022.11",
"releases/2022/v2022.10",
"releases/2022/v2022.9",
"releases/2022/v2022.8",
@ -299,9 +300,10 @@ module.exports = {
},
items: [
"security/policy",
"security/CVE-2022-23555",
"security/CVE-2022-46145",
"security/CVE-2022-46172",
"security/CVE-2022-23555",
"security/CVE-2023-26481",
],
},
],

View File

@ -24,6 +24,7 @@
}
.hero--primary {
padding-bottom: 5.3rem !important;
-webkit-clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - 3vw));
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - 3vw));
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB