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:

committed by
GitHub

parent
d3ebfcaf2f
commit
d817c646bd
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
@ -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:
|
||||
|
@ -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/
|
||||
|
@ -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)
|
||||
|
||||
|
@ -105,6 +105,10 @@ ldap:
|
||||
tls:
|
||||
ciphers: null
|
||||
|
||||
sources:
|
||||
kerberos:
|
||||
task_timeout_hours: 2
|
||||
|
||||
reputation:
|
||||
expiry: 86400
|
||||
|
||||
|
@ -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",
|
||||
|
0
authentik/sources/kerberos/__init__.py
Normal file
0
authentik/sources/kerberos/__init__.py
Normal file
0
authentik/sources/kerberos/api/__init__.py
Normal file
0
authentik/sources/kerberos/api/__init__.py
Normal file
31
authentik/sources/kerberos/api/property_mappings.py
Normal file
31
authentik/sources/kerberos/api/property_mappings.py
Normal 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"]
|
114
authentik/sources/kerberos/api/source.py
Normal file
114
authentik/sources/kerberos/api/source.py
Normal 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)
|
51
authentik/sources/kerberos/api/source_connection.py
Normal file
51
authentik/sources/kerberos/api/source_connection.py
Normal 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
|
13
authentik/sources/kerberos/apps.py
Normal file
13
authentik/sources/kerberos/apps.py
Normal 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
|
116
authentik/sources/kerberos/auth.py
Normal file
116
authentik/sources/kerberos/auth.py
Normal 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
|
4
authentik/sources/kerberos/krb5.conf
Normal file
4
authentik/sources/kerberos/krb5.conf
Normal file
@ -0,0 +1,4 @@
|
||||
[libdefaults]
|
||||
dns_canonicalize_hostname = false
|
||||
dns_fallback = true
|
||||
rnds = false
|
0
authentik/sources/kerberos/management/__init__.py
Normal file
0
authentik/sources/kerberos/management/__init__.py
Normal 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))
|
@ -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)
|
179
authentik/sources/kerberos/migrations/0001_initial.py
Normal file
179
authentik/sources/kerberos/migrations/0001_initial.py
Normal 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",),
|
||||
),
|
||||
]
|
0
authentik/sources/kerberos/migrations/__init__.py
Normal file
0
authentik/sources/kerberos/migrations/__init__.py
Normal file
376
authentik/sources/kerberos/models.py
Normal file
376
authentik/sources/kerberos/models.py
Normal 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")
|
18
authentik/sources/kerberos/settings.py
Normal file
18
authentik/sources/kerberos/settings.py
Normal 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"},
|
||||
},
|
||||
}
|
61
authentik/sources/kerberos/signals.py
Normal file
61
authentik/sources/kerberos/signals.py
Normal 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
|
167
authentik/sources/kerberos/sync.py
Normal file
167
authentik/sources/kerberos/sync.py
Normal 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
|
68
authentik/sources/kerberos/tasks.py
Normal file
68
authentik/sources/kerberos/tasks.py
Normal 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)
|
0
authentik/sources/kerberos/tests/__init__.py
Normal file
0
authentik/sources/kerberos/tests/__init__.py
Normal file
57
authentik/sources/kerberos/tests/test_auth.py
Normal file
57
authentik/sources/kerberos/tests/test_auth.py
Normal 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))
|
78
authentik/sources/kerberos/tests/test_spnego.py
Normal file
78
authentik/sources/kerberos/tests/test_spnego.py
Normal 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)
|
75
authentik/sources/kerberos/tests/test_sync.py
Normal file
75
authentik/sources/kerberos/tests/test_sync.py
Normal 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()
|
||||
)
|
40
authentik/sources/kerberos/tests/utils.py
Normal file
40
authentik/sources/kerberos/tests/utils.py
Normal 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
|
22
authentik/sources/kerberos/urls.py
Normal file
22
authentik/sources/kerberos/urls.py
Normal 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),
|
||||
]
|
181
authentik/sources/kerberos/views.py
Normal file
181
authentik/sources/kerberos/views.py
Normal 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
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
@ -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"),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -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]]
|
||||
|
@ -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",
|
||||
|
55
blueprints/system/sources-kerberos.yaml
Normal file
55
blueprints/system/sources-kerberos.yaml
Normal 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()]
|
||||
}
|
@ -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
80
poetry.lock
generated
@ -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"
|
||||
|
@ -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
1673
schema.yml
File diff suppressed because it is too large
Load Diff
BIN
web/authentik/sources/kerberos.png
Normal file
BIN
web/authentik/sources/kerberos.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 74 KiB |
@ -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";
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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}
|
||||
|
@ -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";
|
||||
|
39
web/src/admin/sources/kerberos/KerberosSourceConnectivity.ts
Normal file
39
web/src/admin/sources/kerberos/KerberosSourceConnectivity.ts
Normal 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;
|
||||
}
|
||||
}
|
456
web/src/admin/sources/kerberos/KerberosSourceForm.ts
Normal file
456
web/src/admin/sources/kerberos/KerberosSourceForm.ts
Normal 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;
|
||||
}
|
||||
}
|
213
web/src/admin/sources/kerberos/KerberosSourceViewPage.ts
Normal file
213
web/src/admin/sources/kerberos/KerberosSourceViewPage.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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).
|
130
website/docs/users-sources/sources/protocols/kerberos/index.md
Normal file
130
website/docs/users-sources/sources/protocols/kerberos/index.md
Normal 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.
|
@ -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",
|
||||
|
Reference in New Issue
Block a user