sources: add Kerberos (#10815)

* sources: introduce new property mappings per-user and group

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* sources/ldap: migrate to new property mappings

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint-fix and make gen

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* web changes

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix tests

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* update tests

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* remove flatten for generic implem

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* rework migration

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint-fix

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix migrations

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* re-add field migration to property mappings

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix migrations

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* more migrations fixes

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* easy fixes

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* migrate to propertymappingmanager

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* ruff and small fixes

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* move mapping things into a separate class

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* migrations: use using(db_alias)

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* migrations: use built-in variable

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* add docs

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* add release notes

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix login reverse

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* refactor source flow manager matching

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* kerberos sync with mode matching

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fixup

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* finish frontend

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Optimised images with calibre/image-actions

* make web

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* add test for internal password update

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix sync tests

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix filter

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* switch to blueprints property mappings, improvements to frontend

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* some more small fixes

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix reverse

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* properly deal with password changes signals

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* actually deal with it properly

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* update docs

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint-fix

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* blueprints: realm as group: make it non default

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* small fixes and improvements

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* wip

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix title

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* add password backend to default flow

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* link docs page properly, add in admin interface, add suggestions for how to apply changes to a fleet of machines

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* add troubleshooting

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix default flow pass backend

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix flaky spnego tests

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* properly convert gssapi name to python str

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix unpickable types

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* make sure the last server token is returned to the client

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* lint

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/developer-docs/setup/full-dev-environment.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/browser.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* Update website/docs/users-sources/sources/protocols/kerberos/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* more docs review

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix missing library

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix missing library again

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix web import

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix sync

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix sync v2

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

* fix sync v3

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>

---------

Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
This commit is contained in:
Marc 'risson' Schmitt
2024-10-23 17:58:29 +02:00
committed by GitHub
parent d3ebfcaf2f
commit d817c646bd
60 changed files with 5037 additions and 15 deletions

View File

@ -14,7 +14,7 @@ runs:
run: |
pipx install poetry || true
sudo apt-get update
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext
sudo apt-get install --no-install-recommends -y libpq-dev openssl libxmlsec1-dev pkg-config gettext libkrb5-dev krb5-kdc krb5-user krb5-admin-server
- name: Setup python and restore poetry
uses: actions/setup-python@v5
with:

View File

@ -110,7 +110,7 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloa
RUN --mount=type=cache,id=apt-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/var/cache/apt \
apt-get update && \
# Required for installing pip packages
apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev
apt-get install -y --no-install-recommends build-essential pkg-config libpq-dev libkrb5-dev
RUN --mount=type=bind,target=./pyproject.toml,src=./pyproject.toml \
--mount=type=bind,target=./poetry.lock,src=./poetry.lock \
@ -141,7 +141,7 @@ WORKDIR /
# We cannot cache this layer otherwise we'll end up with a bigger image
RUN apt-get update && \
# Required for runtime
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates && \
apt-get install -y --no-install-recommends libpq5 libmaxminddb0 ca-certificates libkrb5-3 libkadm5clnt-mit12 libkdb5-10 && \
# Required for bootstrap & healtcheck
apt-get install -y --no-install-recommends runit && \
apt-get clean && \
@ -161,6 +161,7 @@ COPY ./tests /tests
COPY ./manage.py /
COPY ./blueprints /blueprints
COPY ./lifecycle/ /lifecycle
COPY ./authentik/sources/kerberos/krb5.conf /etc/krb5.conf
COPY --from=go-builder /go/authentik /bin/authentik
COPY --from=python-deps /ak-root/venv /ak-root/venv
COPY --from=web-builder /work/web/dist/ /web/dist/

View File

@ -330,11 +330,13 @@ class User(SerializerModel, GuardianUserMixin, AttributesMixin, AbstractUser):
"""superuser == staff user"""
return self.is_superuser # type: ignore
def set_password(self, raw_password, signal=True):
def set_password(self, raw_password, signal=True, sender=None):
if self.pk and signal:
from authentik.core.signals import password_changed
password_changed.send(sender=self, user=self, password=raw_password)
if not sender:
sender = self
password_changed.send(sender=sender, user=self, password=raw_password)
self.password_change_date = now()
return super().set_password(raw_password)

View File

@ -105,6 +105,10 @@ ldap:
tls:
ciphers: null
sources:
kerberos:
task_timeout_hours: 2
reputation:
expiry: 86400

View File

@ -91,6 +91,7 @@ TENANT_APPS = [
"authentik.providers.scim",
"authentik.rbac",
"authentik.recovery",
"authentik.sources.kerberos",
"authentik.sources.ldap",
"authentik.sources.oauth",
"authentik.sources.plex",

View File

View File

@ -0,0 +1,31 @@
"""Kerberos Property Mapping API"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.property_mappings import PropertyMappingFilterSet, PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.kerberos.models import KerberosSourcePropertyMapping
class KerberosSourcePropertyMappingSerializer(PropertyMappingSerializer):
"""Kerberos PropertyMapping Serializer"""
class Meta(PropertyMappingSerializer.Meta):
model = KerberosSourcePropertyMapping
class KerberosSourcePropertyMappingFilter(PropertyMappingFilterSet):
"""Filter for KerberosSourcePropertyMapping"""
class Meta(PropertyMappingFilterSet.Meta):
model = KerberosSourcePropertyMapping
class KerberosSourcePropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""KerberosSource PropertyMapping Viewset"""
queryset = KerberosSourcePropertyMapping.objects.all()
serializer_class = KerberosSourcePropertyMappingSerializer
filterset_class = KerberosSourcePropertyMappingFilter
search_fields = ["name"]
ordering = ["name"]

View File

@ -0,0 +1,114 @@
"""Source API Views"""
from django.core.cache import cache
from drf_spectacular.utils import extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.events.api.tasks import SystemTaskSerializer
from authentik.sources.kerberos.models import KerberosSource
from authentik.sources.kerberos.tasks import CACHE_KEY_STATUS
class KerberosSourceSerializer(SourceSerializer):
"""Kerberos Source Serializer"""
connectivity = SerializerMethodField()
def get_connectivity(self, source: KerberosSource) -> dict[str, str] | None:
"""Get cached source connectivity"""
return cache.get(CACHE_KEY_STATUS + source.slug, None)
class Meta:
model = KerberosSource
fields = SourceSerializer.Meta.fields + [
"group_matching_mode",
"realm",
"krb5_conf",
"sync_users",
"sync_users_password",
"sync_principal",
"sync_password",
"sync_keytab",
"sync_ccache",
"connectivity",
"spnego_server_name",
"spnego_keytab",
"spnego_ccache",
"password_login_update_internal_password",
]
extra_kwargs = {
"sync_password": {"write_only": True},
"sync_keytab": {"write_only": True},
"spnego_keytab": {"write_only": True},
}
class KerberosSyncStatusSerializer(PassiveSerializer):
"""Kerberos Source sync status"""
is_running = BooleanField(read_only=True)
tasks = SystemTaskSerializer(many=True, read_only=True)
class KerberosSourceViewSet(UsedByMixin, ModelViewSet):
"""Kerberos Source Viewset"""
queryset = KerberosSource.objects.all()
serializer_class = KerberosSourceSerializer
lookup_field = "slug"
filterset_fields = [
"name",
"slug",
"enabled",
"realm",
"sync_users",
"sync_users_password",
"sync_principal",
"spnego_server_name",
"password_login_update_internal_password",
]
search_fields = [
"name",
"slug",
"realm",
"krb5_conf",
"sync_principal",
"spnego_server_name",
]
ordering = ["name"]
@extend_schema(
responses={
200: KerberosSyncStatusSerializer(),
}
)
@action(
methods=["GET"],
detail=True,
pagination_class=None,
url_path="sync/status",
filter_backends=[],
)
def sync_status(self, request: Request, slug: str) -> Response:
"""Get source's sync status"""
source: KerberosSource = self.get_object()
tasks = list(
get_objects_for_user(request.user, "authentik_events.view_systemtask").filter(
name="kerberos_sync",
uid__startswith=source.slug,
)
)
with source.sync_lock as lock_acquired:
status = {
"tasks": tasks,
"is_running": not lock_acquired,
}
return Response(KerberosSyncStatusSerializer(status).data)

View File

@ -0,0 +1,51 @@
"""Kerberos Source Serializer"""
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.sources import (
GroupSourceConnectionSerializer,
GroupSourceConnectionViewSet,
UserSourceConnectionSerializer,
)
from authentik.core.api.used_by import UsedByMixin
from authentik.sources.kerberos.models import (
GroupKerberosSourceConnection,
UserKerberosSourceConnection,
)
class UserKerberosSourceConnectionSerializer(UserSourceConnectionSerializer):
"""Kerberos Source Serializer"""
class Meta:
model = UserKerberosSourceConnection
fields = UserSourceConnectionSerializer.Meta.fields + ["identifier"]
class UserKerberosSourceConnectionViewSet(UsedByMixin, ModelViewSet):
"""Source Viewset"""
queryset = UserKerberosSourceConnection.objects.all()
serializer_class = UserKerberosSourceConnectionSerializer
filterset_fields = ["source__slug"]
search_fields = ["source__slug"]
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["source__slug"]
class GroupKerberosSourceConnectionSerializer(GroupSourceConnectionSerializer):
"""OAuth Group-Source connection Serializer"""
class Meta(GroupSourceConnectionSerializer.Meta):
model = GroupKerberosSourceConnection
class GroupKerberosSourceConnectionViewSet(GroupSourceConnectionViewSet):
"""Group-source connection Viewset"""
queryset = GroupKerberosSourceConnection.objects.all()
serializer_class = GroupKerberosSourceConnectionSerializer

View File

@ -0,0 +1,13 @@
"""authentik kerberos source config"""
from authentik.blueprints.apps import ManagedAppConfig
class AuthentikSourceKerberosConfig(ManagedAppConfig):
"""Authentik source kerberos app config"""
name = "authentik.sources.kerberos"
label = "authentik_sources_kerberos"
verbose_name = "authentik Sources.Kerberos"
mountpoint = "source/kerberos/"
default = True

View File

@ -0,0 +1,116 @@
"""authentik Kerberos Authentication Backend"""
import gssapi
from django.http import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.auth import InbuiltBackend
from authentik.core.models import User
from authentik.lib.generators import generate_id
from authentik.sources.kerberos.models import (
KerberosSource,
Krb5ConfContext,
UserKerberosSourceConnection,
)
LOGGER = get_logger()
class KerberosBackend(InbuiltBackend):
"""Authenticate users against Kerberos realm"""
def authenticate(self, request: HttpRequest, **kwargs):
"""Try to authenticate a user via kerberos"""
if "password" not in kwargs or "username" not in kwargs:
return None
username = kwargs.pop("username")
realm = None
if "@" in username:
username, realm = username.rsplit("@", 1)
user, source = self.auth_user(username, realm, **kwargs)
if user:
self.set_method("kerberos", request, source=source)
return user
return None
def auth_user(
self, username: str, realm: str | None, password: str, **filters
) -> tuple[User | None, KerberosSource | None]:
sources = KerberosSource.objects.filter(enabled=True)
user = User.objects.filter(usersourceconnection__source__in=sources, **filters).first()
if user is not None:
# User found, let's get its connections for the sources that are available
user_source_connections = UserKerberosSourceConnection.objects.filter(
user=user, source__in=sources
)
elif realm is not None:
user_source_connections = UserKerberosSourceConnection.objects.filter(
source__in=sources, identifier=f"{username}@{realm}"
)
# no realm specified, we can't do anything
else:
user_source_connections = UserKerberosSourceConnection.objects.none()
if not user_source_connections.exists():
LOGGER.debug("no kerberos source found for user", username=username)
return None, None
for user_source_connection in user_source_connections.prefetch_related().select_related(
"source__kerberossource"
):
# User either has an unusable password,
# or has a password, but couldn't be authenticated by ModelBackend
# This means we check with a kinit to see if the Kerberos password has changed
if self.auth_user_by_kinit(user_source_connection, password):
# Password was successful in kinit to Kerberos, so we save it in database
if (
user_source_connection.source.kerberossource.password_login_update_internal_password
):
LOGGER.debug(
"Updating user's password in DB",
source=user_source_connection.source,
user=user_source_connection.user,
)
user_source_connection.user.set_password(
password, sender=user_source_connection.source
)
user_source_connection.user.save()
return user, user_source_connection.source
# Password doesn't match, onto next source
LOGGER.debug(
"failed to kinit, password invalid",
source=user_source_connection.source,
user=user_source_connection.user,
)
# No source with valid password found
LOGGER.debug("no valid kerberos source found for user", user=user)
return None, None
def auth_user_by_kinit(
self, user_source_connection: UserKerberosSourceConnection, password: str
) -> bool:
"""Attempt authentication by kinit to the source."""
LOGGER.debug(
"Attempting to kinit as user",
user=user_source_connection.user,
source=user_source_connection.source,
principal=user_source_connection.identifier,
)
with Krb5ConfContext(user_source_connection.source.kerberossource):
name = gssapi.raw.import_name(
user_source_connection.identifier.encode(), gssapi.raw.NameType.kerberos_principal
)
try:
# Use a temporary credentials cache to not interfere with whatever is defined
# elsewhere
gssapi.raw.ext_krb5.krb5_ccache_name(f"MEMORY:{generate_id(12)}".encode())
gssapi.raw.ext_password.acquire_cred_with_password(name, password.encode())
# Restore the credentials cache to what it was before
gssapi.raw.ext_krb5.krb5_ccache_name(None)
return True
except gssapi.exceptions.GSSError as exc:
LOGGER.warning("failed to kinit", exc=exc)
return False

View File

@ -0,0 +1,4 @@
[libdefaults]
dns_canonicalize_hostname = false
dns_fallback = true
rnds = false

View File

@ -0,0 +1,25 @@
"""Kerberos Connection check"""
from json import dumps
from structlog.stdlib import get_logger
from authentik.sources.kerberos.models import KerberosSource
from authentik.tenants.management import TenantCommand
LOGGER = get_logger()
class Command(TenantCommand):
"""Check connectivity to Kerberos servers for a source"""
def add_arguments(self, parser):
parser.add_argument("source_slugs", nargs="?", type=str)
def handle_per_tenant(self, **options):
sources = KerberosSource.objects.filter(enabled=True)
if options["source_slugs"]:
sources = KerberosSource.objects.filter(slug__in=options["source_slugs"])
for source in sources.order_by("slug"):
status = source.check_connection()
self.stdout.write(dumps(status, indent=4))

View File

@ -0,0 +1,25 @@
"""Kerberos Sync"""
from structlog.stdlib import get_logger
from authentik.sources.kerberos.models import KerberosSource
from authentik.sources.kerberos.sync import KerberosSync
from authentik.tenants.management import TenantCommand
LOGGER = get_logger()
class Command(TenantCommand):
"""Run sync for an Kerberos Source"""
def add_arguments(self, parser):
parser.add_argument("source_slugs", nargs="+", type=str)
def handle_per_tenant(self, **options):
for source_slug in options["source_slugs"]:
source = KerberosSource.objects.filter(slug=source_slug).first()
if not source:
LOGGER.warning("Source does not exist", slug=source_slug)
continue
user_count = KerberosSync(source).sync()
LOGGER.info(f"Synced {user_count} users", slug=source_slug)

View File

@ -0,0 +1,179 @@
# Generated by Django 5.0.9 on 2024-09-23 11:27
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_core", "0039_source_group_matching_mode_alter_group_name_and_more"),
]
operations = [
migrations.CreateModel(
name="GroupKerberosSourceConnection",
fields=[
(
"groupsourceconnection_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.groupsourceconnection",
),
),
],
options={
"verbose_name": "Group Kerberos Source Connection",
"verbose_name_plural": "Group Kerberos Source Connections",
},
bases=("authentik_core.groupsourceconnection",),
),
migrations.CreateModel(
name="KerberosSource",
fields=[
(
"source_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.source",
),
),
("realm", models.TextField(help_text="Kerberos realm", unique=True)),
(
"krb5_conf",
models.TextField(
blank=True,
help_text="Custom krb5.conf to use. Uses the system one by default",
),
),
(
"sync_users",
models.BooleanField(
db_index=True,
default=False,
help_text="Sync users from Kerberos into authentik",
),
),
(
"sync_users_password",
models.BooleanField(
db_index=True,
default=True,
help_text="When a user changes their password, sync it back to Kerberos",
),
),
(
"sync_principal",
models.TextField(
blank=True, help_text="Principal to authenticate to kadmin for sync."
),
),
(
"sync_password",
models.TextField(
blank=True, help_text="Password to authenticate to kadmin for sync"
),
),
(
"sync_keytab",
models.TextField(
blank=True,
help_text="Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the form TYPE:residual",
),
),
(
"sync_ccache",
models.TextField(
blank=True,
help_text="Credentials cache to authenticate to kadmin for sync. Must be in the form TYPE:residual",
),
),
(
"spnego_server_name",
models.TextField(
blank=True,
help_text="Force the use of a specific server name for SPNEGO. Must be in the form HTTP@hostname",
),
),
(
"spnego_keytab",
models.TextField(
blank=True,
help_text="SPNEGO keytab base64-encoded or path to keytab in the form FILE:path",
),
),
(
"spnego_ccache",
models.TextField(
blank=True,
help_text="Credential cache to use for SPNEGO in form type:residual",
),
),
(
"password_login_update_internal_password",
models.BooleanField(
default=False,
help_text="If enabled, the authentik-stored password will be updated upon login with the Kerberos password backend",
),
),
],
options={
"verbose_name": "Kerberos Source",
"verbose_name_plural": "Kerberos Sources",
},
bases=("authentik_core.source",),
),
migrations.CreateModel(
name="KerberosSourcePropertyMapping",
fields=[
(
"propertymapping_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.propertymapping",
),
),
],
options={
"verbose_name": "Kerberos Source Property Mapping",
"verbose_name_plural": "Kerberos Source Property Mappings",
},
bases=("authentik_core.propertymapping",),
),
migrations.CreateModel(
name="UserKerberosSourceConnection",
fields=[
(
"usersourceconnection_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.usersourceconnection",
),
),
("identifier", models.TextField()),
],
options={
"verbose_name": "User Kerberos Source Connection",
"verbose_name_plural": "User Kerberos Source Connections",
},
bases=("authentik_core.usersourceconnection",),
),
]

View File

@ -0,0 +1,376 @@
"""authentik Kerberos Source Models"""
import os
from pathlib import Path
from tempfile import gettempdir
from typing import Any
import gssapi
import kadmin
import pglock
from django.db import connection, models
from django.db.models.fields import b64decode
from django.http import HttpRequest
from django.shortcuts import reverse
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.core.models import (
GroupSourceConnection,
PropertyMapping,
Source,
UserSourceConnection,
UserTypes,
)
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.challenge import RedirectChallenge
LOGGER = get_logger()
# python-kadmin leaks file descriptors. As such, this global is used to reuse
# existing kadmin connections instead of creating new ones, which results in less to no file
# descriptors leaks
_kadmin_connections: dict[str, Any] = {}
class KerberosSource(Source):
"""Federate Kerberos realm with authentik"""
realm = models.TextField(help_text=_("Kerberos realm"), unique=True)
krb5_conf = models.TextField(
blank=True,
help_text=_("Custom krb5.conf to use. Uses the system one by default"),
)
sync_users = models.BooleanField(
default=False, help_text=_("Sync users from Kerberos into authentik"), db_index=True
)
sync_users_password = models.BooleanField(
default=True,
help_text=_("When a user changes their password, sync it back to Kerberos"),
db_index=True,
)
sync_principal = models.TextField(
help_text=_("Principal to authenticate to kadmin for sync."), blank=True
)
sync_password = models.TextField(
help_text=_("Password to authenticate to kadmin for sync"), blank=True
)
sync_keytab = models.TextField(
help_text=_(
"Keytab to authenticate to kadmin for sync. "
"Must be base64-encoded or in the form TYPE:residual"
),
blank=True,
)
sync_ccache = models.TextField(
help_text=_(
"Credentials cache to authenticate to kadmin for sync. "
"Must be in the form TYPE:residual"
),
blank=True,
)
spnego_server_name = models.TextField(
help_text=_(
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@hostname"
),
blank=True,
)
spnego_keytab = models.TextField(
help_text=_("SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"),
blank=True,
)
spnego_ccache = models.TextField(
help_text=_("Credential cache to use for SPNEGO in form type:residual"),
blank=True,
)
password_login_update_internal_password = models.BooleanField(
default=False,
help_text=_(
"If enabled, the authentik-stored password will be updated upon "
"login with the Kerberos password backend"
),
)
class Meta:
verbose_name = _("Kerberos Source")
verbose_name_plural = _("Kerberos Sources")
def __str__(self):
return f"Kerberos Source {self.name}"
@property
def component(self) -> str:
return "ak-source-kerberos-form"
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.kerberos.api.source import KerberosSourceSerializer
return KerberosSourceSerializer
@property
def property_mapping_type(self) -> type[PropertyMapping]:
return KerberosSourcePropertyMapping
@property
def icon_url(self) -> str:
icon = super().icon_url
if not icon:
return static("authentik/sources/kerberos.png")
return icon
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
return UILoginButton(
challenge=RedirectChallenge(
data={
"to": reverse(
"authentik_sources_kerberos:spnego-login",
kwargs={"source_slug": self.slug},
),
}
),
name=self.name,
icon_url=self.icon_url,
)
def ui_user_settings(self) -> UserSettingSerializer | None:
return UserSettingSerializer(
data={
"title": self.name,
"component": "ak-user-settings-source-kerberos",
"configure_url": reverse(
"authentik_sources_kerberos:spnego-login",
kwargs={"source_slug": self.slug},
),
"icon_url": self.icon_url,
}
)
@property
def sync_lock(self) -> pglock.advisory:
"""Redis lock for syncing Kerberos to prevent multiple parallel syncs happening"""
return pglock.advisory(
lock_id=f"goauthentik.io/{connection.schema_name}/sources/kerberos/sync/{self.slug}",
timeout=0,
side_effect=pglock.Return,
)
def get_base_user_properties(self, principal: str, **kwargs):
localpart, _ = principal.rsplit("@", 1)
return {
"username": localpart,
"type": UserTypes.INTERNAL,
"path": self.get_user_path(),
}
def get_base_group_properties(self, group_id: str, **kwargs):
return {
"name": group_id,
}
@property
def tempdir(self) -> Path:
"""Get temporary storage for Kerberos files"""
path = (
Path(gettempdir())
/ "authentik"
/ connection.schema_name
/ "sources"
/ "kerberos"
/ str(self.pk)
)
path.mkdir(mode=0o700, parents=True, exist_ok=True)
return path
@property
def krb5_conf_path(self) -> str | None:
"""Get krb5.conf path"""
if not self.krb5_conf:
return None
conf_path = self.tempdir / "krb5.conf"
conf_path.write_text(self.krb5_conf)
return str(conf_path)
def _kadmin_init(self) -> "kadmin.KAdmin | None":
# kadmin doesn't use a ccache for its connection
# as such, we don't need to create a separate ccache for each source
if not self.sync_principal:
return None
if self.sync_password:
return kadmin.init_with_password(
self.sync_principal,
self.sync_password,
)
if self.sync_keytab:
keytab = self.sync_keytab
if ":" not in keytab:
keytab_path = self.tempdir / "kadmin_keytab"
keytab_path.touch(mode=0o600)
keytab_path.write_bytes(b64decode(self.sync_keytab))
keytab = f"FILE:{keytab_path}"
return kadmin.init_with_keytab(
self.sync_principal,
keytab,
)
if self.sync_ccache:
return kadmin.init_with_ccache(
self.sync_principal,
self.sync_ccache,
)
return None
def connection(self) -> "kadmin.KAdmin | None":
"""Get kadmin connection"""
if str(self.pk) not in _kadmin_connections:
kadm = self._kadmin_init()
if kadm is not None:
_kadmin_connections[str(self.pk)] = self._kadmin_init()
return _kadmin_connections.get(str(self.pk), None)
def check_connection(self) -> dict[str, str]:
"""Check Kerberos Connection"""
status = {"status": "ok"}
if not self.sync_users:
return status
with Krb5ConfContext(self):
try:
kadm = self.connection()
if kadm is None:
status["status"] = "no connection"
return status
status["principal_exists"] = kadm.principal_exists(self.sync_principal)
except kadmin.KAdminError as exc:
status["status"] = str(exc)
return status
def get_gssapi_store(self) -> dict[str, str]:
"""Get GSSAPI credentials store for this source"""
ccache = self.spnego_ccache
keytab = None
if not ccache:
ccache_path = self.tempdir / "spnego_ccache"
ccache_path.touch(mode=0o600)
ccache = f"FILE:{ccache_path}"
if self.spnego_keytab:
# Keytab is of the form type:residual, use as-is
if ":" in self.spnego_keytab:
keytab = self.spnego_keytab
# Parse the keytab and write it in the file
else:
keytab_path = self.tempdir / "spnego_keytab"
keytab_path.touch(mode=0o600)
keytab_path.write_bytes(b64decode(self.spnego_keytab))
keytab = f"FILE:{keytab_path}"
store = {"ccache": ccache}
if keytab is not None:
store["keytab"] = keytab
return store
def get_gssapi_creds(self) -> gssapi.creds.Credentials | None:
"""Get GSSAPI credentials for this source"""
try:
name = None
if self.spnego_server_name:
# pylint: disable=c-extension-no-member
name = gssapi.names.Name(
base=self.spnego_server_name,
name_type=gssapi.raw.types.NameType.hostbased_service,
)
return gssapi.creds.Credentials(
usage="accept", name=name, store=self.get_gssapi_store()
)
except gssapi.exceptions.GSSError as exc:
LOGGER.warn("GSSAPI credentials failure", exc=exc)
return None
class Krb5ConfContext:
"""
Context manager to set the path to the krb5.conf config file.
"""
def __init__(self, source: KerberosSource):
self._source = source
self._path = self._source.krb5_conf_path
self._previous = None
def __enter__(self):
if not self._path:
return
self._previous = os.environ.get("KRB5_CONFIG", None)
os.environ["KRB5_CONFIG"] = self._path
def __exit__(self, *args, **kwargs):
if not self._path:
return
if self._previous:
os.environ["KRB5_CONFIG"] = self._previous
else:
del os.environ["KRB5_CONFIG"]
class KerberosSourcePropertyMapping(PropertyMapping):
"""Map Kerberos Property to User object attribute"""
@property
def component(self) -> str:
return "ak-property-mapping-source-kerberos-form"
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.kerberos.api.property_mappings import (
KerberosSourcePropertyMappingSerializer,
)
return KerberosSourcePropertyMappingSerializer
def __str__(self):
return str(self.name)
class Meta:
verbose_name = _("Kerberos Source Property Mapping")
verbose_name_plural = _("Kerberos Source Property Mappings")
class UserKerberosSourceConnection(UserSourceConnection):
"""Connection to configured Kerberos Sources."""
identifier = models.TextField()
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.kerberos.api.source_connection import (
UserKerberosSourceConnectionSerializer,
)
return UserKerberosSourceConnectionSerializer
class Meta:
verbose_name = _("User Kerberos Source Connection")
verbose_name_plural = _("User Kerberos Source Connections")
class GroupKerberosSourceConnection(GroupSourceConnection):
"""Connection to configured Kerberos Sources."""
@property
def serializer(self) -> type[Serializer]:
from authentik.sources.kerberos.api.source_connection import (
GroupKerberosSourceConnectionSerializer,
)
return GroupKerberosSourceConnectionSerializer
class Meta:
verbose_name = _("Group Kerberos Source Connection")
verbose_name_plural = _("Group Kerberos Source Connections")

View File

@ -0,0 +1,18 @@
"""LDAP Settings"""
from celery.schedules import crontab
from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = {
"sources_kerberos_sync": {
"task": "authentik.sources.kerberos.tasks.kerberos_sync_all",
"schedule": crontab(minute=fqdn_rand("sources_kerberos_sync"), hour="*/2"),
"options": {"queue": "authentik_scheduled"},
},
"sources_kerberos_connectivity_check": {
"task": "authentik.sources.kerberos.tasks.kerberos_connectivity_check",
"schedule": crontab(minute=fqdn_rand("sources_kerberos_connectivity_check"), hour="*"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -0,0 +1,61 @@
"""authentik kerberos source signals"""
import kadmin
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.core.signals import password_changed
from authentik.events.models import Event, EventAction
from authentik.sources.kerberos.models import (
KerberosSource,
Krb5ConfContext,
UserKerberosSourceConnection,
)
from authentik.sources.kerberos.tasks import kerberos_connectivity_check, kerberos_sync_single
LOGGER = get_logger()
@receiver(post_save, sender=KerberosSource)
def sync_kerberos_source_on_save(sender, instance: KerberosSource, **_):
"""Ensure that source is synced on save (if enabled)"""
if not instance.enabled or not instance.sync_users:
return
kerberos_sync_single.delay(instance.pk)
kerberos_connectivity_check.delay(instance.pk)
@receiver(password_changed)
def kerberos_sync_password(sender, user: User, password: str, **_):
"""Connect to kerberos and update password."""
user_source_connections = UserKerberosSourceConnection.objects.select_related(
"source__kerberossource"
).filter(
user=user,
source__enabled=True,
source__kerberossource__sync_users=True,
source__kerberossource__sync_users_password=True,
)
for user_source_connection in user_source_connections:
source = user_source_connection.source.kerberossource
if source.pk == getattr(sender, "pk", None):
continue
with Krb5ConfContext(source):
try:
source.connection().getprinc(user_source_connection.identifier).change_password(
password
)
except kadmin.KAdminError as exc:
LOGGER.warning("failed to set Kerberos password", exc=exc, source=source)
Event.new(
EventAction.CONFIGURATION_ERROR,
message=(
"Failed to change password in Kerberos source due to remote error: "
f"{exc}"
),
source=source,
).set_user(user).save()
raise ValidationError("Failed to set password") from exc

View File

@ -0,0 +1,167 @@
"""Sync Kerberos users into authentik"""
from typing import Any
import kadmin
from django.core.exceptions import FieldError
from django.db import IntegrityError, transaction
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.models import Group, User, UserTypes
from authentik.core.sources.mapper import SourceMapper
from authentik.core.sources.matcher import Action, SourceMatcher
from authentik.events.models import Event, EventAction
from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.sources.kerberos.models import (
GroupKerberosSourceConnection,
KerberosSource,
Krb5ConfContext,
UserKerberosSourceConnection,
)
class KerberosSync:
"""Sync Kerberos users into authentik"""
_source: KerberosSource
_logger: BoundLogger
_connection: "kadmin.KAdmin"
mapper: SourceMapper
user_manager: PropertyMappingManager
group_manager: PropertyMappingManager
matcher: SourceMatcher
def __init__(self, source: KerberosSource):
self._source = source
with Krb5ConfContext(self._source):
self._connection = self._source.connection()
self._messages = []
self._logger = get_logger().bind(source=self._source, syncer=self.__class__.__name__)
self.mapper = SourceMapper(self._source)
self.user_manager = self.mapper.get_manager(User, ["principal"])
self.group_manager = self.mapper.get_manager(Group, ["group_id", "principal"])
self.matcher = SourceMatcher(
self._source, UserKerberosSourceConnection, GroupKerberosSourceConnection
)
@staticmethod
def name() -> str:
"""UI name for the type of object this class synchronizes"""
return "users"
@property
def messages(self) -> list[str]:
"""Get all UI messages"""
return self._messages
def message(self, *args, **kwargs):
"""Add message that is later added to the System Task and shown to the user"""
formatted_message = " ".join(args)
self._messages.append(formatted_message)
self._logger.warning(*args, **kwargs)
def _handle_principal(self, principal: str) -> bool:
try:
defaults = self.mapper.build_object_properties(
object_type=User,
manager=self.user_manager,
user=None,
request=None,
principal=principal,
)
self._logger.debug("Writing user with attributes", **defaults)
if "username" not in defaults:
raise IntegrityError("Username was not set by propertymappings")
action, connection = self.matcher.get_user_action(principal, defaults)
self._logger.debug("Action returned", action=action, connection=connection)
if action == Action.DENY:
return False
group_properties = {
group_id: self.mapper.build_object_properties(
object_type=Group,
manager=self.group_manager,
user=None,
request=None,
group_id=group_id,
principal=principal,
)
for group_id in defaults.pop("groups", [])
}
if action == Action.ENROLL:
user = User.objects.create(**defaults)
if user.type == UserTypes.INTERNAL_SERVICE_ACCOUNT:
user.set_unusable_password()
user.save()
connection.user = user
connection.save()
elif action in (Action.AUTH, Action.LINK):
user = connection.user
user.update_attributes(defaults)
else:
return False
groups: list[Group] = []
for group_id, properties in group_properties.items():
group = self._handle_group(group_id, properties)
if group:
groups.append(group)
with transaction.atomic():
user.ak_groups.remove(
*user.ak_groups.filter(groupsourceconnection__source=self._source)
)
user.ak_groups.add(*groups)
except PropertyMappingExpressionException as exc:
raise StopSync(exc, None, exc.mapping) from exc
except SkipObjectException:
return False
except (IntegrityError, FieldError, TypeError, AttributeError) as exc:
Event.new(
EventAction.CONFIGURATION_ERROR,
message=(f"Failed to create user: {str(exc)} "),
source=self._source,
principal=principal,
).save()
return False
self._logger.debug("Synced User", user=user.username)
return True
def _handle_group(
self, group_id: str, defaults: dict[str, Any | dict[str, Any]]
) -> Group | None:
action, connection = self.matcher.get_group_action(group_id, defaults)
if action == Action.DENY:
return None
if action == Action.ENROLL:
group = Group.objects.create(**defaults)
connection.group = group
connection.save()
return group
if action in (Action.AUTH, Action.LINK):
group = connection.group
group.update_attributes(defaults)
connection.save()
return group
return None
def sync(self) -> int:
"""Iterate over all Kerberos users and create authentik_core.User instances"""
if not self._source.enabled or not self._source.sync_users:
self.message("Source is disabled or user syncing is disabled for this Source")
return -1
user_count = 0
with Krb5ConfContext(self._source):
for principal in self._connection.principals():
if self._handle_principal(principal):
user_count += 1
return user_count

View File

@ -0,0 +1,68 @@
"""Kerberos Sync tasks"""
from django.core.cache import cache
from structlog.stdlib import get_logger
from authentik.events.models import SystemTask as DBSystemTask
from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask
from authentik.lib.config import CONFIG
from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.lib.utils.errors import exception_to_string
from authentik.root.celery import CELERY_APP
from authentik.sources.kerberos.models import KerberosSource
from authentik.sources.kerberos.sync import KerberosSync
LOGGER = get_logger()
CACHE_KEY_STATUS = "goauthentik.io/sources/kerberos/status/"
@CELERY_APP.task()
def kerberos_sync_all():
"""Sync all sources"""
for source in KerberosSource.objects.filter(enabled=True, sync_users=True):
kerberos_sync_single.delay(str(source.pk))
@CELERY_APP.task()
def kerberos_connectivity_check(pk: str | None = None):
"""Check connectivity for Kerberos Sources"""
# 2 hour timeout, this task should run every hour
timeout = 60 * 60 * 2
sources = KerberosSource.objects.filter(enabled=True)
if pk:
sources = sources.filter(pk=pk)
for source in sources:
status = source.check_connection()
cache.set(CACHE_KEY_STATUS + source.slug, status, timeout=timeout)
@CELERY_APP.task(
bind=True,
base=SystemTask,
# We take the configured hours timeout time by 2.5 as we run user and
# group in parallel and then membership, so 2x is to cover the serial tasks,
# and 0.5x on top of that to give some more leeway
soft_time_limit=(60 * 60 * CONFIG.get_int("sources.kerberos.task_timeout_hours")) * 2.5,
task_time_limit=(60 * 60 * CONFIG.get_int("sources.kerberos.task_timeout_hours")) * 2.5,
)
def kerberos_sync_single(self, source_pk: str):
"""Sync a single source"""
source: KerberosSource = KerberosSource.objects.filter(pk=source_pk).first()
if not source or not source.enabled:
return
try:
with source.sync_lock as lock_acquired:
if not lock_acquired:
LOGGER.debug(
"Failed to acquire lock for Kerberos sync, skipping task", source=source.slug
)
return
# Delete all sync tasks from the cache
DBSystemTask.objects.filter(name="kerberos_sync", uid__startswith=source.slug).delete()
syncer = KerberosSync(source)
syncer.sync()
self.set_status(TaskStatus.SUCCESSFUL, *syncer.messages)
except StopSync as exc:
LOGGER.warning(exception_to_string(exc))
self.set_error(exc)

View File

@ -0,0 +1,57 @@
"""Kerberos Source Auth tests"""
from django.contrib.auth.hashers import is_password_usable
from authentik.core.models import User
from authentik.lib.generators import generate_id
from authentik.sources.kerberos.auth import KerberosBackend
from authentik.sources.kerberos.models import KerberosSource, UserKerberosSourceConnection
from authentik.sources.kerberos.tests.utils import KerberosTestCase
class TestKerberosAuth(KerberosTestCase):
"""Kerberos Auth tests"""
def setUp(self):
self.source = KerberosSource.objects.create(
name="kerberos",
slug="kerberos",
realm=self.realm.realm,
sync_users=False,
sync_users_password=False,
password_login_update_internal_password=True,
)
self.user = User.objects.create(username=generate_id())
self.user.set_unusable_password()
UserKerberosSourceConnection.objects.create(
source=self.source, user=self.user, identifier=self.realm.user_princ
)
def test_auth_username(self):
"""Test auth username"""
backend = KerberosBackend()
self.assertEqual(
backend.authenticate(
None, username=self.user.username, password=self.realm.password("user")
),
self.user,
)
def test_auth_principal(self):
"""Test auth principal"""
backend = KerberosBackend()
self.assertEqual(
backend.authenticate(
None, username=self.realm.user_princ, password=self.realm.password("user")
),
self.user,
)
def test_internal_password_update(self):
"""Test internal password update"""
backend = KerberosBackend()
backend.authenticate(
None, username=self.realm.user_princ, password=self.realm.password("user")
)
self.user.refresh_from_db()
self.assertTrue(is_password_usable(self.user.password))

View File

@ -0,0 +1,78 @@
"""Kerberos Source SPNEGO tests"""
from base64 import b64decode, b64encode
from pathlib import Path
import gssapi
from django.urls import reverse
from authentik.core.tests.utils import create_test_admin_user
from authentik.sources.kerberos.models import KerberosSource
from authentik.sources.kerberos.tests.utils import KerberosTestCase
class TestSPNEGOSource(KerberosTestCase):
"""Kerberos Source SPNEGO tests"""
def setUp(self):
self.source = KerberosSource.objects.create(
name="test",
slug="test",
spnego_keytab=b64encode(Path(self.realm.http_keytab).read_bytes()).decode(),
)
# Force store creation early
self.source.get_gssapi_store()
def test_api_read(self):
"""Test reading a source"""
self.client.force_login(create_test_admin_user())
response = self.client.get(
reverse(
"authentik_api:kerberossource-detail",
kwargs={
"slug": self.source.slug,
},
)
)
self.assertEqual(response.status_code, 200)
def test_source_login(self):
"""test login view"""
response = self.client.get(
reverse(
"authentik_sources_kerberos:spnego-login",
kwargs={"source_slug": self.source.slug},
)
)
self.assertEqual(response.status_code, 302)
endpoint = response.headers["Location"]
response = self.client.get(endpoint)
self.assertEqual(response.status_code, 401)
self.assertEqual(response.headers["WWW-Authenticate"], "Negotiate")
server_name = gssapi.names.Name("HTTP/testserver@")
client_creds = gssapi.creds.Credentials(
usage="initiate", store={"ccache": self.realm.ccache}
)
client_ctx = gssapi.sec_contexts.SecurityContext(
name=server_name, usage="initiate", creds=client_creds
)
status = 401
server_token = None
while status == 401 and not client_ctx.complete: # noqa: PLR2004
client_token = client_ctx.step(server_token)
if not client_token:
break
response = self.client.get(
endpoint,
headers={"Authorization": f"Negotiate {b64encode(client_token).decode('ascii')}"},
)
status = response.status_code
if status == 401: # noqa: PLR2004
server_token = b64decode(response.headers["WWW-Authenticate"][9:].strip())
# 400 because no enroll flow
self.assertEqual(status, 400)

View File

@ -0,0 +1,75 @@
"""Kerberos Source sync tests"""
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import User
from authentik.lib.generators import generate_id
from authentik.sources.kerberos.models import KerberosSource, KerberosSourcePropertyMapping
from authentik.sources.kerberos.sync import KerberosSync
from authentik.sources.kerberos.tasks import kerberos_sync_all
from authentik.sources.kerberos.tests.utils import KerberosTestCase
class TestKerberosSync(KerberosTestCase):
"""Kerberos Sync tests"""
@apply_blueprint("system/sources-kerberos.yaml")
def setUp(self):
self.source: KerberosSource = KerberosSource.objects.create(
name="kerberos",
slug="kerberos",
realm=self.realm.realm,
sync_users=True,
sync_users_password=True,
sync_principal=self.realm.admin_princ,
sync_password=self.realm.password("admin"),
)
self.source.user_property_mappings.set(
KerberosSourcePropertyMapping.objects.filter(
managed__startswith="goauthentik.io/sources/kerberos/user/default/"
)
)
def test_default_mappings(self):
"""Test default mappings"""
KerberosSync(self.source).sync()
self.assertTrue(
User.objects.filter(username=self.realm.user_princ.rsplit("@", 1)[0]).exists()
)
self.assertFalse(
User.objects.filter(username=self.realm.nfs_princ.rsplit("@", 1)[0]).exists()
)
def test_sync_mapping(self):
"""Test property mappings"""
noop = KerberosSourcePropertyMapping.objects.create(
name=generate_id(), expression="return {}"
)
email = KerberosSourcePropertyMapping.objects.create(
name=generate_id(), expression='return {"email": principal.lower()}'
)
dont_sync_service = KerberosSourcePropertyMapping.objects.create(
name=generate_id(),
expression='if "/" in principal:\n return {"username": None}\nreturn {}',
)
self.source.user_property_mappings.set([noop, email, dont_sync_service])
KerberosSync(self.source).sync()
self.assertTrue(
User.objects.filter(username=self.realm.user_princ.rsplit("@", 1)[0]).exists()
)
self.assertEqual(
User.objects.get(username=self.realm.user_princ.rsplit("@", 1)[0]).email,
self.realm.user_princ.lower(),
)
self.assertFalse(
User.objects.filter(username=self.realm.nfs_princ.rsplit("@", 1)[0]).exists()
)
def test_tasks(self):
"""Test Scheduled tasks"""
kerberos_sync_all.delay().get()
self.assertTrue(
User.objects.filter(username=self.realm.user_princ.rsplit("@", 1)[0]).exists()
)

View File

@ -0,0 +1,40 @@
"""Kerberos Source test utils"""
import os
from copy import deepcopy
from time import sleep
from k5test import realm
from rest_framework.test import APITestCase
class KerberosTestCase(APITestCase):
"""Kerberos Test Case"""
@classmethod
def setUpClass(cls):
cls.realm = realm.K5Realm(start_kadmind=True)
cls.realm.http_princ = f"HTTP/testserver@{cls.realm.realm}"
cls.realm.http_keytab = os.path.join(cls.realm.tmpdir, "http_keytab")
cls.realm.addprinc(cls.realm.http_princ)
cls.realm.extract_keytab(cls.realm.http_princ, cls.realm.http_keytab)
cls._saved_env = deepcopy(os.environ)
for k, v in cls.realm.env.items():
os.environ[k] = v
# Wait for everything to start correctly
# Otherwise leads to flaky tests
sleep(5)
@classmethod
def tearDownClass(cls):
cls.realm.stop()
del cls.realm
for k in deepcopy(os.environ):
if k in cls._saved_env:
os.environ[k] = cls._saved_env[k]
else:
del os.environ[k]
cls._saved_env = None

View File

@ -0,0 +1,22 @@
"""Kerberos Source urls"""
from django.urls import path
from authentik.sources.kerberos.api.property_mappings import KerberosSourcePropertyMappingViewSet
from authentik.sources.kerberos.api.source import KerberosSourceViewSet
from authentik.sources.kerberos.api.source_connection import (
GroupKerberosSourceConnectionViewSet,
UserKerberosSourceConnectionViewSet,
)
from authentik.sources.kerberos.views import SPNEGOView
urlpatterns = [
path("<slug:source_slug>/", SPNEGOView.as_view(), name="spnego-login"),
]
api_urlpatterns = [
("propertymappings/source/kerberos", KerberosSourcePropertyMappingViewSet),
("sources/user_connections/kerberos", UserKerberosSourceConnectionViewSet),
("sources/group_connections/kerberos", GroupKerberosSourceConnectionViewSet),
("sources/kerberos", KerberosSourceViewSet),
]

View File

@ -0,0 +1,181 @@
"""Kerberos source SPNEGO views"""
from base64 import b64decode, b64encode
import gssapi
from django.core.cache import cache
from django.core.exceptions import SuspiciousOperation
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render, reverse
from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from django.views import View
from structlog.stdlib import get_logger
from authentik.core.sources.flow_manager import SourceFlowManager
from authentik.sources.kerberos.models import (
GroupKerberosSourceConnection,
KerberosSource,
Krb5ConfContext,
UserKerberosSourceConnection,
)
LOGGER = get_logger()
SPNEGO_REQUEST_STATUS = 401
WWW_AUTHENTICATE = "WWW-Authenticate"
HTTP_AUTHORIZATION = "Authorization"
NEGOTIATE = "Negotiate"
SPNEGO_STATE_CACHE_PREFIX = "goauthentik.io/sources/spnego"
SPNEGO_STATE_CACHE_TIMEOUT = 60 * 5 # 5 minutes
def add_negotiate_to_response(
response: HttpResponse, token: str | bytes | None = None
) -> HttpResponse:
if isinstance(token, str):
token = token.encode()
response[WWW_AUTHENTICATE] = (
NEGOTIATE if token is None else f"{NEGOTIATE} {b64encode(token).decode('ascii')}"
)
return response
class SPNEGOView(View):
"""SPNEGO login"""
source: KerberosSource
def challenge(self, request, token: str | bytes | None = None) -> HttpResponse:
"""Get SNPEGO challenge response"""
response = render(
request,
"if/error.html",
context={
"title": _("SPNEGO authentication required"),
"message": _(
"""
Make sure you have valid tickets (obtainable via kinit)
and configured the browser correctly.
Please contact your administrator.
"""
),
},
status=401,
)
return add_negotiate_to_response(response, token)
def get_authstr(self, request) -> str | None:
"""Get SPNEGO authentication string from headers"""
authorization_header = request.headers.get(HTTP_AUTHORIZATION, "")
if NEGOTIATE.lower() not in authorization_header.lower():
return None
auth_tuple = authorization_header.split(" ", 1)
if not auth_tuple or auth_tuple[0].lower() != NEGOTIATE.lower():
return None
if len(auth_tuple) != 2: # noqa: PLR2004
raise SuspiciousOperation("Malformed authorization header")
return auth_tuple[1]
def new_state(self) -> str:
"""Generate request state"""
return get_random_string(32)
def get_server_ctx(self, key: str) -> gssapi.sec_contexts.SecurityContext | None:
"""Get GSSAPI server context from cache or create it"""
server_creds = self.source.get_gssapi_creds()
if server_creds is None:
return None
state = cache.get(f"{SPNEGO_STATE_CACHE_PREFIX}/{key}", None)
if state:
# pylint: disable=c-extension-no-member
return gssapi.sec_contexts.SecurityContext(
base=gssapi.raw.sec_contexts.import_sec_context(state),
)
return gssapi.sec_contexts.SecurityContext(creds=server_creds, usage="accept")
def set_server_ctx(self, key: str, ctx: gssapi.sec_contexts.SecurityContext):
"""Store the GSSAPI server context in cache"""
cache.set(f"{SPNEGO_STATE_CACHE_PREFIX}/{key}", ctx.export(), SPNEGO_STATE_CACHE_TIMEOUT)
# pylint: disable=too-many-return-statements
def dispatch(self, request, *args, **kwargs) -> HttpResponse:
"""Process SPNEGO request"""
self.source: KerberosSource = get_object_or_404(
KerberosSource,
slug=kwargs.get("source_slug", ""),
enabled=True,
)
qstring = request.GET if request.method == "GET" else request.POST
state = qstring.get("state", None)
if not state:
return redirect(
reverse(
"authentik_sources_kerberos:spnego-login",
kwargs={"source_slug": self.source.slug},
)
+ f"?state={self.new_state()}"
)
authstr = self.get_authstr(request)
if not authstr:
LOGGER.debug("authstr not present, sending challenge")
return self.challenge(request)
try:
in_token = b64decode(authstr)
except (TypeError, ValueError):
return self.challenge(request)
with Krb5ConfContext(self.source):
server_ctx = self.get_server_ctx(state)
if not server_ctx:
return self.challenge(request)
try:
out_token = server_ctx.step(in_token)
except gssapi.exceptions.GSSError as exc:
LOGGER.debug("GSSAPI security context failure", exc=exc)
return self.challenge(request)
if not server_ctx.complete or server_ctx.initiator_name is None:
self.set_server_ctx(state, server_ctx)
return self.challenge(request, out_token)
def name_to_str(n: gssapi.names.Name) -> str:
return n.display_as(n.name_type)
identifier = name_to_str(server_ctx.initiator_name)
context = {
"spnego_info": {
"initiator_name": name_to_str(server_ctx.initiator_name),
"target_name": name_to_str(server_ctx.target_name),
"mech": str(server_ctx.mech),
"actual_flags": server_ctx.actual_flags,
},
}
response = SPNEGOSourceFlowManager(
source=self.source,
request=request,
identifier=identifier,
user_info={
"principal": identifier,
**context,
},
policy_context=context,
).get_flow()
return add_negotiate_to_response(response, out_token)
class SPNEGOSourceFlowManager(SourceFlowManager):
"""Flow manager for Kerberos SPNEGO sources"""
user_connection_type = UserKerberosSourceConnection
group_connection_type = GroupKerberosSourceConnection

View File

@ -43,7 +43,7 @@ class LDAPBackend(InbuiltBackend):
if source.password_login_update_internal_password:
# Password given successfully binds to LDAP, so we save it in our Database
LOGGER.debug("Updating user's password in DB", user=user)
user.set_password(password, signal=False)
user.set_password(password, sender=source)
user.save()
return user
# Password doesn't match

View File

@ -62,6 +62,8 @@ def ldap_sync_password(sender, user: User, password: str, **_):
if not sources.exists():
return
source = sources.first()
if source.pk == getattr(sender, "pk", None):
return
if not LDAPPasswordChanger.should_check_user(user):
return
try:

View File

@ -3,3 +3,4 @@
BACKEND_INBUILT = "authentik.core.auth.InbuiltBackend"
BACKEND_LDAP = "authentik.sources.ldap.auth.LDAPBackend"
BACKEND_APP_PASSWORD = "authentik.core.auth.TokenBackend" # nosec
BACKEND_KERBEROS = "authentik.sources.kerberos.auth.KerberosBackend"

View File

@ -0,0 +1,36 @@
# Generated by Django 5.0.8 on 2024-08-07 22:17
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_password", "0009_passwordstage_allow_show_password"),
]
operations = [
migrations.AlterField(
model_name="passwordstage",
name="backends",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(
choices=[
("authentik.core.auth.InbuiltBackend", "User database + standard password"),
("authentik.core.auth.TokenBackend", "User database + app passwords"),
(
"authentik.sources.ldap.auth.LDAPBackend",
"User database + LDAP password",
),
(
"authentik.sources.kerberos.auth.KerberosBackend",
"User database + Kerberos password",
),
]
),
help_text="Selection of backends to test the password against.",
size=None,
),
),
]

View File

@ -8,7 +8,12 @@ from rest_framework.serializers import BaseSerializer
from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, Stage
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
from authentik.stages.password import (
BACKEND_APP_PASSWORD,
BACKEND_INBUILT,
BACKEND_KERBEROS,
BACKEND_LDAP,
)
def get_authentication_backends():
@ -26,6 +31,10 @@ def get_authentication_backends():
BACKEND_LDAP,
_("User database + LDAP password"),
),
(
BACKEND_KERBEROS,
_("User database + Kerberos password"),
),
]

View File

@ -19,6 +19,7 @@ entries:
- attrs:
backends:
- authentik.core.auth.InbuiltBackend
- authentik.sources.kerberos.auth.KerberosBackend
- authentik.sources.ldap.auth.LDAPBackend
- authentik.core.auth.TokenBackend
configure_flow: !Find [authentik_flows.flow, [slug, default-password-change]]

View File

@ -1081,6 +1081,166 @@
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_sources_kerberos.kerberossource"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossource_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossource"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossource"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_sources_kerberos.kerberossourcepropertymapping"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossourcepropertymapping_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossourcepropertymapping"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_sources_kerberos.kerberossourcepropertymapping"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_sources_kerberos.userkerberossourceconnection"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_sources_kerberos.userkerberossourceconnection_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_sources_kerberos.userkerberossourceconnection"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_sources_kerberos.userkerberossourceconnection"
}
}
},
{
"type": "object",
"required": [
"model",
"identifiers"
],
"properties": {
"model": {
"const": "authentik_sources_kerberos.groupkerberossourceconnection"
},
"id": {
"type": "string"
},
"state": {
"type": "string",
"enum": [
"absent",
"present",
"created",
"must_created"
],
"default": "present"
},
"conditions": {
"type": "array",
"items": {
"type": "boolean"
}
},
"permissions": {
"$ref": "#/$defs/model_authentik_sources_kerberos.groupkerberossourceconnection_permissions"
},
"attrs": {
"$ref": "#/$defs/model_authentik_sources_kerberos.groupkerberossourceconnection"
},
"identifiers": {
"$ref": "#/$defs/model_authentik_sources_kerberos.groupkerberossourceconnection"
}
}
},
{
"type": "object",
"required": [
@ -4311,6 +4471,7 @@
"authentik.providers.scim",
"authentik.rbac",
"authentik.recovery",
"authentik.sources.kerberos",
"authentik.sources.ldap",
"authentik.sources.oauth",
"authentik.sources.plex",
@ -4384,6 +4545,10 @@
"authentik_providers_scim.scimprovider",
"authentik_providers_scim.scimmapping",
"authentik_rbac.role",
"authentik_sources_kerberos.kerberossource",
"authentik_sources_kerberos.kerberossourcepropertymapping",
"authentik_sources_kerberos.userkerberossourceconnection",
"authentik_sources_kerberos.groupkerberossourceconnection",
"authentik_sources_ldap.ldapsource",
"authentik_sources_ldap.ldapsourcepropertymapping",
"authentik_sources_oauth.oauthsource",
@ -6413,6 +6578,22 @@
"authentik_rbac.view_role",
"authentik_rbac.view_system_info",
"authentik_rbac.view_system_settings",
"authentik_sources_kerberos.add_groupkerberossourceconnection",
"authentik_sources_kerberos.add_kerberossource",
"authentik_sources_kerberos.add_kerberossourcepropertymapping",
"authentik_sources_kerberos.add_userkerberossourceconnection",
"authentik_sources_kerberos.change_groupkerberossourceconnection",
"authentik_sources_kerberos.change_kerberossource",
"authentik_sources_kerberos.change_kerberossourcepropertymapping",
"authentik_sources_kerberos.change_userkerberossourceconnection",
"authentik_sources_kerberos.delete_groupkerberossourceconnection",
"authentik_sources_kerberos.delete_kerberossource",
"authentik_sources_kerberos.delete_kerberossourcepropertymapping",
"authentik_sources_kerberos.delete_userkerberossourceconnection",
"authentik_sources_kerberos.view_groupkerberossourceconnection",
"authentik_sources_kerberos.view_kerberossource",
"authentik_sources_kerberos.view_kerberossourcepropertymapping",
"authentik_sources_kerberos.view_userkerberossourceconnection",
"authentik_sources_ldap.add_ldapsource",
"authentik_sources_ldap.add_ldapsourcepropertymapping",
"authentik_sources_ldap.change_ldapsource",
@ -6660,6 +6841,319 @@
}
}
},
"model_authentik_sources_kerberos.kerberossource": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"title": "Name",
"description": "Source's display Name."
},
"slug": {
"type": "string",
"maxLength": 50,
"minLength": 1,
"pattern": "^[-a-zA-Z0-9_]+$",
"title": "Slug",
"description": "Internal source name, used in URLs."
},
"enabled": {
"type": "boolean",
"title": "Enabled"
},
"authentication_flow": {
"type": "string",
"format": "uuid",
"title": "Authentication flow",
"description": "Flow to use when authenticating existing users."
},
"enrollment_flow": {
"type": "string",
"format": "uuid",
"title": "Enrollment flow",
"description": "Flow to use when enrolling new users."
},
"user_property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "User property mappings"
},
"group_property_mappings": {
"type": "array",
"items": {
"type": "string",
"format": "uuid"
},
"title": "Group property mappings"
},
"policy_engine_mode": {
"type": "string",
"enum": [
"all",
"any"
],
"title": "Policy engine mode"
},
"user_matching_mode": {
"type": "string",
"enum": [
"identifier",
"email_link",
"email_deny",
"username_link",
"username_deny"
],
"title": "User matching mode",
"description": "How the source determines if an existing user should be authenticated or a new user enrolled."
},
"user_path_template": {
"type": "string",
"minLength": 1,
"title": "User path template"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
},
"group_matching_mode": {
"type": "string",
"enum": [
"identifier",
"name_link",
"name_deny"
],
"title": "Group matching mode",
"description": "How the source determines if an existing group should be used or a new group created."
},
"realm": {
"type": "string",
"minLength": 1,
"title": "Realm",
"description": "Kerberos realm"
},
"krb5_conf": {
"type": "string",
"title": "Krb5 conf",
"description": "Custom krb5.conf to use. Uses the system one by default"
},
"sync_users": {
"type": "boolean",
"title": "Sync users",
"description": "Sync users from Kerberos into authentik"
},
"sync_users_password": {
"type": "boolean",
"title": "Sync users password",
"description": "When a user changes their password, sync it back to Kerberos"
},
"sync_principal": {
"type": "string",
"title": "Sync principal",
"description": "Principal to authenticate to kadmin for sync."
},
"sync_password": {
"type": "string",
"title": "Sync password",
"description": "Password to authenticate to kadmin for sync"
},
"sync_keytab": {
"type": "string",
"title": "Sync keytab",
"description": "Keytab to authenticate to kadmin for sync. Must be base64-encoded or in the form TYPE:residual"
},
"sync_ccache": {
"type": "string",
"title": "Sync ccache",
"description": "Credentials cache to authenticate to kadmin for sync. Must be in the form TYPE:residual"
},
"spnego_server_name": {
"type": "string",
"title": "Spnego server name",
"description": "Force the use of a specific server name for SPNEGO"
},
"spnego_keytab": {
"type": "string",
"title": "Spnego keytab",
"description": "SPNEGO keytab base64-encoded or path to keytab in the form FILE:path"
},
"spnego_ccache": {
"type": "string",
"title": "Spnego ccache",
"description": "Credential cache to use for SPNEGO in form type:residual"
},
"password_login_update_internal_password": {
"type": "boolean",
"title": "Password login update internal password",
"description": "If enabled, the authentik-stored password will be updated upon login with the Kerberos password backend"
}
},
"required": []
},
"model_authentik_sources_kerberos.kerberossource_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_kerberossource",
"change_kerberossource",
"delete_kerberossource",
"view_kerberossource"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_sources_kerberos.kerberossourcepropertymapping": {
"type": "object",
"properties": {
"managed": {
"type": [
"string",
"null"
],
"minLength": 1,
"title": "Managed by authentik",
"description": "Objects that are managed by authentik. These objects are created and updated automatically. This flag only indicates that an object can be overwritten by migrations. You can still modify the objects via the API, but expect changes to be overwritten in a later update."
},
"name": {
"type": "string",
"minLength": 1,
"title": "Name"
},
"expression": {
"type": "string",
"minLength": 1,
"title": "Expression"
}
},
"required": []
},
"model_authentik_sources_kerberos.kerberossourcepropertymapping_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_kerberossourcepropertymapping",
"change_kerberossourcepropertymapping",
"delete_kerberossourcepropertymapping",
"view_kerberossourcepropertymapping"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_sources_kerberos.userkerberossourceconnection": {
"type": "object",
"properties": {
"user": {
"type": "integer",
"title": "User"
},
"identifier": {
"type": "string",
"minLength": 1,
"title": "Identifier"
},
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
}
},
"required": []
},
"model_authentik_sources_kerberos.userkerberossourceconnection_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_userkerberossourceconnection",
"change_userkerberossourceconnection",
"delete_userkerberossourceconnection",
"view_userkerberossourceconnection"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_sources_kerberos.groupkerberossourceconnection": {
"type": "object",
"properties": {
"icon": {
"type": "string",
"minLength": 1,
"title": "Icon"
}
},
"required": []
},
"model_authentik_sources_kerberos.groupkerberossourceconnection_permissions": {
"type": "array",
"items": {
"type": "object",
"required": [
"permission"
],
"properties": {
"permission": {
"type": "string",
"enum": [
"add_groupkerberossourceconnection",
"change_groupkerberossourceconnection",
"delete_groupkerberossourceconnection",
"view_groupkerberossourceconnection"
]
},
"user": {
"type": "integer"
},
"role": {
"type": "string"
}
}
}
},
"model_authentik_sources_ldap.ldapsource": {
"type": "object",
"properties": {
@ -10544,7 +11038,8 @@
"enum": [
"authentik.core.auth.InbuiltBackend",
"authentik.core.auth.TokenBackend",
"authentik.sources.ldap.auth.LDAPBackend"
"authentik.sources.ldap.auth.LDAPBackend",
"authentik.sources.kerberos.auth.KerberosBackend"
],
"title": "Backends"
},
@ -12081,6 +12576,22 @@
"authentik_rbac.view_role",
"authentik_rbac.view_system_info",
"authentik_rbac.view_system_settings",
"authentik_sources_kerberos.add_groupkerberossourceconnection",
"authentik_sources_kerberos.add_kerberossource",
"authentik_sources_kerberos.add_kerberossourcepropertymapping",
"authentik_sources_kerberos.add_userkerberossourceconnection",
"authentik_sources_kerberos.change_groupkerberossourceconnection",
"authentik_sources_kerberos.change_kerberossource",
"authentik_sources_kerberos.change_kerberossourcepropertymapping",
"authentik_sources_kerberos.change_userkerberossourceconnection",
"authentik_sources_kerberos.delete_groupkerberossourceconnection",
"authentik_sources_kerberos.delete_kerberossource",
"authentik_sources_kerberos.delete_kerberossourcepropertymapping",
"authentik_sources_kerberos.delete_userkerberossourceconnection",
"authentik_sources_kerberos.view_groupkerberossourceconnection",
"authentik_sources_kerberos.view_kerberossource",
"authentik_sources_kerberos.view_kerberossourcepropertymapping",
"authentik_sources_kerberos.view_userkerberossourceconnection",
"authentik_sources_ldap.add_ldapsource",
"authentik_sources_ldap.add_ldapsourcepropertymapping",
"authentik_sources_ldap.change_ldapsource",

View File

@ -0,0 +1,55 @@
version: 1
metadata:
labels:
blueprints.goauthentik.io/system: "true"
name: System - Kerberos Source - Mappings
entries:
- identifiers:
managed: goauthentik.io/sources/kerberos/user/default/multipart-principals-as-service-accounts
model: authentik_sources_kerberos.kerberossourcepropertymapping
attrs:
name: "authentik default Kerberos User Mapping: Multipart principals as service accounts"
expression: |
from authentik.core.models import USER_PATH_SERVICE_ACCOUNT, UserTypes
localpart, _ = principal.rsplit("@", 1)
is_service_account = "/" in localpart
attrs = {}
if is_service_account:
attrs = {
"type": UserTypes.SERVICE_ACCOUNT,
"path": USER_PATH_SERVICE_ACCOUNT,
}
return attrs
- identifiers:
managed: goauthentik.io/sources/kerberos/user/default/ignore-other-realms
model: authentik_sources_kerberos.kerberossourcepropertymapping
attrs:
name: "authentik default Kerberos User Mapping: Ignore other realms"
expression: |
localpart, realm = principal.rsplit("@", 1)
if realm.upper() != source.realm.upper():
raise SkipObject
return {}
- identifiers:
managed: goauthentik.io/sources/kerberos/user/default/ignore-system-principals
model: authentik_sources_kerberos.kerberossourcepropertymapping
attrs:
name: "authentik default Kerberos User Mapping: Ignore system principals"
expression: |
localpart, realm = principal.rsplit("@", 1)
denied_prefixes = ["kadmin/", "krbtgt/", "K/M", "WELLKNOWN/"]
for prefix in denied_prefixes:
if localpart.lower().startswith(prefix.lower()):
raise SkipObject
return {}
- identifiers:
managed: goauthentik.io/sources/kerberos/user/realm-as-group
model: authentik_sources_kerberos.kerberossourcepropertymapping
attrs:
name: "authentik default Kerberos User Mapping: Add realm as group"
expression: |
localpart, realm = principal.rsplit("@", 1)
return {
"groups": [realm.upper()]
}

View File

@ -2,7 +2,7 @@
MODE_FILE="${TMPDIR}/authentik-mode"
function log {
printf '{"event": "%s", "level": "info", "logger": "bootstrap"}\n' "$@" > /dev/stderr
printf '{"event": "%s", "level": "info", "logger": "bootstrap"}\n' "$@" >/dev/stderr
}
function wait_for_db {
@ -45,7 +45,7 @@ function run_authentik {
}
function set_mode {
echo $1 > $MODE_FILE
echo $1 >$MODE_FILE
trap cleanup EXIT
}
@ -54,6 +54,7 @@ function cleanup {
}
function prepare_debug {
apt-get install -y --no-install-recommends krb5-kdc krb5-user krb5-admin-server
VIRTUAL_ENV=/ak-root/venv poetry install --no-ansi --no-interaction
touch /unittest.xml
chown authentik:authentik /unittest.xml

80
poetry.lock generated
View File

@ -1157,6 +1157,17 @@ files = [
{file = "debugpy-1.8.7.zip", hash = "sha256:18b8f731ed3e2e1df8e9cdaa23fb1fc9c24e570cd0081625308ec51c82efe42e"},
]
[[package]]
name = "decorator"
version = "5.1.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.5"
files = [
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
]
[[package]]
name = "deepmerge"
version = "2.0"
@ -1836,6 +1847,42 @@ protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4
[package.extras]
grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"]
[[package]]
name = "gssapi"
version = "1.8.3"
description = "Python GSSAPI Wrapper"
optional = false
python-versions = ">=3.7"
files = [
{file = "gssapi-1.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4e4a83e9b275fe69b5d40be6d5479889866b80333a12c51a9243f2712d4f0554"},
{file = "gssapi-1.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d57d67547e18f4e44a688bfb20abbf176d1b8df547da2b31c3f2df03cfdc269"},
{file = "gssapi-1.8.3-cp310-cp310-win32.whl", hash = "sha256:3a3f63105f39c4af29ffc8f7b6542053d87fe9d63010c689dd9a9f5571facb8e"},
{file = "gssapi-1.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:b031c0f186ab4275186da385b2c7470dd47c9b27522cb3b753757c9ac4bebf11"},
{file = "gssapi-1.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b03d6b30f1fcd66d9a688b45a97e302e4dd3f1386d5c333442731aec73cdb409"},
{file = "gssapi-1.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca6ceb17fc15eda2a69f2e8c6cf10d11e2edb32832255e5d4c65b21b6db4680a"},
{file = "gssapi-1.8.3-cp311-cp311-win32.whl", hash = "sha256:edc8ef3a9e397dbe18bb6016f8e2209969677b534316d20bb139da2865a38efe"},
{file = "gssapi-1.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:8fdb1ff130cee49bc865ec1624dee8cf445cd6c6e93b04bffef2c6f363a60cb9"},
{file = "gssapi-1.8.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:19c373b3ba63ce19cd3163aa1495635e3d01b0de6cc4ff1126095eded1df6e01"},
{file = "gssapi-1.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f1a8046d695f2c9b8d640a6e385780d3945c0741571ed6fee6f94c31e431dc"},
{file = "gssapi-1.8.3-cp312-cp312-win32.whl", hash = "sha256:338db18612e3e6ed64e92b6d849242a535fdc98b365f21122992fb8cae737617"},
{file = "gssapi-1.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:5731c5b40ecc3116cfe7fb7e1d1e128583ec8b3df1e68bf8cd12073160793acd"},
{file = "gssapi-1.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e556878da197ad115a566d36e46a8082d0079731d9c24d1ace795132d725ff2a"},
{file = "gssapi-1.8.3-cp37-cp37m-win32.whl", hash = "sha256:e2bb081f2db2111377effe7d40ba23f9a87359b9d2f4881552b731e9da88b36b"},
{file = "gssapi-1.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4d9ed83f2064cda60aad90e6840ae282096801b2c814b8cbd390bf0df4635aab"},
{file = "gssapi-1.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7d91fe6e2a5c89b32102ea8e374b8ae13b9031d43d7b55f3abc1f194ddce820d"},
{file = "gssapi-1.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d5b28237afc0668046934792756dd4b6b7e957b0d95a608d02f296734a2819ad"},
{file = "gssapi-1.8.3-cp38-cp38-win32.whl", hash = "sha256:791e44f7bea602b8e3da1ec56fbdb383b8ee3326fdeb736f904c2aa9af13a67d"},
{file = "gssapi-1.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:5b4bf84d0a6d7779a4bf11dacfd3db57ae02dd53562e2aeadac4219a68eaee07"},
{file = "gssapi-1.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e40efc88ccefefd6142f8c47b8af498731938958b808bad49990442a91f45160"},
{file = "gssapi-1.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee74b9211c977b9181ff4652d886d7712c9a221560752a35393b58e5ea07887a"},
{file = "gssapi-1.8.3-cp39-cp39-win32.whl", hash = "sha256:465c6788f2ac6ef7c738394ba8fde1ede6004e5721766f386add63891d8c90af"},
{file = "gssapi-1.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:8fb8ee70458f47b51ed881a6881f30b187c987c02af16cc0fff0079255d4d465"},
{file = "gssapi-1.8.3.tar.gz", hash = "sha256:aa3c8d0b1526f52559552bb2c9d2d6be013d76a8e5db00b39a1db5727e93b0b0"},
]
[package.dependencies]
decorator = "*"
[[package]]
name = "gunicorn"
version = "23.0.0"
@ -2228,6 +2275,20 @@ files = [
cryptography = ">=3.4"
typing-extensions = ">=4.5.0"
[[package]]
name = "k5test"
version = "0.10.4"
description = "A library for testing Python applications in self-contained Kerberos 5 environments"
optional = false
python-versions = ">=3.6"
files = [
{file = "k5test-0.10.4-py2.py3-none-any.whl", hash = "sha256:33de7ff10bf99155fe8ee5d5976798ad1db6237214306dadf5a0ae9d6bb0ad03"},
{file = "k5test-0.10.4.tar.gz", hash = "sha256:e152491e6602f6a93b3d533d387bd4590f2476093b6842170ff0b93de64bef30"},
]
[package.extras]
extension-test = ["gssapi"]
[[package]]
name = "kombu"
version = "5.3.7"
@ -3129,8 +3190,6 @@ files = [
{file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
{file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
{file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
{file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
{file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
{file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
{file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
@ -3888,6 +3947,21 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-kadmin"
version = "0.2.0"
description = "Python module for kerberos admin (kadm5)"
optional = false
python-versions = ">=3.8"
files = []
develop = false
[package.source]
type = "git"
url = "https://github.com/authentik-community/python-kadmin.git"
reference = "v0.2.0"
resolved_reference = "6f9ce6ee2427e3488b403a900a9211166c7569e1"
[[package]]
name = "pytz"
version = "2024.1"
@ -5488,4 +5562,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "~3.12"
content-hash = "f3bd82b8ae975dbb660a97fe248f118f780e43687d082d49f37a2d53b450adda"
content-hash = "10aa88f2f0e56cddd91adba8c39c52de92763429fb615a27c3dc218952cff808"

View File

@ -115,6 +115,7 @@ flower = "*"
geoip2 = "*"
google-api-python-client = "*"
gunicorn = "*"
gssapi = "*"
jsonpatch = "*"
jwcrypto = "*"
kubernetes = "*"
@ -130,6 +131,8 @@ pydantic-scim = "*"
pyjwt = "*"
pyrad = "*"
python = "~3.12"
# Fork of python-kadmin with compilation fixes as it's unmaintained
python-kadmin = { git = "https://github.com/authentik-community/python-kadmin.git", tag = "v0.2.0" }
pyyaml = "*"
requests-oauthlib = "*"
scim2-filter-parser = "*"
@ -162,6 +165,7 @@ debugpy = "*"
drf-jsonschema-serializer = "*"
freezegun = "*"
importlib-metadata = "*"
k5test = "*"
pdoc = "*"
pytest = "*"
pytest-django = "*"

1673
schema.yml

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -6,6 +6,7 @@ import "@goauthentik/admin/property-mappings/PropertyMappingProviderRadiusForm";
import "@goauthentik/admin/property-mappings/PropertyMappingProviderSAMLForm";
import "@goauthentik/admin/property-mappings/PropertyMappingProviderSCIMForm";
import "@goauthentik/admin/property-mappings/PropertyMappingProviderScopeForm";
import "@goauthentik/admin/property-mappings/PropertyMappingSourceKerberosForm";
import "@goauthentik/admin/property-mappings/PropertyMappingSourceLDAPForm";
import "@goauthentik/admin/property-mappings/PropertyMappingSourceOAuthForm";
import "@goauthentik/admin/property-mappings/PropertyMappingSourcePlexForm";

View File

@ -0,0 +1,40 @@
import { BasePropertyMappingForm } from "@goauthentik/admin/property-mappings/BasePropertyMappingForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { customElement } from "lit/decorators.js";
import { KerberosSourcePropertyMapping, PropertymappingsApi } from "@goauthentik/api";
@customElement("ak-property-mapping-source-kerberos-form")
export class PropertyMappingSourceKerberosForm extends BasePropertyMappingForm<KerberosSourcePropertyMapping> {
docLink(): string {
return "/docs/sources/property-mappings/expressions?utm_source=authentik";
}
loadInstance(pk: string): Promise<KerberosSourcePropertyMapping> {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceKerberosRetrieve({
pmUuid: pk,
});
}
async send(data: KerberosSourcePropertyMapping): Promise<KerberosSourcePropertyMapping> {
if (this.instance) {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceKerberosUpdate({
pmUuid: this.instance.pk,
kerberosSourcePropertyMappingRequest: data,
});
} else {
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsSourceKerberosCreate({
kerberosSourcePropertyMappingRequest: data,
});
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-property-mapping-source-kerberos-form": PropertyMappingSourceKerberosForm;
}
}

View File

@ -6,6 +6,7 @@ import "@goauthentik/admin/property-mappings/PropertyMappingProviderRadiusForm";
import "@goauthentik/admin/property-mappings/PropertyMappingProviderSAMLForm";
import "@goauthentik/admin/property-mappings/PropertyMappingProviderSCIMForm";
import "@goauthentik/admin/property-mappings/PropertyMappingProviderScopeForm";
import "@goauthentik/admin/property-mappings/PropertyMappingSourceKerberosForm";
import "@goauthentik/admin/property-mappings/PropertyMappingSourceLDAPForm";
import "@goauthentik/admin/property-mappings/PropertyMappingSourceOAuthForm";
import "@goauthentik/admin/property-mappings/PropertyMappingSourcePlexForm";

View File

@ -1,4 +1,5 @@
import "@goauthentik/admin/sources/SourceWizard";
import "@goauthentik/admin/sources/kerberos/KerberosSourceForm";
import "@goauthentik/admin/sources/ldap/LDAPSourceForm";
import "@goauthentik/admin/sources/oauth/OAuthSourceForm";
import "@goauthentik/admin/sources/plex/PlexSourceForm";

View File

@ -1,3 +1,4 @@
import "@goauthentik/admin/sources/kerberos/KerberosSourceViewPage";
import "@goauthentik/admin/sources/ldap/LDAPSourceViewPage";
import "@goauthentik/admin/sources/oauth/OAuthSourceViewPage";
import "@goauthentik/admin/sources/plex/PlexSourceViewPage";
@ -36,6 +37,10 @@ export class SourceViewPage extends AKElement {
return html`<ak-empty-state ?loading=${true} ?fullHeight=${true}></ak-empty-state>`;
}
switch (this.source?.component) {
case "ak-source-kerberos-form":
return html`<ak-source-kerberos-view
sourceSlug=${this.source.slug}
></ak-source-kerberos-view>`;
case "ak-source-ldap-form":
return html`<ak-source-ldap-view
sourceSlug=${this.source.slug}

View File

@ -1,3 +1,4 @@
import "@goauthentik/admin/sources/kerberos/KerberosSourceForm";
import "@goauthentik/admin/sources/ldap/LDAPSourceForm";
import "@goauthentik/admin/sources/oauth/OAuthSourceForm";
import "@goauthentik/admin/sources/plex/PlexSourceForm";

View File

@ -0,0 +1,39 @@
import { AKElement } from "@goauthentik/elements/Base";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-source-kerberos-connectivity")
export class KerberosSourceConnectivity extends AKElement {
@property()
connectivity?: {
[key: string]: {
[key: string]: string;
};
};
static get styles(): CSSResult[] {
return [PFBase, PFList];
}
render(): TemplateResult {
if (!this.connectivity) {
return html``;
}
return html`<ul class="pf-c-list">
${Object.keys(this.connectivity).map((serverKey) => {
return html`<li>${serverKey}: ${this.connectivity![serverKey]}</li>`;
})}
</ul>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-source-kerberos-connectivity": KerberosSourceConnectivity;
}
}

View File

@ -0,0 +1,456 @@
import "@goauthentik/admin/common/ak-flow-search/ak-source-flow-search";
import { iconHelperText, placeholderHelperText } from "@goauthentik/admin/helperText";
import { BaseSourceForm } from "@goauthentik/admin/sources/BaseSourceForm";
import {
GroupMatchingModeToLabel,
UserMatchingModeToLabel,
} from "@goauthentik/admin/sources/oauth/utils";
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import {
CapabilitiesEnum,
WithCapabilitiesConfig,
} from "@goauthentik/elements/Interface/capabilitiesProvider";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
GroupMatchingModeEnum,
KerberosSource,
KerberosSourcePropertyMapping,
KerberosSourceRequest,
PropertymappingsApi,
SourcesApi,
UserMatchingModeEnum,
} from "@goauthentik/api";
async function propertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsSourceKerberosList({
ordering: "managed",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
function makePropertyMappingsSelector(object: string, instanceMappings?: string[]) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<KerberosSourcePropertyMapping>) =>
object == "user" &&
mapping?.managed?.startsWith("goauthentik.io/sources/kerberos/user/default/");
}
@customElement("ak-source-kerberos-form")
export class KerberosSourceForm extends WithCapabilitiesConfig(BaseSourceForm<KerberosSource>) {
async loadInstance(pk: string): Promise<KerberosSource> {
const source = await new SourcesApi(DEFAULT_CONFIG).sourcesKerberosRetrieve({
slug: pk,
});
this.clearIcon = false;
return source;
}
@state()
clearIcon = false;
async send(data: KerberosSource): Promise<KerberosSource> {
let source: KerberosSource;
if (this.instance) {
source = await new SourcesApi(DEFAULT_CONFIG).sourcesKerberosPartialUpdate({
slug: this.instance.slug,
patchedKerberosSourceRequest: data,
});
} else {
source = await new SourcesApi(DEFAULT_CONFIG).sourcesKerberosCreate({
kerberosSourceRequest: data as unknown as KerberosSourceRequest,
});
}
const c = await config();
if (c.capabilities.includes(CapabilitiesEnum.CanSaveMedia)) {
const icon = this.getFormFiles()["icon"];
if (icon || this.clearIcon) {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconCreate({
slug: source.slug,
file: icon,
clear: this.clearIcon,
});
}
} else {
await new SourcesApi(DEFAULT_CONFIG).sourcesAllSetIconUrlCreate({
slug: source.slug,
filePathRequest: {
url: data.icon || "",
},
});
}
return source;
}
renderForm(): TemplateResult {
return html` <ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(this.instance?.name)}
required
></ak-text-input>
<ak-text-input
name="slug"
label=${msg("Slug")}
value=${ifDefined(this.instance?.slug)}
required
></ak-text-input>
<ak-switch-input
name="enabled"
?checked=${first(this.instance?.enabled, true)}
label=${msg("Enabled")}
></ak-switch-input>
<ak-switch-input
name="passwordLoginUpdateInternalPassword"
?checked=${first(this.instance?.passwordLoginUpdateInternalPassword, false)}
label=${msg("Update internal password on login")}
help=${msg(
"When the user logs in to authentik using this source password backend, update their credentials in authentik.",
)}
></ak-switch-input>
<ak-switch-input
name="syncUsers"
?checked=${first(this.instance?.syncUsers, true)}
label=${msg("Sync users")}
></ak-switch-input>
<ak-switch-input
name="syncUsersPassword"
?checked=${first(this.instance?.syncUsersPassword, true)}
label=${msg("User password writeback")}
help=${msg(
"Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled.",
)}
></ak-switch-input>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Realm settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="realm"
label=${msg("Realm")}
value=${ifDefined(this.instance?.realm)}
placeholder="AUTHENTIK.COMPANY"
required
></ak-text-input>
<ak-textarea-input
name="krb5Conf"
label=${msg("Kerberos 5 configuration")}
value=${ifDefined(this.instance?.krb5Conf)}
help=${msg(
"Kerberos 5 configuration. See man krb5.conf(5) for configuration format. If left empty, a default krb5.conf will be used.",
)}
></ak-textarea-input>
<ak-form-element-horizontal
label=${msg("User matching mode")}
?required=${true}
name="userMatchingMode"
>
<select class="pf-c-form-control">
<option
value=${UserMatchingModeEnum.Identifier}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.Identifier}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.Identifier)}
</option>
<option
value=${UserMatchingModeEnum.EmailLink}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.EmailLink}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.EmailLink)}
</option>
<option
value=${UserMatchingModeEnum.EmailDeny}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.EmailDeny}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.EmailDeny)}
</option>
<option
value=${UserMatchingModeEnum.UsernameLink}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.UsernameLink}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.UsernameLink)}
</option>
<option
value=${UserMatchingModeEnum.UsernameDeny}
?selected=${this.instance?.userMatchingMode ===
UserMatchingModeEnum.UsernameDeny}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.UsernameDeny)}
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group matching mode")}
?required=${true}
name="groupMatchingMode"
>
<select class="pf-c-form-control">
<option
value=${GroupMatchingModeEnum.Identifier}
?selected=${this.instance?.groupMatchingMode ===
GroupMatchingModeEnum.Identifier}
>
${UserMatchingModeToLabel(UserMatchingModeEnum.Identifier)}
</option>
<option
value=${GroupMatchingModeEnum.NameLink}
?selected=${this.instance?.groupMatchingMode ===
GroupMatchingModeEnum.NameLink}
>
${GroupMatchingModeToLabel(GroupMatchingModeEnum.NameLink)}
</option>
<option
value=${GroupMatchingModeEnum.NameDeny}
?selected=${this.instance?.groupMatchingMode ===
GroupMatchingModeEnum.NameDeny}
>
${GroupMatchingModeToLabel(GroupMatchingModeEnum.NameDeny)}
</option>
</select>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group .expanded=${false}>
<span slot="header"> ${msg("Sync connection settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="syncPrincipal"
label=${msg("Sync principal")}
value=${ifDefined(this.instance?.syncPrincipal)}
help=${msg("Principal used to authenticate to the KDC for syncing.")}
></ak-text-input>
<ak-form-element-horizontal
name="syncPassword"
label=${msg("Sync password")}
?writeOnly=${this.instance !== undefined}
>
<input type="text" value="" class="pf-c-form-control" />
<p class="pf-c-form__helper-text">
${msg(
"Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
name="syncKeytab"
label=${msg("Sync keytab")}
?writeOnly=${this.instance !== undefined}
>
<textarea class="pf-c-form-control"></textarea>
<p class="pf-c-form__helper-text">
${msg(
"Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
)}
</p>
</ak-form-element-horizontal>
<ak-text-input
name="syncCcache"
label=${msg("Sync credentials cache")}
value=${ifDefined(this.instance?.syncCcache)}
help=${msg(
"Credentials cache used to authenticate to the KDC for syncing. Optional if Sync password or Sync keytab is provided. Must be in the form TYPE:residual.",
)}
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group .expanded=${false}>
<span slot="header"> ${msg("SPNEGO settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="spnegoServerName"
label=${msg("SPNEGO server name")}
value=${ifDefined(this.instance?.spnegoServerName)}
help=${msg(
"Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain",
)}
></ak-text-input>
<ak-form-element-horizontal
name="spnegoKeytab"
label=${msg("SPNEGO keytab")}
?writeOnly=${this.instance !== undefined}
>
<textarea class="pf-c-form-control"></textarea>
<p class="pf-c-form__helper-text">
${msg(
"Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.",
)}
</p>
</ak-form-element-horizontal>
<ak-text-input
name="spnegoCcache"
label=${msg("SPNEGO credentials cache")}
value=${ifDefined(this.instance?.spnegoCcache)}
help=${msg(
"Credentials cache used for SPNEGO. Optional if SPNEGO keytab is provided. Must be in the form TYPE:residual.",
)}
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group ?expanded=${false}>
<span slot="header"> ${msg("Kerberos Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="userPropertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${propertyMappingsProvider}
.selector=${makePropertyMappingsSelector(
"user",
this.instance?.userPropertyMappings,
)}
available-label="${msg("Available User Property Mappings")}"
selected-label="${msg("Selected User Property Mappings")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings for user creation.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="groupPropertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${propertyMappingsProvider}
.selector=${makePropertyMappingsSelector(
"group",
this.instance?.groupPropertyMappings,
)}
available-label="${msg("Available Group Property Mappings")}"
selected-label="${msg("Selected Group Property Mappings")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings for group creation.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
>
<ak-source-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
.instanceId=${this.instance?.pk}
fallback="default-source-authentication"
></ak-source-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow to use when authenticating existing users.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Enrollment flow")}
name="enrollmentFlow"
>
<ak-source-flow-search
flowType=${FlowsInstancesListDesignationEnum.Enrollment}
.currentFlow=${this.instance?.enrollmentFlow}
.instanceId=${this.instance?.pk}
fallback="default-source-enrollment"
></ak-source-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow to use when enrolling new users.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Additional settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="userPathTemplate"
label=${msg("User path")}
value=${first(
this.instance?.userPathTemplate,
"goauthentik.io/sources/%(slug)s",
)}
help=${placeholderHelperText}
></ak-text-input>
</div>
${this.can(CapabilitiesEnum.CanSaveMedia)
? html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input type="file" value="" class="pf-c-form-control" />
${this.instance?.icon
? html`
<p class="pf-c-form__helper-text">
${msg("Currently set to:")} ${this.instance?.icon}
</p>
`
: html``}
</ak-form-element-horizontal>
${this.instance?.icon
? html`
<ak-form-element-horizontal>
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
@change=${(ev: Event) => {
const target = ev.target as HTMLInputElement;
this.clearIcon = target.checked;
}}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">
${msg("Clear icon")}
</span>
</label>
<p class="pf-c-form__helper-text">
${msg("Delete currently set icon.")}
</p>
</ak-form-element-horizontal>
`
: html``}`
: html`<ak-form-element-horizontal label=${msg("Icon")} name="icon">
<input
type="text"
value="${first(this.instance?.icon, "")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${iconHelperText}</p>
</ak-form-element-horizontal>`}
</ak-form-group>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-source-kerberos-form": KerberosSourceForm;
}
}

View File

@ -0,0 +1,213 @@
import "@goauthentik/admin/rbac/ObjectPermissionsPage";
import "@goauthentik/admin/sources/kerberos/KerberosSourceConnectivity";
import "@goauthentik/admin/sources/kerberos/KerberosSourceForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_REFRESH } from "@goauthentik/common/constants";
import "@goauthentik/components/events/ObjectChangelog";
import MDSourceKerberosBrowser from "@goauthentik/docs/users-sources/sources/protocols/kerberos/browser.md";
import { AKElement } from "@goauthentik/elements/Base";
import "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/Markdown";
import "@goauthentik/elements/SyncStatusCard";
import "@goauthentik/elements/Tabs";
import "@goauthentik/elements/buttons/ActionButton";
import "@goauthentik/elements/buttons/SpinnerButton";
import "@goauthentik/elements/forms/ModalForm";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFCard from "@patternfly/patternfly/components/Card/card.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import {
KerberosSource,
RbacPermissionsAssignedByUsersListModelEnum,
SourcesApi,
SyncStatus,
} from "@goauthentik/api";
@customElement("ak-source-kerberos-view")
export class KerberosSourceViewPage extends AKElement {
@property({ type: String })
set sourceSlug(slug: string) {
new SourcesApi(DEFAULT_CONFIG)
.sourcesKerberosRetrieve({
slug: slug,
})
.then((source) => {
this.source = source;
});
}
@property({ attribute: false })
source!: KerberosSource;
@state()
syncState?: SyncStatus;
static get styles(): CSSResult[] {
return [PFBase, PFPage, PFButton, PFGrid, PFContent, PFCard, PFDescriptionList, PFList];
}
constructor() {
super();
this.addEventListener(EVENT_REFRESH, () => {
if (!this.source?.slug) return;
this.sourceSlug = this.source?.slug;
});
}
load(): void {
new SourcesApi(DEFAULT_CONFIG)
.sourcesKerberosSyncStatusRetrieve({
slug: this.source.slug,
})
.then((state) => {
this.syncState = state;
});
}
renderSyncCards(): TemplateResult {
if (!this.source.syncUsers) {
return html``;
}
return html`
<div class="pf-c-card pf-l-grid__item pf-m-2-col">
<div class="pf-c-card__title">
<p>${msg("Connectivity")}</p>
</div>
<div class="pf-c-card__body">
<ak-source-kerberos-connectivity
.connectivity=${this.source.connectivity}
></ak-source-kerberos-connectivity>
</div>
</div>
<div class="pf-l-grid__item pf-m-10-col">
<ak-sync-status-card
.fetch=${() => {
return new SourcesApi(DEFAULT_CONFIG).sourcesKerberosSyncStatusRetrieve({
slug: this.source?.slug,
});
}}
.triggerSync=${() => {
return new SourcesApi(DEFAULT_CONFIG).sourcesKerberosPartialUpdate({
slug: this.source?.slug || "",
patchedKerberosSourceRequest: {},
});
}}
></ak-sync-status-card>
</div>
`;
}
render(): TemplateResult {
if (!this.source) {
return html``;
}
return html`<ak-tabs>
<section
slot="page-overview"
data-tab-title="${msg("Overview")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
@activate=${() => {
this.load();
}}
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-2-col-on-lg">
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Name")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.source.name}
</div>
</dd>
</div>
<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text"
>${msg("Realm")}</span
>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
${this.source.realm}
</div>
</dd>
</div>
</dl>
</div>
<div class="pf-c-card__footer">
<ak-forms-modal>
<span slot="submit"> ${msg("Update")} </span>
<span slot="header"> ${msg("Update Kerberos Source")} </span>
<ak-source-kerberos-form
slot="form"
.instancePk=${this.source.slug}
>
</ak-source-kerberos-form>
<button slot="trigger" class="pf-c-button pf-m-primary">
${msg("Edit")}
</button>
</ak-forms-modal>
</div>
</div>
${this.renderSyncCards()}
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<ak-markdown
.md=${MDSourceKerberosBrowser}
meta="users-sources/protocols/kerberos/browser.md"
;
></ak-markdown>
</div>
</div>
</div>
</section>
<section
slot="page-changelog"
data-tab-title="${msg("Changelog")}"
class="pf-c-page__main-section pf-m-no-padding-mobile"
>
<div class="pf-l-grid pf-m-gutter">
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
<div class="pf-c-card__body">
<ak-object-changelog
targetModelPk=${this.source.pk || ""}
targetModelApp="authentik_sources_kerberos"
targetModelName="kerberossource"
>
</ak-object-changelog>
</div>
</div>
</div>
</section>
<ak-rbac-object-permission-page
slot="page-permissions"
data-tab-title="${msg("Permissions")}"
model=${RbacPermissionsAssignedByUsersListModelEnum.SourcesKerberosKerberossource}
objectPk=${this.source.pk}
></ak-rbac-object-permission-page>
</ak-tabs>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-source-kerberos-view": KerberosSourceViewPage;
}
}

View File

@ -66,6 +66,10 @@ export class PasswordStageForm extends BaseStageForm<PasswordStage> {
name: BackendsEnum.SourcesLdapAuthLdapBackend,
label: msg("User database + LDAP password"),
},
{
name: BackendsEnum.SourcesKerberosAuthKerberosBackend,
label: msg("User database + Kerberos password"),
},
];
return html` <span>

View File

@ -23,7 +23,7 @@ If you use locally installed databases, the PostgreSQL credentials given to auth
## Backend Setup
:::info
Depending on your platform, some native dependencies might be required. On macOS, run `brew install libxmlsec1 libpq`, and for the CLI tools `brew install postgresql redis node@20`
Depending on your platform, some native dependencies might be required. On macOS, run `brew install libxmlsec1 libpq krb5`, and for the CLI tools `brew install postgresql redis node@20`.
:::
1. Create an isolated Python environment. To create the environment and install dependencies, run the following commands in the same directory as your local authentik git repository:

View File

@ -8,7 +8,7 @@ Sources allow you to connect authentik to an external user directory. Sources ca
Sources are in the following general categories:
- **Protocols** ([LDAP](./protocols/ldap/index.md), [OAuth](./protocols/oauth/index.md), [SAML](./protocols/saml/index.md), and [SCIM](./protocols/scim/index.md))
- **Protocols** ([Kerberos](./protocols/kerberos/index.md), [LDAP](./protocols/ldap/index.md), [OAuth](./protocols/oauth/index.md), [SAML](./protocols/saml/index.md), and [SCIM](./protocols/scim/index.md))
- [**Property mappings**](./property-mappings/index.md) or how to import data from a source
- **Directory synchronization** (Active Directory, FreeIPA)
- **Social logins** (Apple, Discord, Twitch, Twitter, and many others)

View File

@ -6,6 +6,7 @@ Source property mappings allow you to modify or gather extra information from so
This page is an overview of how property mappings work. For information about specific protocol, please refer to each protocol page:
- [Kerberos](../protocols/kerberos/#kerberos-source-property-mappings)
- [LDAP](../protocols/ldap/index.md#ldap-source-property-mappings)
- [OAuth](../protocols/oauth/index.md#oauth-source-property-mappings)
- [SAML](../protocols/saml/index.md#saml-source-property-mappings)

View File

@ -0,0 +1,43 @@
---
title: Browser configuration for SPNEGO
---
You might need to configure your web browser to allow SPNEGO. Following are the instructions for major browsers.
## Firefox
1. In the address bar of Firefox, type `about:config` to display the list of current configuration options.
2. In the **Filter** field, type `negotiate` to restrict the list of options.
3. Double-click the `network.negotiate-auth.trusted-uris` entry to display the **Enter string value** dialog box.
4. Enter the name of the domain against which you want to authenticate. For example, `.example.com`.
On Windows environments, to automate the deployment of this configuration use a [Group policy](https://support.mozilla.org/en-US/kb/customizing-firefox-using-group-policy-windows). On Linux or macOS systems, use [policies.json](https://support.mozilla.org/en-US/kb/customizing-firefox-using-policiesjson).
## Chrome
This section applies only for Chrome users on macOS and Linux machines. For Windows, see the instructions below.
1. Make sure you have the necessary directory created by running: `mkdir -p /etc/opt/chrome/policies/managed/`
2. Create a new `/etc/opt/chrome/policies/managed/mydomain.json` file with write privileges limited to the system administrator or root, and include the following line: `{ "AuthServerWhitelist": "*.example.com" }`.
**Note**: if using Chromium, use `/etc/chromium/policies/managed/` instead of `/etc/opt/chrome/policies/managed/`.
To automate the deployment of this configuration use a [Group policy](https://support.google.com/chrome/a/answer/187202).
## Windows / Internet Explorer
Log into the Windows machine using an account of your Kerberos realm (or administrative domain).
Open Internet Explorer, click **Tools** and then click **Internet Options**. You can also find **Internet Options** using the system search.
1. Click the **Security** tab.
2. Click **Local intranet**.
3. Click **Sites**.
4. Click **Advanced**.
5. Add your domain to the list.
6. Click the **Security tab**.
7. Click **Local intranet**.
8. Click **Custom Level**.
9. Select **Automatic login only in Intranet zone**.
To automate the deployment of this configuration use a [Group policy](https://learn.microsoft.com/en-us/previous-versions/troubleshoot/browsers/administration/how-to-configure-group-policy-preference-settings).

View File

@ -0,0 +1,130 @@
---
title: Kerberos
---
This source allows users to enroll themselves with an existing Kerberos identity.
## Preparation
The following placeholders will be used:
- `REALM.COMPANY` is the Kerberos realm.
- `authentik.company` is the FQDN of the authentik install.
Examples are shown for an MIT Krb5 KDC system; you might need to adapt them for you Kerberos installation.
There are three ways to use the Kerberos source:
- As a password backend, where users can log in to authentik with their Kerberos password.
- As a directory source, where users are synced from the KDC.
- With SPNEGO, where users can log in to authentik with their [browser](./browser.md) and their Kerberos credentials.
You can choose to use one or several of those methods.
## Common settings
In the authentik Admin interface, under **Directory** -> **Federation and Social login**, create a new source of type Kerberos with these settings:
- Name: a value of your choosing. This name is shown to users if you use the SPNEGO login method.
- Slug: `kerberos`
- Realm: `REALM.COMPANY`
- Kerberos 5 configuration: If you need to override the default Kerberos configuration, you can do it here. See [man krb5.conf(5)](https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html) for the expected format.
- User matching mode: define how Kerberos users get matched to authentik users.
- Group matching mode: define how Kerberos groups (specified via property mappings) get matched to authentik groups.
- User property mappings and group property mappings: see [Source property mappings](../../property-mappings/index.md) and the section below for details.
## Password backend
No extra configuration is required. Simply select the Kerberos backend in the password stage of your flow.
Note that this only works on users that have been linked to this source, i.e. they must have been created via sync or via SPNEGO.
## Sync
The sync process uses the [Kerberos V5 administration system](https://web.mit.edu/kerberos/krb5-latest/doc/admin/database.html) to list users. Your KDC must support it to sync users with this source.
You need to create both a principal (a unique identity that represents a user or service in a Kerberos network) for authentik and a keytab file:
```bash
$ kadmin
> add_principal authentik/admin@REALM.COMPANY
> ktadd -k /tmp/authentik.keytab authentik/admin@REALM.COMPANY
> exit
$ cat /tmp/authentik.keytab | base64
$ rm /tmp/authentik.keytab
```
In authentik, configure these extra options:
- Sync users: enable it
- Sync principal: `authentik/admin@REALM.COMPANY`
- Sync keytab: the base64-encoded keytab created above.
If you do not wish to use a keytab, you can also configure authentik to authenticate using a password, or an existing credentials cache.
## SPNEGO
You need to create both a principal (a unique identity that represents a user or service in a Kerberos network) for authentik and a keytab file:
```bash
$ kadmin
> add_principal HTTP/authentik.company@REALM.COMPANY
> ktadd -k /tmp/authentik.keytab HTTP/authentik.company@REALM.COMPANY
> exit
$ cat /tmp/authentik.keytab | base64
$ rm /tmp/authentik.keytab
```
In authentik, configure these extra options:
- SPNEGO keytab: the base64-encoded keytab created above.
If you do not wish to use a keytab, you can also configure authentik to use an existing credentials cache.
You can also override the SPNEGO server name if needed.
You might need to configure your web browser to allow SPNEGO. Check out [our documentation](./browser.md) on how to do so. You can now login to authentik using SPNEGO.
### Custom server name
If your authentik instance is accessed from multiple domains, you might want to force the use of a specific server name. You can do so with the **Custom server name** option. The value must be in the form of `HTTP@authentik.company`.
If not specified, the server name defaults to trying out all entries in the keytab/credentials cache until a valid server name is found.
## Extra settings
There are some extra settings you can configure:
- Update internal password on login: when a user logs in to authentik using the Kerberos source as a password backend, their internal authentik password will be updated to match the one from Kerberos.
- Use password writeback: when a user changes their password in authentik, their Kerberos password is automatically updated to match the one from authentik. This is only available if synchronization is configured.
## Kerberos source property mappings
See the [overview](../../property-mappings/index.md) for information on how property mappings work with external sources.
By default, authentik ships with [pre-configured mappings](#built-in-property-mappings) for the most common Kerberos setups. These mappings can be found on the Kerberos Source Configuration page in the Admin interface.
### Built-in property mappings
Kerberos property mappings are used when you define a Kerberos source. These mappings define which Kerberos property maps to which authentik property. By default, the following mappings are created:
- authentik default Kerberos User Mapping: Add realm as group
The realm of the user will be added as a group for that user.
- authentik default Kerberos User Mapping: Ignore other realms
Realms other than the one configured on the source are ignored, and log in is not allowed.
- authentik default Kerberos User Mapping: Ignore system principals
System principals such as `K/M` or `kadmin/admin` are ignored.
- authentik default Kerberos User Mapping: Multipart principals as service accounts
Multipart principals (for example: `HTTP/authentik.company`) have their user type set to **service account**.
These property mappings are configured with the most common Kerberos setups.
### Expression data
The following variable is available to Kerberos source property mappings:
- `principal`: a Python string containing the Kerberos principal. For example `alice@REALM.COMPANY` or `HTTP/authentik.company@REALM.COMPANY`.
## Troubleshooting
You can start authentik with the `KRB5_TRACE=/dev/stderr` environment variable for Kerberos to print errors in the logs.

View File

@ -477,6 +477,17 @@ export default {
label: "Protocols",
collapsed: true,
items: [
{
type: "category",
label: "Kerberos",
link: {
type: "doc",
id: "users-sources/sources/protocols/kerberos/index",
},
items: [
"users-sources/sources/protocols/kerberos/browser",
],
},
"users-sources/sources/protocols/ldap/index",
"users-sources/sources/protocols/oauth/index",
"users-sources/sources/protocols/saml/index",