Skip to content

Commit

Permalink
Merge branch 'main' into web/update-provider-forms-for-invalidation
Browse files Browse the repository at this point in the history
* main: (142 commits)
  core: bump goauthentik.io/api/v3 from 3.2024102.2 to 3.2024104.1 (#12149)
  core: bump debugpy from 1.8.8 to 1.8.9 (#12150)
  core: bump webauthn from 2.2.0 to 2.3.0 (#12151)
  core: bump pydantic from 2.10.0 to 2.10.1 (#12152)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#12156)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#12157)
  core: bump sentry-sdk from 2.18.0 to 2.19.0 (#12153)
  web: bump API Client version (#12147)
  root: Backport version change (#12146)
  website/docs: update info about footer links to match new UI (#12120)
  website/docs: prepare release notes (#12142)
  providers/oauth2: fix migration (#12138)
  providers/oauth2: fix migration dependencies (#12123)
  web: bump API Client version (#12129)
  providers/oauth2: fix redirect uri input (#12122)
  providers/proxy: fix redirect_uri (#12121)
  website/docs: prepare release notes (#12119)
  web: bump API Client version (#12118)
  security: fix CVE 2024 52289 (#12113)
  security: fix CVE 2024 52307 (#12115)
  ...
  • Loading branch information
kensternberg-authentik committed Nov 22, 2024
2 parents d76e3c8 + 785403d commit 76b9add
Show file tree
Hide file tree
Showing 205 changed files with 20,728 additions and 10,574 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.10.0
current_version = 2024.10.4
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
poetry run make test
poetry run coverage xml
- if: ${{ always() }}
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
flags: unit
token: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -140,7 +140,7 @@ jobs:
poetry run coverage run manage.py test tests/integration
poetry run coverage xml
- if: ${{ always() }}
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
flags: integration
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down Expand Up @@ -198,7 +198,7 @@ jobs:
poetry run coverage run manage.py test ${{ matrix.job.glob }}
poetry run coverage xml
- if: ${{ always() }}
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v5
with:
flags: e2e
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \
go build -o /go/authentik ./cmd/server

# Stage 4: MaxMind GeoIP
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.0.1 AS geoip
FROM --platform=${BUILDPLATFORM} ghcr.io/maxmind/geoipupdate:v7.1.0 AS geoip

ENV GEOIPUPDATE_EDITION_IDS="GeoLite2-City GeoLite2-ASN"
ENV GEOIPUPDATE_VERBOSE="1"
Expand Down
2 changes: 1 addition & 1 deletion authentik/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from os import environ

__version__ = "2024.10.0"
__version__ = "2024.10.4"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"


Expand Down
2 changes: 1 addition & 1 deletion authentik/api/templates/api/browser.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{% endblock %}

{% block head %}
{% versioned_script "dist/standalone/api-browser/index-%v.js" %}
<script src="{% versioned_script 'dist/standalone/api-browser/index-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
{% endblock %}
Expand Down
3 changes: 2 additions & 1 deletion authentik/blueprints/tests/test_packaged.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def tester(self: TestPackaged):
base = Path("blueprints/")
rel_path = Path(file_name).relative_to(base)
importer = Importer.from_string(BlueprintInstance(path=str(rel_path)).retrieve())
self.assertTrue(importer.validate()[0])
validation, logs = importer.validate()
self.assertTrue(validation, logs)
self.assertTrue(importer.apply())

return tester
Expand Down
15 changes: 10 additions & 5 deletions authentik/blueprints/v1/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,11 @@ def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer | None: # n

serializer_kwargs = {}
model_instance = existing_models.first()
if not isinstance(model(), BaseMetaModel) and model_instance:
if (
not isinstance(model(), BaseMetaModel)
and model_instance
and entry.state != BlueprintEntryDesiredState.MUST_CREATED
):
self.logger.debug(
"Initialise serializer with instance",
model=model,
Expand All @@ -303,11 +307,12 @@ def _validate_single(self, entry: BlueprintEntry) -> BaseSerializer | None: # n
serializer_kwargs["instance"] = model_instance
serializer_kwargs["partial"] = True
elif model_instance and entry.state == BlueprintEntryDesiredState.MUST_CREATED:
msg = (
f"State is set to {BlueprintEntryDesiredState.MUST_CREATED.value} "
"and object exists already",
)
raise EntryInvalidError.from_entry(
(
f"State is set to {BlueprintEntryDesiredState.MUST_CREATED} "
"and object exists already",
),
ValidationError({k: msg for k in entry.identifiers.keys()}, "unique"),
entry,
)
else:
Expand Down
8 changes: 5 additions & 3 deletions authentik/brands/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.utils.translation import activate
from django.utils.translation import override

from authentik.brands.utils import get_brand_for_request

Expand All @@ -18,10 +18,12 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
self.get_response = get_response

def __call__(self, request: HttpRequest) -> HttpResponse:
locale_to_set = None
if not hasattr(request, "brand"):
brand = get_brand_for_request(request)
request.brand = brand
locale = brand.default_locale
if locale != "":
activate(locale)
return self.get_response(request)
locale_to_set = locale
with override(locale_to_set):
return self.get_response(request)
7 changes: 6 additions & 1 deletion authentik/core/api/devices.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Authenticator Devices API Views"""

from django.utils.translation import gettext_lazy as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework.fields import (
Expand Down Expand Up @@ -40,7 +41,11 @@ def get_type(self, instance: Device) -> str:
def get_extra_description(self, instance: Device) -> str:
"""Get extra description"""
if isinstance(instance, WebAuthnDevice):
return instance.device_type.description
return (
instance.device_type.description
if instance.device_type
else _("Extra description not available")
)
if isinstance(instance, EndpointDevice):
return instance.data.get("deviceSignals", {}).get("deviceModel")
return ""
Expand Down
51 changes: 45 additions & 6 deletions authentik/core/api/transactional_applications.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""transactional application and provider creation"""

from django.apps import apps
from django.db.models import Model
from django.utils.translation import gettext as _
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_field
from rest_framework.exceptions import ValidationError
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.fields import BooleanField, CharField, ChoiceField, DictField, ListField
from rest_framework.permissions import IsAdminUser
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
Expand All @@ -22,6 +24,7 @@
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Provider
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.api.bindings import PolicyBindingSerializer


def get_provider_serializer_mapping():
Expand All @@ -45,13 +48,22 @@ class TransactionProviderField(DictField):
"""Dictionary field which can hold provider creation data"""


class TransactionPolicyBindingSerializer(PolicyBindingSerializer):
"""PolicyBindingSerializer which does not require target as target is set implicitly"""

class Meta(PolicyBindingSerializer.Meta):
fields = [x for x in PolicyBindingSerializer.Meta.fields if x != "target"]


class TransactionApplicationSerializer(PassiveSerializer):
"""Serializer for creating a provider and an application in one transaction"""

app = ApplicationSerializer()
provider_model = ChoiceField(choices=list(get_provider_serializer_mapping().keys()))
provider = TransactionProviderField()

policy_bindings = TransactionPolicyBindingSerializer(many=True, required=False)

_provider_model: type[Provider] = None

def validate_provider_model(self, fq_model_name: str) -> str:
Expand Down Expand Up @@ -96,6 +108,19 @@ def validate(self, attrs: dict) -> dict:
id="app",
)
)
for binding in attrs.get("policy_bindings", []):
binding["target"] = KeyOf(None, ScalarNode(tag="", value="app"))
for key, value in binding.items():
if not isinstance(value, Model):
continue
binding[key] = value.pk
blueprint.entries.append(
BlueprintEntry(
model="authentik_policies.policybinding",
state=BlueprintEntryDesiredState.MUST_CREATED,
identifiers=binding,
)
)
importer = Importer(blueprint, {})
try:
valid, _ = importer.validate(raise_validation_errors=True)
Expand All @@ -120,8 +145,7 @@ class TransactionApplicationResponseSerializer(PassiveSerializer):
class TransactionalApplicationView(APIView):
"""Create provider and application and attach them in a single transaction"""

# TODO: Migrate to a more specific permission
permission_classes = [IsAdminUser]
permission_classes = [IsAuthenticated]

@extend_schema(
request=TransactionApplicationSerializer(),
Expand All @@ -133,8 +157,23 @@ def put(self, request: Request) -> Response:
"""Convert data into a blueprint, validate it and apply it"""
data = TransactionApplicationSerializer(data=request.data)
data.is_valid(raise_exception=True)

importer = Importer(data.validated_data, {})
blueprint: Blueprint = data.validated_data
for entry in blueprint.entries:
full_model = entry.get_model(blueprint)
app, __, model = full_model.partition(".")
if not request.user.has_perm(f"{app}.add_{model}"):
raise PermissionDenied(
{
entry.id: _(
"User lacks permission to create {model}".format_map(
{
"model": full_model,
}
)
)
}
)
importer = Importer(blueprint, {})
applied = importer.apply()
response = {"applied": False, "logs": []}
response["applied"] = applied
Expand Down
15 changes: 13 additions & 2 deletions authentik/core/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,12 @@ def recovery_email(self, request: Request, pk: int) -> Response:

@permission_required("authentik_core.impersonate")
@extend_schema(
request=OpenApiTypes.NONE,
request=inline_serializer(
"ImpersonationSerializer",
{
"reason": CharField(required=True),
},
),
responses={
"204": OpenApiResponse(description="Successfully started impersonation"),
"401": OpenApiResponse(description="Access denied"),
Expand All @@ -679,6 +684,7 @@ def impersonate(self, request: Request, pk: int) -> Response:
LOGGER.debug("User attempted to impersonate", user=request.user)
return Response(status=401)
user_to_be = self.get_object()
reason = request.data.get("reason", "")
# Check both object-level perms and global perms
if not request.user.has_perm(
"authentik_core.impersonate", user_to_be
Expand All @@ -688,11 +694,16 @@ def impersonate(self, request: Request, pk: int) -> Response:
if user_to_be.pk == self.request.user.pk:
LOGGER.debug("User attempted to impersonate themselves", user=request.user)
return Response(status=401)
if not reason and request.tenant.impersonation_require_reason:
LOGGER.debug(
"User attempted to impersonate without providing a reason", user=request.user
)
return Response(status=401)

request.session[SESSION_KEY_IMPERSONATE_ORIGINAL_USER] = request.user
request.session[SESSION_KEY_IMPERSONATE_USER] = user_to_be

Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
Event.new(EventAction.IMPERSONATION_STARTED, reason=reason).from_http(request, user_to_be)

return Response(status=201)

Expand Down
8 changes: 5 additions & 3 deletions authentik/core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from uuid import uuid4

from django.http import HttpRequest, HttpResponse
from django.utils.translation import activate
from django.utils.translation import override
from sentry_sdk.api import set_tag
from structlog.contextvars import STRUCTLOG_KEY_PREFIX

Expand All @@ -31,17 +31,19 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
def __call__(self, request: HttpRequest) -> HttpResponse:
# No permission checks are done here, they need to be checked before
# SESSION_KEY_IMPERSONATE_USER is set.
locale_to_set = None
if request.user.is_authenticated:
locale = request.user.locale(request)
if locale != "":
activate(locale)
locale_to_set = locale

if SESSION_KEY_IMPERSONATE_USER in request.session:
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
# Ensure that the user is active, otherwise nothing will work
request.user.is_active = True

return self.get_response(request)
with override(locale_to_set):
return self.get_response(request)


class RequestIDMiddleware:
Expand Down
5 changes: 5 additions & 0 deletions authentik/core/sources/flow_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ def get_action(self, **kwargs) -> tuple[Action, UserSourceConnection | None]: #
)
new_connection.user = self.request.user
new_connection = self.update_user_connection(new_connection, **kwargs)
if existing := self.user_connection_type.objects.filter(
source=self.source, identifier=self.identifier
).first():
existing = self.update_user_connection(existing)
return Action.AUTH, existing
return Action.LINK, new_connection

action, connection = self.matcher.get_user_action(self.identifier, self.user_properties)
Expand Down
4 changes: 2 additions & 2 deletions authentik/core/templates/base/skeleton.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
{% versioned_script "dist/poly-%v.js" %}
{% versioned_script "dist/standalone/loading/index-%v.js" %}
<script src="{% versioned_script 'dist/poly-%v.js' %}" type="module"></script>
<script src="{% versioned_script 'dist/standalone/loading/index-%v.js' %}" type="module"></script>
{% block head %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />
Expand Down
2 changes: 1 addition & 1 deletion authentik/core/templates/if/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% load authentik_core %}

{% block head %}
{% versioned_script "dist/admin/AdminInterface-%v.js" %}
<script src="{% versioned_script 'dist/admin/AdminInterface-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
{% include "base/header_js.html" %}
Expand Down
2 changes: 1 addition & 1 deletion authentik/core/templates/if/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% load authentik_core %}

{% block head %}
{% versioned_script "dist/user/UserInterface-%v.js" %}
<script src="{% versioned_script 'dist/user/UserInterface-%v.js' %}" type="module"></script>
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %}
Expand Down
9 changes: 1 addition & 8 deletions authentik/core/templatetags/authentik_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from django import template
from django.templatetags.static import static as static_loader
from django.utils.safestring import mark_safe

from authentik import get_full_version

Expand All @@ -12,10 +11,4 @@
@register.simple_tag()
def versioned_script(path: str) -> str:
"""Wrapper around {% static %} tag that supports setting the version"""
returned_lines = [
(
f'<script src="{static_loader(path.replace("%v", get_full_version()))}'
'" type="module"></script>'
),
]
return mark_safe("".join(returned_lines)) # nosec
return static_loader(path.replace("%v", get_full_version()))
4 changes: 2 additions & 2 deletions authentik/core/tests/test_applications_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import OAuth2Provider
from authentik.providers.oauth2.models import OAuth2Provider, RedirectURI, RedirectURIMatchingMode
from authentik.providers.proxy.models import ProxyProvider
from authentik.providers.saml.models import SAMLProvider

Expand All @@ -24,7 +24,7 @@ def setUp(self) -> None:
self.user = create_test_admin_user()
self.provider = OAuth2Provider.objects.create(
name="test",
redirect_uris="http://some-other-domain",
redirect_uris=[RedirectURI(RedirectURIMatchingMode.STRICT, "http://some-other-domain")],
authorization_flow=create_test_flow(),
)
self.allowed: Application = Application.objects.create(
Expand Down
Loading

0 comments on commit 76b9add

Please sign in to comment.