Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sources: add Kerberos #10815

Draft
wants to merge 50 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0e1347e
sources: introduce new property mappings per-user and group
rissson Feb 29, 2024
a99b6ed
sources/ldap: migrate to new property mappings
rissson Feb 29, 2024
02fd08c
lint-fix and make gen
rissson Feb 29, 2024
5bbe030
web changes
rissson Feb 29, 2024
c8f0a4e
Merge branch 'main' into refactor-sources
rissson Mar 1, 2024
0f92e37
fix tests
rissson Mar 1, 2024
1dbd5eb
update tests
rissson Mar 1, 2024
7132d27
Merge branch 'main' into refactor-sources
rissson Mar 1, 2024
3df5c67
remove flatten for generic implem
rissson Mar 1, 2024
4927d2f
Merge branch 'main' into refactor-sources
rissson Mar 1, 2024
d276eb3
rework migration
rissson Mar 1, 2024
4323237
lint-fix
rissson Mar 1, 2024
924461b
wip
rissson Mar 1, 2024
99e2ce7
fix migrations
rissson Mar 1, 2024
44c92c2
re-add field migration to property mappings
rissson Mar 1, 2024
20b18bc
Merge branch 'main' into refactor-sources
rissson Apr 24, 2024
cd66792
fix migrations
rissson Apr 24, 2024
bddfb7f
more migrations fixes
rissson Apr 25, 2024
442248a
Merge branch 'main' into refactor-sources
rissson Jun 6, 2024
193e4ed
easy fixes
rissson Jun 6, 2024
8083ab2
migrate to propertymappingmanager
rissson Jun 6, 2024
a82b5b7
ruff and small fixes
rissson Jun 6, 2024
b9b3f55
Merge branch 'main' into refactor-sources
rissson Jul 15, 2024
dea6dc4
move mapping things into a separate class
rissson Jul 15, 2024
eb1b588
migrations: use using(db_alias)
rissson Jul 15, 2024
4d5a05d
migrations: use built-in variable
rissson Jul 15, 2024
8195352
add docs
rissson Jul 15, 2024
8c60f05
add release notes
rissson Jul 15, 2024
ac554e8
Merge branch 'main' into refactor-sources
rissson Jul 16, 2024
d23fc68
Merge branch 'main' into refactor-sources
rissson Jul 19, 2024
1a3c3da
wip
rissson Jul 22, 2024
964f103
Merge branch 'main' into kerberos-source-reworked
rissson Jul 29, 2024
2c00115
wip
rissson Jul 29, 2024
7e7db89
wip
rissson Jul 29, 2024
4895fed
Merge branch 'main' into kerberos-source-reworked
rissson Aug 7, 2024
98a76d0
wip
rissson Aug 7, 2024
354bea8
wip
rissson Aug 7, 2024
9a159a9
wip
rissson Aug 7, 2024
c74aba2
wip
rissson Aug 7, 2024
33464a4
wip
rissson Aug 7, 2024
893d31d
lint
rissson Aug 7, 2024
bfdc225
fix login reverse
rissson Aug 7, 2024
0df3379
Merge branch 'main' into kerberos-source-reworked
rissson Aug 8, 2024
6a1e193
wip
rissson Aug 8, 2024
fc2cf7e
wip
rissson Aug 12, 2024
e73b651
Merge branch 'main' into kerberos-source-reworked
rissson Sep 18, 2024
9439772
refactor source flow manager matching
rissson Sep 20, 2024
63c0c5f
kerberos sync with mode matching
rissson Sep 20, 2024
41521e2
fixup
rissson Sep 20, 2024
38e7df9
wip
rissson Sep 20, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 && \
# Required for bootstrap & healtcheck
apt-get install -y --no-install-recommends runit && \
apt-get clean && \
Expand All @@ -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/
Expand Down
143 changes: 15 additions & 128 deletions authentik/core/sources/flow_manager.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
"""Source decision helper"""

from enum import Enum
from typing import Any

from django.contrib import messages
from django.db import IntegrityError, transaction
from django.db.models.query_utils import Q
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse
Expand All @@ -16,12 +14,11 @@
Group,
GroupSourceConnection,
Source,
SourceGroupMatchingModes,
SourceUserMatchingModes,
User,
UserSourceConnection,
)
from authentik.core.sources.mapper import SourceMapper
from authentik.core.sources.matcher import Action, SourceMatcher
from authentik.core.sources.stage import (
PLAN_CONTEXT_SOURCES_CONNECTION,
PostSourceStage,
Expand Down Expand Up @@ -54,16 +51,6 @@
PLAN_CONTEXT_SOURCE_GROUPS = "source_groups"


class Action(Enum):
"""Actions that can be decided based on the request
and source settings"""

LINK = "link"
AUTH = "auth"
ENROLL = "enroll"
DENY = "deny"


class MessageStage(StageView):
"""Show a pre-configured message after the flow is done"""

Expand All @@ -86,6 +73,7 @@ class SourceFlowManager:

source: Source
mapper: SourceMapper
matcher: SourceMatcher
request: HttpRequest

identifier: str
Expand All @@ -108,6 +96,9 @@ def __init__(
) -> None:
self.source = source
self.mapper = SourceMapper(self.source)
self.matcher = SourceMatcher(
self.source, self.user_connection_type, self.group_connection_type
)
self.request = request
self.identifier = identifier
self.user_info = user_info
Expand All @@ -131,66 +122,19 @@ def __init__(

def get_action(self, **kwargs) -> tuple[Action, UserSourceConnection | None]: # noqa: PLR0911
"""decide which action should be taken"""
new_connection = self.user_connection_type(source=self.source, identifier=self.identifier)
# When request is authenticated, always link
if self.request.user.is_authenticated:
new_connection = self.user_connection_type(
source=self.source, identifier=self.identifier
)
new_connection.user = self.request.user
new_connection = self.update_user_connection(new_connection, **kwargs)
return Action.LINK, new_connection

existing_connections = self.user_connection_type.objects.filter(
source=self.source, identifier=self.identifier
)
if existing_connections.exists():
connection = existing_connections.first()
return Action.AUTH, self.update_user_connection(connection, **kwargs)
# No connection exists, but we match on identifier, so enroll
if self.source.user_matching_mode == SourceUserMatchingModes.IDENTIFIER:
# We don't save the connection here cause it doesn't have a user assigned yet
return Action.ENROLL, self.update_user_connection(new_connection, **kwargs)

# Check for existing users with matching attributes
query = Q()
# Either query existing user based on email or username
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_LINK,
SourceUserMatchingModes.EMAIL_DENY,
]:
if not self.user_properties.get("email", None):
self._logger.warning("Refusing to use none email")
return Action.DENY, None
query = Q(email__exact=self.user_properties.get("email", None))
if self.source.user_matching_mode in [
SourceUserMatchingModes.USERNAME_LINK,
SourceUserMatchingModes.USERNAME_DENY,
]:
if not self.user_properties.get("username", None):
self._logger.warning("Refusing to use none username")
return Action.DENY, None
query = Q(username__exact=self.user_properties.get("username", None))
self._logger.debug("trying to link with existing user", query=query)
matching_users = User.objects.filter(query)
# No matching users, always enroll
if not matching_users.exists():
self._logger.debug("no matching users found, enrolling")
return Action.ENROLL, self.update_user_connection(new_connection, **kwargs)

user = matching_users.first()
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_LINK,
SourceUserMatchingModes.USERNAME_LINK,
]:
new_connection.user = user
new_connection = self.update_user_connection(new_connection, **kwargs)
return Action.LINK, new_connection
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_DENY,
SourceUserMatchingModes.USERNAME_DENY,
]:
self._logger.info("denying source because user exists", user=user)
return Action.DENY, None
# Should never get here as default enroll case is returned above.
return Action.DENY, None # pragma: no cover
action, connection = self.matcher.get_user_action(self.identifier, self.user_properties)
if connection:
connection = self.update_user_connection(connection, **kwargs)
return action, connection

def update_user_connection(
self, connection: UserSourceConnection, **kwargs
Expand Down Expand Up @@ -408,74 +352,16 @@ def handle_enroll(
class GroupUpdateStage(StageView):
"""Dynamically injected stage which updates the user after enrollment/authentication."""

def get_action(
self, group_id: str, group_properties: dict[str, Any | dict[str, Any]]
) -> tuple[Action, GroupSourceConnection | None]:
"""decide which action should be taken"""
new_connection = self.group_connection_type(source=self.source, identifier=group_id)

existing_connections = self.group_connection_type.objects.filter(
source=self.source, identifier=group_id
)
if existing_connections.exists():
return Action.LINK, existing_connections.first()
# No connection exists, but we match on identifier, so enroll
if self.source.group_matching_mode == SourceGroupMatchingModes.IDENTIFIER:
# We don't save the connection here cause it doesn't have a user assigned yet
return Action.ENROLL, new_connection

# Check for existing groups with matching attributes
query = Q()
if self.source.group_matching_mode in [
SourceGroupMatchingModes.NAME_LINK,
SourceGroupMatchingModes.NAME_DENY,
]:
if not group_properties.get("name", None):
LOGGER.warning(
"Refusing to use none group name", source=self.source, group_id=group_id
)
return Action.DENY, None
query = Q(name__exact=group_properties.get("name"))
LOGGER.debug(
"trying to link with existing group", source=self.source, query=query, group_id=group_id
)
matching_groups = Group.objects.filter(query)
# No matching groups, always enroll
if not matching_groups.exists():
LOGGER.debug(
"no matching groups found, enrolling", source=self.source, group_id=group_id
)
return Action.ENROLL, new_connection

group = matching_groups.first()
if self.source.group_matching_mode in [
SourceGroupMatchingModes.NAME_LINK,
]:
new_connection.group = group
return Action.LINK, new_connection
if self.source.group_matching_mode in [
SourceGroupMatchingModes.NAME_DENY,
]:
LOGGER.info(
"denying source because group exists",
source=self.source,
group=group,
group_id=group_id,
)
return Action.DENY, None
# Should never get here as default enroll case is returned above.
return Action.DENY, None # pragma: no cover

def handle_group(
self, group_id: str, group_properties: dict[str, Any | dict[str, Any]]
) -> Group | None:
action, connection = self.get_action(group_id, group_properties)
action, connection = self.matcher.get_group_action(group_id, group_properties)
if action == Action.ENROLL:
group = Group.objects.create(**group_properties)
connection.group = group
connection.save()
return group
elif action == Action.LINK:
elif action in (Action.LINK, Action.AUTH):
group = connection.group
group.update_attributes(group_properties)
connection.save()
Expand All @@ -489,6 +375,7 @@ def handle_groups(self) -> bool:
self.group_connection_type: GroupSourceConnection = (
self.executor.current_stage.group_connection_type
)
self.matcher = SourceMatcher(self.source, None, self.group_connection_type)

raw_groups: dict[str, dict[str, Any | dict[str, Any]]] = self.executor.plan.context[
PLAN_CONTEXT_SOURCE_GROUPS
Expand Down
Loading
Loading