Compare commits
	
		
			1 Commits
		
	
	
		
			linter-fix
			...
			flows/conc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5797a51993 | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 2025.2.3 | ||||
| current_version = 2025.2.2 | ||||
| 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*))? | ||||
| @ -17,8 +17,6 @@ optional_value = final | ||||
|  | ||||
| [bumpversion:file:pyproject.toml] | ||||
|  | ||||
| [bumpversion:file:uv.lock] | ||||
|  | ||||
| [bumpversion:file:package.json] | ||||
|  | ||||
| [bumpversion:file:docker-compose.yml] | ||||
|  | ||||
							
								
								
									
										22
									
								
								.github/ISSUE_TEMPLATE/docs_issue.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/ISSUE_TEMPLATE/docs_issue.md
									
									
									
									
										vendored
									
									
								
							| @ -1,22 +0,0 @@ | ||||
| --- | ||||
| name: Documentation issue | ||||
| about: Suggest an improvement or report a problem | ||||
| title: "" | ||||
| labels: documentation | ||||
| assignees: "" | ||||
| --- | ||||
|  | ||||
| **Do you see an area that can be clarified or expanded, a technical inaccuracy, or a broken link? Please describe.** | ||||
| A clear and concise description of what the problem is, or where the document can be improved. Ex. I believe we need more details about [...] | ||||
|  | ||||
| **Provide the URL or link to the exact page in the documentation to which you are referring.** | ||||
| If there are multiple pages, list them all, and be sure to state the header or section where the content is. | ||||
|  | ||||
| **Describe the solution you'd like** | ||||
| A clear and concise description of what you want to happen. | ||||
|  | ||||
| **Additional context** | ||||
| Add any other context or screenshots about the documentation issue here. | ||||
|  | ||||
| **Consider opening a PR!** | ||||
| If the issue is one that you can fix, or even make a good pass at, we'd appreciate a PR. For more information about making a contribution to the docs, and using our Style Guide and our templates, refer to ["Writing documentation"](https://docs.goauthentik.io/docs/developer-docs/docs/writing-documentation). | ||||
| @ -44,6 +44,7 @@ if is_release: | ||||
|         ] | ||||
|         if not prerelease: | ||||
|             image_tags += [ | ||||
|                 f"{name}:latest", | ||||
|                 f"{name}:{version_family}", | ||||
|             ] | ||||
| else: | ||||
|  | ||||
							
								
								
									
										2
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-outpost.yml
									
									
									
									
										vendored
									
									
								
							| @ -29,7 +29,7 @@ jobs: | ||||
|       - name: Generate API | ||||
|         run: make gen-client-go | ||||
|       - name: golangci-lint | ||||
|         uses: golangci/golangci-lint-action@v7 | ||||
|         uses: golangci/golangci-lint-action@v6 | ||||
|         with: | ||||
|           version: latest | ||||
|           args: --timeout 5000s --verbose | ||||
|  | ||||
							
								
								
									
										27
									
								
								.github/workflows/semgrep.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/semgrep.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,27 +0,0 @@ | ||||
| name: authentik-semgrep | ||||
| on: | ||||
|   workflow_dispatch: {} | ||||
|   pull_request: {} | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|       - master | ||||
|     paths: | ||||
|       - .github/workflows/semgrep.yml | ||||
|   schedule: | ||||
|     # random HH:MM to avoid a load spike on GitHub Actions at 00:00 | ||||
|     - cron: '12 15 * * *' | ||||
| jobs: | ||||
|   semgrep: | ||||
|     name: semgrep/ci | ||||
|     runs-on: ubuntu-latest | ||||
|     permissions: | ||||
|       contents: read | ||||
|     env: | ||||
|       SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} | ||||
|     container: | ||||
|       image: semgrep/semgrep | ||||
|     if: (github.actor != 'dependabot[bot]') | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - run: semgrep ci | ||||
| @ -43,7 +43,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api | ||||
| RUN npm run build | ||||
|  | ||||
| # Stage 3: Build go proxy | ||||
| FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS go-builder | ||||
| FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS go-builder | ||||
|  | ||||
| ARG TARGETOS | ||||
| ARG TARGETARCH | ||||
| @ -76,7 +76,7 @@ COPY ./go.sum /go/src/goauthentik.io/go.sum | ||||
| RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ | ||||
|     --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ | ||||
|     if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ | ||||
|     CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ | ||||
|     CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \ | ||||
|     go build -o /go/authentik ./cmd/server | ||||
|  | ||||
| # Stage 4: MaxMind GeoIP | ||||
| @ -94,9 +94,9 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \ | ||||
|     /bin/sh -c "/usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0" | ||||
|  | ||||
| # Stage 5: Download uv | ||||
| FROM ghcr.io/astral-sh/uv:0.6.11 AS uv | ||||
| FROM ghcr.io/astral-sh/uv:0.6.8 AS uv | ||||
| # Stage 6: Base python image | ||||
| FROM ghcr.io/goauthentik/fips-python:3.12.9-slim-bookworm-fips AS python-base | ||||
| FROM ghcr.io/goauthentik/fips-python:3.12.8-slim-bookworm-fips AS python-base | ||||
|  | ||||
| ENV VENV_PATH="/ak-root/.venv" \ | ||||
|     PATH="/lifecycle:/ak-root/.venv/bin:$PATH" \ | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|  | ||||
| from os import environ | ||||
|  | ||||
| __version__ = "2025.2.3" | ||||
| __version__ = "2025.2.2" | ||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -59,7 +59,7 @@ class SystemInfoSerializer(PassiveSerializer): | ||||
|             if not isinstance(value, str): | ||||
|                 continue | ||||
|             actual_value = value | ||||
|             if raw_session is not None and raw_session in actual_value: | ||||
|             if raw_session in actual_value: | ||||
|                 actual_value = actual_value.replace( | ||||
|                     raw_session, SafeExceptionReporterFilter.cleansed_substitute | ||||
|                 ) | ||||
|  | ||||
| @ -49,8 +49,6 @@ class BrandSerializer(ModelSerializer): | ||||
|             "branding_title", | ||||
|             "branding_logo", | ||||
|             "branding_favicon", | ||||
|             "branding_custom_css", | ||||
|             "branding_default_flow_background", | ||||
|             "flow_authentication", | ||||
|             "flow_invalidation", | ||||
|             "flow_recovery", | ||||
| @ -88,7 +86,6 @@ class CurrentBrandSerializer(PassiveSerializer): | ||||
|     branding_title = CharField() | ||||
|     branding_logo = CharField(source="branding_logo_url") | ||||
|     branding_favicon = CharField(source="branding_favicon_url") | ||||
|     branding_custom_css = CharField() | ||||
|     ui_footer_links = ListField( | ||||
|         child=FooterLinkSerializer(), | ||||
|         read_only=True, | ||||
| @ -128,7 +125,6 @@ class BrandViewSet(UsedByMixin, ModelViewSet): | ||||
|         "branding_title", | ||||
|         "branding_logo", | ||||
|         "branding_favicon", | ||||
|         "branding_default_flow_background", | ||||
|         "flow_authentication", | ||||
|         "flow_invalidation", | ||||
|         "flow_recovery", | ||||
|  | ||||
| @ -1,35 +0,0 @@ | ||||
| # Generated by Django 5.0.12 on 2025-02-22 01:51 | ||||
|  | ||||
| from pathlib import Path | ||||
| from django.db import migrations, models | ||||
| from django.apps.registry import Apps | ||||
|  | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
|  | ||||
| def migrate_custom_css(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     Brand = apps.get_model("authentik_brands", "brand") | ||||
|  | ||||
|     db_alias = schema_editor.connection.alias | ||||
|  | ||||
|     path = Path("/web/dist/custom.css") | ||||
|     if not path.exists(): | ||||
|         return | ||||
|     css = path.read_text() | ||||
|     Brand.objects.using(db_alias).update(branding_custom_css=css) | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_brands", "0007_brand_default_application"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="brand", | ||||
|             name="branding_custom_css", | ||||
|             field=models.TextField(blank=True, default=""), | ||||
|         ), | ||||
|         migrations.RunPython(migrate_custom_css), | ||||
|     ] | ||||
| @ -1,18 +0,0 @@ | ||||
| # Generated by Django 5.0.13 on 2025-03-19 22:54 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_brands", "0008_brand_branding_custom_css"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name="brand", | ||||
|             name="branding_default_flow_background", | ||||
|             field=models.TextField(default="/static/dist/assets/images/flow_background.jpg"), | ||||
|         ), | ||||
|     ] | ||||
| @ -33,10 +33,6 @@ class Brand(SerializerModel): | ||||
|  | ||||
|     branding_logo = models.TextField(default="/static/dist/assets/icons/icon_left_brand.svg") | ||||
|     branding_favicon = models.TextField(default="/static/dist/assets/icons/icon.png") | ||||
|     branding_custom_css = models.TextField(default="", blank=True) | ||||
|     branding_default_flow_background = models.TextField( | ||||
|         default="/static/dist/assets/images/flow_background.jpg" | ||||
|     ) | ||||
|  | ||||
|     flow_authentication = models.ForeignKey( | ||||
|         Flow, null=True, on_delete=models.SET_NULL, related_name="brand_authentication" | ||||
| @ -88,12 +84,6 @@ class Brand(SerializerModel): | ||||
|             return CONFIG.get("web.path", "/")[:-1] + self.branding_favicon | ||||
|         return self.branding_favicon | ||||
|  | ||||
|     def branding_default_flow_background_url(self) -> str: | ||||
|         """Get branding_default_flow_background with the correct prefix""" | ||||
|         if self.branding_default_flow_background.startswith("/static"): | ||||
|             return CONFIG.get("web.path", "/")[:-1] + self.branding_default_flow_background | ||||
|         return self.branding_default_flow_background | ||||
|  | ||||
|     @property | ||||
|     def serializer(self) -> Serializer: | ||||
|         from authentik.brands.api import BrandSerializer | ||||
|  | ||||
| @ -24,7 +24,6 @@ class TestBrands(APITestCase): | ||||
|                 "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", | ||||
|                 "branding_favicon": "/static/dist/assets/icons/icon.png", | ||||
|                 "branding_title": "authentik", | ||||
|                 "branding_custom_css": "", | ||||
|                 "matched_domain": brand.domain, | ||||
|                 "ui_footer_links": [], | ||||
|                 "ui_theme": Themes.AUTOMATIC, | ||||
| @ -44,7 +43,6 @@ class TestBrands(APITestCase): | ||||
|                 "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", | ||||
|                 "branding_favicon": "/static/dist/assets/icons/icon.png", | ||||
|                 "branding_title": "custom", | ||||
|                 "branding_custom_css": "", | ||||
|                 "matched_domain": "bar.baz", | ||||
|                 "ui_footer_links": [], | ||||
|                 "ui_theme": Themes.AUTOMATIC, | ||||
| @ -61,7 +59,6 @@ class TestBrands(APITestCase): | ||||
|                 "branding_logo": "/static/dist/assets/icons/icon_left_brand.svg", | ||||
|                 "branding_favicon": "/static/dist/assets/icons/icon.png", | ||||
|                 "branding_title": "authentik", | ||||
|                 "branding_custom_css": "", | ||||
|                 "matched_domain": "fallback", | ||||
|                 "ui_footer_links": [], | ||||
|                 "ui_theme": Themes.AUTOMATIC, | ||||
| @ -124,27 +121,3 @@ class TestBrands(APITestCase): | ||||
|                 "subject": None, | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|     def test_branding_url(self): | ||||
|         """Test branding attributes return correct values""" | ||||
|         brand = create_test_brand() | ||||
|         brand.branding_default_flow_background = "https://goauthentik.io/img/icon.png" | ||||
|         brand.branding_favicon = "https://goauthentik.io/img/icon.png" | ||||
|         brand.branding_logo = "https://goauthentik.io/img/icon.png" | ||||
|         brand.save() | ||||
|         self.assertEqual( | ||||
|             brand.branding_default_flow_background_url(), "https://goauthentik.io/img/icon.png" | ||||
|         ) | ||||
|         self.assertJSONEqual( | ||||
|             self.client.get(reverse("authentik_api:brand-current")).content.decode(), | ||||
|             { | ||||
|                 "branding_logo": "https://goauthentik.io/img/icon.png", | ||||
|                 "branding_favicon": "https://goauthentik.io/img/icon.png", | ||||
|                 "branding_title": "authentik", | ||||
|                 "branding_custom_css": "", | ||||
|                 "matched_domain": brand.domain, | ||||
|                 "ui_footer_links": [], | ||||
|                 "ui_theme": Themes.AUTOMATIC, | ||||
|                 "default_locale": "", | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
| @ -46,7 +46,7 @@ LOGGER = get_logger() | ||||
|  | ||||
| def user_app_cache_key(user_pk: str, page_number: int | None = None) -> str: | ||||
|     """Cache key where application list for user is saved""" | ||||
|     key = f"{CACHE_PREFIX}app_access/{user_pk}" | ||||
|     key = f"{CACHE_PREFIX}/app_access/{user_pk}" | ||||
|     if page_number: | ||||
|         key += f"/{page_number}" | ||||
|     return key | ||||
|  | ||||
| @ -1,14 +1,13 @@ | ||||
| """User API Views""" | ||||
|  | ||||
| from datetime import timedelta | ||||
| from importlib import import_module | ||||
| from json import loads | ||||
| from typing import Any | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth import update_session_auth_hash | ||||
| from django.contrib.auth.models import Permission | ||||
| from django.contrib.sessions.backends.base import SessionBase | ||||
| from django.contrib.sessions.backends.cache import KEY_PREFIX | ||||
| from django.core.cache import cache | ||||
| from django.db.models.functions import ExtractHour | ||||
| from django.db.transaction import atomic | ||||
| from django.db.utils import IntegrityError | ||||
| @ -92,7 +91,6 @@ from authentik.stages.email.tasks import send_mails | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore | ||||
|  | ||||
|  | ||||
| class UserGroupSerializer(ModelSerializer): | ||||
| @ -375,7 +373,7 @@ class UsersFilter(FilterSet): | ||||
|         method="filter_attributes", | ||||
|     ) | ||||
|  | ||||
|     is_superuser = BooleanFilter(field_name="ak_groups", method="filter_is_superuser") | ||||
|     is_superuser = BooleanFilter(field_name="ak_groups", lookup_expr="is_superuser") | ||||
|     uuid = UUIDFilter(field_name="uuid") | ||||
|  | ||||
|     path = CharFilter(field_name="path") | ||||
| @ -393,11 +391,6 @@ class UsersFilter(FilterSet): | ||||
|         queryset=Group.objects.all().order_by("name"), | ||||
|     ) | ||||
|  | ||||
|     def filter_is_superuser(self, queryset, name, value): | ||||
|         if value: | ||||
|             return queryset.filter(ak_groups__is_superuser=True).distinct() | ||||
|         return queryset.exclude(ak_groups__is_superuser=True).distinct() | ||||
|  | ||||
|     def filter_attributes(self, queryset, name, value): | ||||
|         """Filter attributes by query args""" | ||||
|         try: | ||||
| @ -776,8 +769,7 @@ class UserViewSet(UsedByMixin, ModelViewSet): | ||||
|         if not instance.is_active: | ||||
|             sessions = AuthenticatedSession.objects.filter(user=instance) | ||||
|             session_ids = sessions.values_list("session_key", flat=True) | ||||
|             for session in session_ids: | ||||
|                 SessionStore(session).delete() | ||||
|             cache.delete_many(f"{KEY_PREFIX}{session}" for session in session_ids) | ||||
|             sessions.delete() | ||||
|             LOGGER.debug("Deleted user's sessions", user=instance.username) | ||||
|         return response | ||||
|  | ||||
| @ -761,17 +761,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): | ||||
|     @property | ||||
|     def component(self) -> str: | ||||
|         """Return component used to edit this object""" | ||||
|         if self.managed == self.MANAGED_INBUILT: | ||||
|             return "" | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     @property | ||||
|     def property_mapping_type(self) -> "type[PropertyMapping]": | ||||
|         """Return property mapping type used by this object""" | ||||
|         if self.managed == self.MANAGED_INBUILT: | ||||
|             from authentik.core.models import PropertyMapping | ||||
|  | ||||
|             return PropertyMapping | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def ui_login_button(self, request: HttpRequest) -> UILoginButton | None: | ||||
| @ -786,14 +780,10 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): | ||||
|  | ||||
|     def get_base_user_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]: | ||||
|         """Get base properties for a user to build final properties upon.""" | ||||
|         if self.managed == self.MANAGED_INBUILT: | ||||
|             return {} | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def get_base_group_properties(self, **kwargs) -> dict[str, Any | dict[str, Any]]: | ||||
|         """Get base properties for a group to build final properties upon.""" | ||||
|         if self.managed == self.MANAGED_INBUILT: | ||||
|             return {} | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def __str__(self): | ||||
|  | ||||
| @ -1,10 +1,7 @@ | ||||
| """authentik core signals""" | ||||
|  | ||||
| from importlib import import_module | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.signals import user_logged_in, user_logged_out | ||||
| from django.contrib.sessions.backends.base import SessionBase | ||||
| from django.contrib.sessions.backends.cache import KEY_PREFIX | ||||
| from django.core.cache import cache | ||||
| from django.core.signals import Signal | ||||
| from django.db.models import Model | ||||
| @ -28,7 +25,6 @@ password_changed = Signal() | ||||
| login_failed = Signal() | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| SessionStore: SessionBase = import_module(settings.SESSION_ENGINE).SessionStore | ||||
|  | ||||
|  | ||||
| @receiver(post_save, sender=Application) | ||||
| @ -64,7 +60,8 @@ def user_logged_out_session(sender, request: HttpRequest, user: User, **_): | ||||
| @receiver(pre_delete, sender=AuthenticatedSession) | ||||
| def authenticated_session_delete(sender: type[Model], instance: "AuthenticatedSession", **_): | ||||
|     """Delete session when authenticated session is deleted""" | ||||
|     SessionStore(instance.session_key).delete() | ||||
|     cache_key = f"{KEY_PREFIX}{instance.session_key}" | ||||
|     cache.delete(cache_key) | ||||
|  | ||||
|  | ||||
| @receiver(pre_save) | ||||
|  | ||||
| @ -36,7 +36,6 @@ from authentik.flows.planner import ( | ||||
| ) | ||||
| from authentik.flows.stage import StageView | ||||
| from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET | ||||
| from authentik.lib.utils.urls import is_url_absolute | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.policies.denied import AccessDeniedResponse | ||||
| from authentik.policies.utils import delete_none_values | ||||
| @ -49,7 +48,6 @@ LOGGER = get_logger() | ||||
|  | ||||
| PLAN_CONTEXT_SOURCE_GROUPS = "source_groups" | ||||
| SESSION_KEY_SOURCE_FLOW_STAGES = "authentik/flows/source_flow_stages" | ||||
| SESSION_KEY_SOURCE_FLOW_CONTEXT = "authentik/flows/source_flow_context" | ||||
| SESSION_KEY_OVERRIDE_FLOW_TOKEN = "authentik/flows/source_override_flow_token"  # nosec | ||||
|  | ||||
|  | ||||
| @ -210,8 +208,6 @@ class SourceFlowManager: | ||||
|         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( | ||||
|             NEXT_ARG_NAME, "authentik_core:if-user" | ||||
|         ) | ||||
|         if not is_url_absolute(final_redirect): | ||||
|             final_redirect = "authentik_core:if-user" | ||||
|         flow_context.update( | ||||
|             { | ||||
|                 # Since we authenticate the user by their token, they have no backend set | ||||
| @ -265,7 +261,6 @@ class SourceFlowManager: | ||||
|                 plan.append_stage(stage) | ||||
|         for stage in self.request.session.get(SESSION_KEY_SOURCE_FLOW_STAGES, []): | ||||
|             plan.append_stage(stage) | ||||
|         plan.context.update(self.request.session.get(SESSION_KEY_SOURCE_FLOW_CONTEXT, {})) | ||||
|         return plan.to_redirect(self.request, flow) | ||||
|  | ||||
|     def handle_auth( | ||||
|  | ||||
| @ -16,7 +16,7 @@ | ||||
|         {% block head_before %} | ||||
|         {% endblock %} | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> | ||||
|         <style>{{ brand.branding_custom_css }}</style> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject> | ||||
|         <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 %} | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block head_before %} | ||||
| <link rel="prefetch" href="{{ request.brand.branding_default_flow_background_url }}" /> | ||||
| <link rel="prefetch" href="{% static 'dist/assets/images/flow_background.jpg' %}" /> | ||||
| <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.min.css' %}"> | ||||
| <link rel="stylesheet" type="text/css" href="{% static 'dist/theme-dark.css' %}" media="(prefers-color-scheme: dark)"> | ||||
| {% include "base/header_js.html" %} | ||||
| @ -13,7 +13,7 @@ | ||||
| {% block head %} | ||||
| <style> | ||||
| :root { | ||||
|     --ak-flow-background: url("{{ request.brand.branding_default_flow_background_url }}"); | ||||
|     --ak-flow-background: url("{% static 'dist/assets/images/flow_background.jpg' %}"); | ||||
|     --pf-c-background-image--BackgroundImage: var(--ak-flow-background); | ||||
|     --pf-c-background-image--BackgroundImage-2x: var(--ak-flow-background); | ||||
|     --pf-c-background-image--BackgroundImage--sm: var(--ak-flow-background); | ||||
|  | ||||
| @ -1,19 +0,0 @@ | ||||
| from django.apps import apps | ||||
| from django.urls import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
|  | ||||
|  | ||||
| class TestSourceAPI(APITestCase): | ||||
|     def setUp(self) -> None: | ||||
|         self.user = create_test_admin_user() | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_builtin_source_used_by(self): | ||||
|         """Test Providers's types endpoint""" | ||||
|         apps.get_app_config("authentik_core").source_inbuilt() | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:source-used-by", kwargs={"slug": "authentik-built-in"}), | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
| @ -1,7 +1,6 @@ | ||||
| """Test Users API""" | ||||
|  | ||||
| from datetime import datetime | ||||
| from json import loads | ||||
|  | ||||
| from django.contrib.sessions.backends.cache import KEY_PREFIX | ||||
| from django.core.cache import cache | ||||
| @ -16,12 +15,7 @@ from authentik.core.models import ( | ||||
|     User, | ||||
|     UserTypes, | ||||
| ) | ||||
| from authentik.core.tests.utils import ( | ||||
|     create_test_admin_user, | ||||
|     create_test_brand, | ||||
|     create_test_flow, | ||||
|     create_test_user, | ||||
| ) | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow | ||||
| from authentik.flows.models import FlowDesignation | ||||
| from authentik.lib.generators import generate_id, generate_key | ||||
| from authentik.stages.email.models import EmailStage | ||||
| @ -32,7 +26,7 @@ class TestUsersAPI(APITestCase): | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         self.admin = create_test_admin_user() | ||||
|         self.user = create_test_user() | ||||
|         self.user = User.objects.create(username="test-user") | ||||
|  | ||||
|     def test_filter_type(self): | ||||
|         """Test API filtering by type""" | ||||
| @ -47,35 +41,6 @@ class TestUsersAPI(APITestCase): | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_filter_is_superuser(self): | ||||
|         """Test API filtering by superuser status""" | ||||
|         User.objects.all().delete() | ||||
|         admin = create_test_admin_user() | ||||
|         self.client.force_login(admin) | ||||
|         # Test superuser | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:user-list"), | ||||
|             data={ | ||||
|                 "is_superuser": True, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content) | ||||
|         self.assertEqual(len(body["results"]), 1) | ||||
|         self.assertEqual(body["results"][0]["username"], admin.username) | ||||
|         # Test non-superuser | ||||
|         user = create_test_user() | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:user-list"), | ||||
|             data={ | ||||
|                 "is_superuser": False, | ||||
|             }, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         body = loads(response.content) | ||||
|         self.assertEqual(len(body["results"]), 1, body) | ||||
|         self.assertEqual(body["results"][0]["username"], user.username) | ||||
|  | ||||
|     def test_list_with_groups(self): | ||||
|         """Test listing with groups""" | ||||
|         self.client.force_login(self.admin) | ||||
| @ -134,8 +99,6 @@ class TestUsersAPI(APITestCase): | ||||
|     def test_recovery_email_no_flow(self): | ||||
|         """Test user recovery link (no recovery flow set)""" | ||||
|         self.client.force_login(self.admin) | ||||
|         self.user.email = "" | ||||
|         self.user.save() | ||||
|         response = self.client.post( | ||||
|             reverse("authentik_api:user-recovery-email", kwargs={"pk": self.user.pk}) | ||||
|         ) | ||||
|  | ||||
| @ -11,14 +11,13 @@ from guardian.shortcuts import get_anonymous_user | ||||
| from authentik.core.models import Source, User | ||||
| from authentik.core.sources.flow_manager import ( | ||||
|     SESSION_KEY_OVERRIDE_FLOW_TOKEN, | ||||
|     SESSION_KEY_SOURCE_FLOW_CONTEXT, | ||||
|     SESSION_KEY_SOURCE_FLOW_STAGES, | ||||
| ) | ||||
| from authentik.core.types import UILoginButton | ||||
| from authentik.enterprise.stages.source.models import SourceStage | ||||
| from authentik.flows.challenge import Challenge, ChallengeResponse | ||||
| from authentik.flows.models import FlowToken, in_memory_stage | ||||
| from authentik.flows.planner import PLAN_CONTEXT_IS_REDIRECTED, PLAN_CONTEXT_IS_RESTORED | ||||
| from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED | ||||
| from authentik.flows.stage import ChallengeStageView, StageView | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
|  | ||||
| @ -54,9 +53,6 @@ class SourceStageView(ChallengeStageView): | ||||
|         resume_token = self.create_flow_token() | ||||
|         self.request.session[SESSION_KEY_OVERRIDE_FLOW_TOKEN] = resume_token | ||||
|         self.request.session[SESSION_KEY_SOURCE_FLOW_STAGES] = [in_memory_stage(SourceStageFinal)] | ||||
|         self.request.session[SESSION_KEY_SOURCE_FLOW_CONTEXT] = { | ||||
|             PLAN_CONTEXT_IS_REDIRECTED: self.executor.flow, | ||||
|         } | ||||
|         return self.login_button.challenge | ||||
|  | ||||
|     def create_flow_token(self) -> FlowToken: | ||||
|  | ||||
| @ -50,8 +50,7 @@ class NotificationTransportSerializer(ModelSerializer): | ||||
|             "mode", | ||||
|             "mode_verbose", | ||||
|             "webhook_url", | ||||
|             "webhook_mapping_body", | ||||
|             "webhook_mapping_headers", | ||||
|             "webhook_mapping", | ||||
|             "send_once", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| @ -1,43 +0,0 @@ | ||||
| # Generated by Django 5.0.13 on 2025-03-20 19:54 | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_events", "0008_event_authentik_e_expires_8c73a8_idx_and_more"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RenameField( | ||||
|             model_name="notificationtransport", | ||||
|             old_name="webhook_mapping", | ||||
|             new_name="webhook_mapping_body", | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="notificationtransport", | ||||
|             name="webhook_mapping_body", | ||||
|             field=models.ForeignKey( | ||||
|                 default=None, | ||||
|                 help_text="Customize the body of the request. Mapping should return data that is JSON-serializable.", | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, | ||||
|                 related_name="+", | ||||
|                 to="authentik_events.notificationwebhookmapping", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AddField( | ||||
|             model_name="notificationtransport", | ||||
|             name="webhook_mapping_headers", | ||||
|             field=models.ForeignKey( | ||||
|                 default=None, | ||||
|                 help_text="Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs", | ||||
|                 null=True, | ||||
|                 on_delete=django.db.models.deletion.SET_DEFAULT, | ||||
|                 related_name="+", | ||||
|                 to="authentik_events.notificationwebhookmapping", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -336,27 +336,8 @@ class NotificationTransport(SerializerModel): | ||||
|     mode = models.TextField(choices=TransportMode.choices, default=TransportMode.LOCAL) | ||||
|  | ||||
|     webhook_url = models.TextField(blank=True, validators=[DomainlessURLValidator()]) | ||||
|     webhook_mapping_body = models.ForeignKey( | ||||
|         "NotificationWebhookMapping", | ||||
|         on_delete=models.SET_DEFAULT, | ||||
|         null=True, | ||||
|         default=None, | ||||
|         related_name="+", | ||||
|         help_text=_( | ||||
|             "Customize the body of the request. " | ||||
|             "Mapping should return data that is JSON-serializable." | ||||
|         ), | ||||
|     ) | ||||
|     webhook_mapping_headers = models.ForeignKey( | ||||
|         "NotificationWebhookMapping", | ||||
|         on_delete=models.SET_DEFAULT, | ||||
|         null=True, | ||||
|         default=None, | ||||
|         related_name="+", | ||||
|         help_text=_( | ||||
|             "Configure additional headers to be sent. " | ||||
|             "Mapping should return a dictionary of key-value pairs" | ||||
|         ), | ||||
|     webhook_mapping = models.ForeignKey( | ||||
|         "NotificationWebhookMapping", on_delete=models.SET_DEFAULT, null=True, default=None | ||||
|     ) | ||||
|     send_once = models.BooleanField( | ||||
|         default=False, | ||||
| @ -379,8 +360,8 @@ class NotificationTransport(SerializerModel): | ||||
|  | ||||
|     def send_local(self, notification: "Notification") -> list[str]: | ||||
|         """Local notification delivery""" | ||||
|         if self.webhook_mapping_body: | ||||
|             self.webhook_mapping_body.evaluate( | ||||
|         if self.webhook_mapping: | ||||
|             self.webhook_mapping.evaluate( | ||||
|                 user=notification.user, | ||||
|                 request=None, | ||||
|                 notification=notification, | ||||
| @ -399,18 +380,9 @@ class NotificationTransport(SerializerModel): | ||||
|         if notification.event and notification.event.user: | ||||
|             default_body["event_user_email"] = notification.event.user.get("email", None) | ||||
|             default_body["event_user_username"] = notification.event.user.get("username", None) | ||||
|         headers = {} | ||||
|         if self.webhook_mapping_body: | ||||
|         if self.webhook_mapping: | ||||
|             default_body = sanitize_item( | ||||
|                 self.webhook_mapping_body.evaluate( | ||||
|                     user=notification.user, | ||||
|                     request=None, | ||||
|                     notification=notification, | ||||
|                 ) | ||||
|             ) | ||||
|         if self.webhook_mapping_headers: | ||||
|             headers = sanitize_item( | ||||
|                 self.webhook_mapping_headers.evaluate( | ||||
|                 self.webhook_mapping.evaluate( | ||||
|                     user=notification.user, | ||||
|                     request=None, | ||||
|                     notification=notification, | ||||
| @ -420,7 +392,6 @@ class NotificationTransport(SerializerModel): | ||||
|             response = get_http_session().post( | ||||
|                 self.webhook_url, | ||||
|                 json=default_body, | ||||
|                 headers=headers, | ||||
|             ) | ||||
|             response.raise_for_status() | ||||
|         except RequestException as exc: | ||||
|  | ||||
| @ -120,7 +120,7 @@ class TestEventsNotifications(APITestCase): | ||||
|         ) | ||||
|  | ||||
|         transport = NotificationTransport.objects.create( | ||||
|             name=generate_id(), webhook_mapping_body=mapping, mode=TransportMode.LOCAL | ||||
|             name=generate_id(), webhook_mapping=mapping, mode=TransportMode.LOCAL | ||||
|         ) | ||||
|         NotificationRule.objects.filter(name__startswith="default").delete() | ||||
|         trigger = NotificationRule.objects.create(name=generate_id(), group=self.group) | ||||
|  | ||||
| @ -60,25 +60,20 @@ class TestEventTransports(TestCase): | ||||
|  | ||||
|     def test_transport_webhook_mapping(self): | ||||
|         """Test webhook transport with custom mapping""" | ||||
|         mapping_body = NotificationWebhookMapping.objects.create( | ||||
|         mapping = NotificationWebhookMapping.objects.create( | ||||
|             name=generate_id(), expression="return request.user" | ||||
|         ) | ||||
|         mapping_headers = NotificationWebhookMapping.objects.create( | ||||
|             name=generate_id(), expression="""return {"foo": "bar"}""" | ||||
|         ) | ||||
|         transport: NotificationTransport = NotificationTransport.objects.create( | ||||
|             name=generate_id(), | ||||
|             mode=TransportMode.WEBHOOK, | ||||
|             webhook_url="http://localhost:1234/test", | ||||
|             webhook_mapping_body=mapping_body, | ||||
|             webhook_mapping_headers=mapping_headers, | ||||
|             webhook_mapping=mapping, | ||||
|         ) | ||||
|         with Mocker() as mocker: | ||||
|             mocker.post("http://localhost:1234/test") | ||||
|             transport.send(self.notification) | ||||
|             self.assertEqual(mocker.call_count, 1) | ||||
|             self.assertEqual(mocker.request_history[0].method, "POST") | ||||
|             self.assertEqual(mocker.request_history[0].headers["foo"], "bar") | ||||
|             self.assertJSONEqual( | ||||
|                 mocker.request_history[0].body.decode(), | ||||
|                 {"email": self.user.email, "pk": self.user.pk, "username": self.user.username}, | ||||
|  | ||||
| @ -54,6 +54,7 @@ class Challenge(PassiveSerializer): | ||||
|  | ||||
|     flow_info = ContextualFlowInfo(required=False) | ||||
|     component = CharField(default="") | ||||
|     xid = CharField(required=False) | ||||
|  | ||||
|     response_errors = DictField( | ||||
|         child=ErrorDetailSerializer(many=True), allow_empty=True, required=False | ||||
|  | ||||
| @ -6,7 +6,6 @@ from typing import TYPE_CHECKING | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from django.db import models | ||||
| from django.http import HttpRequest | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from model_utils.managers import InheritanceManager | ||||
| from rest_framework.serializers import BaseSerializer | ||||
| @ -179,12 +178,11 @@ class Flow(SerializerModel, PolicyBindingModel): | ||||
|         help_text=_("Required level of authentication and authorization to access a flow."), | ||||
|     ) | ||||
|  | ||||
|     def background_url(self, request: HttpRequest | None = None) -> str: | ||||
|     @property | ||||
|     def background_url(self) -> str: | ||||
|         """Get the URL to the background image. If the name is /static or starts with http | ||||
|         it is returned as-is""" | ||||
|         if not self.background: | ||||
|             if request: | ||||
|                 return request.brand.branding_default_flow_background_url() | ||||
|             return ( | ||||
|                 CONFIG.get("web.path", "/")[:-1] + "/static/dist/assets/images/flow_background.jpg" | ||||
|             ) | ||||
|  | ||||
| @ -143,10 +143,12 @@ class FlowPlan: | ||||
|         request: HttpRequest, | ||||
|         flow: Flow, | ||||
|         allowed_silent_types: list["StageView"] | None = None, | ||||
|         **get_params, | ||||
|     ) -> HttpResponse: | ||||
|         """Redirect to the flow executor for this flow plan""" | ||||
|         from authentik.flows.views.executor import ( | ||||
|             SESSION_KEY_PLAN, | ||||
|             FlowContainer, | ||||
|             FlowExecutorView, | ||||
|         ) | ||||
|  | ||||
| @ -157,6 +159,7 @@ class FlowPlan: | ||||
|             # No unskippable stages found, so we can directly return the response of the last stage | ||||
|             final_stage: type[StageView] = self.bindings[-1].stage.view | ||||
|             temp_exec = FlowExecutorView(flow=flow, request=request, plan=self) | ||||
|             temp_exec.container = FlowContainer(request) | ||||
|             temp_exec.current_stage = self.bindings[-1].stage | ||||
|             temp_exec.current_stage_view = final_stage | ||||
|             temp_exec.setup(request, flow.slug) | ||||
| @ -174,6 +177,9 @@ class FlowPlan: | ||||
|         ): | ||||
|             get_qs["inspector"] = "available" | ||||
|  | ||||
|         for key, value in get_params: | ||||
|             get_qs[key] = value | ||||
|  | ||||
|         return redirect_with_qs( | ||||
|             "authentik_core:if-flow", | ||||
|             get_qs, | ||||
|  | ||||
| @ -184,13 +184,14 @@ class ChallengeStageView(StageView): | ||||
|                 flow_info = ContextualFlowInfo( | ||||
|                     data={ | ||||
|                         "title": self.format_title(), | ||||
|                         "background": self.executor.flow.background_url(self.request), | ||||
|                         "background": self.executor.flow.background_url, | ||||
|                         "cancel_url": reverse("authentik_flows:cancel"), | ||||
|                         "layout": self.executor.flow.layout, | ||||
|                     } | ||||
|                 ) | ||||
|                 flow_info.is_valid() | ||||
|                 challenge.initial_data["flow_info"] = flow_info.data | ||||
|             challenge.initial_data["xid"] = self.executor.container.exec_id | ||||
|             if isinstance(challenge, WithUserInfoChallenge): | ||||
|                 # If there's a pending user, update the `username` field | ||||
|                 # this field is only used by password managers. | ||||
|  | ||||
| @ -28,7 +28,7 @@ window.authentik.flow = { | ||||
|  | ||||
| {% block body %} | ||||
| <ak-message-container></ak-message-container> | ||||
| <ak-flow-executor flowSlug="{{ flow.slug }}"> | ||||
| <ak-flow-executor flowSlug="{{ flow.slug }}" xid="{{ xid }}"> | ||||
|     <ak-loading></ak-loading> | ||||
| </ak-flow-executor> | ||||
| {% endblock %} | ||||
|  | ||||
| @ -27,6 +27,7 @@ class FlowTestCase(APITestCase): | ||||
|         self.assertIsNotNone(raw_response["component"]) | ||||
|         if flow: | ||||
|             self.assertIn("flow_info", raw_response) | ||||
|             self.assertEqual(raw_response["flow_info"]["background"], flow.background_url) | ||||
|             self.assertEqual( | ||||
|                 raw_response["flow_info"]["cancel_url"], reverse("authentik_flows:cancel") | ||||
|             ) | ||||
|  | ||||
| @ -1,11 +1,9 @@ | ||||
| """API flow tests""" | ||||
|  | ||||
| from json import loads | ||||
|  | ||||
| from django.urls import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.core.tests.utils import create_test_admin_user | ||||
| from authentik.flows.api.stages import StageSerializer, StageViewSet | ||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage | ||||
| from authentik.lib.generators import generate_id | ||||
| @ -79,22 +77,6 @@ class TestFlowsAPI(APITestCase): | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertJSONEqual(response.content, {"diagram": DIAGRAM_EXPECTED}) | ||||
|  | ||||
|     def test_api_background(self): | ||||
|         """Test custom background""" | ||||
|         user = create_test_admin_user() | ||||
|         self.client.force_login(user) | ||||
|  | ||||
|         flow = create_test_flow() | ||||
|         response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug})) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["background"], "/static/dist/assets/images/flow_background.jpg") | ||||
|  | ||||
|         flow.background = "https://goauthentik.io/img/icon.png" | ||||
|         flow.save() | ||||
|         response = self.client.get(reverse("authentik_api:flow-detail", kwargs={"slug": flow.slug})) | ||||
|         body = loads(response.content.decode()) | ||||
|         self.assertEqual(body["background"], "https://goauthentik.io/img/icon.png") | ||||
|  | ||||
|     def test_api_diagram_no_stages(self): | ||||
|         """Test flow diagram with no stages.""" | ||||
|         user = create_test_admin_user() | ||||
|  | ||||
| @ -49,7 +49,7 @@ class TestFlowInspector(APITestCase): | ||||
|                 "captcha_stage": None, | ||||
|                 "component": "ak-stage-identification", | ||||
|                 "flow_info": { | ||||
|                     "background": "/static/dist/assets/images/flow_background.jpg", | ||||
|                     "background": flow.background_url, | ||||
|                     "cancel_url": reverse("authentik_flows:cancel"), | ||||
|                     "title": flow.title, | ||||
|                     "layout": "stacked", | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """authentik multi-stage authentication engine""" | ||||
|  | ||||
| from copy import deepcopy | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| @ -64,14 +65,15 @@ from authentik.policies.engine import PolicyEngine | ||||
| LOGGER = get_logger() | ||||
| # Argument used to redirect user after login | ||||
| NEXT_ARG_NAME = "next" | ||||
| SESSION_KEY_PLAN_CONTAINER = "authentik/flows/plan_container/%s" | ||||
| SESSION_KEY_PLAN = "authentik/flows/plan" | ||||
| SESSION_KEY_APPLICATION_PRE = "authentik/flows/application_pre" | ||||
| SESSION_KEY_GET = "authentik/flows/get" | ||||
| SESSION_KEY_POST = "authentik/flows/post" | ||||
| SESSION_KEY_HISTORY = "authentik/flows/history" | ||||
| SESSION_KEY_AUTH_STARTED = "authentik/flows/auth_started" | ||||
| QS_KEY_TOKEN = "flow_token"  # nosec | ||||
| QS_QUERY = "query" | ||||
| QS_EXEC_ID = "xid" | ||||
|  | ||||
|  | ||||
| def challenge_types(): | ||||
| @ -98,6 +100,88 @@ class InvalidStageError(SentryIgnoredException): | ||||
|     """Error raised when a challenge from a stage is not valid""" | ||||
|  | ||||
|  | ||||
| class FlowContainer: | ||||
|     """Allow for multiple concurrent flow executions in the same session""" | ||||
|  | ||||
|     def __init__(self, request: HttpRequest, exec_id: str | None = None) -> None: | ||||
|         self.request = request | ||||
|         self.exec_id = exec_id | ||||
|  | ||||
|     @staticmethod | ||||
|     def new(request: HttpRequest): | ||||
|         exec_id = str(uuid4()) | ||||
|         request.session[SESSION_KEY_PLAN_CONTAINER % exec_id] = {} | ||||
|         return FlowContainer(request, exec_id) | ||||
|  | ||||
|     def exists(self) -> bool: | ||||
|         """Check if flow exists in container/session""" | ||||
|         return SESSION_KEY_PLAN in self.session | ||||
|  | ||||
|     def save(self): | ||||
|         self.request.session.modified = True | ||||
|  | ||||
|     @property | ||||
|     def session(self): | ||||
|         # Backwards compatibility: store session plan/etc directly in session | ||||
|         if not self.exec_id: | ||||
|             return self.request.session | ||||
|         self.request.session.setdefault(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {}) | ||||
|         return self.request.session.get(SESSION_KEY_PLAN_CONTAINER % self.exec_id, {}) | ||||
|  | ||||
|     @property | ||||
|     def plan(self) -> FlowPlan: | ||||
|         return self.session.get(SESSION_KEY_PLAN) | ||||
|  | ||||
|     def to_redirect( | ||||
|         self, | ||||
|         request: HttpRequest, | ||||
|         flow: Flow, | ||||
|         allowed_silent_types: list[StageView] | None = None, | ||||
|         **get_params, | ||||
|     ) -> HttpResponse: | ||||
|         get_params[QS_EXEC_ID] = self.exec_id | ||||
|         return self.plan.to_redirect( | ||||
|             request, flow, allowed_silent_types=allowed_silent_types, **get_params | ||||
|         ) | ||||
|  | ||||
|     @plan.setter | ||||
|     def plan(self, value: FlowPlan): | ||||
|         self.session[SESSION_KEY_PLAN] = value | ||||
|         self.request.session.modified = True | ||||
|         self.save() | ||||
|  | ||||
|     @property | ||||
|     def application_pre(self): | ||||
|         return self.session.get(SESSION_KEY_APPLICATION_PRE) | ||||
|  | ||||
|     @property | ||||
|     def get(self) -> QueryDict: | ||||
|         return self.session.get(SESSION_KEY_GET) | ||||
|  | ||||
|     @get.setter | ||||
|     def get(self, value: QueryDict): | ||||
|         self.session[SESSION_KEY_GET] = value | ||||
|         self.save() | ||||
|  | ||||
|     @property | ||||
|     def post(self) -> QueryDict: | ||||
|         return self.session.get(SESSION_KEY_POST) | ||||
|  | ||||
|     @post.setter | ||||
|     def post(self, value: QueryDict): | ||||
|         self.session[SESSION_KEY_POST] = value | ||||
|         self.save() | ||||
|  | ||||
|     @property | ||||
|     def history(self) -> list[FlowPlan]: | ||||
|         return self.session.get(SESSION_KEY_HISTORY) | ||||
|  | ||||
|     @history.setter | ||||
|     def history(self, value: list[FlowPlan]): | ||||
|         self.session[SESSION_KEY_HISTORY] = value | ||||
|         self.save() | ||||
|  | ||||
|  | ||||
| @method_decorator(xframe_options_sameorigin, name="dispatch") | ||||
| class FlowExecutorView(APIView): | ||||
|     """Flow executor, passing requests to Stage Views""" | ||||
| @ -105,8 +189,9 @@ class FlowExecutorView(APIView): | ||||
|     permission_classes = [AllowAny] | ||||
|  | ||||
|     flow: Flow = None | ||||
|  | ||||
|     plan: FlowPlan | None = None | ||||
|     container: FlowContainer | ||||
|  | ||||
|     current_binding: FlowStageBinding | None = None | ||||
|     current_stage: Stage | ||||
|     current_stage_view: View | ||||
| @ -161,10 +246,12 @@ class FlowExecutorView(APIView): | ||||
|             if QS_KEY_TOKEN in get_params: | ||||
|                 plan = self._check_flow_token(get_params[QS_KEY_TOKEN]) | ||||
|                 if plan: | ||||
|                     self.request.session[SESSION_KEY_PLAN] = plan | ||||
|                     container = FlowContainer.new(request) | ||||
|                     container.plan = plan | ||||
|             # Early check if there's an active Plan for the current session | ||||
|             if SESSION_KEY_PLAN in self.request.session: | ||||
|                 self.plan: FlowPlan = self.request.session[SESSION_KEY_PLAN] | ||||
|             self.container = FlowContainer(request, request.GET.get(QS_EXEC_ID)) | ||||
|             if self.container.exists(): | ||||
|                 self.plan: FlowPlan = self.container.plan | ||||
|                 if self.plan.flow_pk != self.flow.pk.hex: | ||||
|                     self._logger.warning( | ||||
|                         "f(exec): Found existing plan for other flow, deleting plan", | ||||
| @ -177,13 +264,14 @@ class FlowExecutorView(APIView): | ||||
|                     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 | ||||
|             self.container.get = get_params | ||||
|             # Don't check session again as we've either already loaded the plan or we need to plan | ||||
|             if not self.plan: | ||||
|                 request.session[SESSION_KEY_HISTORY] = [] | ||||
|                 self.container.history = [] | ||||
|                 self._logger.debug("f(exec): No active Plan found, initiating planner") | ||||
|                 try: | ||||
|                     self.plan = self._initiate_plan() | ||||
|                     self.container.plan = self.plan | ||||
|                 except FlowNonApplicableException as exc: | ||||
|                     self._logger.warning("f(exec): Flow not applicable to current user", exc=exc) | ||||
|                     return self.handle_invalid_flow(exc) | ||||
| @ -255,12 +343,19 @@ class FlowExecutorView(APIView): | ||||
|         request=OpenApiTypes.NONE, | ||||
|         parameters=[ | ||||
|             OpenApiParameter( | ||||
|                 name="query", | ||||
|                 name=QS_QUERY, | ||||
|                 location=OpenApiParameter.QUERY, | ||||
|                 required=True, | ||||
|                 description="Querystring as received", | ||||
|                 type=OpenApiTypes.STR, | ||||
|             ) | ||||
|             ), | ||||
|             OpenApiParameter( | ||||
|                 name=QS_EXEC_ID, | ||||
|                 location=OpenApiParameter.QUERY, | ||||
|                 required=False, | ||||
|                 description="Flow execution ID", | ||||
|                 type=OpenApiTypes.STR, | ||||
|             ), | ||||
|         ], | ||||
|         operation_id="flows_executor_get", | ||||
|     ) | ||||
| @ -287,7 +382,7 @@ class FlowExecutorView(APIView): | ||||
|                 span.set_data("authentik Stage", self.current_stage_view) | ||||
|                 span.set_data("authentik Flow", self.flow.slug) | ||||
|                 stage_response = self.current_stage_view.dispatch(request) | ||||
|                 return to_stage_response(request, stage_response) | ||||
|                 return to_stage_response(request, stage_response, self.container.exec_id) | ||||
|         except Exception as exc: | ||||
|             return self.handle_exception(exc) | ||||
|  | ||||
| @ -306,12 +401,19 @@ class FlowExecutorView(APIView): | ||||
|         ), | ||||
|         parameters=[ | ||||
|             OpenApiParameter( | ||||
|                 name="query", | ||||
|                 name=QS_QUERY, | ||||
|                 location=OpenApiParameter.QUERY, | ||||
|                 required=True, | ||||
|                 description="Querystring as received", | ||||
|                 type=OpenApiTypes.STR, | ||||
|             ) | ||||
|             ), | ||||
|             OpenApiParameter( | ||||
|                 name=QS_EXEC_ID, | ||||
|                 location=OpenApiParameter.QUERY, | ||||
|                 required=True, | ||||
|                 description="Flow execution ID", | ||||
|                 type=OpenApiTypes.STR, | ||||
|             ), | ||||
|         ], | ||||
|         operation_id="flows_executor_solve", | ||||
|     ) | ||||
| @ -338,14 +440,15 @@ class FlowExecutorView(APIView): | ||||
|                 span.set_data("authentik Stage", self.current_stage_view) | ||||
|                 span.set_data("authentik Flow", self.flow.slug) | ||||
|                 stage_response = self.current_stage_view.dispatch(request) | ||||
|                 return to_stage_response(request, stage_response) | ||||
|                 return to_stage_response(request, stage_response, self.container.exec_id) | ||||
|         except Exception as exc: | ||||
|             return self.handle_exception(exc) | ||||
|  | ||||
|     def _initiate_plan(self) -> FlowPlan: | ||||
|         planner = FlowPlanner(self.flow) | ||||
|         plan = planner.plan(self.request) | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         container = FlowContainer.new(self.request) | ||||
|         container.plan = plan | ||||
|         try: | ||||
|             # Call the has_stages getter to check that | ||||
|             # there are no issues with the class we might've gotten | ||||
| @ -369,7 +472,7 @@ class FlowExecutorView(APIView): | ||||
|         except FlowNonApplicableException as exc: | ||||
|             self._logger.warning("f(exec): Flow restart not applicable to current user", exc=exc) | ||||
|             return self.handle_invalid_flow(exc) | ||||
|         self.request.session[SESSION_KEY_PLAN] = plan | ||||
|         self.container.plan = plan | ||||
|         kwargs = self.kwargs | ||||
|         kwargs.update({"flow_slug": self.flow.slug}) | ||||
|         return redirect_with_qs("authentik_api:flow-executor", self.request.GET, **kwargs) | ||||
| @ -391,9 +494,13 @@ class FlowExecutorView(APIView): | ||||
|         ) | ||||
|         self.cancel() | ||||
|         if next_param and not is_url_absolute(next_param): | ||||
|             return to_stage_response(self.request, redirect_with_qs(next_param)) | ||||
|             return to_stage_response( | ||||
|                 self.request, redirect_with_qs(next_param), self.container.exec_id | ||||
|             ) | ||||
|         return to_stage_response( | ||||
|             self.request, self.stage_invalid(error_message=_("Invalid next URL")) | ||||
|             self.request, | ||||
|             self.stage_invalid(error_message=_("Invalid next URL")), | ||||
|             self.container.exec_id, | ||||
|         ) | ||||
|  | ||||
|     def stage_ok(self) -> HttpResponse: | ||||
| @ -407,7 +514,7 @@ class FlowExecutorView(APIView): | ||||
|             self.current_stage_view.cleanup() | ||||
|         self.request.session.get(SESSION_KEY_HISTORY, []).append(deepcopy(self.plan)) | ||||
|         self.plan.pop() | ||||
|         self.request.session[SESSION_KEY_PLAN] = self.plan | ||||
|         self.container.plan = self.plan | ||||
|         if self.plan.bindings: | ||||
|             self._logger.debug( | ||||
|                 "f(exec): Continuing with next stage", | ||||
| @ -450,11 +557,11 @@ class FlowExecutorView(APIView): | ||||
|  | ||||
|     def cancel(self): | ||||
|         """Cancel current flow execution""" | ||||
|         # TODO: Clean up container | ||||
|         keys_to_delete = [ | ||||
|             SESSION_KEY_APPLICATION_PRE, | ||||
|             SESSION_KEY_PLAN, | ||||
|             SESSION_KEY_GET, | ||||
|             SESSION_KEY_AUTH_STARTED, | ||||
|             # We might need the initial POST payloads for later requests | ||||
|             # SESSION_KEY_POST, | ||||
|             # We don't delete the history on purpose, as a user might | ||||
| @ -473,8 +580,8 @@ class CancelView(View): | ||||
|  | ||||
|     def get(self, request: HttpRequest) -> HttpResponse: | ||||
|         """View which canels the currently active plan""" | ||||
|         if SESSION_KEY_PLAN in request.session: | ||||
|             del request.session[SESSION_KEY_PLAN] | ||||
|         if FlowContainer(request, request.GET.get(QS_EXEC_ID)).exists(): | ||||
|             del request.session[SESSION_KEY_PLAN_CONTAINER % request.GET.get(QS_EXEC_ID)] | ||||
|             LOGGER.debug("Canceled current plan") | ||||
|         return redirect("authentik_flows:default-invalidation") | ||||
|  | ||||
| @ -522,19 +629,12 @@ class ToDefaultFlow(View): | ||||
|  | ||||
|     def dispatch(self, request: HttpRequest) -> HttpResponse: | ||||
|         flow = self.get_flow() | ||||
|         # If user already has a pending plan, clear it so we don't have to later. | ||||
|         if SESSION_KEY_PLAN in self.request.session: | ||||
|             plan: FlowPlan = self.request.session[SESSION_KEY_PLAN] | ||||
|             if plan.flow_pk != flow.pk.hex: | ||||
|                 LOGGER.warning( | ||||
|                     "f(def): Found existing plan for other flow, deleting plan", | ||||
|                     flow_slug=flow.slug, | ||||
|                 ) | ||||
|                 del self.request.session[SESSION_KEY_PLAN] | ||||
|         return redirect_with_qs("authentik_core:if-flow", request.GET, flow_slug=flow.slug) | ||||
|         get_qs = request.GET.copy() | ||||
|         get_qs[QS_EXEC_ID] = str(uuid4()) | ||||
|         return redirect_with_qs("authentik_core:if-flow", get_qs, flow_slug=flow.slug) | ||||
|  | ||||
|  | ||||
| def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpResponse: | ||||
| def to_stage_response(request: HttpRequest, source: HttpResponse, xid: str) -> HttpResponse: | ||||
|     """Convert normal HttpResponse into JSON Response""" | ||||
|     if ( | ||||
|         isinstance(source, HttpResponseRedirect) | ||||
| @ -553,6 +653,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons | ||||
|             RedirectChallenge( | ||||
|                 { | ||||
|                     "to": str(redirect_url), | ||||
|                     "xid": xid, | ||||
|                 } | ||||
|             ) | ||||
|         ) | ||||
| @ -561,6 +662,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons | ||||
|             ShellChallenge( | ||||
|                 { | ||||
|                     "body": source.render().content.decode("utf-8"), | ||||
|                     "xid": xid, | ||||
|                 } | ||||
|             ) | ||||
|         ) | ||||
| @ -570,6 +672,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons | ||||
|             ShellChallenge( | ||||
|                 { | ||||
|                     "body": source.content.decode("utf-8"), | ||||
|                     "xid": xid, | ||||
|                 } | ||||
|             ) | ||||
|         ) | ||||
| @ -601,4 +704,6 @@ class ConfigureFlowInitView(LoginRequiredMixin, View): | ||||
|         except FlowNonApplicableException: | ||||
|             LOGGER.warning("Flow not applicable to user") | ||||
|             raise Http404 from None | ||||
|         return plan.to_redirect(request, stage.configure_flow) | ||||
|         container = FlowContainer.new(request) | ||||
|         container.plan = plan | ||||
|         return container.to_redirect(request, stage.configure_flow) | ||||
|  | ||||
| @ -6,23 +6,17 @@ from django.shortcuts import get_object_or_404 | ||||
| from ua_parser.user_agent_parser import Parse | ||||
|  | ||||
| from authentik.core.views.interface import InterfaceView | ||||
| from authentik.flows.models import Flow, FlowDesignation | ||||
| from authentik.flows.views.executor import SESSION_KEY_AUTH_STARTED | ||||
| from authentik.flows.models import Flow | ||||
| from authentik.flows.views.executor import QS_EXEC_ID | ||||
|  | ||||
|  | ||||
| class FlowInterfaceView(InterfaceView): | ||||
|     """Flow interface""" | ||||
|  | ||||
|     def get_context_data(self, **kwargs: Any) -> dict[str, Any]: | ||||
|         flow = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) | ||||
|         kwargs["flow"] = flow | ||||
|         if ( | ||||
|             not self.request.user.is_authenticated | ||||
|             and flow.designation == FlowDesignation.AUTHENTICATION | ||||
|         ): | ||||
|             self.request.session[SESSION_KEY_AUTH_STARTED] = True | ||||
|             self.request.session.save() | ||||
|         kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug")) | ||||
|         kwargs["inspector"] = "inspector" in self.request.GET | ||||
|         kwargs["xid"] = self.request.GET.get(QS_EXEC_ID) | ||||
|         return super().get_context_data(**kwargs) | ||||
|  | ||||
|     def compat_needs_sfe(self) -> bool: | ||||
|  | ||||
| @ -1,20 +1,5 @@ | ||||
| # authentik configuration | ||||
| # | ||||
| # https://docs.goauthentik.io/docs/install-config/configuration/ | ||||
| # | ||||
| # To override the settings in this file, run the following command from the repository root: | ||||
| # | ||||
| # ```shell | ||||
| # make gen-dev-config | ||||
| # ``` | ||||
| # | ||||
| # You may edit the generated file to override the configuration below.   | ||||
| # | ||||
| # When making modifying the default configuration file,  | ||||
| # ensure that the corresponding documentation is updated to match. | ||||
| # | ||||
| # @see {@link ../../website/docs/install-config/configuration/configuration.mdx Configuration documentation} for more information. | ||||
|  | ||||
| # update website/docs/install-config/configuration/configuration.mdx | ||||
| # This is the default configuration file | ||||
| postgresql: | ||||
|   host: localhost | ||||
|   name: authentik | ||||
| @ -60,8 +45,6 @@ redis: | ||||
| #   url: "" | ||||
| #   transport_options: "" | ||||
|  | ||||
| http_timeout: 30 | ||||
|  | ||||
| cache: | ||||
|   # url: "" | ||||
|   timeout: 300 | ||||
|  | ||||
| @ -18,15 +18,6 @@ class SerializerModel(models.Model): | ||||
|     @property | ||||
|     def serializer(self) -> type[BaseSerializer]: | ||||
|         """Get serializer for this model""" | ||||
|         # Special handling for built-in source | ||||
|         if ( | ||||
|             hasattr(self, "managed") | ||||
|             and hasattr(self, "MANAGED_INBUILT") | ||||
|             and self.managed == self.MANAGED_INBUILT | ||||
|         ): | ||||
|             from authentik.core.api.sources import SourceSerializer | ||||
|  | ||||
|             return SourceSerializer | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -16,40 +16,7 @@ def authentik_user_agent() -> str: | ||||
|     return f"authentik@{get_full_version()}" | ||||
|  | ||||
|  | ||||
| class TimeoutSession(Session): | ||||
|     """Always set a default HTTP request timeout""" | ||||
|  | ||||
|     def __init__(self, default_timeout=None): | ||||
|         super().__init__() | ||||
|         self.timeout = default_timeout | ||||
|  | ||||
|     def send( | ||||
|         self, | ||||
|         request, | ||||
|         *, | ||||
|         stream=..., | ||||
|         verify=..., | ||||
|         proxies=..., | ||||
|         cert=..., | ||||
|         timeout=..., | ||||
|         allow_redirects=..., | ||||
|         **kwargs, | ||||
|     ): | ||||
|         if not timeout and self.timeout: | ||||
|             timeout = self.timeout | ||||
|         return super().send( | ||||
|             request, | ||||
|             stream=stream, | ||||
|             verify=verify, | ||||
|             proxies=proxies, | ||||
|             cert=cert, | ||||
|             timeout=timeout, | ||||
|             allow_redirects=allow_redirects, | ||||
|             **kwargs, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class DebugSession(TimeoutSession): | ||||
| class DebugSession(Session): | ||||
|     """requests session which logs http requests and responses""" | ||||
|  | ||||
|     def send(self, req: PreparedRequest, *args, **kwargs): | ||||
| @ -75,9 +42,8 @@ class DebugSession(TimeoutSession): | ||||
|  | ||||
| def get_http_session() -> Session: | ||||
|     """Get a requests session with common headers""" | ||||
|     session = TimeoutSession() | ||||
|     session = Session() | ||||
|     if CONFIG.get_bool("debug") or CONFIG.get("log_level") == "trace": | ||||
|         session = DebugSession() | ||||
|     session.headers["User-Agent"] = authentik_user_agent() | ||||
|     session.timeout = CONFIG.get_optional_int("http_timeout") | ||||
|     return session | ||||
|  | ||||
| @ -13,7 +13,6 @@ from paramiko.ssh_exception import SSHException | ||||
| from structlog.stdlib import get_logger | ||||
| from yaml import safe_dump | ||||
|  | ||||
| from authentik import __version__ | ||||
| from authentik.outposts.apps import MANAGED_OUTPOST | ||||
| from authentik.outposts.controllers.base import BaseClient, BaseController, ControllerException | ||||
| from authentik.outposts.docker_ssh import DockerInlineSSH, SSHManagedExternallyException | ||||
| @ -185,7 +184,7 @@ class DockerController(BaseController): | ||||
|         try: | ||||
|             self.client.images.pull(image) | ||||
|         except DockerException:  # pragma: no cover | ||||
|             image = f"ghcr.io/goauthentik/{self.outpost.type}:{__version__}" | ||||
|             image = f"ghcr.io/goauthentik/{self.outpost.type}:latest" | ||||
|             self.client.images.pull(image) | ||||
|         return image | ||||
|  | ||||
|  | ||||
| @ -35,4 +35,3 @@ class AuthentikPoliciesConfig(ManagedAppConfig): | ||||
|     label = "authentik_policies" | ||||
|     verbose_name = "authentik Policies" | ||||
|     default = True | ||||
|     mountpoint = "policy/" | ||||
|  | ||||
| @ -1,89 +0,0 @@ | ||||
| {% extends 'login/base_full.html' %} | ||||
|  | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block head %} | ||||
| {{ block.super }} | ||||
| <script> | ||||
|   let redirecting = false; | ||||
|   const checkAuth = async () => { | ||||
|     if (redirecting) return true; | ||||
|     const url = "{{ check_auth_url }}"; | ||||
|     console.debug("authentik/policies/buffer: Checking authentication..."); | ||||
|     try { | ||||
|       const result = await fetch(url, { | ||||
|         method: "HEAD", | ||||
|       }); | ||||
|       if (result.status >= 400) { | ||||
|         return false | ||||
|       } | ||||
|       console.debug("authentik/policies/buffer: Continuing"); | ||||
|       redirecting = true; | ||||
|       if ("{{ auth_req_method }}" === "post") { | ||||
|         document.querySelector("form").submit(); | ||||
|       } else { | ||||
|         window.location.assign("{{ continue_url|escapejs }}"); | ||||
|       } | ||||
|     } catch { | ||||
|       return false; | ||||
|     } | ||||
|   }; | ||||
|   let timeout = 100; | ||||
|   let offset = 20; | ||||
|   let attempt = 0; | ||||
|   const main = async () => { | ||||
|     attempt += 1; | ||||
|     await checkAuth(); | ||||
|     console.debug(`authentik/policies/buffer: Waiting ${timeout}ms...`); | ||||
|     setTimeout(main, timeout); | ||||
|     timeout += (offset * attempt); | ||||
|     if (timeout >= 2000) { | ||||
|       timeout = 2000; | ||||
|     } | ||||
|   } | ||||
|   document.addEventListener("visibilitychange", async () => { | ||||
|     if (document.hidden) return; | ||||
|     console.debug("authentik/policies/buffer: Checking authentication on tab activate..."); | ||||
|     await checkAuth(); | ||||
|   }); | ||||
|   main(); | ||||
| </script> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block title %} | ||||
| {% trans 'Waiting for authentication...' %} - {{ brand.branding_title }} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block card_title %} | ||||
| {% trans 'Waiting for authentication...' %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block card %} | ||||
| <form class="pf-c-form" method="{{ auth_req_method }}" action="{{ continue_url }}"> | ||||
|   {% if auth_req_method == "post" %} | ||||
|     {% for key, value in auth_req_body.items %} | ||||
|       <input type="hidden" name="{{ key }}" value="{{ value }}" /> | ||||
|     {% endfor %} | ||||
|   {% endif %} | ||||
|   <div class="pf-c-empty-state"> | ||||
|     <div class="pf-c-empty-state__content"> | ||||
|       <div class="pf-c-empty-state__icon"> | ||||
|         <span class="pf-c-spinner pf-m-xl" role="progressbar"> | ||||
|           <span class="pf-c-spinner__clipper"></span> | ||||
|           <span class="pf-c-spinner__lead-ball"></span> | ||||
|           <span class="pf-c-spinner__tail-ball"></span> | ||||
|         </span> | ||||
|       </div> | ||||
|       <h1 class="pf-c-title pf-m-lg"> | ||||
|         {% trans "You're already authenticating in another tab. This page will refresh once authentication is completed." %} | ||||
|       </h1> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="pf-c-form__group pf-m-action"> | ||||
|     <a href="{{ auth_req_url }}" class="pf-c-button pf-m-primary pf-m-block"> | ||||
|       {% trans "Authenticate in this tab" %} | ||||
|     </a> | ||||
|   </div> | ||||
| </form> | ||||
| {% endblock %} | ||||
| @ -1,121 +0,0 @@ | ||||
| from django.contrib.auth.models import AnonymousUser | ||||
| from django.contrib.sessions.middleware import SessionMiddleware | ||||
| from django.http import HttpResponse | ||||
| from django.test import RequestFactory, TestCase | ||||
| from django.urls import reverse | ||||
|  | ||||
| from authentik.core.models import Application, Provider | ||||
| from authentik.core.tests.utils import create_test_flow, create_test_user | ||||
| from authentik.flows.models import FlowDesignation | ||||
| from authentik.flows.planner import FlowPlan | ||||
| from authentik.flows.views.executor import SESSION_KEY_PLAN | ||||
| from authentik.lib.generators import generate_id | ||||
| from authentik.lib.tests.utils import dummy_get_response | ||||
| from authentik.policies.views import ( | ||||
|     QS_BUFFER_ID, | ||||
|     SESSION_KEY_BUFFER, | ||||
|     BufferedPolicyAccessView, | ||||
|     BufferView, | ||||
|     PolicyAccessView, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class TestPolicyViews(TestCase): | ||||
|     """Test PolicyAccessView""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         self.factory = RequestFactory() | ||||
|         self.user = create_test_user() | ||||
|  | ||||
|     def test_pav(self): | ||||
|         """Test simple policy access view""" | ||||
|         provider = Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|         ) | ||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) | ||||
|  | ||||
|         class TestView(PolicyAccessView): | ||||
|             def resolve_provider_application(self): | ||||
|                 self.provider = provider | ||||
|                 self.application = app | ||||
|  | ||||
|             def get(self, *args, **kwargs): | ||||
|                 return HttpResponse("foo") | ||||
|  | ||||
|         req = self.factory.get("/") | ||||
|         req.user = self.user | ||||
|         res = TestView.as_view()(req) | ||||
|         self.assertEqual(res.status_code, 200) | ||||
|         self.assertEqual(res.content, b"foo") | ||||
|  | ||||
|     def test_pav_buffer(self): | ||||
|         """Test simple policy access view""" | ||||
|         provider = Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|         ) | ||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) | ||||
|         flow = create_test_flow(FlowDesignation.AUTHENTICATION) | ||||
|  | ||||
|         class TestView(BufferedPolicyAccessView): | ||||
|             def resolve_provider_application(self): | ||||
|                 self.provider = provider | ||||
|                 self.application = app | ||||
|  | ||||
|             def get(self, *args, **kwargs): | ||||
|                 return HttpResponse("foo") | ||||
|  | ||||
|         req = self.factory.get("/") | ||||
|         req.user = AnonymousUser() | ||||
|         middleware = SessionMiddleware(dummy_get_response) | ||||
|         middleware.process_request(req) | ||||
|         req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk) | ||||
|         req.session.save() | ||||
|         res = TestView.as_view()(req) | ||||
|         self.assertEqual(res.status_code, 302) | ||||
|         self.assertTrue(res.url.startswith(reverse("authentik_policies:buffer"))) | ||||
|  | ||||
|     def test_pav_buffer_skip(self): | ||||
|         """Test simple policy access view (skip buffer)""" | ||||
|         provider = Provider.objects.create( | ||||
|             name=generate_id(), | ||||
|         ) | ||||
|         app = Application.objects.create(name=generate_id(), slug=generate_id(), provider=provider) | ||||
|         flow = create_test_flow(FlowDesignation.AUTHENTICATION) | ||||
|  | ||||
|         class TestView(BufferedPolicyAccessView): | ||||
|             def resolve_provider_application(self): | ||||
|                 self.provider = provider | ||||
|                 self.application = app | ||||
|  | ||||
|             def get(self, *args, **kwargs): | ||||
|                 return HttpResponse("foo") | ||||
|  | ||||
|         req = self.factory.get("/?skip_buffer=true") | ||||
|         req.user = AnonymousUser() | ||||
|         middleware = SessionMiddleware(dummy_get_response) | ||||
|         middleware.process_request(req) | ||||
|         req.session[SESSION_KEY_PLAN] = FlowPlan(flow.pk) | ||||
|         req.session.save() | ||||
|         res = TestView.as_view()(req) | ||||
|         self.assertEqual(res.status_code, 302) | ||||
|         self.assertTrue(res.url.startswith(reverse("authentik_flows:default-authentication"))) | ||||
|  | ||||
|     def test_buffer(self): | ||||
|         """Test buffer view""" | ||||
|         uid = generate_id() | ||||
|         req = self.factory.get(f"/?{QS_BUFFER_ID}={uid}") | ||||
|         req.user = AnonymousUser() | ||||
|         middleware = SessionMiddleware(dummy_get_response) | ||||
|         middleware.process_request(req) | ||||
|         ts = generate_id() | ||||
|         req.session[SESSION_KEY_BUFFER % uid] = { | ||||
|             "method": "get", | ||||
|             "body": {}, | ||||
|             "url": f"/{ts}", | ||||
|         } | ||||
|         req.session.save() | ||||
|  | ||||
|         res = BufferView.as_view()(req) | ||||
|         self.assertEqual(res.status_code, 200) | ||||
|         self.assertIn(ts, res.render().content.decode()) | ||||
| @ -1,14 +1,7 @@ | ||||
| """API URLs""" | ||||
|  | ||||
| from django.urls import path | ||||
|  | ||||
| from authentik.policies.api.bindings import PolicyBindingViewSet | ||||
| from authentik.policies.api.policies import PolicyViewSet | ||||
| from authentik.policies.views import BufferView | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path("buffer", BufferView.as_view(), name="buffer"), | ||||
| ] | ||||
|  | ||||
| api_urlpatterns = [ | ||||
|     ("policies/all", PolicyViewSet), | ||||
|  | ||||
| @ -1,37 +1,23 @@ | ||||
| """authentik access helper classes""" | ||||
|  | ||||
| from typing import Any | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from django.contrib import messages | ||||
| from django.contrib.auth.mixins import AccessMixin | ||||
| from django.contrib.auth.views import redirect_to_login | ||||
| from django.http import HttpRequest, HttpResponse, QueryDict | ||||
| from django.shortcuts import redirect | ||||
| from django.urls import reverse | ||||
| from django.utils.http import urlencode | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic.base import TemplateView, View | ||||
| from django.views.generic.base import View | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import Application, Provider, User | ||||
| from authentik.flows.models import Flow, FlowDesignation | ||||
| from authentik.flows.planner import FlowPlan | ||||
| from authentik.flows.views.executor import ( | ||||
|     SESSION_KEY_APPLICATION_PRE, | ||||
|     SESSION_KEY_AUTH_STARTED, | ||||
|     SESSION_KEY_PLAN, | ||||
|     SESSION_KEY_POST, | ||||
| ) | ||||
| from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, SESSION_KEY_POST | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
| from authentik.policies.denied import AccessDeniedResponse | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.policies.types import PolicyRequest, PolicyResult | ||||
|  | ||||
| LOGGER = get_logger() | ||||
| QS_BUFFER_ID = "af_bf_id" | ||||
| QS_SKIP_BUFFER = "skip_buffer" | ||||
| SESSION_KEY_BUFFER = "authentik/policies/pav_buffer/%s" | ||||
|  | ||||
|  | ||||
| class RequestValidationError(SentryIgnoredException): | ||||
| @ -139,65 +125,3 @@ class PolicyAccessView(AccessMixin, View): | ||||
|             for message in result.messages: | ||||
|                 messages.error(self.request, _(message)) | ||||
|         return result | ||||
|  | ||||
|  | ||||
| def url_with_qs(url: str, **kwargs): | ||||
|     """Update/set querystring of `url` with the parameters in `kwargs`. Original query string | ||||
|     parameters are retained""" | ||||
|     if "?" not in url: | ||||
|         return url + f"?{urlencode(kwargs)}" | ||||
|     url, _, qs = url.partition("?") | ||||
|     qs = QueryDict(qs, mutable=True) | ||||
|     qs.update(kwargs) | ||||
|     return url + f"?{urlencode(qs.items())}" | ||||
|  | ||||
|  | ||||
| class BufferView(TemplateView): | ||||
|     """Buffer view""" | ||||
|  | ||||
|     template_name = "policies/buffer.html" | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         buf_id = self.request.GET.get(QS_BUFFER_ID) | ||||
|         buffer: dict = self.request.session.get(SESSION_KEY_BUFFER % buf_id) | ||||
|         kwargs["auth_req_method"] = buffer["method"] | ||||
|         kwargs["auth_req_body"] = buffer["body"] | ||||
|         kwargs["auth_req_url"] = url_with_qs(buffer["url"], **{QS_SKIP_BUFFER: True}) | ||||
|         kwargs["check_auth_url"] = reverse("authentik_api:user-me") | ||||
|         kwargs["continue_url"] = url_with_qs(buffer["url"], **{QS_BUFFER_ID: buf_id}) | ||||
|         return super().get_context_data(**kwargs) | ||||
|  | ||||
|  | ||||
| class BufferedPolicyAccessView(PolicyAccessView): | ||||
|     """PolicyAccessView which buffers access requests in case the user is not logged in""" | ||||
|  | ||||
|     def handle_no_permission(self): | ||||
|         plan: FlowPlan | None = self.request.session.get(SESSION_KEY_PLAN) | ||||
|         authenticating = self.request.session.get(SESSION_KEY_AUTH_STARTED) | ||||
|         if plan: | ||||
|             flow = Flow.objects.filter(pk=plan.flow_pk).first() | ||||
|             if not flow or flow.designation != FlowDesignation.AUTHENTICATION: | ||||
|                 LOGGER.debug("Not buffering request, no flow or flow not for authentication") | ||||
|                 return super().handle_no_permission() | ||||
|         if not plan and authenticating is None: | ||||
|             LOGGER.debug("Not buffering request, no flow plan active") | ||||
|             return super().handle_no_permission() | ||||
|         if self.request.GET.get(QS_SKIP_BUFFER): | ||||
|             LOGGER.debug("Not buffering request, explicit skip") | ||||
|             return super().handle_no_permission() | ||||
|         buffer_id = str(uuid4()) | ||||
|         LOGGER.debug("Buffering access request", bf_id=buffer_id) | ||||
|         self.request.session[SESSION_KEY_BUFFER % buffer_id] = { | ||||
|             "body": self.request.POST, | ||||
|             "url": self.request.build_absolute_uri(self.request.get_full_path()), | ||||
|             "method": self.request.method.lower(), | ||||
|         } | ||||
|         return redirect( | ||||
|             url_with_qs(reverse("authentik_policies:buffer"), **{QS_BUFFER_ID: buffer_id}) | ||||
|         ) | ||||
|  | ||||
|     def dispatch(self, request, *args, **kwargs): | ||||
|         response = super().dispatch(request, *args, **kwargs) | ||||
|         if QS_BUFFER_ID in self.request.GET: | ||||
|             self.request.session.pop(SESSION_KEY_BUFFER % self.request.GET[QS_BUFFER_ID], None) | ||||
|         return response | ||||
|  | ||||
| @ -30,7 +30,7 @@ from authentik.flows.stage import StageView | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.policies.types import PolicyRequest | ||||
| from authentik.policies.views import BufferedPolicyAccessView, RequestValidationError | ||||
| from authentik.policies.views import PolicyAccessView, RequestValidationError | ||||
| from authentik.providers.oauth2.constants import ( | ||||
|     PKCE_METHOD_PLAIN, | ||||
|     PKCE_METHOD_S256, | ||||
| @ -328,7 +328,7 @@ class OAuthAuthorizationParams: | ||||
|         return code | ||||
|  | ||||
|  | ||||
| class AuthorizationFlowInitView(BufferedPolicyAccessView): | ||||
| class AuthorizationFlowInitView(PolicyAccessView): | ||||
|     """OAuth2 Flow initializer, checks access to application and starts flow""" | ||||
|  | ||||
|     params: OAuthAuthorizationParams | ||||
|  | ||||
| @ -18,11 +18,11 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, FlowPlanner | ||||
| from authentik.flows.stage import RedirectStage | ||||
| from authentik.lib.utils.time import timedelta_from_string | ||||
| from authentik.policies.engine import PolicyEngine | ||||
| from authentik.policies.views import BufferedPolicyAccessView | ||||
| from authentik.policies.views import PolicyAccessView | ||||
| from authentik.providers.rac.models import ConnectionToken, Endpoint, RACProvider | ||||
|  | ||||
|  | ||||
| class RACStartView(BufferedPolicyAccessView): | ||||
| class RACStartView(PolicyAccessView): | ||||
|     """Start a RAC connection by checking access and creating a connection token""" | ||||
|  | ||||
|     endpoint: Endpoint | ||||
|  | ||||
| @ -1,22 +0,0 @@ | ||||
| # Generated by Django 5.0.13 on 2025-03-31 13:50 | ||||
|  | ||||
| import authentik.lib.models | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_providers_saml", "0017_samlprovider_authn_context_class_ref_mapping"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="samlprovider", | ||||
|             name="acs_url", | ||||
|             field=models.TextField( | ||||
|                 validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))], | ||||
|                 verbose_name="ACS URL", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -10,7 +10,6 @@ from structlog.stdlib import get_logger | ||||
| from authentik.core.api.object_types import CreatableType | ||||
| from authentik.core.models import PropertyMapping, Provider | ||||
| from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.lib.models import DomainlessURLValidator | ||||
| from authentik.lib.utils.time import timedelta_string_validator | ||||
| from authentik.sources.saml.processors.constants import ( | ||||
|     DSA_SHA1, | ||||
| @ -41,9 +40,7 @@ class SAMLBindings(models.TextChoices): | ||||
| class SAMLProvider(Provider): | ||||
|     """SAML 2.0 Endpoint for applications which support SAML.""" | ||||
|  | ||||
|     acs_url = models.TextField( | ||||
|         validators=[DomainlessURLValidator(schemes=("http", "https"))], verbose_name=_("ACS URL") | ||||
|     ) | ||||
|     acs_url = models.URLField(verbose_name=_("ACS URL")) | ||||
|     audience = models.TextField( | ||||
|         default="", | ||||
|         blank=True, | ||||
|  | ||||
| @ -15,7 +15,7 @@ from authentik.flows.models import in_memory_stage | ||||
| from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO, FlowPlanner | ||||
| from authentik.flows.views.executor import SESSION_KEY_POST | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.policies.views import BufferedPolicyAccessView | ||||
| from authentik.policies.views import PolicyAccessView | ||||
| from authentik.providers.saml.exceptions import CannotHandleAssertion | ||||
| from authentik.providers.saml.models import SAMLBindings, SAMLProvider | ||||
| from authentik.providers.saml.processors.authn_request_parser import AuthNRequestParser | ||||
| @ -35,7 +35,7 @@ from authentik.stages.consent.stage import ( | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| class SAMLSSOView(BufferedPolicyAccessView): | ||||
| class SAMLSSOView(PolicyAccessView): | ||||
|     """SAML SSO Base View, which plans a flow and injects our final stage. | ||||
|     Calls get/post handler.""" | ||||
|  | ||||
| @ -83,7 +83,7 @@ class SAMLSSOView(BufferedPolicyAccessView): | ||||
|  | ||||
|     def post(self, request: HttpRequest, application_slug: str) -> HttpResponse: | ||||
|         """GET and POST use the same handler, but we can't | ||||
|         override .dispatch easily because BufferedPolicyAccessView's dispatch""" | ||||
|         override .dispatch easily because PolicyAccessView's dispatch""" | ||||
|         return self.get(request, application_slug) | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -243,10 +243,9 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]): | ||||
|             if user.value not in users_should: | ||||
|                 users_to_remove.append(user.value) | ||||
|         # Check users that should be in the group and add them | ||||
|         if current_group.members is not None: | ||||
|             for user in users_should: | ||||
|                 if len([x for x in current_group.members if x.value == user]) < 1: | ||||
|                     users_to_add.append(user) | ||||
|         for user in users_should: | ||||
|             if len([x for x in current_group.members if x.value == user]) < 1: | ||||
|                 users_to_add.append(user) | ||||
|         # Only send request if we need to make changes | ||||
|         if len(users_to_add) < 1 and len(users_to_remove) < 1: | ||||
|             return | ||||
|  | ||||
| @ -1,35 +0,0 @@ | ||||
| # Generated by Django 5.0.13 on 2025-03-31 13:53 | ||||
|  | ||||
| import authentik.lib.models | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_sources_saml", "0017_fix_x509subjectname"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="samlsource", | ||||
|             name="slo_url", | ||||
|             field=models.TextField( | ||||
|                 blank=True, | ||||
|                 default=None, | ||||
|                 help_text="Optional URL if your IDP supports Single-Logout.", | ||||
|                 null=True, | ||||
|                 validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))], | ||||
|                 verbose_name="SLO URL", | ||||
|             ), | ||||
|         ), | ||||
|         migrations.AlterField( | ||||
|             model_name="samlsource", | ||||
|             name="sso_url", | ||||
|             field=models.TextField( | ||||
|                 help_text="URL that the initial Login request is sent to.", | ||||
|                 validators=[authentik.lib.models.DomainlessURLValidator(schemes=("http", "https"))], | ||||
|                 verbose_name="SSO URL", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -20,7 +20,6 @@ from authentik.crypto.models import CertificateKeyPair | ||||
| from authentik.flows.challenge import RedirectChallenge | ||||
| from authentik.flows.models import Flow | ||||
| from authentik.lib.expression.evaluator import BaseEvaluator | ||||
| from authentik.lib.models import DomainlessURLValidator | ||||
| from authentik.lib.utils.time import timedelta_string_validator | ||||
| from authentik.sources.saml.processors.constants import ( | ||||
|     DSA_SHA1, | ||||
| @ -92,13 +91,11 @@ class SAMLSource(Source): | ||||
|         help_text=_("Also known as Entity ID. Defaults the Metadata URL."), | ||||
|     ) | ||||
|  | ||||
|     sso_url = models.TextField( | ||||
|         validators=[DomainlessURLValidator(schemes=("http", "https"))], | ||||
|     sso_url = models.URLField( | ||||
|         verbose_name=_("SSO URL"), | ||||
|         help_text=_("URL that the initial Login request is sent to."), | ||||
|     ) | ||||
|     slo_url = models.TextField( | ||||
|         validators=[DomainlessURLValidator(schemes=("http", "https"))], | ||||
|     slo_url = models.URLField( | ||||
|         default=None, | ||||
|         blank=True, | ||||
|         null=True, | ||||
|  | ||||
| @ -33,7 +33,6 @@ from authentik.flows.planner import ( | ||||
| ) | ||||
| from authentik.flows.stage import ChallengeStageView | ||||
| from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN | ||||
| from authentik.lib.utils.urls import is_url_absolute | ||||
| from authentik.lib.views import bad_request_message | ||||
| from authentik.providers.saml.utils.encoding import nice64 | ||||
| from authentik.sources.saml.exceptions import MissingSAMLResponse, UnsupportedNameIDFormat | ||||
| @ -74,8 +73,6 @@ class InitiateView(View): | ||||
|         final_redirect = self.request.session.get(SESSION_KEY_GET, {}).get( | ||||
|             NEXT_ARG_NAME, "authentik_core:if-user" | ||||
|         ) | ||||
|         if not is_url_absolute(final_redirect): | ||||
|             final_redirect = "authentik_core:if-user" | ||||
|         kwargs.update( | ||||
|             { | ||||
|                 PLAN_CONTEXT_SSO: True, | ||||
|  | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -8,7 +8,7 @@ from django.core.mail.backends.locmem import EmailBackend | ||||
| from django.urls import reverse | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow, create_test_user | ||||
| from authentik.core.tests.utils import create_test_admin_user, create_test_flow | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.flows.markers import StageMarker | ||||
| from authentik.flows.models import FlowDesignation, FlowStageBinding | ||||
| @ -67,36 +67,6 @@ class TestEmailStageSending(FlowTestCase): | ||||
|             self.assertEqual(event.context["to_email"], [f"{self.user.name} <{self.user.email}>"]) | ||||
|             self.assertEqual(event.context["from_email"], "system@authentik.local") | ||||
|  | ||||
|     def test_newlines_long_name(self): | ||||
|         """Test with pending user""" | ||||
|         plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()]) | ||||
|         long_user = create_test_user() | ||||
|         long_user.name = "Test User\r\n Many Words\r\n" | ||||
|         long_user.save() | ||||
|         plan.context[PLAN_CONTEXT_PENDING_USER] = long_user | ||||
|         session = self.client.session | ||||
|         session[SESSION_KEY_PLAN] = plan | ||||
|         session.save() | ||||
|         Event.objects.filter(action=EventAction.EMAIL_SENT).delete() | ||||
|  | ||||
|         url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}) | ||||
|         with patch( | ||||
|             "authentik.stages.email.models.EmailStage.backend_class", | ||||
|             PropertyMock(return_value=EmailBackend), | ||||
|         ): | ||||
|             response = self.client.post(url) | ||||
|             self.assertEqual(response.status_code, 200) | ||||
|             self.assertStageResponse( | ||||
|                 response, | ||||
|                 self.flow, | ||||
|                 response_errors={ | ||||
|                     "non_field_errors": [{"string": "email-sent", "code": "email-sent"}] | ||||
|                 }, | ||||
|             ) | ||||
|             self.assertEqual(len(mail.outbox), 1) | ||||
|             self.assertEqual(mail.outbox[0].subject, "authentik") | ||||
|             self.assertEqual(mail.outbox[0].to, [f"Test User   Many Words   <{long_user.email}>"]) | ||||
|  | ||||
|     def test_pending_fake_user(self): | ||||
|         """Test with pending (fake) user""" | ||||
|         self.flow.designation = FlowDesignation.RECOVERY | ||||
|  | ||||
| @ -32,14 +32,7 @@ class TemplateEmailMessage(EmailMultiAlternatives): | ||||
|         sanitized_to = [] | ||||
|         # Ensure that all recipients are valid | ||||
|         for recipient_name, recipient_email in to: | ||||
|             # Remove any newline characters from name and email before sanitizing | ||||
|             clean_name = ( | ||||
|                 recipient_name.replace("\n", " ").replace("\r", " ") if recipient_name else "" | ||||
|             ) | ||||
|             clean_email = ( | ||||
|                 recipient_email.replace("\n", "").replace("\r", "") if recipient_email else "" | ||||
|             ) | ||||
|             sanitized_to.append(sanitize_address((clean_name, clean_email), "utf-8")) | ||||
|             sanitized_to.append(sanitize_address((recipient_name, recipient_email), "utf-8")) | ||||
|         super().__init__(to=sanitized_to, **kwargs) | ||||
|         if not template_name: | ||||
|             return | ||||
|  | ||||
| @ -142,38 +142,35 @@ class IdentificationChallengeResponse(ChallengeResponse): | ||||
|             raise ValidationError("Failed to authenticate.") | ||||
|         self.pre_user = pre_user | ||||
|  | ||||
|         # Password check | ||||
|         if current_stage.password_stage: | ||||
|             password = attrs.get("password", None) | ||||
|             if not password: | ||||
|                 self.stage.logger.warning("Password not set for ident+auth attempt") | ||||
|             try: | ||||
|                 with start_span( | ||||
|                     op="authentik.stages.identification.authenticate", | ||||
|                     name="User authenticate call (combo stage)", | ||||
|                 ): | ||||
|                     user = authenticate( | ||||
|                         self.stage.request, | ||||
|                         current_stage.password_stage.backends, | ||||
|                         current_stage, | ||||
|                         username=self.pre_user.username, | ||||
|                         password=password, | ||||
|                     ) | ||||
|                 if not user: | ||||
|                     raise ValidationError("Failed to authenticate.") | ||||
|                 self.pre_user = user | ||||
|             except PermissionDenied as exc: | ||||
|                 raise ValidationError(str(exc)) from exc | ||||
|  | ||||
|         # Captcha check | ||||
|         if captcha_stage := current_stage.captcha_stage: | ||||
|             captcha_token = attrs.get("captcha_token", None) | ||||
|             if not captcha_token: | ||||
|                 self.stage.logger.warning("Token not set for captcha attempt") | ||||
|             verify_captcha_token(captcha_stage, captcha_token, client_ip) | ||||
|  | ||||
|         # Password check | ||||
|         if not current_stage.password_stage: | ||||
|             # No password stage select, don't validate the password | ||||
|             return attrs | ||||
|  | ||||
|         password = attrs.get("password", None) | ||||
|         if not password: | ||||
|             self.stage.logger.warning("Password not set for ident+auth attempt") | ||||
|         try: | ||||
|             with start_span( | ||||
|                 op="authentik.stages.identification.authenticate", | ||||
|                 name="User authenticate call (combo stage)", | ||||
|             ): | ||||
|                 user = authenticate( | ||||
|                     self.stage.request, | ||||
|                     current_stage.password_stage.backends, | ||||
|                     current_stage, | ||||
|                     username=self.pre_user.username, | ||||
|                     password=password, | ||||
|                 ) | ||||
|             if not user: | ||||
|                 raise ValidationError("Failed to authenticate.") | ||||
|             self.pre_user = user | ||||
|         except PermissionDenied as exc: | ||||
|             raise ValidationError(str(exc)) from exc | ||||
|         return attrs | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|     "$schema": "http://json-schema.org/draft-07/schema", | ||||
|     "$id": "https://goauthentik.io/blueprints/schema.json", | ||||
|     "type": "object", | ||||
|     "title": "authentik 2025.2.3 Blueprint schema", | ||||
|     "title": "authentik 2025.2.2 Blueprint schema", | ||||
|     "required": [ | ||||
|         "version", | ||||
|         "entries" | ||||
| @ -6423,6 +6423,8 @@ | ||||
|                 }, | ||||
|                 "acs_url": { | ||||
|                     "type": "string", | ||||
|                     "format": "uri", | ||||
|                     "maxLength": 200, | ||||
|                     "minLength": 1, | ||||
|                     "title": "ACS URL" | ||||
|                 }, | ||||
| @ -8731,6 +8733,8 @@ | ||||
|                 }, | ||||
|                 "sso_url": { | ||||
|                     "type": "string", | ||||
|                     "format": "uri", | ||||
|                     "maxLength": 200, | ||||
|                     "minLength": 1, | ||||
|                     "title": "SSO URL", | ||||
|                     "description": "URL that the initial Login request is sent to." | ||||
| @ -8740,6 +8744,8 @@ | ||||
|                         "string", | ||||
|                         "null" | ||||
|                     ], | ||||
|                     "format": "uri", | ||||
|                     "maxLength": 200, | ||||
|                     "title": "SLO URL", | ||||
|                     "description": "Optional URL if your IDP supports Single-Logout." | ||||
|                 }, | ||||
| @ -13010,15 +13016,6 @@ | ||||
|                     "minLength": 1, | ||||
|                     "title": "Branding favicon" | ||||
|                 }, | ||||
|                 "branding_custom_css": { | ||||
|                     "type": "string", | ||||
|                     "title": "Branding custom css" | ||||
|                 }, | ||||
|                 "branding_default_flow_background": { | ||||
|                     "type": "string", | ||||
|                     "minLength": 1, | ||||
|                     "title": "Branding default flow background" | ||||
|                 }, | ||||
|                 "flow_authentication": { | ||||
|                     "type": "string", | ||||
|                     "format": "uuid", | ||||
| @ -14900,15 +14897,9 @@ | ||||
|                     "type": "string", | ||||
|                     "title": "Webhook url" | ||||
|                 }, | ||||
|                 "webhook_mapping_body": { | ||||
|                 "webhook_mapping": { | ||||
|                     "type": "integer", | ||||
|                     "title": "Webhook mapping body", | ||||
|                     "description": "Customize the body of the request. Mapping should return data that is JSON-serializable." | ||||
|                 }, | ||||
|                 "webhook_mapping_headers": { | ||||
|                     "type": "integer", | ||||
|                     "title": "Webhook mapping headers", | ||||
|                     "description": "Configure additional headers to be sent. Mapping should return a dictionary of key-value pairs" | ||||
|                     "title": "Webhook mapping" | ||||
|                 }, | ||||
|                 "send_once": { | ||||
|                     "type": "boolean", | ||||
|  | ||||
| @ -31,7 +31,7 @@ services: | ||||
|     volumes: | ||||
|       - redis:/data | ||||
|   server: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2} | ||||
|     restart: unless-stopped | ||||
|     command: server | ||||
|     environment: | ||||
| @ -54,7 +54,7 @@ services: | ||||
|       redis: | ||||
|         condition: service_healthy | ||||
|   worker: | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.3} | ||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.2.2} | ||||
|     restart: unless-stopped | ||||
|     command: worker | ||||
|     environment: | ||||
|  | ||||
							
								
								
									
										13
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								go.mod
									
									
									
									
									
								
							| @ -1,6 +1,9 @@ | ||||
| module goauthentik.io | ||||
|  | ||||
| go 1.24.0 | ||||
| go 1.23.0 | ||||
|  | ||||
| toolchain go1.24.0 | ||||
|  | ||||
| require ( | ||||
| 	beryju.io/ldap v0.1.0 | ||||
| 	github.com/coreos/go-oidc/v3 v3.13.0 | ||||
| @ -8,7 +11,7 @@ require ( | ||||
| 	github.com/go-http-utils/etag v0.0.0-20161124023236-513ea8f21eb1 | ||||
| 	github.com/go-ldap/ldap/v3 v3.4.10 | ||||
| 	github.com/go-openapi/runtime v0.28.0 | ||||
| 	github.com/golang-jwt/jwt/v5 v5.2.2 | ||||
| 	github.com/golang-jwt/jwt/v5 v5.2.1 | ||||
| 	github.com/google/uuid v1.6.0 | ||||
| 	github.com/gorilla/handlers v1.5.2 | ||||
| 	github.com/gorilla/mux v1.8.1 | ||||
| @ -20,13 +23,13 @@ require ( | ||||
| 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | ||||
| 	github.com/pires/go-proxyproto v0.8.0 | ||||
| 	github.com/prometheus/client_golang v1.21.1 | ||||
| 	github.com/redis/go-redis/v9 v9.7.3 | ||||
| 	github.com/redis/go-redis/v9 v9.7.1 | ||||
| 	github.com/sethvargo/go-envconfig v1.1.1 | ||||
| 	github.com/sirupsen/logrus v1.9.3 | ||||
| 	github.com/spf13/cobra v1.9.1 | ||||
| 	github.com/stretchr/testify v1.10.0 | ||||
| 	github.com/wwt/guac v1.3.2 | ||||
| 	goauthentik.io/api/v3 v3.2025023.2 | ||||
| 	goauthentik.io/api/v3 v3.2025022.3 | ||||
| 	golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab | ||||
| 	golang.org/x/oauth2 v0.28.0 | ||||
| 	golang.org/x/sync v0.12.0 | ||||
| @ -79,3 +82,5 @@ require ( | ||||
| 	google.golang.org/protobuf v1.36.1 // indirect | ||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||
| ) | ||||
|  | ||||
| replace goauthentik.io/api/v3 => ./gen-go-api | ||||
|  | ||||
							
								
								
									
										12
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								go.sum
									
									
									
									
									
								
							| @ -113,8 +113,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr | ||||
| github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= | ||||
| github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= | ||||
| github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= | ||||
| github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= | ||||
| github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= | ||||
| github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= | ||||
| github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= | ||||
| github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | ||||
| github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | ||||
| github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= | ||||
| @ -248,8 +248,8 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ | ||||
| github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= | ||||
| github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= | ||||
| github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= | ||||
| github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= | ||||
| github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= | ||||
| github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc= | ||||
| github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= | ||||
| github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= | ||||
| github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= | ||||
| github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= | ||||
| @ -299,8 +299,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y | ||||
| go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= | ||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||
| goauthentik.io/api/v3 v3.2025023.2 h1:4XHlnykN5jQH78liQ4cp2Jf8eigvQImIJp+A+bsq1nA= | ||||
| goauthentik.io/api/v3 v3.2025023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||
| goauthentik.io/api/v3 v3.2025022.3 h1:cipaxl0il4/s1fU2f6+CD7nzgAktbV0XD7r5qHh0fUc= | ||||
| goauthentik.io/api/v3 v3.2025022.3/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= | ||||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
| golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
| golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
|  | ||||
| @ -162,14 +162,13 @@ func (c *Config) parseScheme(rawVal string) string { | ||||
| 	if err != nil { | ||||
| 		return rawVal | ||||
| 	} | ||||
| 	switch u.Scheme { | ||||
| 	case "env": | ||||
| 	if u.Scheme == "env" { | ||||
| 		e, ok := os.LookupEnv(u.Host) | ||||
| 		if ok { | ||||
| 			return e | ||||
| 		} | ||||
| 		return u.RawQuery | ||||
| 	case "file": | ||||
| 	} else if u.Scheme == "file" { | ||||
| 		d, err := os.ReadFile(u.Path) | ||||
| 		if err != nil { | ||||
| 			return u.RawQuery | ||||
|  | ||||
| @ -10,7 +10,7 @@ import ( | ||||
| ) | ||||
|  | ||||
| func TestConfigEnv(t *testing.T) { | ||||
| 	assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "bar")) | ||||
| 	os.Setenv("AUTHENTIK_SECRET_KEY", "bar") | ||||
| 	cfg = nil | ||||
| 	if err := Get().fromEnv(); err != nil { | ||||
| 		panic(err) | ||||
| @ -19,8 +19,8 @@ func TestConfigEnv(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func TestConfigEnv_Scheme(t *testing.T) { | ||||
| 	assert.NoError(t, os.Setenv("foo", "bar")) | ||||
| 	assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")) | ||||
| 	os.Setenv("foo", "bar") | ||||
| 	os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo") | ||||
| 	cfg = nil | ||||
| 	if err := Get().fromEnv(); err != nil { | ||||
| 		panic(err) | ||||
| @ -33,15 +33,13 @@ func TestConfigEnv_File(t *testing.T) { | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		assert.NoError(t, os.Remove(file.Name())) | ||||
| 	}() | ||||
| 	defer os.Remove(file.Name()) | ||||
| 	_, err = file.Write([]byte("bar")) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
|  | ||||
| 	assert.NoError(t, os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))) | ||||
| 	os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name())) | ||||
| 	cfg = nil | ||||
| 	if err := Get().fromEnv(); err != nil { | ||||
| 		panic(err) | ||||
|  | ||||
| @ -29,4 +29,4 @@ func UserAgent() string { | ||||
| 	return fmt.Sprintf("authentik@%s", FullVersion()) | ||||
| } | ||||
|  | ||||
| const VERSION = "2025.2.3" | ||||
| const VERSION = "2025.2.2" | ||||
|  | ||||
							
								
								
									
										5
									
								
								internal/crypto/backend/fips_disabled.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								internal/crypto/backend/fips_disabled.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| //go:build requirefips | ||||
|  | ||||
| package backend | ||||
|  | ||||
| var FipsEnabled = true | ||||
							
								
								
									
										5
									
								
								internal/crypto/backend/fips_enabled.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								internal/crypto/backend/fips_enabled.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| //go:build !requirefips | ||||
|  | ||||
| package backend | ||||
|  | ||||
| var FipsEnabled = false | ||||
| @ -35,7 +35,7 @@ func EnableDebugServer() { | ||||
| 			if err != nil { | ||||
| 				return nil | ||||
| 			} | ||||
| 			_, err = fmt.Fprintf(w, "<a href='%[1]s'>%[1]s</a><br>", tpl) | ||||
| 			_, err = w.Write([]byte(fmt.Sprintf("<a href='%[1]s'>%[1]s</a><br>", tpl))) | ||||
| 			if err != nil { | ||||
| 				l.WithError(err).Warning("failed to write index") | ||||
| 				return nil | ||||
|  | ||||
| @ -44,11 +44,10 @@ func New(healthcheck func() bool) *GoUnicorn { | ||||
| 	signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2) | ||||
| 	go func() { | ||||
| 		for sig := range c { | ||||
| 			switch sig { | ||||
| 			case syscall.SIGHUP: | ||||
| 			if sig == syscall.SIGHUP { | ||||
| 				g.log.Info("SIGHUP received, forwarding to gunicorn") | ||||
| 				g.Reload() | ||||
| 			case syscall.SIGUSR2: | ||||
| 			} else if sig == syscall.SIGUSR2 { | ||||
| 				g.log.Info("SIGUSR2 received, restarting gunicorn") | ||||
| 				g.Restart() | ||||
| 			} | ||||
|  | ||||
| @ -2,7 +2,6 @@ package ak | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/fips140" | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"net/http" | ||||
| @ -204,7 +203,7 @@ func (a *APIController) getWebsocketPingArgs() map[string]interface{} { | ||||
| 		"golangVersion":  runtime.Version(), | ||||
| 		"opensslEnabled": cryptobackend.OpensslEnabled, | ||||
| 		"opensslVersion": cryptobackend.OpensslVersion(), | ||||
| 		"fipsEnabled":    fips140.Enabled(), | ||||
| 		"fipsEnabled":    cryptobackend.FipsEnabled, | ||||
| 	} | ||||
| 	hostname, err := os.Hostname() | ||||
| 	if err == nil { | ||||
|  | ||||
| @ -35,19 +35,13 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]]( | ||||
| 	req PaginatorRequest[Treq, Tres], | ||||
| 	opts PaginatorOptions, | ||||
| ) ([]Tobj, error) { | ||||
| 	if opts.Logger == nil { | ||||
| 		opts.Logger = log.NewEntry(log.StandardLogger()) | ||||
| 	} | ||||
| 	var bfreq, cfreq interface{} | ||||
| 	fetchOffset := func(page int32) (Tres, error) { | ||||
| 		bfreq = req.Page(page) | ||||
| 		cfreq = bfreq.(PaginatorRequest[Treq, Tres]).PageSize(int32(opts.PageSize)) | ||||
| 		res, hres, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute() | ||||
| 		res, _, err := cfreq.(PaginatorRequest[Treq, Tres]).Execute() | ||||
| 		if err != nil { | ||||
| 			opts.Logger.WithError(err).WithField("page", page).Warning("failed to fetch page") | ||||
| 			if hres != nil && hres.StatusCode >= 400 && hres.StatusCode < 500 { | ||||
| 				return res, err | ||||
| 			} | ||||
| 		} | ||||
| 		return res, err | ||||
| 	} | ||||
| @ -57,9 +51,6 @@ func Paginator[Tobj any, Treq any, Tres PaginatorResponse[Tobj]]( | ||||
| 	for { | ||||
| 		apiObjects, err := fetchOffset(page) | ||||
| 		if err != nil { | ||||
| 			if page == 1 { | ||||
| 				return objects, err | ||||
| 			} | ||||
| 			errs = append(errs, err) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| @ -1,64 +1,5 @@ | ||||
| package ak | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"goauthentik.io/api/v3" | ||||
| ) | ||||
|  | ||||
| type fakeAPIType struct{} | ||||
|  | ||||
| type fakeAPIResponse struct { | ||||
| 	results    []fakeAPIType | ||||
| 	pagination api.Pagination | ||||
| } | ||||
|  | ||||
| func (fapi *fakeAPIResponse) GetResults() []fakeAPIType     { return fapi.results } | ||||
| func (fapi *fakeAPIResponse) GetPagination() api.Pagination { return fapi.pagination } | ||||
|  | ||||
| type fakeAPIRequest struct { | ||||
| 	res  *fakeAPIResponse | ||||
| 	http *http.Response | ||||
| 	err  error | ||||
| } | ||||
|  | ||||
| func (fapi *fakeAPIRequest) Page(page int32) *fakeAPIRequest     { return fapi } | ||||
| func (fapi *fakeAPIRequest) PageSize(size int32) *fakeAPIRequest { return fapi } | ||||
| func (fapi *fakeAPIRequest) Execute() (*fakeAPIResponse, *http.Response, error) { | ||||
| 	return fapi.res, fapi.http, fapi.err | ||||
| } | ||||
|  | ||||
| func Test_Simple(t *testing.T) { | ||||
| 	req := &fakeAPIRequest{ | ||||
| 		res: &fakeAPIResponse{ | ||||
| 			results: []fakeAPIType{ | ||||
| 				{}, | ||||
| 			}, | ||||
| 			pagination: api.Pagination{ | ||||
| 				TotalPages: 1, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	res, err := Paginator(req, PaginatorOptions{}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Len(t, res, 1) | ||||
| } | ||||
|  | ||||
| func Test_BadRequest(t *testing.T) { | ||||
| 	req := &fakeAPIRequest{ | ||||
| 		http: &http.Response{ | ||||
| 			StatusCode: 400, | ||||
| 		}, | ||||
| 		err: errors.New("foo"), | ||||
| 	} | ||||
| 	res, err := Paginator(req, PaginatorOptions{}) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.Equal(t, []fakeAPIType{}, res) | ||||
| } | ||||
|  | ||||
| // func Test_PaginatorCompile(t *testing.T) { | ||||
| // 	req := api.ApiCoreUsersListRequest{} | ||||
| // 	Paginator(req, PaginatorOptions{ | ||||
|  | ||||
| @ -148,8 +148,7 @@ func (ac *APIController) startWSHandler() { | ||||
| 			"outpost_type": ac.Server.Type(), | ||||
| 			"uuid":         ac.instanceUUID.String(), | ||||
| 		}).Set(1) | ||||
| 		switch wsMsg.Instruction { | ||||
| 		case WebsocketInstructionTriggerUpdate: | ||||
| 		if wsMsg.Instruction == WebsocketInstructionTriggerUpdate { | ||||
| 			time.Sleep(ac.reloadOffset) | ||||
| 			logger.Debug("Got update trigger...") | ||||
| 			err := ac.OnRefresh() | ||||
| @ -164,7 +163,7 @@ func (ac *APIController) startWSHandler() { | ||||
| 					"build":        constants.BUILD(""), | ||||
| 				}).SetToCurrentTime() | ||||
| 			} | ||||
| 		case WebsocketInstructionProviderSpecific: | ||||
| 		} else if wsMsg.Instruction == WebsocketInstructionProviderSpecific { | ||||
| 			for _, h := range ac.wsHandlers { | ||||
| 				h(context.Background(), wsMsg.Args) | ||||
| 			} | ||||
|  | ||||
| @ -66,12 +66,7 @@ func (ls *LDAPServer) StartLDAPServer() error { | ||||
| 		return err | ||||
| 	} | ||||
| 	proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} | ||||
| 	defer func() { | ||||
| 		err := proxyListener.Close() | ||||
| 		if err != nil { | ||||
| 			ls.log.WithError(err).Warning("failed to close proxy listener") | ||||
| 		} | ||||
| 	}() | ||||
| 	defer proxyListener.Close() | ||||
|  | ||||
| 	ls.log.WithField("listen", listen).Info("Starting LDAP server") | ||||
| 	err = ls.s.Serve(proxyListener) | ||||
|  | ||||
| @ -49,12 +49,7 @@ func (ls *LDAPServer) StartLDAPTLSServer() error { | ||||
| 	} | ||||
|  | ||||
| 	proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} | ||||
| 	defer func() { | ||||
| 		err := proxyListener.Close() | ||||
| 		if err != nil { | ||||
| 			ls.log.WithError(err).Warning("failed to close proxy listener") | ||||
| 		} | ||||
| 	}() | ||||
| 	defer proxyListener.Close() | ||||
|  | ||||
| 	tln := tls.NewListener(proxyListener, tlsConfig) | ||||
|  | ||||
|  | ||||
| @ -98,7 +98,7 @@ func (ms *MemorySearcher) Search(req *search.Request) (ldap.ServerSearchResult, | ||||
|  | ||||
| 	entries := make([]*ldap.Entry, 0) | ||||
|  | ||||
| 	scope := req.Scope | ||||
| 	scope := req.SearchRequest.Scope | ||||
| 	needUsers, needGroups := ms.si.GetNeededObjects(scope, req.BaseDN, req.FilterObjectClass) | ||||
|  | ||||
| 	if scope >= 0 && strings.EqualFold(req.BaseDN, baseDN) { | ||||
|  | ||||
| @ -56,7 +56,7 @@ func GetOIDCEndpoint(p api.ProxyOutpostConfig, authentikHost string, embedded bo | ||||
| 	if !embedded && hostBrowser == "" { | ||||
| 		return ep | ||||
| 	} | ||||
| 	var newHost = aku | ||||
| 	var newHost *url.URL = aku | ||||
| 	var newBrowserHost *url.URL | ||||
| 	if embedded { | ||||
| 		if authentikHost == "" { | ||||
|  | ||||
| @ -130,12 +130,7 @@ func (ps *ProxyServer) ServeHTTP() { | ||||
| 		return | ||||
| 	} | ||||
| 	proxyListener := &proxyproto.Listener{Listener: listener, ConnPolicy: utils.GetProxyConnectionPolicy()} | ||||
| 	defer func() { | ||||
| 		err := proxyListener.Close() | ||||
| 		if err != nil { | ||||
| 			ps.log.WithError(err).Warning("failed to close proxy listener") | ||||
| 		} | ||||
| 	}() | ||||
| 	defer proxyListener.Close() | ||||
|  | ||||
| 	ps.log.WithField("listen", listenAddress).Info("Starting HTTP server") | ||||
| 	ps.serve(proxyListener) | ||||
| @ -154,12 +149,7 @@ func (ps *ProxyServer) ServeHTTPS() { | ||||
| 		return | ||||
| 	} | ||||
| 	proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()} | ||||
| 	defer func() { | ||||
| 		err := proxyListener.Close() | ||||
| 		if err != nil { | ||||
| 			ps.log.WithError(err).Warning("failed to close proxy listener") | ||||
| 		} | ||||
| 	}() | ||||
| 	defer proxyListener.Close() | ||||
|  | ||||
| 	tlsListener := tls.NewListener(proxyListener, tlsConfig) | ||||
| 	ps.log.WithField("listen", listenAddress).Info("Starting HTTPS server") | ||||
|  | ||||
| @ -72,13 +72,11 @@ func (s *RedisStore) New(r *http.Request, name string) (*sessions.Session, error | ||||
| 	session.ID = c.Value | ||||
|  | ||||
| 	err = s.load(r.Context(), session) | ||||
| 	if err != nil { | ||||
| 		if errors.Is(err, redis.Nil) { | ||||
| 			return session, nil | ||||
| 		} | ||||
| 		return session, err | ||||
| 	if err == nil { | ||||
| 		session.IsNew = false | ||||
| 	} else if err == redis.Nil { | ||||
| 		err = nil // no data stored | ||||
| 	} | ||||
| 	session.IsNew = false | ||||
| 	return session, err | ||||
| } | ||||
|  | ||||
|  | ||||
| @ -8,6 +8,7 @@ | ||||
|         <link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png"> | ||||
|         <link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css"> | ||||
|         <link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/authentik.css"> | ||||
|         <link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/custom.css"> | ||||
|         <link rel="prefetch" href="/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg" /> | ||||
|         <style> | ||||
|             .pf-c-background-image::before { | ||||
|  | ||||
| @ -156,12 +156,7 @@ func (ws *WebServer) listenPlain() { | ||||
| 		return | ||||
| 	} | ||||
| 	proxyListener := &proxyproto.Listener{Listener: ln, ConnPolicy: utils.GetProxyConnectionPolicy()} | ||||
| 	defer func() { | ||||
| 		err := proxyListener.Close() | ||||
| 		if err != nil { | ||||
| 			ws.log.WithError(err).Warning("failed to close proxy listener") | ||||
| 		} | ||||
| 	}() | ||||
| 	defer proxyListener.Close() | ||||
|  | ||||
| 	ws.log.WithField("listen", config.Get().Listen.HTTP).Info("Starting HTTP server") | ||||
| 	ws.serve(proxyListener) | ||||
|  | ||||
| @ -46,12 +46,7 @@ func (ws *WebServer) listenTLS() { | ||||
| 		return | ||||
| 	} | ||||
| 	proxyListener := &proxyproto.Listener{Listener: web.TCPKeepAliveListener{TCPListener: ln.(*net.TCPListener)}, ConnPolicy: utils.GetProxyConnectionPolicy()} | ||||
| 	defer func() { | ||||
| 		err := proxyListener.Close() | ||||
| 		if err != nil { | ||||
| 			ws.log.WithError(err).Warning("failed to close proxy listener") | ||||
| 		} | ||||
| 	}() | ||||
| 	defer proxyListener.Close() | ||||
|  | ||||
| 	tlsListener := tls.NewListener(proxyListener, tlsConfig) | ||||
| 	ws.log.WithField("listen", config.Get().Listen.HTTPS).Info("Starting HTTPS server") | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| # syntax=docker/dockerfile:1 | ||||
|  | ||||
| # Stage 1: Build | ||||
| FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder | ||||
| FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder | ||||
|  | ||||
| ARG TARGETOS | ||||
| ARG TARGETARCH | ||||
| @ -27,7 +27,7 @@ COPY . . | ||||
| RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ | ||||
|     --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ | ||||
|     if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ | ||||
|     CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ | ||||
|     CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \ | ||||
|     go build -o /go/ldap ./cmd/ldap | ||||
|  | ||||
| # Stage 2: Run | ||||
|  | ||||
							
								
								
									
										8
									
								
								lifecycle/aws/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								lifecycle/aws/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -9,7 +9,7 @@ | ||||
|             "version": "0.0.0", | ||||
|             "license": "MIT", | ||||
|             "devDependencies": { | ||||
|                 "aws-cdk": "^2.1006.0", | ||||
|                 "aws-cdk": "^2.1005.0", | ||||
|                 "cross-env": "^7.0.3" | ||||
|             }, | ||||
|             "engines": { | ||||
| @ -17,9 +17,9 @@ | ||||
|             } | ||||
|         }, | ||||
|         "node_modules/aws-cdk": { | ||||
|             "version": "2.1006.0", | ||||
|             "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1006.0.tgz", | ||||
|             "integrity": "sha512-6qYnCt4mBN+3i/5F+FC2yMETkDHY/IL7gt3EuqKVPcaAO4jU7oXfVSlR60CYRkZWL4fnAurUV14RkJuJyVG/IA==", | ||||
|             "version": "2.1005.0", | ||||
|             "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1005.0.tgz", | ||||
|             "integrity": "sha512-4ejfGGrGCEl0pg1xcqkxK0lpBEZqNI48wtrXhk6dYOFYPYMZtqn1kdla29ONN+eO2unewkNF4nLP1lPYhlf9Pg==", | ||||
|             "dev": true, | ||||
|             "license": "Apache-2.0", | ||||
|             "bin": { | ||||
|  | ||||
| @ -10,7 +10,7 @@ | ||||
|         "node": ">=20" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "aws-cdk": "^2.1006.0", | ||||
|         "aws-cdk": "^2.1005.0", | ||||
|         "cross-env": "^7.0.3" | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -26,7 +26,7 @@ Parameters: | ||||
|     Description: authentik Docker image | ||||
|   AuthentikVersion: | ||||
|     Type: String | ||||
|     Default: 2025.2.3 | ||||
|     Default: 2025.2.2 | ||||
|     Description: authentik Docker image tag | ||||
|   AuthentikServerCPU: | ||||
|     Type: Number | ||||
|  | ||||
| @ -8,7 +8,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-03-31 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-03-13 00:10+0000\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| @ -616,18 +616,6 @@ msgstr "" | ||||
| msgid "Email" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Customize the body of the request. Mapping should return data that is JSON-" | ||||
| "serializable." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Configure additional headers to be sent. Mapping should return a dictionary " | ||||
| "of key-value pairs" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Only send notification once, for example when sending a webhook into a chat " | ||||
| @ -1220,20 +1208,6 @@ msgstr "" | ||||
| msgid "Reputation Scores" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "Waiting for authentication..." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "" | ||||
| "You're already authenticating in another tab. This page will refresh once " | ||||
| "authentication is completed." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "Authenticate in this tab" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/policies/templates/policies/denied.html | ||||
| msgid "Permission denied" | ||||
| msgstr "" | ||||
| @ -1782,17 +1756,6 @@ msgid "" | ||||
| "NameIDPolicy of the incoming request will be considered" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "AuthnContextClassRef Property Mapping" | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| "Configure how the AuthnContextClassRef value will be created. When left " | ||||
| "empty, the AuthnContextClassRef will be set based on which authentication " | ||||
| "methods the user used to authenticate." | ||||
| msgstr "" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| "Assertion valid not before current time + this value (Format: hours=-1;" | ||||
|  | ||||
| @ -10,8 +10,8 @@ | ||||
| # Manuel Viens, 2023 | ||||
| # Mordecai, 2023 | ||||
| # nerdinator <florian.dupret@gmail.com>, 2024 | ||||
| # Tina, 2024 | ||||
| # Charles Leclerc, 2025 | ||||
| # Tina, 2025 | ||||
| # Marc Schmitt, 2025 | ||||
| #  | ||||
| #, fuzzy | ||||
| @ -19,7 +19,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-03-31 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-03-13 00:10+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: Marc Schmitt, 2025\n" | ||||
| "Language-Team: French (https://app.transifex.com/authentik/teams/119923/fr/)\n" | ||||
| @ -676,22 +676,6 @@ msgstr "Webhook Slack (ou Discord)" | ||||
| msgid "Email" | ||||
| msgstr "Courriel" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Customize the body of the request. Mapping should return data that is JSON-" | ||||
| "serializable." | ||||
| msgstr "" | ||||
| "Personnalise le corps de la requête. Le mappage doit renvoyer des données " | ||||
| "sérialisables en JSON." | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Configure additional headers to be sent. Mapping should return a dictionary " | ||||
| "of key-value pairs" | ||||
| msgstr "" | ||||
| "Configure les en-têtes supplémentaires à envoyer. Le mappage doit renvoyer " | ||||
| "un dictionnaire de paires clé-valeur." | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Only send notification once, for example when sending a webhook into a chat " | ||||
| @ -1347,22 +1331,6 @@ msgstr "Score de Réputation" | ||||
| msgid "Reputation Scores" | ||||
| msgstr "Scores de Réputation" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "Waiting for authentication..." | ||||
| msgstr "En attente de l'authentification..." | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "" | ||||
| "You're already authenticating in another tab. This page will refresh once " | ||||
| "authentication is completed." | ||||
| msgstr "" | ||||
| "Vous êtes déjà en cours d'authentification dans un autre onglet. Cette page " | ||||
| "se rafraîchira lorsque l'authentification sera terminée." | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "Authenticate in this tab" | ||||
| msgstr "S'authentifier dans cet onglet" | ||||
|  | ||||
| #: authentik/policies/templates/policies/denied.html | ||||
| msgid "Permission denied" | ||||
| msgstr "Permission refusée" | ||||
| @ -1988,20 +1956,6 @@ msgstr "" | ||||
| "Configure la manière dont la valeur NameID sera créée. Si laissé vide, la " | ||||
| "NameIDPolicy de la requête entrante sera prise en compte" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "AuthnContextClassRef Property Mapping" | ||||
| msgstr "Mappage de propriété AuthnContextClassRef" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| "Configure how the AuthnContextClassRef value will be created. When left " | ||||
| "empty, the AuthnContextClassRef will be set based on which authentication " | ||||
| "methods the user used to authenticate." | ||||
| msgstr "" | ||||
| "Configure comment la valeur AuthnContextClassRef sera créée. Lorsque non " | ||||
| "sélectionné, AuthnContextClassRef sera défini en fonction de quelle méthode " | ||||
| "d'authentification l'utilisateur a utilisé pour s'authentifier." | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| "Assertion valid not before current time + this value (Format: " | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -15,7 +15,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-03-22 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-03-13 00:10+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: deluxghost, 2025\n" | ||||
| "Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n" | ||||
| @ -627,18 +627,6 @@ msgstr "Slack Webhook(Slack/Discord)" | ||||
| msgid "Email" | ||||
| msgstr "电子邮箱" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Customize the body of the request. Mapping should return data that is JSON-" | ||||
| "serializable." | ||||
| msgstr "自定义请求体。映射应该返回 JSON 序列化的数据。" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Configure additional headers to be sent. Mapping should return a dictionary " | ||||
| "of key-value pairs" | ||||
| msgstr "配置要发送的额外标头。映射应该返回键值对字典。" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Only send notification once, for example when sending a webhook into a chat " | ||||
| @ -1794,18 +1782,6 @@ msgid "" | ||||
| "NameIDPolicy of the incoming request will be considered" | ||||
| msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "AuthnContextClassRef Property Mapping" | ||||
| msgstr "AuthnContextClassRef 属性映射" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| "Configure how the AuthnContextClassRef value will be created. When left " | ||||
| "empty, the AuthnContextClassRef will be set based on which authentication " | ||||
| "methods the user used to authenticate." | ||||
| msgstr "" | ||||
| "配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| "Assertion valid not before current time + this value (Format: " | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -14,7 +14,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2025-03-31 00:10+0000\n" | ||||
| "POT-Creation-Date: 2025-03-13 00:10+0000\n" | ||||
| "PO-Revision-Date: 2022-09-26 16:47+0000\n" | ||||
| "Last-Translator: deluxghost, 2025\n" | ||||
| "Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n" | ||||
| @ -626,18 +626,6 @@ msgstr "Slack Webhook(Slack/Discord)" | ||||
| msgid "Email" | ||||
| msgstr "电子邮箱" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Customize the body of the request. Mapping should return data that is JSON-" | ||||
| "serializable." | ||||
| msgstr "自定义请求体。映射应该返回 JSON 序列化的数据。" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Configure additional headers to be sent. Mapping should return a dictionary " | ||||
| "of key-value pairs" | ||||
| msgstr "配置要发送的额外标头。映射应该返回键值对字典。" | ||||
|  | ||||
| #: authentik/events/models.py | ||||
| msgid "" | ||||
| "Only send notification once, for example when sending a webhook into a chat " | ||||
| @ -1234,20 +1222,6 @@ msgstr "信誉分数" | ||||
| msgid "Reputation Scores" | ||||
| msgstr "信誉分数" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "Waiting for authentication..." | ||||
| msgstr "正在等待身份验证…" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "" | ||||
| "You're already authenticating in another tab. This page will refresh once " | ||||
| "authentication is completed." | ||||
| msgstr "您正在另一个标签页中验证身份。身份验证完成后,此页面会刷新。" | ||||
|  | ||||
| #: authentik/policies/templates/policies/buffer.html | ||||
| msgid "Authenticate in this tab" | ||||
| msgstr "在此标签页中验证身份" | ||||
|  | ||||
| #: authentik/policies/templates/policies/denied.html | ||||
| msgid "Permission denied" | ||||
| msgstr "权限被拒绝" | ||||
| @ -1807,18 +1781,6 @@ msgid "" | ||||
| "NameIDPolicy of the incoming request will be considered" | ||||
| msgstr "配置如何创建 NameID 值。如果留空,将考虑传入请求的 NameIDPolicy" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "AuthnContextClassRef Property Mapping" | ||||
| msgstr "AuthnContextClassRef 属性映射" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| "Configure how the AuthnContextClassRef value will be created. When left " | ||||
| "empty, the AuthnContextClassRef will be set based on which authentication " | ||||
| "methods the user used to authenticate." | ||||
| msgstr "" | ||||
| "配置如何创建 AuthnContextClassRef 值。留空时,AuthnContextClassRef 会基于用户使用的身份验证方式设置。" | ||||
|  | ||||
| #: authentik/providers/saml/models.py | ||||
| msgid "" | ||||
| "Assertion valid not before current time + this value (Format: " | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| { | ||||
|     "name": "@goauthentik/authentik", | ||||
|     "version": "2025.2.3", | ||||
|     "version": "2025.2.2", | ||||
|     "private": true | ||||
| } | ||||
|  | ||||
| @ -17,7 +17,7 @@ COPY web . | ||||
| RUN npm run build-proxy | ||||
|  | ||||
| # Stage 2: Build | ||||
| FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder | ||||
| FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder | ||||
|  | ||||
| ARG TARGETOS | ||||
| ARG TARGETARCH | ||||
| @ -43,7 +43,7 @@ COPY . . | ||||
| RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ | ||||
|     --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ | ||||
|     if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ | ||||
|     CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ | ||||
|     CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \ | ||||
|     go build -o /go/proxy ./cmd/proxy | ||||
|  | ||||
| # Stage 3: Run | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| [project] | ||||
| name = "authentik" | ||||
| version = "2025.2.3" | ||||
| version = "2025.2.2" | ||||
| description = "" | ||||
| authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }] | ||||
| requires-python = "==3.12.*" | ||||
| @ -103,7 +103,7 @@ dev = [ | ||||
|  | ||||
| [tool.uv.sources] | ||||
| django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch = "authentik-fixes" } | ||||
| opencontainers = { git = "https://github.com/BeryJu/oci-python", rev = "c791b19056769cd67957322806809ab70f5bead8" } | ||||
| opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "20d69d9cc50a0fef31605b46f06da0c94f1ec3cf" } | ||||
|  | ||||
| [project.scripts] | ||||
| ak = "lifecycle.ak:main" | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| # syntax=docker/dockerfile:1 | ||||
|  | ||||
| # Stage 1: Build | ||||
| FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder | ||||
| FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder | ||||
|  | ||||
| ARG TARGETOS | ||||
| ARG TARGETARCH | ||||
| @ -27,7 +27,7 @@ COPY . . | ||||
| RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ | ||||
|     --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ | ||||
|     if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ | ||||
|     CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ | ||||
|     CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \ | ||||
|     go build -o /go/rac ./cmd/rac | ||||
|  | ||||
| # Stage 2: Run | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| # syntax=docker/dockerfile:1 | ||||
|  | ||||
| # Stage 1: Build | ||||
| FROM --platform=${BUILDPLATFORM} docker.io/library/golang:1.24-bookworm AS builder | ||||
| FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.23-fips-bookworm AS builder | ||||
|  | ||||
| ARG TARGETOS | ||||
| ARG TARGETARCH | ||||
| @ -27,7 +27,7 @@ COPY . . | ||||
| RUN --mount=type=cache,sharing=locked,target=/go/pkg/mod \ | ||||
|     --mount=type=cache,id=go-build-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/root/.cache/go-build \ | ||||
|     if [ "$TARGETARCH" = "arm64" ]; then export CC=aarch64-linux-gnu-gcc && export CC_FOR_TARGET=gcc-aarch64-linux-gnu; fi && \ | ||||
|     CGO_ENABLED=1 GOFIPS140=latest GOARM="${TARGETVARIANT#v}" \ | ||||
|     CGO_ENABLED=1 GOEXPERIMENT="systemcrypto" GOFLAGS="-tags=requirefips" GOARM="${TARGETVARIANT#v}" \ | ||||
|     go build -o /go/radius ./cmd/radius | ||||
|  | ||||
| # Stage 2: Run | ||||
|  | ||||
							
								
								
									
										170
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										170
									
								
								schema.yml
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | ||||
| openapi: 3.0.3 | ||||
| info: | ||||
|   title: authentik | ||||
|   version: 2025.2.3 | ||||
|   version: 2025.2.2 | ||||
|   description: Making authentication simple. | ||||
|   contact: | ||||
|     email: hello@goauthentik.io | ||||
| @ -4447,10 +4447,6 @@ paths: | ||||
|         schema: | ||||
|           type: string | ||||
|           format: uuid | ||||
|       - in: query | ||||
|         name: branding_default_flow_background | ||||
|         schema: | ||||
|           type: string | ||||
|       - in: query | ||||
|         name: branding_favicon | ||||
|         schema: | ||||
| @ -8921,6 +8917,11 @@ paths: | ||||
|           type: string | ||||
|         description: Querystring as received | ||||
|         required: true | ||||
|       - in: query | ||||
|         name: xid | ||||
|         schema: | ||||
|           type: string | ||||
|         description: Flow execution ID | ||||
|       tags: | ||||
|       - flows | ||||
|       security: | ||||
| @ -8961,6 +8962,12 @@ paths: | ||||
|           type: string | ||||
|         description: Querystring as received | ||||
|         required: true | ||||
|       - in: query | ||||
|         name: xid | ||||
|         schema: | ||||
|           type: string | ||||
|         description: Flow execution ID | ||||
|         required: true | ||||
|       tags: | ||||
|       - flows | ||||
|       requestBody: | ||||
| @ -39441,6 +39448,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-access-denied | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -39456,6 +39465,7 @@ components: | ||||
|       required: | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - xid | ||||
|     AlgEnum: | ||||
|       enum: | ||||
|       - rsa | ||||
| @ -39555,6 +39565,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-source-oauth-apple | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -39574,6 +39586,7 @@ components: | ||||
|       - redirect_uri | ||||
|       - scope | ||||
|       - state | ||||
|       - xid | ||||
|     Application: | ||||
|       type: object | ||||
|       description: Application Serializer | ||||
| @ -39882,6 +39895,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-authenticator-duo | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -39904,6 +39919,7 @@ components: | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - stage_uuid | ||||
|       - xid | ||||
|     AuthenticatorDuoChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Pseudo class for duo response | ||||
| @ -40041,6 +40057,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-authenticator-email | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -40060,6 +40078,7 @@ components: | ||||
|       required: | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - xid | ||||
|     AuthenticatorEmailChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Authenticator Email Challenge response, device is set by get_response_instance | ||||
| @ -40297,6 +40316,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-authenticator-sms | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -40313,6 +40334,7 @@ components: | ||||
|       required: | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - xid | ||||
|     AuthenticatorSMSChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: SMS Challenge response, device is set by get_response_instance | ||||
| @ -40460,6 +40482,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-authenticator-static | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -40478,6 +40502,7 @@ components: | ||||
|       - codes | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - xid | ||||
|     AuthenticatorStaticChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Pseudo class for static response | ||||
| @ -40581,6 +40606,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-authenticator-totp | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -40597,6 +40624,7 @@ components: | ||||
|       - config_url | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - xid | ||||
|     AuthenticatorTOTPChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: TOTP Challenge response, device is set by get_response_instance | ||||
| @ -40808,6 +40836,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-authenticator-validate | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -40831,6 +40861,7 @@ components: | ||||
|       - device_challenges | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - xid | ||||
|     AuthenticatorValidationChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Challenge used for Code-based and WebAuthn authenticators | ||||
| @ -40861,6 +40892,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-authenticator-webauthn | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -40878,6 +40911,7 @@ components: | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - registration | ||||
|       - xid | ||||
|     AuthenticatorWebAuthnChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: WebAuthn Challenge response | ||||
| @ -41010,6 +41044,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-autosubmit | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -41027,6 +41063,7 @@ components: | ||||
|       required: | ||||
|       - attrs | ||||
|       - url | ||||
|       - xid | ||||
|     BackendsEnum: | ||||
|       enum: | ||||
|       - authentik.core.auth.InbuiltBackend | ||||
| @ -41149,10 +41186,6 @@ components: | ||||
|           type: string | ||||
|         branding_favicon: | ||||
|           type: string | ||||
|         branding_custom_css: | ||||
|           type: string | ||||
|         branding_default_flow_background: | ||||
|           type: string | ||||
|         flow_authentication: | ||||
|           type: string | ||||
|           format: uuid | ||||
| @ -41212,11 +41245,6 @@ components: | ||||
|         branding_favicon: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         branding_custom_css: | ||||
|           type: string | ||||
|         branding_default_flow_background: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         flow_authentication: | ||||
|           type: string | ||||
|           format: uuid | ||||
| @ -41282,6 +41310,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-captcha | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -41304,6 +41334,7 @@ components: | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - site_key | ||||
|       - xid | ||||
|     CaptchaChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Validate captcha token | ||||
| @ -41687,6 +41718,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-consent | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -41715,6 +41748,7 @@ components: | ||||
|       - pending_user_avatar | ||||
|       - permissions | ||||
|       - token | ||||
|       - xid | ||||
|     ConsentChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Consent challenge response, any valid response request is valid | ||||
| @ -42109,8 +42143,6 @@ components: | ||||
|           type: string | ||||
|         branding_favicon: | ||||
|           type: string | ||||
|         branding_custom_css: | ||||
|           type: string | ||||
|         ui_footer_links: | ||||
|           type: array | ||||
|           items: | ||||
| @ -42137,7 +42169,6 @@ components: | ||||
|           type: string | ||||
|           readOnly: true | ||||
|       required: | ||||
|       - branding_custom_css | ||||
|       - branding_favicon | ||||
|       - branding_logo | ||||
|       - branding_title | ||||
| @ -42491,6 +42522,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-dummy | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -42501,6 +42534,7 @@ components: | ||||
|           type: string | ||||
|       required: | ||||
|       - name | ||||
|       - xid | ||||
|     DummyChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Dummy challenge response | ||||
| @ -42693,12 +42727,16 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-email | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
|             type: array | ||||
|             items: | ||||
|               $ref: '#/components/schemas/ErrorDetail' | ||||
|       required: | ||||
|       - xid | ||||
|     EmailChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: |- | ||||
| @ -43617,6 +43655,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-flow-error | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -43631,6 +43671,7 @@ components: | ||||
|           type: string | ||||
|       required: | ||||
|       - request_id | ||||
|       - xid | ||||
|     FlowImportResult: | ||||
|       type: object | ||||
|       description: Logs of an attempted flow import | ||||
| @ -43945,6 +43986,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: xak-flow-frame | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -43961,6 +44004,7 @@ components: | ||||
|       required: | ||||
|       - loading_text | ||||
|       - url | ||||
|       - xid | ||||
|     FrameChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Base class for all challenge responses | ||||
| @ -44763,6 +44807,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-identification | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -44807,6 +44853,7 @@ components: | ||||
|       - primary_action | ||||
|       - show_source_labels | ||||
|       - user_fields | ||||
|       - xid | ||||
|     IdentificationChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Identification challenge | ||||
| @ -46890,18 +46937,10 @@ components: | ||||
|         webhook_url: | ||||
|           type: string | ||||
|           format: uri | ||||
|         webhook_mapping_body: | ||||
|         webhook_mapping: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|           description: Customize the body of the request. Mapping should return data | ||||
|             that is JSON-serializable. | ||||
|         webhook_mapping_headers: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|           description: Configure additional headers to be sent. Mapping should return | ||||
|             a dictionary of key-value pairs | ||||
|         send_once: | ||||
|           type: boolean | ||||
|           description: Only send notification once, for example when sending a webhook | ||||
| @ -46929,18 +46968,10 @@ components: | ||||
|         webhook_url: | ||||
|           type: string | ||||
|           format: uri | ||||
|         webhook_mapping_body: | ||||
|         webhook_mapping: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|           description: Customize the body of the request. Mapping should return data | ||||
|             that is JSON-serializable. | ||||
|         webhook_mapping_headers: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|           description: Configure additional headers to be sent. Mapping should return | ||||
|             a dictionary of key-value pairs | ||||
|         send_once: | ||||
|           type: boolean | ||||
|           description: Only send notification once, for example when sending a webhook | ||||
| @ -47265,12 +47296,16 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-provider-oauth2-device-code | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
|             type: array | ||||
|             items: | ||||
|               $ref: '#/components/schemas/ErrorDetail' | ||||
|       required: | ||||
|       - xid | ||||
|     OAuthDeviceCodeChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Response that includes the user-entered device code | ||||
| @ -47293,12 +47328,16 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-provider-oauth2-device-code-finish | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
|             type: array | ||||
|             items: | ||||
|               $ref: '#/components/schemas/ErrorDetail' | ||||
|       required: | ||||
|       - xid | ||||
|     OAuthDeviceCodeFinishChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Response that device has been authenticated and tab can be closed | ||||
| @ -49443,6 +49482,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-password | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -49461,6 +49502,7 @@ components: | ||||
|       required: | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - xid | ||||
|     PasswordChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Password challenge response | ||||
| @ -50157,11 +50199,6 @@ components: | ||||
|         branding_favicon: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         branding_custom_css: | ||||
|           type: string | ||||
|         branding_default_flow_background: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|         flow_authentication: | ||||
|           type: string | ||||
|           format: uuid | ||||
| @ -51374,18 +51411,10 @@ components: | ||||
|         webhook_url: | ||||
|           type: string | ||||
|           format: uri | ||||
|         webhook_mapping_body: | ||||
|         webhook_mapping: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|           description: Customize the body of the request. Mapping should return data | ||||
|             that is JSON-serializable. | ||||
|         webhook_mapping_headers: | ||||
|           type: string | ||||
|           format: uuid | ||||
|           nullable: true | ||||
|           description: Configure additional headers to be sent. Mapping should return | ||||
|             a dictionary of key-value pairs | ||||
|         send_once: | ||||
|           type: boolean | ||||
|           description: Only send notification once, for example when sending a webhook | ||||
| @ -52245,8 +52274,9 @@ components: | ||||
|             format: uuid | ||||
|         acs_url: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|           format: uri | ||||
|           minLength: 1 | ||||
|           maxLength: 200 | ||||
|         audience: | ||||
|           type: string | ||||
|           description: Value of the audience restriction field of the assertion. When | ||||
| @ -52403,14 +52433,16 @@ components: | ||||
|           description: Also known as Entity ID. Defaults the Metadata URL. | ||||
|         sso_url: | ||||
|           type: string | ||||
|           format: uri | ||||
|           minLength: 1 | ||||
|           description: URL that the initial Login request is sent to. | ||||
|           format: uri | ||||
|           maxLength: 200 | ||||
|         slo_url: | ||||
|           type: string | ||||
|           format: uri | ||||
|           nullable: true | ||||
|           description: Optional URL if your IDP supports Single-Logout. | ||||
|           format: uri | ||||
|           maxLength: 200 | ||||
|         allow_idp_initiated: | ||||
|           type: boolean | ||||
|           description: Allows authentication flows initiated by the IdP. This can | ||||
| @ -53032,6 +53064,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-source-plex | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -53045,6 +53079,7 @@ components: | ||||
|       required: | ||||
|       - client_id | ||||
|       - slug | ||||
|       - xid | ||||
|     PlexAuthenticationChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Pseudo class for plex response | ||||
| @ -53557,6 +53592,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-prompt | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -53569,6 +53606,7 @@ components: | ||||
|             $ref: '#/components/schemas/StagePrompt' | ||||
|       required: | ||||
|       - fields | ||||
|       - xid | ||||
|     PromptChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: |- | ||||
| @ -54753,6 +54791,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: xak-flow-redirect | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -54763,6 +54803,7 @@ components: | ||||
|           type: string | ||||
|       required: | ||||
|       - to | ||||
|       - xid | ||||
|     RedirectChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: Redirect challenge response | ||||
| @ -55211,6 +55252,7 @@ components: | ||||
|         acs_url: | ||||
|           type: string | ||||
|           format: uri | ||||
|           maxLength: 200 | ||||
|         audience: | ||||
|           type: string | ||||
|           description: Value of the audience restriction field of the assertion. When | ||||
| @ -55377,8 +55419,9 @@ components: | ||||
|             format: uuid | ||||
|         acs_url: | ||||
|           type: string | ||||
|           minLength: 1 | ||||
|           format: uri | ||||
|           minLength: 1 | ||||
|           maxLength: 200 | ||||
|         audience: | ||||
|           type: string | ||||
|           description: Value of the audience restriction field of the assertion. When | ||||
| @ -55551,13 +55594,15 @@ components: | ||||
|           description: Also known as Entity ID. Defaults the Metadata URL. | ||||
|         sso_url: | ||||
|           type: string | ||||
|           description: URL that the initial Login request is sent to. | ||||
|           format: uri | ||||
|           description: URL that the initial Login request is sent to. | ||||
|           maxLength: 200 | ||||
|         slo_url: | ||||
|           type: string | ||||
|           format: uri | ||||
|           nullable: true | ||||
|           description: Optional URL if your IDP supports Single-Logout. | ||||
|           format: uri | ||||
|           maxLength: 200 | ||||
|         allow_idp_initiated: | ||||
|           type: boolean | ||||
|           description: Allows authentication flows initiated by the IdP. This can | ||||
| @ -55740,14 +55785,16 @@ components: | ||||
|           description: Also known as Entity ID. Defaults the Metadata URL. | ||||
|         sso_url: | ||||
|           type: string | ||||
|           format: uri | ||||
|           minLength: 1 | ||||
|           description: URL that the initial Login request is sent to. | ||||
|           format: uri | ||||
|           maxLength: 200 | ||||
|         slo_url: | ||||
|           type: string | ||||
|           format: uri | ||||
|           nullable: true | ||||
|           description: Optional URL if your IDP supports Single-Logout. | ||||
|           format: uri | ||||
|           maxLength: 200 | ||||
|         allow_idp_initiated: | ||||
|           type: boolean | ||||
|           description: Allows authentication flows initiated by the IdP. This can | ||||
| @ -56652,6 +56699,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-session-end | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -56674,6 +56723,7 @@ components: | ||||
|       - brand_name | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - xid | ||||
|     SessionUser: | ||||
|       type: object | ||||
|       description: |- | ||||
| @ -56786,6 +56836,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: xak-flow-shell | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -56796,6 +56848,7 @@ components: | ||||
|           type: string | ||||
|       required: | ||||
|       - body | ||||
|       - xid | ||||
|     SignatureAlgorithmEnum: | ||||
|       enum: | ||||
|       - http://www.w3.org/2000/09/xmldsig#rsa-sha1 | ||||
| @ -58070,6 +58123,8 @@ components: | ||||
|         component: | ||||
|           type: string | ||||
|           default: ak-stage-user-login | ||||
|         xid: | ||||
|           type: string | ||||
|         response_errors: | ||||
|           type: object | ||||
|           additionalProperties: | ||||
| @ -58083,6 +58138,7 @@ components: | ||||
|       required: | ||||
|       - pending_user | ||||
|       - pending_user_avatar | ||||
|       - xid | ||||
|     UserLoginChallengeResponseRequest: | ||||
|       type: object | ||||
|       description: User login challenge | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	