Skip to content

Commit

Permalink
Merge branch 'main' into dev
Browse files Browse the repository at this point in the history
* main: (31 commits)
  root: support redis username (#8935)
  core: bump black from 24.2.0 to 24.3.0 (#8945)
  web: bump the wdio group in /tests/wdio with 2 updates (#8939)
  web: bump the sentry group in /web with 1 update (#8941)
  website: bump postcss from 8.4.35 to 8.4.36 in /website (#8940)
  core: bump twilio from 9.0.1 to 9.0.2 (#8942)
  core: bump ruff from 0.3.2 to 0.3.3 (#8943)
  events: discard notification if user has empty email (#8938)
  ci: always run ci-main on branch pushes (#8950)
  core: bump goauthentik.io/api/v3 from 3.2024022.2 to 3.2024022.3 (#8946)
  website/docs: add new name "Microsft Entra ID" for Azure AD  (#8930)
  outposts: Enhance config options for k8s outposts (#7363)
  website/docs: add link to CRUD docs (#8925)
  web: bump API Client version (#8927)
  outpost: improved set secret answers for flow execution (#8013)
  stages/user_write: ensure user data is json-serializable (#8926)
  website/docs: update example ldapsearch commands (#8906)
  admin: Handle latest  version unknown in admin dashboard (#8858)
  core: bump coverage from 7.4.3 to 7.4.4 (#8917)
  core: bump urllib3 from 1.26.18 to 2.2.1 (#8918)
  ...
  • Loading branch information
kensternberg-authentik committed Mar 18, 2024
2 parents 8b4e036 + 104e70c commit db96e1a
Show file tree
Hide file tree
Showing 68 changed files with 1,595 additions and 440 deletions.
2 changes: 0 additions & 2 deletions .github/workflows/ci-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ on:
- main
- next
- version-*
paths-ignore:
- website/**
pull_request:
branches:
- main
Expand Down
7 changes: 6 additions & 1 deletion authentik/admin/api/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from rest_framework.views import APIView

from authentik import __version__, get_build_hash
from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version
from authentik.admin.tasks import VERSION_CACHE_KEY, VERSION_NULL, update_latest_version
from authentik.core.api.utils import PassiveSerializer


Expand All @@ -19,6 +19,7 @@ class VersionSerializer(PassiveSerializer):

version_current = SerializerMethodField()
version_latest = SerializerMethodField()
version_latest_valid = SerializerMethodField()
build_hash = SerializerMethodField()
outdated = SerializerMethodField()

Expand All @@ -38,6 +39,10 @@ def get_version_latest(self, _) -> str:
return __version__
return version_in_cache

def get_version_latest_valid(self, _) -> str:
"""Check if latest version is valid"""
return cache.get(VERSION_CACHE_KEY) != VERSION_NULL

def get_outdated(self, instance) -> bool:
"""Check if we're running the latest version"""
return parse(self.get_version_current(instance)) < parse(self.get_version_latest(instance))
Expand Down
5 changes: 3 additions & 2 deletions authentik/admin/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from authentik.root.celery import CELERY_APP

LOGGER = get_logger()
VERSION_NULL = "0.0.0"
VERSION_CACHE_KEY = "authentik_latest_version"
VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
# Chop of the first ^ because we want to search the entire string
Expand Down Expand Up @@ -55,7 +56,7 @@ def clear_update_notifications():
def update_latest_version(self: SystemTask):
"""Update latest version info"""
if CONFIG.get_bool("disable_update_check"):
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT)
self.set_status(TaskStatus.WARNING, "Version check disabled.")
return
try:
Expand All @@ -82,7 +83,7 @@ def update_latest_version(self: SystemTask):
event_dict["message"] = f"Changelog: {match.group()}"
Event.new(EventAction.UPDATE_AVAILABLE, **event_dict).save()
except (RequestException, IndexError) as exc:
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT)
self.set_error(exc)


Expand Down
3 changes: 3 additions & 0 deletions authentik/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,9 @@ def serializer(self) -> type[Serializer]:
"""Get serializer for this model"""
raise NotImplementedError

def __str__(self) -> str:
return f"User-source connection (user={self.user.username}, source={self.source.slug})"

class Meta:
unique_together = (("user", "source"),)

Expand Down
38 changes: 31 additions & 7 deletions authentik/core/sources/flow_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
from authentik.core.sources.stage import PLAN_CONTEXT_SOURCES_CONNECTION, PostUserEnrollmentStage
from authentik.events.models import Event, EventAction
from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.models import Flow, Stage, in_memory_stage
from authentik.flows.models import Flow, FlowToken, Stage, in_memory_stage
from authentik.flows.planner import (
PLAN_CONTEXT_IS_RESTORED,
PLAN_CONTEXT_PENDING_USER,
PLAN_CONTEXT_REDIRECT,
PLAN_CONTEXT_SOURCE,
Expand All @@ -35,6 +36,8 @@
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
from authentik.stages.user_write.stage import PLAN_CONTEXT_USER_PATH

SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token" # nosec


class Action(Enum):
"""Actions that can be decided based on the request
Expand Down Expand Up @@ -222,22 +225,43 @@ def _prepare_flow(
**kwargs,
) -> HttpResponse:
"""Prepare Authentication Plan, redirect user FlowExecutor"""
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
kwargs.update(
{
# Since we authenticate the user by their token, they have no backend set
PLAN_CONTEXT_AUTHENTICATION_BACKEND: BACKEND_INBUILT,
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_SOURCE: self.source,
PLAN_CONTEXT_REDIRECT: final_redirect,
PLAN_CONTEXT_SOURCES_CONNECTION: connection,
}
)
kwargs.update(self.policy_context)
if SESSION_KEY_OVERRIDE_FLOW_TOKEN in self.request.session:
token: FlowToken = self.request.session.get(SESSION_KEY_OVERRIDE_FLOW_TOKEN)
self._logger.info("Replacing source flow with overridden flow", flow=token.flow.slug)
plan = token.plan
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
plan.context.update(kwargs)
for stage in self.get_stages_to_append(flow):
plan.append_stage(stage)
if stages:
for stage in stages:
plan.append_stage(stage)
self.request.session[SESSION_KEY_PLAN] = plan
flow_slug = token.flow.slug
token.delete()
return redirect_with_qs(
"authentik_core:if-flow",
self.request.GET,
flow_slug=flow_slug,
)
# Ensure redirect is carried through when user was trying to
# authorize application
final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get(
NEXT_ARG_NAME, "authentik_core:if-user"
)
if PLAN_CONTEXT_REDIRECT not in kwargs:
kwargs[PLAN_CONTEXT_REDIRECT] = final_redirect

if not flow:
return bad_request_message(
self.request,
Expand Down
8 changes: 4 additions & 4 deletions authentik/enterprise/providers/rac/api/connection_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet

from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.enterprise.providers.rac.models import ConnectionToken, Endpoint
from authentik.enterprise.providers.rac.models import ConnectionToken


class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
Expand All @@ -23,7 +23,7 @@ class ConnectionTokenSerializer(EnterpriseRequiredMixin, ModelSerializer):
user = GroupMemberSerializer(source="session.user", read_only=True)

class Meta:
model = Endpoint
model = ConnectionToken
fields = [
"pk",
"provider",
Expand All @@ -49,5 +49,5 @@ class ConnectionTokenViewSet(
filterset_fields = ["endpoint", "session__user", "provider"]
search_fields = ["endpoint__name", "provider__name"]
ordering = ["endpoint__name", "provider__name"]
permission_classes = [OwnerPermissions]
permission_classes = [OwnerSuperuserPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
1 change: 1 addition & 0 deletions authentik/enterprise/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
TENANT_APPS = [
"authentik.enterprise.audit",
"authentik.enterprise.providers.rac",
"authentik.enterprise.stages.source",
]

MIDDLEWARE = ["authentik.enterprise.middleware.EnterpriseMiddleware"]
Empty file.
Empty file.
38 changes: 38 additions & 0 deletions authentik/enterprise/stages/source/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Source Stage API Views"""

from rest_framework.exceptions import ValidationError
from rest_framework.viewsets import ModelViewSet

from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Source
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.stages.source.models import SourceStage
from authentik.flows.api.stages import StageSerializer


class SourceStageSerializer(EnterpriseRequiredMixin, StageSerializer):
"""SourceStage Serializer"""

def validate_source(self, _source: Source) -> Source:
"""Ensure configured source supports web-based login"""
source = Source.objects.filter(pk=_source.pk).select_subclasses().first()
if not source:
raise ValidationError("Invalid source")
login_button = source.ui_login_button(self.context["request"])
if not login_button:
raise ValidationError("Invalid source selected, only web-based sources are supported.")
return source

class Meta:
model = SourceStage
fields = StageSerializer.Meta.fields + ["source", "resume_timeout"]


class SourceStageViewSet(UsedByMixin, ModelViewSet):
"""SourceStage Viewset"""

queryset = SourceStage.objects.all()
serializer_class = SourceStageSerializer
filterset_fields = "__all__"
ordering = ["name"]
search_fields = ["name"]
12 changes: 12 additions & 0 deletions authentik/enterprise/stages/source/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""authentik stage app config"""

from authentik.enterprise.apps import EnterpriseConfig


class AuthentikEnterpriseStageSourceConfig(EnterpriseConfig):
"""authentik source stage config"""

name = "authentik.enterprise.stages.source"
label = "authentik_stages_source"
verbose_name = "authentik Enterprise.Stages.Source"
default = True
53 changes: 53 additions & 0 deletions authentik/enterprise/stages/source/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 5.0.2 on 2024-02-25 20:44

import authentik.lib.utils.time
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
("authentik_core", "0033_alter_user_options"),
("authentik_flows", "0027_auto_20231028_1424"),
]

operations = [
migrations.CreateModel(
name="SourceStage",
fields=[
(
"stage_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_flows.stage",
),
),
(
"resume_timeout",
models.TextField(
default="minutes=10",
help_text="Amount of time a user can take to return from the source to continue the flow (Format: hours=-1;minutes=-2;seconds=-3)",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
(
"source",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.source"
),
),
],
options={
"verbose_name": "Source Stage",
"verbose_name_plural": "Source Stages",
},
bases=("authentik_flows.stage",),
),
]
Empty file.
45 changes: 45 additions & 0 deletions authentik/enterprise/stages/source/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Source stage models"""

from django.db import models
from django.utils.translation import gettext_lazy as _
from django.views import View
from rest_framework.serializers import BaseSerializer

from authentik.flows.models import Stage
from authentik.lib.utils.time import timedelta_string_validator


class SourceStage(Stage):
"""Suspend the current flow execution and send the user to a source,
after which this flow execution is resumed."""

source = models.ForeignKey("authentik_core.Source", on_delete=models.CASCADE)

resume_timeout = models.TextField(
default="minutes=10",
validators=[timedelta_string_validator],
help_text=_(
"Amount of time a user can take to return from the source to continue the flow "
"(Format: hours=-1;minutes=-2;seconds=-3)"
),
)

@property
def serializer(self) -> type[BaseSerializer]:
from authentik.enterprise.stages.source.api import SourceStageSerializer

return SourceStageSerializer

@property
def view(self) -> type[View]:
from authentik.enterprise.stages.source.stage import SourceStageView

return SourceStageView

@property
def component(self) -> str:
return "ak-stage-source-form"

class Meta:
verbose_name = _("Source Stage")
verbose_name_plural = _("Source Stages")

0 comments on commit db96e1a

Please sign in to comment.