Skip to content

Commit

Permalink
Merge branch 'goauthentik:main' into fix/11973
Browse files Browse the repository at this point in the history
  • Loading branch information
4d62 authored Dec 16, 2024
2 parents 5aed115 + d5a7f0f commit 2a508df
Show file tree
Hide file tree
Showing 45 changed files with 1,594 additions and 94 deletions.
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ tests/wdio/ @goauthentik/frontend
# Docs & Website
website/ @goauthentik/docs
# Security
SECURITY.md @goauthentik/security
website/docs/security/ @goauthentik/security
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ authentik takes security very seriously. We follow the rules of [responsible di

## Independent audits and pentests

We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specfic audits and pentests, refer to "Audits and Certificates" in our [Security documentation]](https://docs.goauthentik.io/docs/security).
We are committed to engaging in regular pentesting and security audits of authentik. Defining and adhering to a cadence of external testing ensures a stronger probability that our code base, our features, and our architecture is as secure and non-exploitable as possible. For more details about specfic audits and pentests, refer to "Audits and Certificates" in our [Security documentation](https://docs.goauthentik.io/docs/security).

## What authentik classifies as a CVE

Expand Down
1 change: 1 addition & 0 deletions authentik/flows/migrations/0027_auto_20231028_1424.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class Migration(migrations.Migration):
("require_authenticated", "Require Authenticated"),
("require_unauthenticated", "Require Unauthenticated"),
("require_superuser", "Require Superuser"),
("require_redirect", "Require Redirect"),
("require_outpost", "Require Outpost"),
],
default="none",
Expand Down
1 change: 1 addition & 0 deletions authentik/flows/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class FlowAuthenticationRequirement(models.TextChoices):
REQUIRE_AUTHENTICATED = "require_authenticated"
REQUIRE_UNAUTHENTICATED = "require_unauthenticated"
REQUIRE_SUPERUSER = "require_superuser"
REQUIRE_REDIRECT = "require_redirect"
REQUIRE_OUTPOST = "require_outpost"


Expand Down
20 changes: 11 additions & 9 deletions authentik/flows/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
# Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan
# was restored.
PLAN_CONTEXT_IS_RESTORED = "is_restored"
PLAN_CONTEXT_IS_REDIRECTED = "is_redirected"
PLAN_CONTEXT_REDIRECT_STAGE_TARGET = "redirect_stage_target"
CACHE_TIMEOUT = CONFIG.get_int("cache.timeout_flows")
CACHE_PREFIX = "goauthentik.io/flows/planner/"

Expand Down Expand Up @@ -181,7 +183,7 @@ def __init__(self, flow: Flow):
self.flow = flow
self._logger = get_logger().bind(flow_slug=flow.slug)

def _check_authentication(self, request: HttpRequest):
def _check_authentication(self, request: HttpRequest, context: dict[str, Any]):
"""Check the flow's authentication level is matched by `request`"""
if (
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED
Expand All @@ -198,6 +200,11 @@ def _check_authentication(self, request: HttpRequest):
and not request.user.is_superuser
):
raise FlowNonApplicableException()
if (
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_REDIRECT
and context.get(PLAN_CONTEXT_IS_REDIRECTED) is None
):
raise FlowNonApplicableException()
outpost_user = ClientIPMiddleware.get_outpost_user(request)
if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST:
if not outpost_user:
Expand Down Expand Up @@ -229,18 +236,13 @@ def plan(self, request: HttpRequest, default_context: dict[str, Any] | None = No
)
context = default_context or {}
# Bit of a workaround here, if there is a pending user set in the default context
# we use that user for our cache key
# to make sure they don't get the generic response
# we use that user for our cache key to make sure they don't get the generic response
if context and PLAN_CONTEXT_PENDING_USER in context:
user = context[PLAN_CONTEXT_PENDING_USER]
else:
user = request.user
# We only need to check the flow authentication if it's planned without a user
# in the context, as a user in the context can only be set via the explicit code API
# or if a flow is restarted due to `invalid_response_action` being set to
# `restart_with_context`, which can only happen if the user was already authorized
# to use the flow
context.update(self._check_authentication(request))

context.update(self._check_authentication(request, context))
# First off, check the flow's direct policy bindings
# to make sure the user even has access to the flow
engine = PolicyEngine(self.flow, user, request)
Expand Down
12 changes: 6 additions & 6 deletions authentik/flows/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ def get_response_instance(self, data: QueryDict) -> ChallengeResponse:

def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Return a challenge for the frontend to solve"""
challenge = self._get_challenge(*args, **kwargs)
try:
challenge = self._get_challenge(*args, **kwargs)
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
if not challenge.is_valid():
self.logger.warning(
"f(ch): Invalid challenge",
Expand Down Expand Up @@ -169,11 +173,7 @@ def _get_challenge(self, *args, **kwargs) -> Challenge:
stage_type=self.__class__.__name__, method="get_challenge"
).time(),
):
try:
challenge = self.get_challenge(*args, **kwargs)
except StageInvalidException as exc:
self.logger.debug("Got StageInvalidException", exc=exc)
return self.executor.stage_invalid()
challenge = self.get_challenge(*args, **kwargs)
with start_span(
op="authentik.flow.stage._get_challenge",
name=self.__class__.__name__,
Expand Down
25 changes: 24 additions & 1 deletion authentik/flows/tests/test_planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
FlowStageBinding,
in_memory_stage,
)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
from authentik.flows.planner import (
PLAN_CONTEXT_IS_REDIRECTED,
PLAN_CONTEXT_PENDING_USER,
FlowPlanner,
cache_key,
)
from authentik.flows.stage import StageView
from authentik.lib.tests.utils import dummy_get_response
from authentik.outposts.apps import MANAGED_OUTPOST
Expand Down Expand Up @@ -81,6 +86,24 @@ def test_authentication(self):
planner.allow_empty_flows = True
planner.plan(request)

def test_authentication_redirect_required(self):
"""Test flow authentication (redirect required)"""
flow = create_test_flow()
flow.authentication = FlowAuthenticationRequirement.REQUIRE_REDIRECT
request = self.request_factory.get(
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
)
request.user = AnonymousUser()
planner = FlowPlanner(flow)
planner.allow_empty_flows = True

with self.assertRaises(FlowNonApplicableException):
planner.plan(request)

context = {}
context[PLAN_CONTEXT_IS_REDIRECTED] = create_test_flow()
planner.plan(request, context)

@reconcile_app("authentik_outposts")
def test_authentication_outpost(self):
"""Test flow authentication (outpost)"""
Expand Down
3 changes: 2 additions & 1 deletion authentik/flows/views/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
# Existing plan is deleted from session and instance
self.plan = None
self.cancel()
self._logger.debug("f(exec): Continuing existing plan")
else:
self._logger.debug("f(exec): Continuing existing plan")

# Initial flow request, check if we have an upstream query string passed in
request.session[SESSION_KEY_GET] = get_params
Expand Down
53 changes: 53 additions & 0 deletions authentik/lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
from collections.abc import Mapping
from contextlib import contextmanager
from copy import deepcopy
from dataclasses import dataclass, field
from enum import Enum
from glob import glob
Expand Down Expand Up @@ -336,6 +337,58 @@ def redis_url(db: int) -> str:
return _redis_url


def django_db_config(config: ConfigLoader | None = None) -> dict:
if not config:
config = CONFIG
db = {
"default": {
"ENGINE": "authentik.root.db",
"HOST": config.get("postgresql.host"),
"NAME": config.get("postgresql.name"),
"USER": config.get("postgresql.user"),
"PASSWORD": config.get("postgresql.password"),
"PORT": config.get("postgresql.port"),
"OPTIONS": {
"sslmode": config.get("postgresql.sslmode"),
"sslrootcert": config.get("postgresql.sslrootcert"),
"sslcert": config.get("postgresql.sslcert"),
"sslkey": config.get("postgresql.sslkey"),
},
"TEST": {
"NAME": config.get("postgresql.test.name"),
},
}
}

if config.get_bool("postgresql.use_pgpool", False):
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True

if config.get_bool("postgresql.use_pgbouncer", False):
# https://docs.djangoproject.com/en/4.0/ref/databases/#transaction-pooling-server-side-cursors
db["default"]["DISABLE_SERVER_SIDE_CURSORS"] = True
# https://docs.djangoproject.com/en/4.0/ref/databases/#persistent-connections
db["default"]["CONN_MAX_AGE"] = None # persistent

for replica in config.get_keys("postgresql.read_replicas"):
_database = deepcopy(db["default"])
for setting, current_value in db["default"].items():
if isinstance(current_value, dict):
continue
override = config.get(
f"postgresql.read_replicas.{replica}.{setting.lower()}", default=UNSET
)
if override is not UNSET:
_database[setting] = override
for setting in db["default"]["OPTIONS"].keys():
override = config.get(
f"postgresql.read_replicas.{replica}.{setting.lower()}", default=UNSET
)
if override is not UNSET:
_database["OPTIONS"][setting] = override
db[f"replica_{replica}"] = _database
return db


if __name__ == "__main__":
if len(argv) < 2: # noqa: PLR2004
print(dumps(CONFIG.raw, indent=4, cls=AttrEncoder))
Expand Down
Loading

0 comments on commit 2a508df

Please sign in to comment.