Compare commits
	
		
			171 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4a1acd377b | |||
| c5b84a91d1 | |||
| e77ecda3b8 | |||
| 4e317c10c5 | |||
| eb05a3ddb8 | |||
| a22d6a0924 | |||
| 3f0d67779a | |||
| 0a937ae8e9 | |||
| f8d94f3039 | |||
| 6bb261ac62 | |||
| 45f2c5bae7 | |||
| 5d8c1aa0b0 | |||
| 0101368369 | |||
| 4854f81592 | |||
| 4bed6e02e5 | |||
| 908f123d0e | |||
| 256dd24a1e | |||
| d4284407f9 | |||
| 80da5dfc52 | |||
| b6edf990e0 | |||
| a66dcf9382 | |||
| 9095a840d5 | |||
| 72259f6479 | |||
| 0973c74b9d | |||
| c7ed4f7ac1 | |||
| 3d577cf15e | |||
| 5474a32573 | |||
| a5940b88e3 | |||
| ff15716012 | |||
| c040b13b29 | |||
| 4915e980c5 | |||
| df362dd9ea | |||
| d4e4f93cb4 | |||
| 3af0de6a00 | |||
| 4f24d61290 | |||
| 4c5c4dcf2c | |||
| 660b5cb6c6 | |||
| 6ff1ea73a9 | |||
| 3de224690a | |||
| d4624b510a | |||
| 8856d762d0 | |||
| 5d1cbf14d1 | |||
| 6d5207f644 | |||
| 3b6497cd51 | |||
| ff7320b0f8 | |||
| e5a393c534 | |||
| bb4be944dc | |||
| 21efee8f44 | |||
| f61549a60f | |||
| 0a7bafd1b2 | |||
| b3987c5fa0 | |||
| 0da043a9fe | |||
| f336f204cb | |||
| 3bfcf18492 | |||
| dfafe8b43d | |||
| b5d43b15f8 | |||
| 2ccab75021 | |||
| 9070df6c26 | |||
| a1c8ad55ad | |||
| 872c05c690 | |||
| a9528dc1b5 | |||
| 0e59ade1f2 | |||
| 5ac49c695d | |||
| 3a30ecbe76 | |||
| 1f838bb2aa | |||
| cc42830e23 | |||
| 593eb959ca | |||
| 5bb6785ad6 | |||
| 535c11a729 | |||
| a0fa8d8524 | |||
| c14025c579 | |||
| 8bc3db7c90 | |||
| eaad564e23 | |||
| 511a94975b | |||
| 015810a2fd | |||
| e70e6b84c2 | |||
| d0b9c9a26f | |||
| 3e403fa348 | |||
| 48f4a971ef | |||
| 6314be14ad | |||
| 1a072c6c39 | |||
| ef2eed0bdf | |||
| 91227b1e96 | |||
| 67d68629da | |||
| e875db8f66 | |||
| 055a76393d | |||
| 0754821628 | |||
| fca88d9896 | |||
| dfe0404c51 | |||
| fa61696b46 | |||
| e5773738f4 | |||
| cac8539d79 | |||
| cf600f6f26 | |||
| e194715c3e | |||
| 787f02d5dc | |||
| a0ed01a610 | |||
| 02ba493759 | |||
| a7fea5434d | |||
| 4fb783e953 | |||
| affbf85699 | |||
| 0d92112a3f | |||
| b1ad3ec9db | |||
| c0601baca6 | |||
| 057c5c5e9a | |||
| 05429ab848 | |||
| b66d51a699 | |||
| f834bc0ff2 | |||
| 93fd883d7a | |||
| 7e080d4d68 | |||
| 3e3ca22d04 | |||
| e741caa6b3 | |||
| 4343246a41 | |||
| 3f6f83b4b6 | |||
| c63e1c9b87 | |||
| f44cf06d22 | |||
| 3f609b8601 | |||
| edd89b44a4 | |||
| 3e58748862 | |||
| 7088a6b0e6 | |||
| 6c880e0e62 | |||
| cb1e70be7f | |||
| 6ba150f737 | |||
| 131769ea73 | |||
| e68adbb30d | |||
| f1eef09099 | |||
| 5ab3c7fa9f | |||
| d0cec39a0f | |||
| e15f53a39a | |||
| 25fb995663 | |||
| eac658c64f | |||
| 15e2032493 | |||
| c87f6cd9d9 | |||
| e758995458 | |||
| 20c284a188 | |||
| b0936ea8f3 | |||
| bfc0f4a413 | |||
| 1a9a90cf6a | |||
| 00f1a6fa48 | |||
| 33754a06d2 | |||
| 69b838e1cf | |||
| d5e04a2301 | |||
| fbf251280f | |||
| eaadf62f01 | |||
| 8c33e7a7c1 | |||
| a7d9a80a28 | |||
| 2ea5dce8d3 | |||
| 14bf01efe4 | |||
| 67b24a60e4 | |||
| e6775297cb | |||
| 4e4e2b36b6 | |||
| 3189c56fc3 | |||
| 5b5ea47b7a | |||
| caa382f898 | |||
| 2d63488197 | |||
| c1c8e4c8d4 | |||
| a0e451c5e5 | |||
| eaba8006e6 | |||
| 39ff202f8c | |||
| 654e0d6245 | |||
| ec04443493 | |||
| d247c262af | |||
| dff49b2bef | |||
| 50666a76fb | |||
| b51a7f9746 | |||
| 001dfd9f6c | |||
| 5e4fbeeb25 | |||
| 2c910bf6ca | |||
| 9b11319e81 | |||
| 40dc4b3fb8 | |||
| 0e37b98968 | |||
| 7e132eb014 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2022.1.4 | current_version = 2022.2.1 | ||||||
| tag = True | tag = True | ||||||
| commit = True | commit = True | ||||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) | parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*) | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/ci-main.yml
									
									
									
									
										vendored
									
									
								
							| @ -86,10 +86,9 @@ jobs: | |||||||
|           cp authentik/lib/default.yml local.env.yml |           cp authentik/lib/default.yml local.env.yml | ||||||
|           cp -R .github .. |           cp -R .github .. | ||||||
|           cp -R scripts .. |           cp -R scripts .. | ||||||
|           cp -R poetry.lock pyproject.toml .. |  | ||||||
|           git checkout $(git describe --abbrev=0 --match 'version/*') |           git checkout $(git describe --abbrev=0 --match 'version/*') | ||||||
|           rm -rf .github/ scripts/ |           rm -rf .github/ scripts/ | ||||||
|           mv ../.github ../scripts ../poetry.lock ../pyproject.toml . |           mv ../.github ../scripts . | ||||||
|       - name: prepare |       - name: prepare | ||||||
|         env: |         env: | ||||||
|           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} |           INSTALL: ${{ steps.cache-poetry.outputs.cache-hit }} | ||||||
|  | |||||||
							
								
								
									
										14
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/release-publish.yml
									
									
									
									
										vendored
									
									
								
							| @ -30,14 +30,14 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           push: ${{ github.event_name == 'release' }} |           push: ${{ github.event_name == 'release' }} | ||||||
|           tags: | |           tags: | | ||||||
|             beryju/authentik:2022.1.4, |             beryju/authentik:2022.2.1, | ||||||
|             beryju/authentik:latest, |             beryju/authentik:latest, | ||||||
|             ghcr.io/goauthentik/server:2022.1.4, |             ghcr.io/goauthentik/server:2022.2.1, | ||||||
|             ghcr.io/goauthentik/server:latest |             ghcr.io/goauthentik/server:latest | ||||||
|           platforms: linux/amd64,linux/arm64 |           platforms: linux/amd64,linux/arm64 | ||||||
|           context: . |           context: . | ||||||
|       - name: Building Docker Image (stable) |       - name: Building Docker Image (stable) | ||||||
|         if: ${{ github.event_name == 'release' && !contains('2022.1.4', 'rc') }} |         if: ${{ github.event_name == 'release' && !contains('2022.2.1', 'rc') }} | ||||||
|         run: | |         run: | | ||||||
|           docker pull beryju/authentik:latest |           docker pull beryju/authentik:latest | ||||||
|           docker tag beryju/authentik:latest beryju/authentik:stable |           docker tag beryju/authentik:latest beryju/authentik:stable | ||||||
| @ -78,14 +78,14 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           push: ${{ github.event_name == 'release' }} |           push: ${{ github.event_name == 'release' }} | ||||||
|           tags: | |           tags: | | ||||||
|             beryju/authentik-${{ matrix.type }}:2022.1.4, |             beryju/authentik-${{ matrix.type }}:2022.2.1, | ||||||
|             beryju/authentik-${{ matrix.type }}:latest, |             beryju/authentik-${{ matrix.type }}:latest, | ||||||
|             ghcr.io/goauthentik/${{ matrix.type }}:2022.1.4, |             ghcr.io/goauthentik/${{ matrix.type }}:2022.2.1, | ||||||
|             ghcr.io/goauthentik/${{ matrix.type }}:latest |             ghcr.io/goauthentik/${{ matrix.type }}:latest | ||||||
|           file: ${{ matrix.type }}.Dockerfile |           file: ${{ matrix.type }}.Dockerfile | ||||||
|           platforms: linux/amd64,linux/arm64 |           platforms: linux/amd64,linux/arm64 | ||||||
|       - name: Building Docker Image (stable) |       - name: Building Docker Image (stable) | ||||||
|         if: ${{ github.event_name == 'release' && !contains('2022.1.4', 'rc') }} |         if: ${{ github.event_name == 'release' && !contains('2022.2.1', 'rc') }} | ||||||
|         run: | |         run: | | ||||||
|           docker pull beryju/authentik-${{ matrix.type }}:latest |           docker pull beryju/authentik-${{ matrix.type }}:latest | ||||||
|           docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable |           docker tag beryju/authentik-${{ matrix.type }}:latest beryju/authentik-${{ matrix.type }}:stable | ||||||
| @ -170,7 +170,7 @@ jobs: | |||||||
|           SENTRY_PROJECT: authentik |           SENTRY_PROJECT: authentik | ||||||
|           SENTRY_URL: https://sentry.beryju.org |           SENTRY_URL: https://sentry.beryju.org | ||||||
|         with: |         with: | ||||||
|           version: authentik@2022.1.4 |           version: authentik@2022.2.1 | ||||||
|           environment: beryjuorg-prod |           environment: beryjuorg-prod | ||||||
|           sourcemaps: './web/dist' |           sourcemaps: './web/dist' | ||||||
|           url_prefix: '~/static/dist' |           url_prefix: '~/static/dist' | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release-tag.yml
									
									
									
									
										vendored
									
									
								
							| @ -27,7 +27,7 @@ jobs: | |||||||
|           docker-compose run -u root server test |           docker-compose run -u root server test | ||||||
|       - name: Extract version number |       - name: Extract version number | ||||||
|         id: get_version |         id: get_version | ||||||
|         uses: actions/github-script@v5 |         uses: actions/github-script@v6 | ||||||
|         with: |         with: | ||||||
|           github-token: ${{ secrets.GITHUB_TOKEN }} |           github-token: ${{ secrets.GITHUB_TOKEN }} | ||||||
|           script: | |           script: | | ||||||
|  | |||||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							| @ -12,7 +12,8 @@ | |||||||
|         "totp", |         "totp", | ||||||
|         "webauthn", |         "webauthn", | ||||||
|         "traefik", |         "traefik", | ||||||
|         "passwordless" |         "passwordless", | ||||||
|  |         "kubernetes" | ||||||
|     ], |     ], | ||||||
|     "python.linting.pylintEnabled": true, |     "python.linting.pylintEnabled": true, | ||||||
|     "todo-tree.tree.showCountsInTree": true, |     "todo-tree.tree.showCountsInTree": true, | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ ENV NODE_ENV=production | |||||||
| RUN cd /work/web && npm i && npm run build | RUN cd /work/web && npm i && npm run build | ||||||
|  |  | ||||||
| # Stage 3: Build go proxy | # Stage 3: Build go proxy | ||||||
| FROM docker.io/golang:1.17.6-bullseye AS builder | FROM docker.io/golang:1.17.7-bullseye AS builder | ||||||
|  |  | ||||||
| WORKDIR /work | WORKDIR /work | ||||||
|  |  | ||||||
|  | |||||||
| @ -6,8 +6,8 @@ | |||||||
|  |  | ||||||
| | Version    | Supported          | | | Version    | Supported          | | ||||||
| | ---------- | ------------------ | | | ---------- | ------------------ | | ||||||
| | 2021.10.x  | :white_check_mark: | | | 2022.1.x   | :white_check_mark: | | ||||||
| | 2021.12.x  | :white_check_mark: | | | 2022.2.x   | :white_check_mark: | | ||||||
|  |  | ||||||
| ## Reporting a Vulnerability | ## Reporting a Vulnerability | ||||||
|  |  | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ | |||||||
| from os import environ | from os import environ | ||||||
| from typing import Optional | from typing import Optional | ||||||
|  |  | ||||||
| __version__ = "2022.1.4" | __version__ = "2022.2.1" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -12,10 +12,13 @@ from rest_framework.permissions import IsAdminUser | |||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.viewsets import ViewSet | from rest_framework.viewsets import ViewSet | ||||||
|  | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.api.utils import PassiveSerializer | from authentik.core.api.utils import PassiveSerializer | ||||||
| from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus | from authentik.events.monitored_tasks import TaskInfo, TaskResultStatus | ||||||
|  |  | ||||||
|  | LOGGER = get_logger() | ||||||
|  |  | ||||||
|  |  | ||||||
| class TaskSerializer(PassiveSerializer): | class TaskSerializer(PassiveSerializer): | ||||||
|     """Serialize TaskInfo and TaskResult""" |     """Serialize TaskInfo and TaskResult""" | ||||||
| @ -89,6 +92,7 @@ class TaskViewSet(ViewSet): | |||||||
|         try: |         try: | ||||||
|             task_module = import_module(task.task_call_module) |             task_module = import_module(task.task_call_module) | ||||||
|             task_func = getattr(task_module, task.task_call_func) |             task_func = getattr(task_module, task.task_call_func) | ||||||
|  |             LOGGER.debug("Running task", task=task_func) | ||||||
|             task_func.delay(*task.task_call_args, **task.task_call_kwargs) |             task_func.delay(*task.task_call_args, **task.task_call_kwargs) | ||||||
|             messages.success( |             messages.success( | ||||||
|                 self.request, |                 self.request, | ||||||
| @ -96,6 +100,7 @@ class TaskViewSet(ViewSet): | |||||||
|             ) |             ) | ||||||
|             return Response(status=204) |             return Response(status=204) | ||||||
|         except (ImportError, AttributeError):  # pragma: no cover |         except (ImportError, AttributeError):  # pragma: no cover | ||||||
|  |             LOGGER.warning("Failed to run task, remove state", task=task) | ||||||
|             # if we get an import error, the module path has probably changed |             # if we get an import error, the module path has probably changed | ||||||
|             task.delete() |             task.delete() | ||||||
|             return Response(status=500) |             return Response(status=500) | ||||||
|  | |||||||
| @ -1,10 +1,9 @@ | |||||||
| """core Configs API""" | """core Configs API""" | ||||||
| from os import environ, path | from os import path | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.db import models | from django.db import models | ||||||
| from drf_spectacular.utils import extend_schema | from drf_spectacular.utils import extend_schema | ||||||
| from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME |  | ||||||
| from rest_framework.fields import ( | from rest_framework.fields import ( | ||||||
|     BooleanField, |     BooleanField, | ||||||
|     CharField, |     CharField, | ||||||
| @ -28,7 +27,6 @@ class Capabilities(models.TextChoices): | |||||||
|  |  | ||||||
|     CAN_SAVE_MEDIA = "can_save_media" |     CAN_SAVE_MEDIA = "can_save_media" | ||||||
|     CAN_GEO_IP = "can_geo_ip" |     CAN_GEO_IP = "can_geo_ip" | ||||||
|     CAN_BACKUP = "can_backup" |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class ErrorReportingConfigSerializer(PassiveSerializer): | class ErrorReportingConfigSerializer(PassiveSerializer): | ||||||
| @ -65,13 +63,6 @@ class ConfigView(APIView): | |||||||
|             caps.append(Capabilities.CAN_SAVE_MEDIA) |             caps.append(Capabilities.CAN_SAVE_MEDIA) | ||||||
|         if GEOIP_READER.enabled: |         if GEOIP_READER.enabled: | ||||||
|             caps.append(Capabilities.CAN_GEO_IP) |             caps.append(Capabilities.CAN_GEO_IP) | ||||||
|         if SERVICE_HOST_ENV_NAME in environ: |  | ||||||
|             # Running in k8s, only s3 backup is supported |  | ||||||
|             if CONFIG.y("postgresql.s3_backup"): |  | ||||||
|                 caps.append(Capabilities.CAN_BACKUP) |  | ||||||
|         else: |  | ||||||
|             # Running in compose, backup is always supported |  | ||||||
|             caps.append(Capabilities.CAN_BACKUP) |  | ||||||
|         return caps |         return caps | ||||||
|  |  | ||||||
|     @extend_schema(responses={200: ConfigSerializer(many=False)}) |     @extend_schema(responses={200: ConfigSerializer(many=False)}) | ||||||
|  | |||||||
| @ -1,13 +1,16 @@ | |||||||
| """Application API Views""" | """Application API Views""" | ||||||
|  | from typing import Optional | ||||||
|  |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.db.models import QuerySet | from django.db.models import QuerySet | ||||||
| from django.http.response import HttpResponseBadRequest | from django.http.response import HttpResponseBadRequest | ||||||
| from django.shortcuts import get_object_or_404 | from django.shortcuts import get_object_or_404 | ||||||
|  | from django.utils.functional import SimpleLazyObject | ||||||
| from drf_spectacular.types import OpenApiTypes | from drf_spectacular.types import OpenApiTypes | ||||||
| from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema | from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema | ||||||
| from guardian.shortcuts import get_objects_for_user | from guardian.shortcuts import get_objects_for_user | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.fields import ReadOnlyField | from rest_framework.fields import ReadOnlyField, SerializerMethodField | ||||||
| from rest_framework.parsers import MultiPartParser | from rest_framework.parsers import MultiPartParser | ||||||
| from rest_framework.request import Request | from rest_framework.request import Request | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| @ -39,11 +42,26 @@ def user_app_cache_key(user_pk: str) -> str: | |||||||
| class ApplicationSerializer(ModelSerializer): | class ApplicationSerializer(ModelSerializer): | ||||||
|     """Application Serializer""" |     """Application Serializer""" | ||||||
|  |  | ||||||
|     launch_url = ReadOnlyField(source="get_launch_url") |     launch_url = SerializerMethodField() | ||||||
|     provider_obj = ProviderSerializer(source="get_provider", required=False) |     provider_obj = ProviderSerializer(source="get_provider", required=False) | ||||||
|  |  | ||||||
|     meta_icon = ReadOnlyField(source="get_meta_icon") |     meta_icon = ReadOnlyField(source="get_meta_icon") | ||||||
|  |  | ||||||
|  |     def get_launch_url(self, app: Application) -> Optional[str]: | ||||||
|  |         """Allow formatting of launch URL""" | ||||||
|  |         url = app.get_launch_url() | ||||||
|  |         if not url: | ||||||
|  |             return url | ||||||
|  |         user = self.context["request"].user | ||||||
|  |         if isinstance(user, SimpleLazyObject): | ||||||
|  |             user._setup() | ||||||
|  |             user = user._wrapped | ||||||
|  |         try: | ||||||
|  |             return url % user.__dict__ | ||||||
|  |         except ValueError as exc: | ||||||
|  |             LOGGER.warning("Failed to format launch url", exc=exc) | ||||||
|  |             return url | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = Application |         model = Application | ||||||
|  | |||||||
| @ -1,17 +1,7 @@ | |||||||
| """authentik core tasks""" | """authentik core tasks""" | ||||||
| from datetime import datetime |  | ||||||
| from io import StringIO |  | ||||||
| from os import environ |  | ||||||
|  |  | ||||||
| from boto3.exceptions import Boto3Error |  | ||||||
| from botocore.exceptions import BotoCoreError, ClientError |  | ||||||
| from dbbackup.db.exceptions import CommandConnectorError |  | ||||||
| from django.contrib.humanize.templatetags.humanize import naturaltime |  | ||||||
| from django.contrib.sessions.backends.cache import KEY_PREFIX | from django.contrib.sessions.backends.cache import KEY_PREFIX | ||||||
| from django.core import management |  | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.utils.timezone import now | from django.utils.timezone import now | ||||||
| from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME |  | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.core.models import AuthenticatedSession, ExpiringModel | from authentik.core.models import AuthenticatedSession, ExpiringModel | ||||||
| @ -21,7 +11,6 @@ from authentik.events.monitored_tasks import ( | |||||||
|     TaskResultStatus, |     TaskResultStatus, | ||||||
|     prefill_task, |     prefill_task, | ||||||
| ) | ) | ||||||
| from authentik.lib.config import CONFIG |  | ||||||
| from authentik.root.celery import CELERY_APP | from authentik.root.celery import CELERY_APP | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
| @ -53,46 +42,3 @@ def clean_expired_models(self: MonitoredTask): | |||||||
|     LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount) |     LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount) | ||||||
|     messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}") |     messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}") | ||||||
|     self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages)) |     self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def should_backup() -> bool: |  | ||||||
|     """Check if we should be doing backups""" |  | ||||||
|     if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup.bucket"): |  | ||||||
|         LOGGER.info("Running in k8s and s3 backups are not configured, skipping") |  | ||||||
|         return False |  | ||||||
|     if not CONFIG.y_bool("postgresql.backup.enabled"): |  | ||||||
|         return False |  | ||||||
|     return True |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @CELERY_APP.task(bind=True, base=MonitoredTask) |  | ||||||
| @prefill_task |  | ||||||
| def backup_database(self: MonitoredTask):  # pragma: no cover |  | ||||||
|     """Database backup""" |  | ||||||
|     self.result_timeout_hours = 25 |  | ||||||
|     if not should_backup(): |  | ||||||
|         self.set_status(TaskResult(TaskResultStatus.UNKNOWN, ["Backups are not configured."])) |  | ||||||
|         return |  | ||||||
|     try: |  | ||||||
|         start = datetime.now() |  | ||||||
|         out = StringIO() |  | ||||||
|         management.call_command("dbbackup", quiet=True, stdout=out) |  | ||||||
|         self.set_status( |  | ||||||
|             TaskResult( |  | ||||||
|                 TaskResultStatus.SUCCESSFUL, |  | ||||||
|                 [ |  | ||||||
|                     f"Successfully finished database backup {naturaltime(start)} {out.getvalue()}", |  | ||||||
|                 ], |  | ||||||
|             ) |  | ||||||
|         ) |  | ||||||
|         LOGGER.info("Successfully backed up database.") |  | ||||||
|     except ( |  | ||||||
|         IOError, |  | ||||||
|         BotoCoreError, |  | ||||||
|         ClientError, |  | ||||||
|         Boto3Error, |  | ||||||
|         PermissionError, |  | ||||||
|         CommandConnectorError, |  | ||||||
|         ValueError, |  | ||||||
|     ) as exc: |  | ||||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) |  | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ | |||||||
|         {% block head_before %} |         {% block head_before %} | ||||||
|         {% endblock %} |         {% endblock %} | ||||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> |         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> | ||||||
|  |         <link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}"> | ||||||
|         <script src="{% static 'dist/poly.js' %}" type="module"></script> |         <script src="{% static 'dist/poly.js' %}" type="module"></script> | ||||||
|         {% block head %} |         {% block head %} | ||||||
|         {% endblock %} |         {% endblock %} | ||||||
|  | |||||||
| @ -13,7 +13,9 @@ class TestApplicationsAPI(APITestCase): | |||||||
|  |  | ||||||
|     def setUp(self) -> None: |     def setUp(self) -> None: | ||||||
|         self.user = create_test_admin_user() |         self.user = create_test_admin_user() | ||||||
|         self.allowed = Application.objects.create(name="allowed", slug="allowed") |         self.allowed = Application.objects.create( | ||||||
|  |             name="allowed", slug="allowed", meta_launch_url="https://goauthentik.io/%(username)s" | ||||||
|  |         ) | ||||||
|         self.denied = Application.objects.create(name="denied", slug="denied") |         self.denied = Application.objects.create(name="denied", slug="denied") | ||||||
|         PolicyBinding.objects.create( |         PolicyBinding.objects.create( | ||||||
|             target=self.denied, |             target=self.denied, | ||||||
| @ -64,8 +66,8 @@ class TestApplicationsAPI(APITestCase): | |||||||
|                         "slug": "allowed", |                         "slug": "allowed", | ||||||
|                         "provider": None, |                         "provider": None, | ||||||
|                         "provider_obj": None, |                         "provider_obj": None, | ||||||
|                         "launch_url": None, |                         "launch_url": f"https://goauthentik.io/{self.user.username}", | ||||||
|                         "meta_launch_url": "", |                         "meta_launch_url": "https://goauthentik.io/%(username)s", | ||||||
|                         "meta_icon": None, |                         "meta_icon": None, | ||||||
|                         "meta_description": "", |                         "meta_description": "", | ||||||
|                         "meta_publisher": "", |                         "meta_publisher": "", | ||||||
| @ -100,8 +102,8 @@ class TestApplicationsAPI(APITestCase): | |||||||
|                         "slug": "allowed", |                         "slug": "allowed", | ||||||
|                         "provider": None, |                         "provider": None, | ||||||
|                         "provider_obj": None, |                         "provider_obj": None, | ||||||
|                         "launch_url": None, |                         "launch_url": f"https://goauthentik.io/{self.user.username}", | ||||||
|                         "meta_launch_url": "", |                         "meta_launch_url": "https://goauthentik.io/%(username)s", | ||||||
|                         "meta_icon": None, |                         "meta_icon": None, | ||||||
|                         "meta_description": "", |                         "meta_description": "", | ||||||
|                         "meta_publisher": "", |                         "meta_publisher": "", | ||||||
|  | |||||||
| @ -1,7 +1,5 @@ | |||||||
| """events GeoIP Reader""" | """events GeoIP Reader""" | ||||||
| from datetime import datetime |  | ||||||
| from os import stat | from os import stat | ||||||
| from time import time |  | ||||||
| from typing import Optional, TypedDict | from typing import Optional, TypedDict | ||||||
|  |  | ||||||
| from geoip2.database import Reader | from geoip2.database import Reader | ||||||
| @ -46,14 +44,18 @@ class GeoIPReader: | |||||||
|             LOGGER.warning("Failed to load GeoIP database", exc=exc) |             LOGGER.warning("Failed to load GeoIP database", exc=exc) | ||||||
|  |  | ||||||
|     def __check_expired(self): |     def __check_expired(self): | ||||||
|         """Check if the geoip database has been opened longer than 8 hours, |         """Check if the modification date of the GeoIP database has | ||||||
|         and re-open it, as it will probably will have been re-downloaded""" |         changed, and reload it if so""" | ||||||
|         now = time() |         path = CONFIG.y("geoip") | ||||||
|         diff = datetime.fromtimestamp(now) - datetime.fromtimestamp(self.__last_mtime) |         try: | ||||||
|         diff_hours = diff.total_seconds() // 3600 |             mtime = stat(path).st_mtime | ||||||
|         if diff_hours >= 8: |             diff = self.__last_mtime < mtime | ||||||
|             LOGGER.info("GeoIP databased loaded too long, re-opening", diff=diff) |             if diff > 0: | ||||||
|  |                 LOGGER.info("Found new GeoIP Database, reopening", diff=diff) | ||||||
|                 self.__open() |                 self.__open() | ||||||
|  |         except OSError as exc: | ||||||
|  |             LOGGER.warning("Failed to check GeoIP age", exc=exc) | ||||||
|  |             return | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def enabled(self) -> bool: |     def enabled(self) -> bool: | ||||||
|  | |||||||
| @ -5,16 +5,6 @@ postgresql: | |||||||
|   user: authentik |   user: authentik | ||||||
|   port: 5432 |   port: 5432 | ||||||
|   password: 'env://POSTGRES_PASSWORD' |   password: 'env://POSTGRES_PASSWORD' | ||||||
|   backup: |  | ||||||
|     enabled: false |  | ||||||
|   s3_backup: |  | ||||||
|     access_key: "" |  | ||||||
|     secret_key: "" |  | ||||||
|     bucket: "" |  | ||||||
|     region: eu-central-1 |  | ||||||
|     host: "" |  | ||||||
|     location: "" |  | ||||||
|     insecure_skip_verify: false |  | ||||||
|  |  | ||||||
| web: | web: | ||||||
|   listen: 0.0.0.0:9000 |   listen: 0.0.0.0:9000 | ||||||
| @ -65,6 +55,7 @@ outposts: | |||||||
|   # %(version)s: Current version; 2021.4.1 |   # %(version)s: Current version; 2021.4.1 | ||||||
|   # %(build_hash)s: Build hash if you're running a beta version |   # %(build_hash)s: Build hash if you're running a beta version | ||||||
|   container_image_base: ghcr.io/goauthentik/%(type)s:%(version)s |   container_image_base: ghcr.io/goauthentik/%(type)s:%(version)s | ||||||
|  |   discover: true | ||||||
|  |  | ||||||
| cookie_domain: null | cookie_domain: null | ||||||
| disable_update_check: false | disable_update_check: false | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								authentik/lib/merge.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								authentik/lib/merge.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | """merge utils""" | ||||||
|  | from deepmerge import Merger | ||||||
|  |  | ||||||
|  | MERGE_LIST_UNIQUE = Merger( | ||||||
|  |     [(list, ["append_unique"]), (dict, ["merge"]), (set, ["union"])], ["override"], ["override"] | ||||||
|  | ) | ||||||
| @ -3,8 +3,6 @@ from typing import Optional | |||||||
|  |  | ||||||
| from aioredis.errors import ConnectionClosedError, ReplyError | from aioredis.errors import ConnectionClosedError, ReplyError | ||||||
| from billiard.exceptions import SoftTimeLimitExceeded, WorkerLostError | from billiard.exceptions import SoftTimeLimitExceeded, WorkerLostError | ||||||
| from botocore.client import ClientError |  | ||||||
| from botocore.exceptions import BotoCoreError |  | ||||||
| from celery.exceptions import CeleryError | from celery.exceptions import CeleryError | ||||||
| from channels.middleware import BaseMiddleware | from channels.middleware import BaseMiddleware | ||||||
| from channels_redis.core import ChannelFull | from channels_redis.core import ChannelFull | ||||||
| @ -81,9 +79,6 @@ def before_send(event: dict, hint: dict) -> Optional[dict]: | |||||||
|         WorkerLostError, |         WorkerLostError, | ||||||
|         CeleryError, |         CeleryError, | ||||||
|         SoftTimeLimitExceeded, |         SoftTimeLimitExceeded, | ||||||
|         # S3 errors |  | ||||||
|         BotoCoreError, |  | ||||||
|         ClientError, |  | ||||||
|         # custom baseclass |         # custom baseclass | ||||||
|         SentryIgnoredException, |         SentryIgnoredException, | ||||||
|         # ldap errors |         # ldap errors | ||||||
| @ -101,8 +96,6 @@ def before_send(event: dict, hint: dict) -> Optional[dict]: | |||||||
|             return None |             return None | ||||||
|     if "logger" in event: |     if "logger" in event: | ||||||
|         if event["logger"] in [ |         if event["logger"] in [ | ||||||
|             "dbbackup", |  | ||||||
|             "botocore", |  | ||||||
|             "kombu", |             "kombu", | ||||||
|             "asyncio", |             "asyncio", | ||||||
|             "multiprocessing", |             "multiprocessing", | ||||||
|  | |||||||
| @ -55,6 +55,10 @@ class OutpostConsumer(AuthJsonConsumer): | |||||||
|  |  | ||||||
|     first_msg = False |     first_msg = False | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |         self.logger = get_logger() | ||||||
|  |  | ||||||
|     def connect(self): |     def connect(self): | ||||||
|         super().connect() |         super().connect() | ||||||
|         uuid = self.scope["url_route"]["kwargs"]["pk"] |         uuid = self.scope["url_route"]["kwargs"]["pk"] | ||||||
| @ -65,7 +69,7 @@ class OutpostConsumer(AuthJsonConsumer): | |||||||
|         ) |         ) | ||||||
|         if not outpost: |         if not outpost: | ||||||
|             raise DenyConnection() |             raise DenyConnection() | ||||||
|         self.logger = get_logger().bind(outpost=outpost) |         self.logger = self.logger.bind(outpost=outpost) | ||||||
|         try: |         try: | ||||||
|             self.accept() |             self.accept() | ||||||
|         except RuntimeError as exc: |         except RuntimeError as exc: | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
|  |  | ||||||
| from kubernetes.client.models.v1_container_port import V1ContainerPort | from kubernetes.client.models.v1_container_port import V1ContainerPort | ||||||
|  | from kubernetes.client.models.v1_service_port import V1ServicePort | ||||||
| from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME | from kubernetes.config.incluster_config import SERVICE_TOKEN_FILENAME | ||||||
|  |  | ||||||
| from authentik.outposts.controllers.k8s.triggers import NeedsRecreate | from authentik.outposts.controllers.k8s.triggers import NeedsRecreate | ||||||
| @ -16,10 +17,31 @@ def get_namespace() -> str: | |||||||
|     return "default" |     return "default" | ||||||
|  |  | ||||||
|  |  | ||||||
| def compare_ports(current: list[V1ContainerPort], reference: list[V1ContainerPort]): | def compare_port( | ||||||
|  |     current: V1ServicePort | V1ContainerPort, reference: V1ServicePort | V1ContainerPort | ||||||
|  | ) -> bool: | ||||||
|  |     """Compare a single port""" | ||||||
|  |     if current.name != reference.name: | ||||||
|  |         return False | ||||||
|  |     if current.protocol != reference.protocol: | ||||||
|  |         return False | ||||||
|  |     if isinstance(current, V1ServicePort) and isinstance(reference, V1ServicePort): | ||||||
|  |         # We only care about the target port | ||||||
|  |         if current.target_port != reference.target_port: | ||||||
|  |             return False | ||||||
|  |     if isinstance(current, V1ContainerPort) and isinstance(reference, V1ContainerPort): | ||||||
|  |         # We only care about the target port | ||||||
|  |         if current.container_port != reference.container_port: | ||||||
|  |             return False | ||||||
|  |     return True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def compare_ports( | ||||||
|  |     current: list[V1ServicePort | V1ContainerPort], reference: list[V1ServicePort | V1ContainerPort] | ||||||
|  | ): | ||||||
|     """Compare ports of a list""" |     """Compare ports of a list""" | ||||||
|     if len(current) != len(reference): |     if len(current) != len(reference): | ||||||
|         raise NeedsRecreate() |         raise NeedsRecreate() | ||||||
|     for port in reference: |     for port in reference: | ||||||
|         if port not in current: |         if not any(compare_port(port, current_port) for current_port in current): | ||||||
|             raise NeedsRecreate() |             raise NeedsRecreate() | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ import os | |||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from tempfile import gettempdir | from tempfile import gettempdir | ||||||
|  |  | ||||||
|  | from docker.errors import DockerException | ||||||
|  |  | ||||||
| from authentik.crypto.models import CertificateKeyPair | from authentik.crypto.models import CertificateKeyPair | ||||||
|  |  | ||||||
| HEADER = "### Managed by authentik" | HEADER = "### Managed by authentik" | ||||||
| @ -27,6 +29,8 @@ class DockerInlineSSH: | |||||||
|     def __init__(self, host: str, keypair: CertificateKeyPair) -> None: |     def __init__(self, host: str, keypair: CertificateKeyPair) -> None: | ||||||
|         self.host = host |         self.host = host | ||||||
|         self.keypair = keypair |         self.keypair = keypair | ||||||
|  |         if not self.keypair: | ||||||
|  |             raise DockerException("keypair must be set for SSH connections") | ||||||
|         self.config_path = Path("~/.ssh/config").expanduser() |         self.config_path = Path("~/.ssh/config").expanduser() | ||||||
|         self.header = f"{HEADER} - {self.host}\n" |         self.header = f"{HEADER} - {self.host}\n" | ||||||
|  |  | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ from authentik.events.monitored_tasks import ( | |||||||
|     TaskResultStatus, |     TaskResultStatus, | ||||||
|     prefill_task, |     prefill_task, | ||||||
| ) | ) | ||||||
|  | from authentik.lib.config import CONFIG | ||||||
| from authentik.lib.utils.reflection import path_to_class | from authentik.lib.utils.reflection import path_to_class | ||||||
| from authentik.outposts.controllers.base import BaseController, ControllerException | from authentik.outposts.controllers.base import BaseController, ControllerException | ||||||
| from authentik.outposts.controllers.docker import DockerClient | from authentik.outposts.controllers.docker import DockerClient | ||||||
| @ -231,6 +232,9 @@ def _outpost_single_update(outpost: Outpost, layer=None): | |||||||
| @CELERY_APP.task() | @CELERY_APP.task() | ||||||
| def outpost_local_connection(): | def outpost_local_connection(): | ||||||
|     """Checks the local environment and create Service connections.""" |     """Checks the local environment and create Service connections.""" | ||||||
|  |     if not CONFIG.y_bool("outposts.discover"): | ||||||
|  |         LOGGER.debug("outpost integration discovery is disabled") | ||||||
|  |         return | ||||||
|     # Explicitly check against token filename, as that's |     # Explicitly check against token filename, as that's | ||||||
|     # only present when the integration is enabled |     # only present when the integration is enabled | ||||||
|     if Path(SERVICE_TOKEN_FILENAME).exists(): |     if Path(SERVICE_TOKEN_FILENAME).exists(): | ||||||
|  | |||||||
| @ -45,6 +45,13 @@ class GrantTypes(models.TextChoices): | |||||||
|     HYBRID = "hybrid" |     HYBRID = "hybrid" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ResponseMode(models.TextChoices): | ||||||
|  |     """https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#OAuth.Post""" | ||||||
|  |  | ||||||
|  |     QUERY = "query" | ||||||
|  |     FRAGMENT = "fragment" | ||||||
|  |  | ||||||
|  |  | ||||||
| class SubModes(models.TextChoices): | class SubModes(models.TextChoices): | ||||||
|     """Mode after which 'sub' attribute is generateed, for compatibility reasons""" |     """Mode after which 'sub' attribute is generateed, for compatibility reasons""" | ||||||
|  |  | ||||||
|  | |||||||
| @ -43,7 +43,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://local.invalid", |             redirect_uris="http://local.invalid/Foo", | ||||||
|         ) |         ) | ||||||
|         with self.assertRaises(AuthorizeError): |         with self.assertRaises(AuthorizeError): | ||||||
|             request = self.factory.get( |             request = self.factory.get( | ||||||
| @ -51,7 +51,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|                 data={ |                 data={ | ||||||
|                     "response_type": "code", |                     "response_type": "code", | ||||||
|                     "client_id": "test", |                     "client_id": "test", | ||||||
|                     "redirect_uri": "http://local.invalid", |                     "redirect_uri": "http://local.invalid/Foo", | ||||||
|                     "request": "foo", |                     "request": "foo", | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
| @ -105,26 +105,30 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             name="test", |             name="test", | ||||||
|             client_id="test", |             client_id="test", | ||||||
|             authorization_flow=create_test_flow(), |             authorization_flow=create_test_flow(), | ||||||
|             redirect_uris="http://local.invalid", |             redirect_uris="http://local.invalid/Foo", | ||||||
|         ) |         ) | ||||||
|         request = self.factory.get( |         request = self.factory.get( | ||||||
|             "/", |             "/", | ||||||
|             data={ |             data={ | ||||||
|                 "response_type": "code", |                 "response_type": "code", | ||||||
|                 "client_id": "test", |                 "client_id": "test", | ||||||
|                 "redirect_uri": "http://local.invalid", |                 "redirect_uri": "http://local.invalid/Foo", | ||||||
|             }, |             }, | ||||||
|         ) |         ) | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             OAuthAuthorizationParams.from_request(request).grant_type, |             OAuthAuthorizationParams.from_request(request).grant_type, | ||||||
|             GrantTypes.AUTHORIZATION_CODE, |             GrantTypes.AUTHORIZATION_CODE, | ||||||
|         ) |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             OAuthAuthorizationParams.from_request(request).redirect_uri, | ||||||
|  |             "http://local.invalid/Foo", | ||||||
|  |         ) | ||||||
|         request = self.factory.get( |         request = self.factory.get( | ||||||
|             "/", |             "/", | ||||||
|             data={ |             data={ | ||||||
|                 "response_type": "id_token", |                 "response_type": "id_token", | ||||||
|                 "client_id": "test", |                 "client_id": "test", | ||||||
|                 "redirect_uri": "http://local.invalid", |                 "redirect_uri": "http://local.invalid/Foo", | ||||||
|                 "scope": "openid", |                 "scope": "openid", | ||||||
|                 "state": "foo", |                 "state": "foo", | ||||||
|             }, |             }, | ||||||
| @ -140,7 +144,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|                 data={ |                 data={ | ||||||
|                     "response_type": "id_token", |                     "response_type": "id_token", | ||||||
|                     "client_id": "test", |                     "client_id": "test", | ||||||
|                     "redirect_uri": "http://local.invalid", |                     "redirect_uri": "http://local.invalid/Foo", | ||||||
|                     "state": "foo", |                     "state": "foo", | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
| @ -153,7 +157,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|             data={ |             data={ | ||||||
|                 "response_type": "code token", |                 "response_type": "code token", | ||||||
|                 "client_id": "test", |                 "client_id": "test", | ||||||
|                 "redirect_uri": "http://local.invalid", |                 "redirect_uri": "http://local.invalid/Foo", | ||||||
|                 "scope": "openid", |                 "scope": "openid", | ||||||
|                 "state": "foo", |                 "state": "foo", | ||||||
|             }, |             }, | ||||||
| @ -167,7 +171,7 @@ class TestAuthorize(OAuthTestCase): | |||||||
|                 data={ |                 data={ | ||||||
|                     "response_type": "invalid", |                     "response_type": "invalid", | ||||||
|                     "client_id": "test", |                     "client_id": "test", | ||||||
|                     "redirect_uri": "http://local.invalid", |                     "redirect_uri": "http://local.invalid/Foo", | ||||||
|                 }, |                 }, | ||||||
|             ) |             ) | ||||||
|             OAuthAuthorizationParams.from_request(request) |             OAuthAuthorizationParams.from_request(request) | ||||||
|  | |||||||
| @ -44,6 +44,7 @@ from authentik.providers.oauth2.models import ( | |||||||
|     AuthorizationCode, |     AuthorizationCode, | ||||||
|     GrantTypes, |     GrantTypes, | ||||||
|     OAuth2Provider, |     OAuth2Provider, | ||||||
|  |     ResponseMode, | ||||||
|     ResponseTypes, |     ResponseTypes, | ||||||
| ) | ) | ||||||
| from authentik.providers.oauth2.utils import HttpResponseRedirectScheme | from authentik.providers.oauth2.utils import HttpResponseRedirectScheme | ||||||
| @ -99,7 +100,7 @@ class OAuthAuthorizationParams: | |||||||
|         # and POST request. |         # and POST request. | ||||||
|         query_dict = request.POST if request.method == "POST" else request.GET |         query_dict = request.POST if request.method == "POST" else request.GET | ||||||
|         state = query_dict.get("state") |         state = query_dict.get("state") | ||||||
|         redirect_uri = query_dict.get("redirect_uri", "").lower() |         redirect_uri = query_dict.get("redirect_uri", "") | ||||||
|  |  | ||||||
|         response_type = query_dict.get("response_type", "") |         response_type = query_dict.get("response_type", "") | ||||||
|         grant_type = None |         grant_type = None | ||||||
| @ -153,7 +154,10 @@ class OAuthAuthorizationParams: | |||||||
|     def check_redirect_uri(self): |     def check_redirect_uri(self): | ||||||
|         """Redirect URI validation.""" |         """Redirect URI validation.""" | ||||||
|         allowed_redirect_urls = self.provider.redirect_uris.split() |         allowed_redirect_urls = self.provider.redirect_uris.split() | ||||||
|         if not self.redirect_uri: |         # We don't want to actually lowercase the final URL we redirect to, | ||||||
|  |         # we only lowercase it for comparison | ||||||
|  |         redirect_uri = self.redirect_uri.lower() | ||||||
|  |         if not redirect_uri: | ||||||
|             LOGGER.warning("Missing redirect uri.") |             LOGGER.warning("Missing redirect uri.") | ||||||
|             raise RedirectUriError("", allowed_redirect_urls) |             raise RedirectUriError("", allowed_redirect_urls) | ||||||
|  |  | ||||||
| @ -169,7 +173,7 @@ class OAuthAuthorizationParams: | |||||||
|                 allow=self.redirect_uri, |                 allow=self.redirect_uri, | ||||||
|             ) |             ) | ||||||
|             return |             return | ||||||
|         if self.redirect_uri not in [x.lower() for x in allowed_redirect_urls]: |         if redirect_uri not in [x.lower() for x in allowed_redirect_urls]: | ||||||
|             LOGGER.warning( |             LOGGER.warning( | ||||||
|                 "Invalid redirect uri", |                 "Invalid redirect uri", | ||||||
|                 redirect_uri=self.redirect_uri, |                 redirect_uri=self.redirect_uri, | ||||||
| @ -299,13 +303,23 @@ class OAuthFulfillmentStage(StageView): | |||||||
|                 code = self.params.create_code(self.request) |                 code = self.params.create_code(self.request) | ||||||
|                 code.save(force_insert=True) |                 code.save(force_insert=True) | ||||||
|  |  | ||||||
|             if self.params.grant_type == GrantTypes.AUTHORIZATION_CODE: |             query_dict = self.request.POST if self.request.method == "POST" else self.request.GET | ||||||
|  |             response_mode = ResponseMode.QUERY | ||||||
|  |             # Get response mode from url param, otherwise decide based on grant type | ||||||
|  |             if "response_mode" in query_dict: | ||||||
|  |                 response_mode = query_dict["response_mode"] | ||||||
|  |             elif self.params.grant_type == GrantTypes.AUTHORIZATION_CODE: | ||||||
|  |                 response_mode = ResponseMode.QUERY | ||||||
|  |             elif self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: | ||||||
|  |                 response_mode = ResponseMode.FRAGMENT | ||||||
|  |  | ||||||
|  |             if response_mode == ResponseMode.QUERY: | ||||||
|                 query_params["code"] = code.code |                 query_params["code"] = code.code | ||||||
|                 query_params["state"] = [str(self.params.state) if self.params.state else ""] |                 query_params["state"] = [str(self.params.state) if self.params.state else ""] | ||||||
|  |  | ||||||
|                 uri = uri._replace(query=urlencode(query_params, doseq=True)) |                 uri = uri._replace(query=urlencode(query_params, doseq=True)) | ||||||
|                 return urlunsplit(uri) |                 return urlunsplit(uri) | ||||||
|             if self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]: |             if response_mode == ResponseMode.FRAGMENT: | ||||||
|                 query_fragment = self.create_implicit_response(code) |                 query_fragment = self.create_implicit_response(code) | ||||||
|  |  | ||||||
|                 uri = uri._replace( |                 uri = uri._replace( | ||||||
|  | |||||||
| @ -12,4 +12,8 @@ class AuthentikProviderProxyConfig(AppConfig): | |||||||
|     verbose_name = "authentik Providers.Proxy" |     verbose_name = "authentik Providers.Proxy" | ||||||
|  |  | ||||||
|     def ready(self) -> None: |     def ready(self) -> None: | ||||||
|  |         from authentik.providers.proxy.tasks import proxy_set_defaults | ||||||
|  |  | ||||||
|         import_module("authentik.providers.proxy.managed") |         import_module("authentik.providers.proxy.managed") | ||||||
|  |  | ||||||
|  |         proxy_set_defaults.delay() | ||||||
|  | |||||||
| @ -28,12 +28,12 @@ class ProxyDockerController(DockerController): | |||||||
|         labels["traefik.enable"] = "true" |         labels["traefik.enable"] = "true" | ||||||
|         labels[ |         labels[ | ||||||
|             f"traefik.http.routers.{traefik_name}-router.rule" |             f"traefik.http.routers.{traefik_name}-router.rule" | ||||||
|         ] = f"Host({','.join(hosts)}) && PathPrefix(`/akprox`)" |         ] = f"Host({','.join(hosts)}) && PathPrefix(`/outpost.goauthentik.io`)" | ||||||
|         labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true" |         labels[f"traefik.http.routers.{traefik_name}-router.tls"] = "true" | ||||||
|         labels[f"traefik.http.routers.{traefik_name}-router.service"] = f"{traefik_name}-service" |         labels[f"traefik.http.routers.{traefik_name}-router.service"] = f"{traefik_name}-service" | ||||||
|         labels[ |         labels[ | ||||||
|             f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path" |             f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.path" | ||||||
|         ] = "/akprox/ping" |         ] = "/outpost.goauthentik.io/ping" | ||||||
|         labels[ |         labels[ | ||||||
|             f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.port" |             f"traefik.http.services.{traefik_name}-service.loadbalancer.healthcheck.port" | ||||||
|         ] = "9300" |         ] = "9300" | ||||||
|  | |||||||
| @ -92,6 +92,8 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]): | |||||||
|             # Buffer sizes for large headers with JWTs |             # Buffer sizes for large headers with JWTs | ||||||
|             "nginx.ingress.kubernetes.io/proxy-buffers-number": "4", |             "nginx.ingress.kubernetes.io/proxy-buffers-number": "4", | ||||||
|             "nginx.ingress.kubernetes.io/proxy-buffer-size": "16k", |             "nginx.ingress.kubernetes.io/proxy-buffer-size": "16k", | ||||||
|  |             # Enable TLS in traefik | ||||||
|  |             "traefik.ingress.kubernetes.io/router.tls": "true", | ||||||
|         } |         } | ||||||
|         annotations.update(self.controller.outpost.config.kubernetes_ingress_annotations) |         annotations.update(self.controller.outpost.config.kubernetes_ingress_annotations) | ||||||
|         return annotations |         return annotations | ||||||
| @ -126,7 +128,7 @@ class IngressReconciler(KubernetesObjectReconciler[V1Ingress]): | |||||||
|                                         port=V1ServiceBackendPort(name="http"), |                                         port=V1ServiceBackendPort(name="http"), | ||||||
|                                     ), |                                     ), | ||||||
|                                 ), |                                 ), | ||||||
|                                 path="/akprox", |                                 path="/outpost.goauthentik.io", | ||||||
|                                 path_type="ImplementationSpecific", |                                 path_type="ImplementationSpecific", | ||||||
|                             ) |                             ) | ||||||
|                         ] |                         ] | ||||||
|  | |||||||
| @ -119,7 +119,10 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]) | |||||||
|             ), |             ), | ||||||
|             spec=TraefikMiddlewareSpec( |             spec=TraefikMiddlewareSpec( | ||||||
|                 forwardAuth=TraefikMiddlewareSpecForwardAuth( |                 forwardAuth=TraefikMiddlewareSpecForwardAuth( | ||||||
|                     address=f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik", |                     address=( | ||||||
|  |                         f"http://{self.name}.{self.namespace}:9000/" | ||||||
|  |                         "outpost.goauthentik.io/auth/traefik" | ||||||
|  |                     ), | ||||||
|                     authResponseHeaders=[ |                     authResponseHeaders=[ | ||||||
|                         "X-authentik-username", |                         "X-authentik-username", | ||||||
|                         "X-authentik-groups", |                         "X-authentik-groups", | ||||||
|  | |||||||
| @ -27,7 +27,7 @@ def get_cookie_secret(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def _get_callback_url(uri: str) -> str: | def _get_callback_url(uri: str) -> str: | ||||||
|     return urljoin(uri, "/akprox/callback") |     return urljoin(uri, "outpost.goauthentik.io/callback") | ||||||
|  |  | ||||||
|  |  | ||||||
| class ProxyMode(models.TextChoices): | class ProxyMode(models.TextChoices): | ||||||
|  | |||||||
							
								
								
									
										11
									
								
								authentik/providers/proxy/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								authentik/providers/proxy/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | """proxy provider tasks""" | ||||||
|  | from authentik.providers.proxy.models import ProxyProvider | ||||||
|  | from authentik.root.celery import CELERY_APP | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @CELERY_APP.task() | ||||||
|  | def proxy_set_defaults(): | ||||||
|  |     """Ensure correct defaults are set for all providers""" | ||||||
|  |     for provider in ProxyProvider.objects.all(): | ||||||
|  |         provider.set_oauth_defaults() | ||||||
|  |         provider.save() | ||||||
| @ -15,6 +15,7 @@ from authentik.providers.saml.processors.request_parser import AuthNRequestParse | |||||||
| from authentik.sources.saml.exceptions import MismatchedRequestID | from authentik.sources.saml.exceptions import MismatchedRequestID | ||||||
| from authentik.sources.saml.models import SAMLSource | from authentik.sources.saml.models import SAMLSource | ||||||
| from authentik.sources.saml.processors.constants import ( | from authentik.sources.saml.processors.constants import ( | ||||||
|  |     SAML_BINDING_REDIRECT, | ||||||
|     SAML_NAME_ID_FORMAT_EMAIL, |     SAML_NAME_ID_FORMAT_EMAIL, | ||||||
|     SAML_NAME_ID_FORMAT_UNSPECIFIED, |     SAML_NAME_ID_FORMAT_UNSPECIFIED, | ||||||
| ) | ) | ||||||
| @ -98,6 +99,9 @@ class TestAuthNRequest(TestCase): | |||||||
|  |  | ||||||
|         # First create an AuthNRequest |         # First create an AuthNRequest | ||||||
|         request_proc = RequestProcessor(self.source, http_request, "test_state") |         request_proc = RequestProcessor(self.source, http_request, "test_state") | ||||||
|  |         auth_n = request_proc.get_auth_n() | ||||||
|  |         self.assertEqual(auth_n.attrib["ProtocolBinding"], SAML_BINDING_REDIRECT) | ||||||
|  |  | ||||||
|         request = request_proc.build_auth_n() |         request = request_proc.build_auth_n() | ||||||
|         # Now we check the ID and signature |         # Now we check the ID and signature | ||||||
|         parsed_request = AuthNRequestParser(self.provider).parse( |         parsed_request = AuthNRequestParser(self.provider).parse( | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ import os | |||||||
| import sys | import sys | ||||||
| from hashlib import sha512 | from hashlib import sha512 | ||||||
| from json import dumps | from json import dumps | ||||||
| from tempfile import gettempdir |  | ||||||
| from time import time | from time import time | ||||||
| from urllib.parse import quote_plus | from urllib.parse import quote_plus | ||||||
|  |  | ||||||
| @ -137,7 +136,6 @@ INSTALLED_APPS = [ | |||||||
|     "guardian", |     "guardian", | ||||||
|     "django_prometheus", |     "django_prometheus", | ||||||
|     "channels", |     "channels", | ||||||
|     "dbbackup", |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| GUARDIAN_MONKEY_PATCH = False | GUARDIAN_MONKEY_PATCH = False | ||||||
| @ -357,32 +355,6 @@ CELERY_RESULT_BACKEND = ( | |||||||
|     f"{_redis_url}/{CONFIG.y('redis.message_queue_db')}{REDIS_CELERY_TLS_REQUIREMENTS}" |     f"{_redis_url}/{CONFIG.y('redis.message_queue_db')}{REDIS_CELERY_TLS_REQUIREMENTS}" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| # Database backup |  | ||||||
| DBBACKUP_STORAGE = "django.core.files.storage.FileSystemStorage" |  | ||||||
| DBBACKUP_STORAGE_OPTIONS = {"location": "./backups" if DEBUG else "/backups"} |  | ||||||
| DBBACKUP_FILENAME_TEMPLATE = f"authentik-backup-{__version__}-{{datetime}}.sql" |  | ||||||
| DBBACKUP_CONNECTOR_MAPPING = { |  | ||||||
|     "django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector", |  | ||||||
| } |  | ||||||
| DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp"  # nosec |  | ||||||
| DBBACKUP_CLEANUP_KEEP = 10 |  | ||||||
| if CONFIG.y("postgresql.s3_backup.bucket", "") != "": |  | ||||||
|     DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" |  | ||||||
|     DBBACKUP_STORAGE_OPTIONS = { |  | ||||||
|         "access_key": CONFIG.y("postgresql.s3_backup.access_key"), |  | ||||||
|         "secret_key": CONFIG.y("postgresql.s3_backup.secret_key"), |  | ||||||
|         "bucket_name": CONFIG.y("postgresql.s3_backup.bucket"), |  | ||||||
|         "region_name": CONFIG.y("postgresql.s3_backup.region", "eu-central-1"), |  | ||||||
|         "default_acl": "private", |  | ||||||
|         "endpoint_url": CONFIG.y("postgresql.s3_backup.host"), |  | ||||||
|         "location": CONFIG.y("postgresql.s3_backup.location", ""), |  | ||||||
|         "verify": not CONFIG.y_bool("postgresql.s3_backup.insecure_skip_verify", False), |  | ||||||
|     } |  | ||||||
|     j_print( |  | ||||||
|         "Database backup to S3 is configured", |  | ||||||
|         host=CONFIG.y("postgresql.s3_backup.host"), |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
| # Sentry integration | # Sentry integration | ||||||
| SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8" | SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8" | ||||||
|  |  | ||||||
| @ -493,12 +465,9 @@ _LOGGING_HANDLER_MAP = { | |||||||
|     "urllib3": "WARNING", |     "urllib3": "WARNING", | ||||||
|     "websockets": "WARNING", |     "websockets": "WARNING", | ||||||
|     "daphne": "WARNING", |     "daphne": "WARNING", | ||||||
|     "dbbackup": "ERROR", |  | ||||||
|     "kubernetes": "INFO", |     "kubernetes": "INFO", | ||||||
|     "asyncio": "WARNING", |     "asyncio": "WARNING", | ||||||
|     "aioredis": "WARNING", |     "aioredis": "WARNING", | ||||||
|     "s3transfer": "WARNING", |  | ||||||
|     "botocore": "WARNING", |  | ||||||
| } | } | ||||||
| for handler_name, level in _LOGGING_HANDLER_MAP.items(): | for handler_name, level in _LOGGING_HANDLER_MAP.items(): | ||||||
|     # pyright: reportGeneralTypeIssues=false |     # pyright: reportGeneralTypeIssues=false | ||||||
|  | |||||||
| @ -1,13 +1,13 @@ | |||||||
| """Sync LDAP Users and groups into authentik""" | """Sync LDAP Users and groups into authentik""" | ||||||
| from typing import Any | from typing import Any | ||||||
|  |  | ||||||
| from deepmerge import always_merger |  | ||||||
| from django.db.models.base import Model | from django.db.models.base import Model | ||||||
| from django.db.models.query import QuerySet | from django.db.models.query import QuerySet | ||||||
| from structlog.stdlib import BoundLogger, get_logger | from structlog.stdlib import BoundLogger, get_logger | ||||||
|  |  | ||||||
| from authentik.core.exceptions import PropertyMappingExpressionException | from authentik.core.exceptions import PropertyMappingExpressionException | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
|  | from authentik.lib.merge import MERGE_LIST_UNIQUE | ||||||
| from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME | from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME | ||||||
| from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource | from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource | ||||||
|  |  | ||||||
| @ -123,8 +123,8 @@ class BaseLDAPSynchronizer: | |||||||
|                 continue |                 continue | ||||||
|             setattr(instance, key, value) |             setattr(instance, key, value) | ||||||
|         final_atttributes = {} |         final_atttributes = {} | ||||||
|         always_merger.merge(final_atttributes, instance.attributes) |         MERGE_LIST_UNIQUE.merge(final_atttributes, instance.attributes) | ||||||
|         always_merger.merge(final_atttributes, data.get("attributes", {})) |         MERGE_LIST_UNIQUE.merge(final_atttributes, data.get("attributes", {})) | ||||||
|         instance.attributes = final_atttributes |         instance.attributes = final_atttributes | ||||||
|         instance.save() |         instance.save() | ||||||
|         return (instance, False) |         return (instance, False) | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ from ldap3.core.exceptions import LDAPException | |||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
| from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||||
|  | from authentik.lib.utils.errors import exception_to_string | ||||||
| from authentik.lib.utils.reflection import class_to_path, path_to_class | from authentik.lib.utils.reflection import class_to_path, path_to_class | ||||||
| from authentik.root.celery import CELERY_APP | from authentik.root.celery import CELERY_APP | ||||||
| from authentik.sources.ldap.models import LDAPSource | from authentik.sources.ldap.models import LDAPSource | ||||||
| @ -52,5 +53,5 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str): | |||||||
|         ) |         ) | ||||||
|     except LDAPException as exc: |     except LDAPException as exc: | ||||||
|         # No explicit event is created here as .set_status with an error will do that |         # No explicit event is created here as .set_status with an error will do that | ||||||
|         LOGGER.debug(exc) |         LOGGER.warning(exception_to_string(exc)) | ||||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) |         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||||
|  | |||||||
| @ -18,6 +18,8 @@ from authentik.sources.saml.processors.constants import ( | |||||||
|     RSA_SHA256, |     RSA_SHA256, | ||||||
|     RSA_SHA384, |     RSA_SHA384, | ||||||
|     RSA_SHA512, |     RSA_SHA512, | ||||||
|  |     SAML_BINDING_POST, | ||||||
|  |     SAML_BINDING_REDIRECT, | ||||||
|     SAML_NAME_ID_FORMAT_EMAIL, |     SAML_NAME_ID_FORMAT_EMAIL, | ||||||
|     SAML_NAME_ID_FORMAT_PERSISTENT, |     SAML_NAME_ID_FORMAT_PERSISTENT, | ||||||
|     SAML_NAME_ID_FORMAT_TRANSIENT, |     SAML_NAME_ID_FORMAT_TRANSIENT, | ||||||
| @ -37,6 +39,15 @@ class SAMLBindingTypes(models.TextChoices): | |||||||
|     POST = "POST", _("POST Binding") |     POST = "POST", _("POST Binding") | ||||||
|     POST_AUTO = "POST_AUTO", _("POST Binding with auto-confirmation") |     POST_AUTO = "POST_AUTO", _("POST Binding with auto-confirmation") | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def uri(self) -> str: | ||||||
|  |         """Convert database field to URI""" | ||||||
|  |         return { | ||||||
|  |             SAMLBindingTypes.POST: SAML_BINDING_POST, | ||||||
|  |             SAMLBindingTypes.POST_AUTO: SAML_BINDING_POST, | ||||||
|  |             SAMLBindingTypes.REDIRECT: SAML_BINDING_REDIRECT, | ||||||
|  |         }[self] | ||||||
|  |  | ||||||
|  |  | ||||||
| class SAMLNameIDPolicy(models.TextChoices): | class SAMLNameIDPolicy(models.TextChoices): | ||||||
|     """SAML NameID Policies""" |     """SAML NameID Policies""" | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ from lxml.etree import Element  # nosec | |||||||
| from authentik.providers.saml.utils import get_random_id | from authentik.providers.saml.utils import get_random_id | ||||||
| from authentik.providers.saml.utils.encoding import deflate_and_base64_encode | from authentik.providers.saml.utils.encoding import deflate_and_base64_encode | ||||||
| from authentik.providers.saml.utils.time import get_time_string | from authentik.providers.saml.utils.time import get_time_string | ||||||
| from authentik.sources.saml.models import SAMLSource | from authentik.sources.saml.models import SAMLBindingTypes, SAMLSource | ||||||
| from authentik.sources.saml.processors.constants import ( | from authentik.sources.saml.processors.constants import ( | ||||||
|     DIGEST_ALGORITHM_TRANSLATION_MAP, |     DIGEST_ALGORITHM_TRANSLATION_MAP, | ||||||
|     NS_MAP, |     NS_MAP, | ||||||
| @ -62,7 +62,7 @@ class RequestProcessor: | |||||||
|         auth_n_request.attrib["Destination"] = self.source.sso_url |         auth_n_request.attrib["Destination"] = self.source.sso_url | ||||||
|         auth_n_request.attrib["ID"] = self.request_id |         auth_n_request.attrib["ID"] = self.request_id | ||||||
|         auth_n_request.attrib["IssueInstant"] = self.issue_instant |         auth_n_request.attrib["IssueInstant"] = self.issue_instant | ||||||
|         auth_n_request.attrib["ProtocolBinding"] = self.source.binding_type |         auth_n_request.attrib["ProtocolBinding"] = SAMLBindingTypes(self.source.binding_type).uri | ||||||
|         auth_n_request.attrib["Version"] = "2.0" |         auth_n_request.attrib["Version"] = "2.0" | ||||||
|         # Create issuer object |         # Create issuer object | ||||||
|         auth_n_request.append(self.get_issuer()) |         auth_n_request.append(self.get_issuer()) | ||||||
|  | |||||||
| @ -13,8 +13,8 @@ class AuthenticatorValidateStageSerializer(StageSerializer): | |||||||
|  |  | ||||||
|     def validate_not_configured_action(self, value): |     def validate_not_configured_action(self, value): | ||||||
|         """Ensure that a configuration stage is set when not_configured_action is configure""" |         """Ensure that a configuration stage is set when not_configured_action is configure""" | ||||||
|         configuration_stage = self.initial_data.get("configuration_stage") |         configuration_stages = self.initial_data.get("configuration_stages") | ||||||
|         if value == NotConfiguredAction.CONFIGURE and configuration_stage is None: |         if value == NotConfiguredAction.CONFIGURE and configuration_stages is None: | ||||||
|             raise ValidationError( |             raise ValidationError( | ||||||
|                 ( |                 ( | ||||||
|                     'When "Not configured action" is set to "Configure", ' |                     'When "Not configured action" is set to "Configure", ' | ||||||
| @ -29,7 +29,7 @@ class AuthenticatorValidateStageSerializer(StageSerializer): | |||||||
|         fields = StageSerializer.Meta.fields + [ |         fields = StageSerializer.Meta.fields + [ | ||||||
|             "not_configured_action", |             "not_configured_action", | ||||||
|             "device_classes", |             "device_classes", | ||||||
|             "configuration_stage", |             "configuration_stages", | ||||||
|         ] |         ] | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -38,5 +38,5 @@ class AuthenticatorValidateStageViewSet(UsedByMixin, ModelViewSet): | |||||||
|  |  | ||||||
|     queryset = AuthenticatorValidateStage.objects.all() |     queryset = AuthenticatorValidateStage.objects.all() | ||||||
|     serializer_class = AuthenticatorValidateStageSerializer |     serializer_class = AuthenticatorValidateStageSerializer | ||||||
|     filterset_fields = ["name", "not_configured_action", "configuration_stage"] |     filterset_fields = ["name", "not_configured_action", "configuration_stages"] | ||||||
|     ordering = ["name"] |     ordering = ["name"] | ||||||
|  | |||||||
| @ -0,0 +1,44 @@ | |||||||
|  | # Generated by Django 4.0.1 on 2022-01-05 22:09 | ||||||
|  |  | ||||||
|  | from django.apps.registry import Apps | ||||||
|  | from django.db import migrations, models | ||||||
|  | from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def migrate_configuration_stage(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||||
|  |     db_alias = schema_editor.connection.alias | ||||||
|  |     AuthenticatorValidateStage = apps.get_model( | ||||||
|  |         "authentik_stages_authenticator_validate", "AuthenticatorValidateStage" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     for stage in AuthenticatorValidateStage.objects.using(db_alias).all(): | ||||||
|  |         if stage.configuration_stage: | ||||||
|  |             stage.configuration_stages.set([stage.configuration_stage]) | ||||||
|  |             stage.save() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_flows", "0021_auto_20211227_2103"), | ||||||
|  |         ("authentik_stages_authenticator_validate", "0009_default_stage"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AddField( | ||||||
|  |             model_name="authenticatorvalidatestage", | ||||||
|  |             name="configuration_stages", | ||||||
|  |             field=models.ManyToManyField( | ||||||
|  |                 blank=True, | ||||||
|  |                 default=None, | ||||||
|  |                 help_text="Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.", | ||||||
|  |                 related_name="+", | ||||||
|  |                 to="authentik_flows.Stage", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |         migrations.RunPython(migrate_configuration_stage), | ||||||
|  |         migrations.RemoveField( | ||||||
|  |             model_name="authenticatorvalidatestage", | ||||||
|  |             name="configuration_stage", | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -38,16 +38,14 @@ class AuthenticatorValidateStage(Stage): | |||||||
|         choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP |         choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     configuration_stage = models.ForeignKey( |     configuration_stages = models.ManyToManyField( | ||||||
|         Stage, |         Stage, | ||||||
|         null=True, |  | ||||||
|         blank=True, |         blank=True, | ||||||
|         default=None, |         default=None, | ||||||
|         on_delete=models.SET_DEFAULT, |  | ||||||
|         related_name="+", |         related_name="+", | ||||||
|         help_text=_( |         help_text=_( | ||||||
|             ( |             ( | ||||||
|                 "Stage used to configure Authenticator when user doesn't have any compatible " |                 "Stages used to configure Authenticator when user doesn't have any compatible " | ||||||
|                 "devices. After this configuration Stage passes, the user is not prompted again." |                 "devices. After this configuration Stage passes, the user is not prompted again." | ||||||
|             ) |             ) | ||||||
|         ), |         ), | ||||||
|  | |||||||
| @ -1,10 +1,12 @@ | |||||||
| """Authenticator Validation""" | """Authenticator Validation""" | ||||||
| from django.http import HttpRequest, HttpResponse | from django.http import HttpRequest, HttpResponse | ||||||
| from django_otp import devices_for_user | from django_otp import devices_for_user | ||||||
| from rest_framework.fields import CharField, IntegerField, JSONField, ListField | from rest_framework.fields import CharField, IntegerField, JSONField, ListField, UUIDField | ||||||
| from rest_framework.serializers import ValidationError | from rest_framework.serializers import ValidationError | ||||||
| from structlog.stdlib import get_logger | from structlog.stdlib import get_logger | ||||||
|  |  | ||||||
|  | from authentik.core.api.utils import PassiveSerializer | ||||||
|  | from authentik.core.models import User | ||||||
| from authentik.events.models import Event, EventAction | from authentik.events.models import Event, EventAction | ||||||
| from authentik.events.utils import cleanse_dict, sanitize_dict | from authentik.events.utils import cleanse_dict, sanitize_dict | ||||||
| from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge | from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge | ||||||
| @ -26,6 +28,18 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice | |||||||
| from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS | ||||||
|  |  | ||||||
| LOGGER = get_logger() | LOGGER = get_logger() | ||||||
|  | SESSION_STAGES = "goauthentik.io/stages/authenticator_validate/stages" | ||||||
|  | SESSION_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage" | ||||||
|  | SESSION_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SelectableStageSerializer(PassiveSerializer): | ||||||
|  |     """Serializer for stages which can be selected by users""" | ||||||
|  |  | ||||||
|  |     pk = UUIDField() | ||||||
|  |     name = CharField() | ||||||
|  |     verbose_name = CharField() | ||||||
|  |     meta_model_name = CharField() | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthenticatorValidationChallenge(WithUserInfoChallenge): | class AuthenticatorValidationChallenge(WithUserInfoChallenge): | ||||||
| @ -33,12 +47,14 @@ class AuthenticatorValidationChallenge(WithUserInfoChallenge): | |||||||
|  |  | ||||||
|     device_challenges = ListField(child=DeviceChallenge()) |     device_challenges = ListField(child=DeviceChallenge()) | ||||||
|     component = CharField(default="ak-stage-authenticator-validate") |     component = CharField(default="ak-stage-authenticator-validate") | ||||||
|  |     configuration_stages = ListField(child=SelectableStageSerializer()) | ||||||
|  |  | ||||||
|  |  | ||||||
| class AuthenticatorValidationChallengeResponse(ChallengeResponse): | class AuthenticatorValidationChallengeResponse(ChallengeResponse): | ||||||
|     """Challenge used for Code-based and WebAuthn authenticators""" |     """Challenge used for Code-based and WebAuthn authenticators""" | ||||||
|  |  | ||||||
|     selected_challenge = DeviceChallenge(required=False) |     selected_challenge = DeviceChallenge(required=False) | ||||||
|  |     selected_stage = CharField(required=False) | ||||||
|  |  | ||||||
|     code = CharField(required=False) |     code = CharField(required=False) | ||||||
|     webauthn = JSONField(required=False) |     webauthn = JSONField(required=False) | ||||||
| @ -46,7 +62,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | |||||||
|     component = CharField(default="ak-stage-authenticator-validate") |     component = CharField(default="ak-stage-authenticator-validate") | ||||||
|  |  | ||||||
|     def _challenge_allowed(self, classes: list): |     def _challenge_allowed(self, classes: list): | ||||||
|         device_challenges: list[dict] = self.stage.request.session.get("device_challenges") |         device_challenges: list[dict] = self.stage.request.session.get(SESSION_DEVICE_CHALLENGES) | ||||||
|         if not any(x["device_class"] in classes for x in device_challenges): |         if not any(x["device_class"] in classes for x in device_challenges): | ||||||
|             raise ValidationError("No compatible device class allowed") |             raise ValidationError("No compatible device class allowed") | ||||||
|  |  | ||||||
| @ -71,7 +87,7 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | |||||||
|     def validate_selected_challenge(self, challenge: dict) -> dict: |     def validate_selected_challenge(self, challenge: dict) -> dict: | ||||||
|         """Check which challenge the user has selected. Actual logic only used for SMS stage.""" |         """Check which challenge the user has selected. Actual logic only used for SMS stage.""" | ||||||
|         # First check if the challenge is valid |         # First check if the challenge is valid | ||||||
|         for device_challenge in self.stage.request.session.get("device_challenges"): |         for device_challenge in self.stage.request.session.get(SESSION_DEVICE_CHALLENGES): | ||||||
|             if device_challenge.get("device_class", "") != challenge.get("device_class", ""): |             if device_challenge.get("device_class", "") != challenge.get("device_class", ""): | ||||||
|                 raise ValidationError("invalid challenge selected") |                 raise ValidationError("invalid challenge selected") | ||||||
|             if device_challenge.get("device_uid", "") != challenge.get("device_uid", ""): |             if device_challenge.get("device_uid", "") != challenge.get("device_uid", ""): | ||||||
| @ -84,6 +100,15 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): | |||||||
|         select_challenge(self.stage.request, devices.first()) |         select_challenge(self.stage.request, devices.first()) | ||||||
|         return challenge |         return challenge | ||||||
|  |  | ||||||
|  |     def validate_selected_stage(self, stage_pk: str) -> str: | ||||||
|  |         """Check that the selected stage is valid""" | ||||||
|  |         stages = self.stage.request.session.get(SESSION_STAGES, []) | ||||||
|  |         if not any(str(stage.pk) == stage_pk for stage in stages): | ||||||
|  |             raise ValidationError("Selected stage is invalid") | ||||||
|  |         LOGGER.debug("Setting selected stage to ", stage=stage_pk) | ||||||
|  |         self.stage.request.session[SESSION_SELECTED_STAGE] = stage_pk | ||||||
|  |         return stage_pk | ||||||
|  |  | ||||||
|     def validate(self, attrs: dict): |     def validate(self, attrs: dict): | ||||||
|         # Checking if the given data is from a valid device class is done above |         # Checking if the given data is from a valid device class is done above | ||||||
|         # Here we only check if the any data was sent at all |         # Here we only check if the any data was sent at all | ||||||
| @ -164,7 +189,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|             else: |             else: | ||||||
|                 LOGGER.debug("No pending user, continuing") |                 LOGGER.debug("No pending user, continuing") | ||||||
|                 return self.executor.stage_ok() |                 return self.executor.stage_ok() | ||||||
|         self.request.session["device_challenges"] = challenges |         self.request.session[SESSION_DEVICE_CHALLENGES] = challenges | ||||||
|  |  | ||||||
|         # No allowed devices |         # No allowed devices | ||||||
|         if len(challenges) < 1: |         if len(challenges) < 1: | ||||||
| @ -175,7 +200,16 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|                 LOGGER.debug("Authenticator not configured, denying") |                 LOGGER.debug("Authenticator not configured, denying") | ||||||
|                 return self.executor.stage_invalid() |                 return self.executor.stage_invalid() | ||||||
|             if stage.not_configured_action == NotConfiguredAction.CONFIGURE: |             if stage.not_configured_action == NotConfiguredAction.CONFIGURE: | ||||||
|                 if not stage.configuration_stage: |                 LOGGER.debug("Authenticator not configured, forcing configure") | ||||||
|  |                 return self.prepare_stages(user) | ||||||
|  |         return super().get(request, *args, **kwargs) | ||||||
|  |  | ||||||
|  |     def prepare_stages(self, user: User, *args, **kwargs) -> HttpResponse: | ||||||
|  |         """Check how the user can configure themselves. If no stages are set, return an error. | ||||||
|  |         If a single stage is set, insert that stage directly. If multiple are selected, include | ||||||
|  |         them in the challenge.""" | ||||||
|  |         stage: AuthenticatorValidateStage = self.executor.current_stage | ||||||
|  |         if not stage.configuration_stages.exists(): | ||||||
|             Event.new( |             Event.new( | ||||||
|                 EventAction.CONFIGURATION_ERROR, |                 EventAction.CONFIGURATION_ERROR, | ||||||
|                 message=( |                 message=( | ||||||
| @ -185,22 +219,55 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|                 stage=self, |                 stage=self, | ||||||
|             ).from_http(self.request).set_user(user).save() |             ).from_http(self.request).set_user(user).save() | ||||||
|             return self.executor.stage_invalid() |             return self.executor.stage_invalid() | ||||||
|                 LOGGER.debug("Authenticator not configured, sending user to configure") |         if stage.configuration_stages.count() == 1: | ||||||
|  |             next_stage = Stage.objects.get_subclass(pk=stage.configuration_stages.first().pk) | ||||||
|  |             LOGGER.debug("Single stage configured, auto-selecting", stage=next_stage) | ||||||
|  |             self.request.session[SESSION_SELECTED_STAGE] = next_stage | ||||||
|  |             # Because that normal insetion only happens on post, we directly inject it here and | ||||||
|  |             # return it | ||||||
|  |             self.executor.plan.insert_stage(next_stage) | ||||||
|  |             return self.executor.stage_ok() | ||||||
|  |         stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses() | ||||||
|  |         self.request.session[SESSION_STAGES] = stages | ||||||
|  |         return super().get(self.request, *args, **kwargs) | ||||||
|  |  | ||||||
|  |     def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: | ||||||
|  |         res = super().post(request, *args, **kwargs) | ||||||
|  |         if ( | ||||||
|  |             SESSION_SELECTED_STAGE in self.request.session | ||||||
|  |             and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE | ||||||
|  |         ): | ||||||
|  |             LOGGER.debug("Got selected stage in session, running that") | ||||||
|  |             stage_pk = self.request.session.get(SESSION_SELECTED_STAGE) | ||||||
|             # Because the foreign key to stage.configuration_stage points to |             # Because the foreign key to stage.configuration_stage points to | ||||||
|             # a base stage class, we need to do another lookup |             # a base stage class, we need to do another lookup | ||||||
|                 stage = Stage.objects.get_subclass(pk=stage.configuration_stage.pk) |             stage = Stage.objects.get_subclass(pk=stage_pk) | ||||||
|             # plan.insert inserts at 1 index, so when stage_ok pops 0, |             # plan.insert inserts at 1 index, so when stage_ok pops 0, | ||||||
|             # the configuration stage is next |             # the configuration stage is next | ||||||
|             self.executor.plan.insert_stage(stage) |             self.executor.plan.insert_stage(stage) | ||||||
|             return self.executor.stage_ok() |             return self.executor.stage_ok() | ||||||
|         return super().get(request, *args, **kwargs) |         return res | ||||||
|  |  | ||||||
|     def get_challenge(self) -> AuthenticatorValidationChallenge: |     def get_challenge(self) -> AuthenticatorValidationChallenge: | ||||||
|         challenges = self.request.session["device_challenges"] |         challenges = self.request.session.get(SESSION_DEVICE_CHALLENGES, []) | ||||||
|  |         stages = self.request.session.get(SESSION_STAGES, []) | ||||||
|  |         stage_challenges = [] | ||||||
|  |         for stage in stages: | ||||||
|  |             serializer = SelectableStageSerializer( | ||||||
|  |                 data={ | ||||||
|  |                     "pk": stage.pk, | ||||||
|  |                     "name": stage.name, | ||||||
|  |                     "verbose_name": str(stage._meta.verbose_name), | ||||||
|  |                     "meta_model_name": f"{stage._meta.app_label}.{stage._meta.model_name}", | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |             serializer.is_valid() | ||||||
|  |             stage_challenges.append(serializer.data) | ||||||
|         return AuthenticatorValidationChallenge( |         return AuthenticatorValidationChallenge( | ||||||
|             data={ |             data={ | ||||||
|                 "type": ChallengeTypes.NATIVE.value, |                 "type": ChallengeTypes.NATIVE.value, | ||||||
|                 "device_challenges": challenges, |                 "device_challenges": challenges, | ||||||
|  |                 "configuration_stages": stage_challenges, | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -43,8 +43,8 @@ class AuthenticatorValidateStageTests(FlowTestCase): | |||||||
|         stage = AuthenticatorValidateStage.objects.create( |         stage = AuthenticatorValidateStage.objects.create( | ||||||
|             name="foo", |             name="foo", | ||||||
|             not_configured_action=NotConfiguredAction.CONFIGURE, |             not_configured_action=NotConfiguredAction.CONFIGURE, | ||||||
|             configuration_stage=conf_stage, |  | ||||||
|         ) |         ) | ||||||
|  |         stage.configuration_stages.set([conf_stage]) | ||||||
|         flow = Flow.objects.create(name="test", slug="test", title="test") |         flow = Flow.objects.create(name="test", slug="test", title="test") | ||||||
|         FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0) |         FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0) | ||||||
|         FlowStageBinding.objects.create(target=flow, stage=stage, order=1) |         FlowStageBinding.objects.create(target=flow, stage=stage, order=1) | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ services: | |||||||
|     image: redis:alpine |     image: redis:alpine | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|   server: |   server: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.2.1} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: server |     command: server | ||||||
|     environment: |     environment: | ||||||
| @ -38,7 +38,7 @@ services: | |||||||
|       - "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000" |       - "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000" | ||||||
|       - "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443" |       - "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443" | ||||||
|   worker: |   worker: | ||||||
|     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.1.4} |     image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.2.1} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							| @ -8,16 +8,16 @@ require ( | |||||||
| 	github.com/coreos/go-oidc v2.2.1+incompatible | 	github.com/coreos/go-oidc v2.2.1+incompatible | ||||||
| 	github.com/garyburd/redigo v1.6.2 // indirect | 	github.com/garyburd/redigo v1.6.2 // indirect | ||||||
| 	github.com/getsentry/sentry-go v0.12.0 | 	github.com/getsentry/sentry-go v0.12.0 | ||||||
| 	github.com/go-ldap/ldap/v3 v3.4.1 | 	github.com/go-ldap/ldap/v3 v3.4.2 | ||||||
| 	github.com/go-openapi/runtime v0.22.0 | 	github.com/go-openapi/runtime v0.23.0 | ||||||
| 	github.com/go-openapi/strfmt v0.21.1 | 	github.com/go-openapi/strfmt v0.21.2 | ||||||
| 	github.com/golang-jwt/jwt v3.2.2+incompatible | 	github.com/golang-jwt/jwt v3.2.2+incompatible | ||||||
| 	github.com/google/uuid v1.3.0 | 	github.com/google/uuid v1.3.0 | ||||||
| 	github.com/gorilla/handlers v1.5.1 | 	github.com/gorilla/handlers v1.5.1 | ||||||
| 	github.com/gorilla/mux v1.8.0 | 	github.com/gorilla/mux v1.8.0 | ||||||
| 	github.com/gorilla/securecookie v1.1.1 | 	github.com/gorilla/securecookie v1.1.1 | ||||||
| 	github.com/gorilla/sessions v1.2.1 | 	github.com/gorilla/sessions v1.2.1 | ||||||
| 	github.com/gorilla/websocket v1.4.2 | 	github.com/gorilla/websocket v1.5.0 | ||||||
| 	github.com/imdario/mergo v0.3.12 | 	github.com/imdario/mergo v0.3.12 | ||||||
| 	github.com/mailru/easyjson v0.7.7 // indirect | 	github.com/mailru/easyjson v0.7.7 // indirect | ||||||
| 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | 	github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 | ||||||
|  | |||||||
							
								
								
									
										16
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								go.sum
									
									
									
									
									
								
							| @ -125,8 +125,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 | |||||||
| github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | ||||||
| github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | ||||||
| github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= | ||||||
| github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU= | github.com/go-ldap/ldap/v3 v3.4.2 h1:zFZKcXKLqZpFMrMQGHeHWKXbDTdNCmhGY9AK41zPh+8= | ||||||
| github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= | github.com/go-ldap/ldap/v3 v3.4.2/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg= | ||||||
| github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= | ||||||
| github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= | ||||||
| github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= | ||||||
| @ -183,8 +183,8 @@ github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29g | |||||||
| github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= | github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= | ||||||
| github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= | github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= | ||||||
| github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= | github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= | ||||||
| github.com/go-openapi/runtime v0.22.0 h1:vY2D0u807kkcwidaj0YJuq4zyAWQnjLNDpJcVBrUFNs= | github.com/go-openapi/runtime v0.23.0 h1:HX6ET2sHCIvaKeDDQoU01CtO1ekg5EkekHSkLTtWXH0= | ||||||
| github.com/go-openapi/runtime v0.22.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs= | github.com/go-openapi/runtime v0.23.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs= | ||||||
| github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= | github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= | ||||||
| github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= | github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= | ||||||
| github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= | github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= | ||||||
| @ -208,8 +208,8 @@ github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLs | |||||||
| github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= | github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= | ||||||
| github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk= | github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk= | ||||||
| github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= | github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= | ||||||
| github.com/go-openapi/strfmt v0.21.1 h1:G6s2t5V5kGCHLVbSdZ/6lI8Wm4OzoPFkc3/cjAsKQrM= | github.com/go-openapi/strfmt v0.21.2 h1:5NDNgadiX1Vhemth/TH4gCGopWSTdDjxl60H3B7f+os= | ||||||
| github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= | github.com/go-openapi/strfmt v0.21.2/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= | ||||||
| github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= | ||||||
| github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= | github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= | ||||||
| github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= | ||||||
| @ -334,8 +334,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+ | |||||||
| github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= | ||||||
| github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | ||||||
| github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||||
| github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= | ||||||
| github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||||
| github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= | ||||||
| github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | ||||||
| github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= | ||||||
|  | |||||||
| @ -25,4 +25,4 @@ func OutpostUserAgent() string { | |||||||
| 	return fmt.Sprintf("authentik-outpost@%s", FullVersion()) | 	return fmt.Sprintf("authentik-outpost@%s", FullVersion()) | ||||||
| } | } | ||||||
|  |  | ||||||
| const VERSION = "2022.1.4" | const VERSION = "2022.2.1" | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ var ( | |||||||
| func RunServer() { | func RunServer() { | ||||||
| 	m := mux.NewRouter() | 	m := mux.NewRouter() | ||||||
| 	l := log.WithField("logger", "authentik.outpost.metrics") | 	l := log.WithField("logger", "authentik.outpost.metrics") | ||||||
| 	m.HandleFunc("/akprox/ping", func(rw http.ResponseWriter, r *http.Request) { | 	m.HandleFunc("/outpost.goauthentik.io/ping", func(rw http.ResponseWriter, r *http.Request) { | ||||||
| 		rw.WriteHeader(204) | 		rw.WriteHeader(204) | ||||||
| 	}) | 	}) | ||||||
| 	m.Path("/metrics").Handler(promhttp.Handler()) | 	m.Path("/metrics").Handler(promhttp.Handler()) | ||||||
|  | |||||||
| @ -78,7 +78,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore | |||||||
| 	oauth2Config := oauth2.Config{ | 	oauth2Config := oauth2.Config{ | ||||||
| 		ClientID:     *p.ClientId, | 		ClientID:     *p.ClientId, | ||||||
| 		ClientSecret: *p.ClientSecret, | 		ClientSecret: *p.ClientSecret, | ||||||
| 		RedirectURL:  urlJoin(p.ExternalHost, "/akprox/callback"), | 		RedirectURL:  urlJoin(p.ExternalHost, "/outpost.goauthentik.io/callback"), | ||||||
| 		Endpoint:     endpoint.Endpoint, | 		Endpoint:     endpoint.Endpoint, | ||||||
| 		Scopes:       p.ScopesToRequest, | 		Scopes:       p.ScopesToRequest, | ||||||
| 	} | 	} | ||||||
| @ -145,10 +145,10 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore | |||||||
| 	mux.Use(sentryhttp.New(sentryhttp.Options{}).Handle) | 	mux.Use(sentryhttp.New(sentryhttp.Options{}).Handle) | ||||||
|  |  | ||||||
| 	// Support /start and /sign_in for backwards compatibility | 	// Support /start and /sign_in for backwards compatibility | ||||||
| 	mux.HandleFunc("/akprox/start", a.handleRedirect) | 	mux.HandleFunc("/outpost.goauthentik.io/start", a.handleRedirect) | ||||||
| 	mux.HandleFunc("/akprox/sign_in", a.handleRedirect) | 	mux.HandleFunc("/outpost.goauthentik.io/sign_in", a.handleRedirect) | ||||||
| 	mux.HandleFunc("/akprox/callback", a.handleCallback) | 	mux.HandleFunc("/outpost.goauthentik.io/callback", a.handleCallback) | ||||||
| 	mux.HandleFunc("/akprox/sign_out", a.handleSignOut) | 	mux.HandleFunc("/outpost.goauthentik.io/sign_out", a.handleSignOut) | ||||||
| 	switch *p.Mode { | 	switch *p.Mode { | ||||||
| 	case api.PROXYMODE_PROXY: | 	case api.PROXYMODE_PROXY: | ||||||
| 		err = a.configureProxy() | 		err = a.configureProxy() | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ type Claims struct { | |||||||
| 	Exp               int          `json:"exp"` | 	Exp               int          `json:"exp"` | ||||||
| 	Email             string       `json:"email"` | 	Email             string       `json:"email"` | ||||||
| 	Verified          bool         `json:"email_verified"` | 	Verified          bool         `json:"email_verified"` | ||||||
| 	Proxy             ProxyClaims `json:"ak_proxy"` | 	Proxy             *ProxyClaims `json:"ak_proxy"` | ||||||
| 	Name              string       `json:"name"` | 	Name              string       `json:"name"` | ||||||
| 	PreferredUsername string       `json:"preferred_username"` | 	PreferredUsername string       `json:"preferred_username"` | ||||||
| 	Groups            []string     `json:"groups"` | 	Groups            []string     `json:"groups"` | ||||||
|  | |||||||
| @ -18,7 +18,7 @@ func (a *Application) ErrorPage(rw http.ResponseWriter, r *http.Request, err str | |||||||
| 	data := ErrorPageData{ | 	data := ErrorPageData{ | ||||||
| 		Title:       "Bad Gateway", | 		Title:       "Bad Gateway", | ||||||
| 		Message:     "Error proxying to upstream server", | 		Message:     "Error proxying to upstream server", | ||||||
| 		ProxyPrefix: "/akprox", | 		ProxyPrefix: "/outpost.goauthentik.io", | ||||||
| 	} | 	} | ||||||
| 	if claims != nil && len(err) > 0 { | 	if claims != nil && len(err) > 0 { | ||||||
| 		data.Message = err | 		data.Message = err | ||||||
|  | |||||||
| @ -12,15 +12,15 @@ import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| func (a *Application) configureForward() error { | func (a *Application) configureForward() error { | ||||||
| 	a.mux.HandleFunc("/akprox/auth", func(rw http.ResponseWriter, r *http.Request) { | 	a.mux.HandleFunc("/outpost.goauthentik.io/auth", func(rw http.ResponseWriter, r *http.Request) { | ||||||
| 		if _, ok := r.URL.Query()["traefik"]; ok { | 		if _, ok := r.URL.Query()["traefik"]; ok { | ||||||
| 			a.forwardHandleTraefik(rw, r) | 			a.forwardHandleTraefik(rw, r) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		a.forwardHandleNginx(rw, r) | 		a.forwardHandleNginx(rw, r) | ||||||
| 	}) | 	}) | ||||||
| 	a.mux.HandleFunc("/akprox/auth/traefik", a.forwardHandleTraefik) | 	a.mux.HandleFunc("/outpost.goauthentik.io/auth/traefik", a.forwardHandleTraefik) | ||||||
| 	a.mux.HandleFunc("/akprox/auth/nginx", a.forwardHandleNginx) | 	a.mux.HandleFunc("/outpost.goauthentik.io/auth/nginx", a.forwardHandleNginx) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @ -49,8 +49,8 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque | |||||||
| 		a.log.Trace("path can be accessed without authentication") | 		a.log.Trace("path can be accessed without authentication") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if strings.HasPrefix(r.Header.Get("X-Forwarded-Uri"), "/akprox") { | 	if strings.HasPrefix(r.Header.Get("X-Forwarded-Uri"), "/outpost.goauthentik.io") { | ||||||
| 		a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access") | 		a.log.WithField("url", r.URL.String()).Trace("path begins with /outpost.goauthentik.io, allowing access") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	host := "" | 	host := "" | ||||||
| @ -80,7 +80,7 @@ func (a *Application) forwardHandleTraefik(rw http.ResponseWriter, r *http.Reque | |||||||
| 	if proto != "" { | 	if proto != "" { | ||||||
| 		proto = proto + ":" | 		proto = proto + ":" | ||||||
| 	} | 	} | ||||||
| 	rdFinal := fmt.Sprintf("%s//%s%s", proto, host, "/akprox/start") | 	rdFinal := fmt.Sprintf("%s//%s%s", proto, host, "/outpost.goauthentik.io/start") | ||||||
| 	a.log.WithField("url", rdFinal).Debug("Redirecting to login") | 	a.log.WithField("url", rdFinal).Debug("Redirecting to login") | ||||||
| 	http.Redirect(rw, r, rdFinal, http.StatusTemporaryRedirect) | 	http.Redirect(rw, r, rdFinal, http.StatusTemporaryRedirect) | ||||||
| } | } | ||||||
| @ -119,8 +119,8 @@ func (a *Application) forwardHandleNginx(rw http.ResponseWriter, r *http.Request | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if fwd.String() != r.URL.String() { | 	if fwd.String() != r.URL.String() { | ||||||
| 		if strings.HasPrefix(fwd.Path, "/akprox") { | 		if strings.HasPrefix(fwd.Path, "/outpost.goauthentik.io") { | ||||||
| 			a.log.WithField("url", r.URL.String()).Trace("path begins with /akprox, allowing access") | 			a.log.WithField("url", r.URL.String()).Trace("path begins with /outpost.goauthentik.io, allowing access") | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ import ( | |||||||
|  |  | ||||||
| func TestForwardHandleNginx_Single_Blank(t *testing.T) { | func TestForwardHandleNginx_Single_Blank(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil) | ||||||
|  |  | ||||||
| 	rr := httptest.NewRecorder() | 	rr := httptest.NewRecorder() | ||||||
| 	a.forwardHandleNginx(rr, req) | 	a.forwardHandleNginx(rr, req) | ||||||
| @ -22,7 +22,7 @@ func TestForwardHandleNginx_Single_Blank(t *testing.T) { | |||||||
|  |  | ||||||
| func TestForwardHandleNginx_Single_Skip(t *testing.T) { | func TestForwardHandleNginx_Single_Skip(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil) | ||||||
| 	req.Header.Set("X-Original-URL", "http://test.goauthentik.io/skip") | 	req.Header.Set("X-Original-URL", "http://test.goauthentik.io/skip") | ||||||
|  |  | ||||||
| 	rr := httptest.NewRecorder() | 	rr := httptest.NewRecorder() | ||||||
| @ -33,7 +33,7 @@ func TestForwardHandleNginx_Single_Skip(t *testing.T) { | |||||||
|  |  | ||||||
| func TestForwardHandleNginx_Single_Headers(t *testing.T) { | func TestForwardHandleNginx_Single_Headers(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil) | ||||||
| 	req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app") | 	req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app") | ||||||
|  |  | ||||||
| 	rr := httptest.NewRecorder() | 	rr := httptest.NewRecorder() | ||||||
| @ -47,7 +47,7 @@ func TestForwardHandleNginx_Single_Headers(t *testing.T) { | |||||||
|  |  | ||||||
| func TestForwardHandleNginx_Single_URI(t *testing.T) { | func TestForwardHandleNginx_Single_URI(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	req, _ := http.NewRequest("GET", "https://foo.bar/akprox/auth/nginx", nil) | 	req, _ := http.NewRequest("GET", "https://foo.bar/outpost.goauthentik.io/auth/nginx", nil) | ||||||
| 	req.Header.Set("X-Original-URI", "/app") | 	req.Header.Set("X-Original-URI", "/app") | ||||||
|  |  | ||||||
| 	rr := httptest.NewRecorder() | 	rr := httptest.NewRecorder() | ||||||
| @ -61,7 +61,7 @@ func TestForwardHandleNginx_Single_URI(t *testing.T) { | |||||||
|  |  | ||||||
| func TestForwardHandleNginx_Single_Claims(t *testing.T) { | func TestForwardHandleNginx_Single_Claims(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil) | ||||||
| 	req.Header.Set("X-Original-URI", "/") | 	req.Header.Set("X-Original-URI", "/") | ||||||
|  |  | ||||||
| 	rr := httptest.NewRecorder() | 	rr := httptest.NewRecorder() | ||||||
| @ -70,7 +70,7 @@ func TestForwardHandleNginx_Single_Claims(t *testing.T) { | |||||||
| 	s, _ := a.sessions.Get(req, constants.SeesionName) | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
| 	s.Values[constants.SessionClaims] = Claims{ | 	s.Values[constants.SessionClaims] = Claims{ | ||||||
| 		Sub: "foo", | 		Sub: "foo", | ||||||
| 		Proxy: ProxyClaims{ | 		Proxy: &ProxyClaims{ | ||||||
| 			UserAttributes: map[string]interface{}{ | 			UserAttributes: map[string]interface{}{ | ||||||
| 				"username": "foo", | 				"username": "foo", | ||||||
| 				"password": "bar", | 				"password": "bar", | ||||||
| @ -108,7 +108,7 @@ func TestForwardHandleNginx_Domain_Blank(t *testing.T) { | |||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
| 	a.proxyConfig.CookieDomain = api.PtrString("foo") | 	a.proxyConfig.CookieDomain = api.PtrString("foo") | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil) | ||||||
|  |  | ||||||
| 	rr := httptest.NewRecorder() | 	rr := httptest.NewRecorder() | ||||||
| 	a.forwardHandleNginx(rr, req) | 	a.forwardHandleNginx(rr, req) | ||||||
| @ -121,7 +121,7 @@ func TestForwardHandleNginx_Domain_Header(t *testing.T) { | |||||||
| 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
| 	a.proxyConfig.CookieDomain = api.PtrString("foo") | 	a.proxyConfig.CookieDomain = api.PtrString("foo") | ||||||
| 	a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io" | 	a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io" | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/nginx", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/nginx", nil) | ||||||
| 	req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app") | 	req.Header.Set("X-Original-URL", "http://test.goauthentik.io/app") | ||||||
|  |  | ||||||
| 	rr := httptest.NewRecorder() | 	rr := httptest.NewRecorder() | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ import ( | |||||||
|  |  | ||||||
| func TestForwardHandleTraefik_Single_Blank(t *testing.T) { | func TestForwardHandleTraefik_Single_Blank(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil) | ||||||
|  |  | ||||||
| 	rr := httptest.NewRecorder() | 	rr := httptest.NewRecorder() | ||||||
| 	a.forwardHandleTraefik(rr, req) | 	a.forwardHandleTraefik(rr, req) | ||||||
| @ -22,7 +22,7 @@ func TestForwardHandleTraefik_Single_Blank(t *testing.T) { | |||||||
|  |  | ||||||
| func TestForwardHandleTraefik_Single_Skip(t *testing.T) { | func TestForwardHandleTraefik_Single_Skip(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil) | ||||||
| 	req.Header.Set("X-Forwarded-Proto", "http") | 	req.Header.Set("X-Forwarded-Proto", "http") | ||||||
| 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | ||||||
| 	req.Header.Set("X-Forwarded-Uri", "/skip") | 	req.Header.Set("X-Forwarded-Uri", "/skip") | ||||||
| @ -35,7 +35,7 @@ func TestForwardHandleTraefik_Single_Skip(t *testing.T) { | |||||||
|  |  | ||||||
| func TestForwardHandleTraefik_Single_Headers(t *testing.T) { | func TestForwardHandleTraefik_Single_Headers(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil) | ||||||
| 	req.Header.Set("X-Forwarded-Proto", "http") | 	req.Header.Set("X-Forwarded-Proto", "http") | ||||||
| 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | ||||||
| 	req.Header.Set("X-Forwarded-Uri", "/app") | 	req.Header.Set("X-Forwarded-Uri", "/app") | ||||||
| @ -45,7 +45,7 @@ func TestForwardHandleTraefik_Single_Headers(t *testing.T) { | |||||||
|  |  | ||||||
| 	assert.Equal(t, rr.Code, http.StatusTemporaryRedirect) | 	assert.Equal(t, rr.Code, http.StatusTemporaryRedirect) | ||||||
| 	loc, _ := rr.Result().Location() | 	loc, _ := rr.Result().Location() | ||||||
| 	assert.Equal(t, loc.String(), "http://test.goauthentik.io/akprox/start") | 	assert.Equal(t, loc.String(), "http://test.goauthentik.io/outpost.goauthentik.io/start") | ||||||
|  |  | ||||||
| 	s, _ := a.sessions.Get(req, constants.SeesionName) | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
| 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) | 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) | ||||||
| @ -53,7 +53,7 @@ func TestForwardHandleTraefik_Single_Headers(t *testing.T) { | |||||||
|  |  | ||||||
| func TestForwardHandleTraefik_Single_Claims(t *testing.T) { | func TestForwardHandleTraefik_Single_Claims(t *testing.T) { | ||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil) | ||||||
| 	req.Header.Set("X-Forwarded-Proto", "http") | 	req.Header.Set("X-Forwarded-Proto", "http") | ||||||
| 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | ||||||
| 	req.Header.Set("X-Forwarded-Uri", "/app") | 	req.Header.Set("X-Forwarded-Uri", "/app") | ||||||
| @ -64,7 +64,7 @@ func TestForwardHandleTraefik_Single_Claims(t *testing.T) { | |||||||
| 	s, _ := a.sessions.Get(req, constants.SeesionName) | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
| 	s.Values[constants.SessionClaims] = Claims{ | 	s.Values[constants.SessionClaims] = Claims{ | ||||||
| 		Sub: "foo", | 		Sub: "foo", | ||||||
| 		Proxy: ProxyClaims{ | 		Proxy: &ProxyClaims{ | ||||||
| 			UserAttributes: map[string]interface{}{ | 			UserAttributes: map[string]interface{}{ | ||||||
| 				"username": "foo", | 				"username": "foo", | ||||||
| 				"password": "bar", | 				"password": "bar", | ||||||
| @ -102,7 +102,7 @@ func TestForwardHandleTraefik_Domain_Blank(t *testing.T) { | |||||||
| 	a := newTestApplication() | 	a := newTestApplication() | ||||||
| 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
| 	a.proxyConfig.CookieDomain = api.PtrString("foo") | 	a.proxyConfig.CookieDomain = api.PtrString("foo") | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil) | ||||||
|  |  | ||||||
| 	rr := httptest.NewRecorder() | 	rr := httptest.NewRecorder() | ||||||
| 	a.forwardHandleTraefik(rr, req) | 	a.forwardHandleTraefik(rr, req) | ||||||
| @ -115,7 +115,7 @@ func TestForwardHandleTraefik_Domain_Header(t *testing.T) { | |||||||
| 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
| 	a.proxyConfig.CookieDomain = api.PtrString("foo") | 	a.proxyConfig.CookieDomain = api.PtrString("foo") | ||||||
| 	a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io" | 	a.proxyConfig.ExternalHost = "http://auth.test.goauthentik.io" | ||||||
| 	req, _ := http.NewRequest("GET", "/akprox/auth/traefik", nil) | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/traefik", nil) | ||||||
| 	req.Header.Set("X-Forwarded-Proto", "http") | 	req.Header.Set("X-Forwarded-Proto", "http") | ||||||
| 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | 	req.Header.Set("X-Forwarded-Host", "test.goauthentik.io") | ||||||
| 	req.Header.Set("X-Forwarded-Uri", "/app") | 	req.Header.Set("X-Forwarded-Uri", "/app") | ||||||
| @ -125,7 +125,7 @@ func TestForwardHandleTraefik_Domain_Header(t *testing.T) { | |||||||
|  |  | ||||||
| 	assert.Equal(t, http.StatusTemporaryRedirect, rr.Code) | 	assert.Equal(t, http.StatusTemporaryRedirect, rr.Code) | ||||||
| 	loc, _ := rr.Result().Location() | 	loc, _ := rr.Result().Location() | ||||||
| 	assert.Equal(t, "http://auth.test.goauthentik.io/akprox/start", loc.String()) | 	assert.Equal(t, "http://auth.test.goauthentik.io/outpost.goauthentik.io/start", loc.String()) | ||||||
|  |  | ||||||
| 	s, _ := a.sessions.Get(req, constants.SeesionName) | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
| 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) | 	assert.Equal(t, "http://test.goauthentik.io/app", s.Values[constants.SessionRedirect]) | ||||||
|  | |||||||
| @ -73,19 +73,21 @@ func (a *Application) configureProxy() error { | |||||||
|  |  | ||||||
| func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) { | func (a *Application) proxyModifyRequest(ou *url.URL) func(req *http.Request) { | ||||||
| 	return func(r *http.Request) { | 	return func(r *http.Request) { | ||||||
|  | 		r.Header.Set("X-Forwarded-Host", r.Host) | ||||||
| 		claims, _ := a.getClaims(r) | 		claims, _ := a.getClaims(r) | ||||||
| 		if claims.Proxy.BackendOverride != "" { | 		r.URL.Scheme = ou.Scheme | ||||||
|  | 		r.URL.Host = ou.Host | ||||||
|  | 		if claims != nil && claims.Proxy != nil && claims.Proxy.BackendOverride != "" { | ||||||
| 			u, err := url.Parse(claims.Proxy.BackendOverride) | 			u, err := url.Parse(claims.Proxy.BackendOverride) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override") | 				a.log.WithField("backend_override", claims.Proxy.BackendOverride).WithError(err).Warning("failed parse user backend override") | ||||||
| 			} | 			} else { | ||||||
| 				r.URL.Scheme = u.Scheme | 				r.URL.Scheme = u.Scheme | ||||||
| 				r.URL.Host = u.Host | 				r.URL.Host = u.Host | ||||||
| 		} else { |  | ||||||
| 			r.URL.Scheme = ou.Scheme |  | ||||||
| 			r.URL.Host = ou.Host |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 		a.log.WithField("upstream_url", r.URL.String()).Trace("final upstream url") | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) proxyModifyResponse(res *http.Response) error { | func (a *Application) proxyModifyResponse(res *http.Response) error { | ||||||
|  | |||||||
							
								
								
									
										82
									
								
								internal/outpost/proxyv2/application/mode_proxy_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								internal/outpost/proxyv2/application/mode_proxy_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | |||||||
|  | package application | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
|  | 	"net/url" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"goauthentik.io/internal/outpost/proxyv2/constants" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestProxy_ModifyRequest(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "http://frontend/foo", nil) | ||||||
|  | 	u, err := url.Parse("http://backend:8012") | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	a.proxyModifyRequest(u)(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, "frontend", req.Header.Get("X-Forwarded-Host")) | ||||||
|  | 	assert.Equal(t, "/foo", req.URL.Path) | ||||||
|  | 	assert.Equal(t, "backend:8012", req.URL.Host) | ||||||
|  | 	assert.Equal(t, "frontend", req.Host) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestProxy_ModifyRequest_Claims(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "http://frontend/foo", nil) | ||||||
|  | 	u, err := url.Parse("http://backend:8012") | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	s.Values[constants.SessionClaims] = Claims{ | ||||||
|  | 		Sub: "foo", | ||||||
|  | 		Proxy: &ProxyClaims{ | ||||||
|  | 			BackendOverride: "http://other-backend:8123", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	err = a.sessions.Save(req, rr, s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	a.proxyModifyRequest(u)(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, "/foo", req.URL.Path) | ||||||
|  | 	assert.Equal(t, "other-backend:8123", req.URL.Host) | ||||||
|  | 	assert.Equal(t, "frontend", req.Host) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestProxy_ModifyRequest_Claims_Invalid(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "http://frontend/foo", nil) | ||||||
|  | 	u, err := url.Parse("http://backend:8012") | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	rr := httptest.NewRecorder() | ||||||
|  |  | ||||||
|  | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
|  | 	s.Values[constants.SessionClaims] = Claims{ | ||||||
|  | 		Sub: "foo", | ||||||
|  | 		Proxy: &ProxyClaims{ | ||||||
|  | 			BackendOverride: ":qewr", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	err = a.sessions.Save(req, rr, s) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	a.proxyModifyRequest(u)(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, "/foo", req.URL.Path) | ||||||
|  | 	assert.Equal(t, "backend:8012", req.URL.Host) | ||||||
|  | 	assert.Equal(t, "frontend", req.Host) | ||||||
|  | } | ||||||
| @ -3,14 +3,46 @@ package application | |||||||
| import ( | import ( | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/gorilla/securecookie" | 	"github.com/gorilla/securecookie" | ||||||
|  | 	"goauthentik.io/api" | ||||||
| 	"goauthentik.io/internal/outpost/proxyv2/constants" | 	"goauthentik.io/internal/outpost/proxyv2/constants" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	redirectParam = "rd" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func (a *Application) checkRedirectParam(r *http.Request) (string, bool) { | ||||||
|  | 	rd := r.URL.Query().Get(redirectParam) | ||||||
|  | 	if rd == "" { | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	u, err := url.Parse(rd) | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.log.WithError(err).Warning("Failed to parse redirect URL") | ||||||
|  | 		return "", false | ||||||
|  | 	} | ||||||
|  | 	// Check to make sure we only redirect to allowed places | ||||||
|  | 	if a.Mode() == api.PROXYMODE_PROXY || a.Mode() == api.PROXYMODE_FORWARD_SINGLE { | ||||||
|  | 		if !strings.Contains(u.String(), a.proxyConfig.ExternalHost) { | ||||||
|  | 			a.log.WithField("url", u.String()).WithField("ext", a.proxyConfig.ExternalHost).Warning("redirect URI did not contain external host") | ||||||
|  | 			return "", false | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		if !strings.HasSuffix(u.Host, *a.proxyConfig.CookieDomain) { | ||||||
|  | 			a.log.WithField("host", u.Host).WithField("dom", *a.proxyConfig.CookieDomain).Warning("redirect URI Host was not included in cookie domain") | ||||||
|  | 			return "", false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return u.String(), true | ||||||
|  | } | ||||||
|  |  | ||||||
| func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) { | func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) { | ||||||
| 	newState := base64.RawStdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)) | 	newState := base64.RawURLEncoding.EncodeToString(securecookie.GenerateRandomKey(32)) | ||||||
| 	s, err := a.sessions.Get(r, constants.SeesionName) | 	s, err := a.sessions.Get(r, constants.SeesionName) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		s.Values[constants.SessionOAuthState] = []string{} | 		s.Values[constants.SessionOAuthState] = []string{} | ||||||
| @ -20,6 +52,11 @@ func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) { | |||||||
| 		s.Values[constants.SessionOAuthState] = []string{} | 		s.Values[constants.SessionOAuthState] = []string{} | ||||||
| 		state = []string{} | 		state = []string{} | ||||||
| 	} | 	} | ||||||
|  | 	rd, ok := a.checkRedirectParam(r) | ||||||
|  | 	if ok { | ||||||
|  | 		s.Values[constants.SessionRedirect] = rd | ||||||
|  | 		a.log.WithField("rd", rd).Trace("Setting redirect") | ||||||
|  | 	} | ||||||
| 	s.Values[constants.SessionOAuthState] = append(state, newState) | 	s.Values[constants.SessionOAuthState] = append(state, newState) | ||||||
| 	err = s.Save(r, rw) | 	err = s.Save(r, rw) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -29,7 +66,10 @@ func (a *Application) handleRedirect(rw http.ResponseWriter, r *http.Request) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (a *Application) handleCallback(rw http.ResponseWriter, r *http.Request) { | func (a *Application) handleCallback(rw http.ResponseWriter, r *http.Request) { | ||||||
| 	s, _ := a.sessions.Get(r, constants.SeesionName) | 	s, err := a.sessions.Get(r, constants.SeesionName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		a.log.WithError(err).Trace("failed to get session") | ||||||
|  | 	} | ||||||
| 	state, ok := s.Values[constants.SessionOAuthState] | 	state, ok := s.Values[constants.SessionOAuthState] | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		a.log.Warning("No state saved in session") | 		a.log.Warning("No state saved in session") | ||||||
| @ -62,8 +102,8 @@ func (a *Application) handleCallback(rw http.ResponseWriter, r *http.Request) { | |||||||
| 	redirect := a.proxyConfig.ExternalHost | 	redirect := a.proxyConfig.ExternalHost | ||||||
| 	redirectR, ok := s.Values[constants.SessionRedirect] | 	redirectR, ok := s.Values[constants.SessionRedirect] | ||||||
| 	if ok { | 	if ok { | ||||||
| 		a.log.WithField("redirect", redirectR).Trace("got final redirect from session") |  | ||||||
| 		redirect = redirectR.(string) | 		redirect = redirectR.(string) | ||||||
| 	} | 	} | ||||||
|  | 	a.log.WithField("redirect", redirect).Trace("final redirect") | ||||||
| 	http.Redirect(rw, r, redirect, http.StatusFound) | 	http.Redirect(rw, r, redirect, http.StatusFound) | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										51
									
								
								internal/outpost/proxyv2/application/oauth_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								internal/outpost/proxyv2/application/oauth_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | package application | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | 	"goauthentik.io/api" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestCheckRedirectParam(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	req, _ := http.NewRequest("GET", "/outpost.goauthentik.io/auth/start", nil) | ||||||
|  |  | ||||||
|  | 	rd, ok := a.checkRedirectParam(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, false, ok) | ||||||
|  | 	assert.Equal(t, "", rd) | ||||||
|  |  | ||||||
|  | 	req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://google.com", nil) | ||||||
|  |  | ||||||
|  | 	rd, ok = a.checkRedirectParam(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, false, ok) | ||||||
|  | 	assert.Equal(t, "", rd) | ||||||
|  |  | ||||||
|  | 	req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test", nil) | ||||||
|  |  | ||||||
|  | 	rd, ok = a.checkRedirectParam(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, true, ok) | ||||||
|  | 	assert.Equal(t, "https://ext.t.goauthentik.io/test", rd) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestCheckRedirectParam_Domain(t *testing.T) { | ||||||
|  | 	a := newTestApplication() | ||||||
|  | 	a.proxyConfig.Mode = api.PROXYMODE_FORWARD_DOMAIN.Ptr() | ||||||
|  | 	a.proxyConfig.CookieDomain = api.PtrString("t.goauthentik.io") | ||||||
|  | 	req, _ := http.NewRequest("GET", "https://a.t.goauthentik.io/outpost.goauthentik.io/auth/start", nil) | ||||||
|  |  | ||||||
|  | 	rd, ok := a.checkRedirectParam(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, false, ok) | ||||||
|  | 	assert.Equal(t, "", rd) | ||||||
|  | 	req, _ = http.NewRequest("GET", "/outpost.goauthentik.io/auth/start?rd=https://ext.t.goauthentik.io/test", nil) | ||||||
|  |  | ||||||
|  | 	rd, ok = a.checkRedirectParam(req) | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, true, ok) | ||||||
|  | 	assert.Equal(t, "https://ext.t.goauthentik.io/test", rd) | ||||||
|  | } | ||||||
| @ -15,6 +15,7 @@ func newTestApplication() *Application { | |||||||
| 			ClientId:                   api.PtrString(ak.TestSecret()), | 			ClientId:                   api.PtrString(ak.TestSecret()), | ||||||
| 			ClientSecret:               api.PtrString(ak.TestSecret()), | 			ClientSecret:               api.PtrString(ak.TestSecret()), | ||||||
| 			CookieSecret:               api.PtrString(ak.TestSecret()), | 			CookieSecret:               api.PtrString(ak.TestSecret()), | ||||||
|  | 			ExternalHost:               "https://ext.t.goauthentik.io", | ||||||
| 			CookieDomain:               api.PtrString(""), | 			CookieDomain:               api.PtrString(""), | ||||||
| 			Mode:                       api.PROXYMODE_FORWARD_SINGLE.Ptr(), | 			Mode:                       api.PROXYMODE_FORWARD_SINGLE.Ptr(), | ||||||
| 			SkipPathRegex:              api.PtrString("/skip.*"), | 			SkipPathRegex:              api.PtrString("/skip.*"), | ||||||
|  | |||||||
| @ -42,7 +42,7 @@ func (a *Application) redirectToStart(rw http.ResponseWriter, r *http.Request) { | |||||||
| 		a.log.WithError(err).Warning("failed to save session before redirect") | 		a.log.WithError(err).Warning("failed to save session before redirect") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	authUrl := urlJoin(a.proxyConfig.ExternalHost, "/akprox/start") | 	authUrl := urlJoin(a.proxyConfig.ExternalHost, "/outpost.goauthentik.io/start") | ||||||
| 	http.Redirect(rw, r, authUrl, http.StatusFound) | 	http.Redirect(rw, r, authUrl, http.StatusFound) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
| @ -21,7 +21,7 @@ func TestRedirectToStart_Proxy(t *testing.T) { | |||||||
|  |  | ||||||
| 	assert.Equal(t, http.StatusFound, rr.Code) | 	assert.Equal(t, http.StatusFound, rr.Code) | ||||||
| 	loc, _ := rr.Result().Location() | 	loc, _ := rr.Result().Location() | ||||||
| 	assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String()) | 	assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start", loc.String()) | ||||||
|  |  | ||||||
| 	s, _ := a.sessions.Get(req, constants.SeesionName) | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
| 	assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect]) | 	assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect]) | ||||||
| @ -38,7 +38,7 @@ func TestRedirectToStart_Forward(t *testing.T) { | |||||||
|  |  | ||||||
| 	assert.Equal(t, http.StatusFound, rr.Code) | 	assert.Equal(t, http.StatusFound, rr.Code) | ||||||
| 	loc, _ := rr.Result().Location() | 	loc, _ := rr.Result().Location() | ||||||
| 	assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String()) | 	assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start", loc.String()) | ||||||
|  |  | ||||||
| 	s, _ := a.sessions.Get(req, constants.SeesionName) | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
| 	assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect]) | 	assert.Equal(t, "https://test.goauthentik.io/foo/bar/baz", s.Values[constants.SessionRedirect]) | ||||||
| @ -56,7 +56,7 @@ func TestRedirectToStart_Forward_Domain_Invalid(t *testing.T) { | |||||||
|  |  | ||||||
| 	assert.Equal(t, http.StatusFound, rr.Code) | 	assert.Equal(t, http.StatusFound, rr.Code) | ||||||
| 	loc, _ := rr.Result().Location() | 	loc, _ := rr.Result().Location() | ||||||
| 	assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String()) | 	assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start", loc.String()) | ||||||
|  |  | ||||||
| 	s, _ := a.sessions.Get(req, constants.SeesionName) | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
| 	assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect]) | 	assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect]) | ||||||
| @ -74,7 +74,7 @@ func TestRedirectToStart_Forward_Domain(t *testing.T) { | |||||||
|  |  | ||||||
| 	assert.Equal(t, http.StatusFound, rr.Code) | 	assert.Equal(t, http.StatusFound, rr.Code) | ||||||
| 	loc, _ := rr.Result().Location() | 	loc, _ := rr.Result().Location() | ||||||
| 	assert.Equal(t, "https://test.goauthentik.io/akprox/start", loc.String()) | 	assert.Equal(t, "https://test.goauthentik.io/outpost.goauthentik.io/start", loc.String()) | ||||||
|  |  | ||||||
| 	s, _ := a.sessions.Get(req, constants.SeesionName) | 	s, _ := a.sessions.Get(req, constants.SeesionName) | ||||||
| 	assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect]) | 	assert.Equal(t, "https://test.goauthentik.io", s.Values[constants.SessionRedirect]) | ||||||
|  | |||||||
| @ -32,7 +32,7 @@ func (ps *ProxyServer) HandlePing(rw http.ResponseWriter, r *http.Request) { | |||||||
|  |  | ||||||
| func (ps *ProxyServer) HandleStatic(rw http.ResponseWriter, r *http.Request) { | func (ps *ProxyServer) HandleStatic(rw http.ResponseWriter, r *http.Request) { | ||||||
| 	before := time.Now() | 	before := time.Now() | ||||||
| 	web.DisableIndex(http.StripPrefix("/akprox/static/dist", staticWeb.StaticHandler)).ServeHTTP(rw, r) | 	web.DisableIndex(http.StripPrefix("/outpost.goauthentik.io/static/dist", staticWeb.StaticHandler)).ServeHTTP(rw, r) | ||||||
| 	after := time.Since(before) | 	after := time.Since(before) | ||||||
| 	metrics.Requests.With(prometheus.Labels{ | 	metrics.Requests.With(prometheus.Labels{ | ||||||
| 		"outpost_name": ps.akAPI.Outpost.Name, | 		"outpost_name": ps.akAPI.Outpost.Name, | ||||||
| @ -90,11 +90,11 @@ func (ps *ProxyServer) lookupApp(r *http.Request) (*application.Application, str | |||||||
| } | } | ||||||
|  |  | ||||||
| func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) { | func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) { | ||||||
| 	if strings.HasPrefix(r.URL.Path, "/akprox/static") { | 	if strings.HasPrefix(r.URL.Path, "/outpost.goauthentik.io/static") { | ||||||
| 		ps.HandleStatic(rw, r) | 		ps.HandleStatic(rw, r) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if strings.HasPrefix(r.URL.Path, "/akprox/ping") { | 	if strings.HasPrefix(r.URL.Path, "/outpost.goauthentik.io/ping") { | ||||||
| 		ps.HandlePing(rw, r) | 		ps.HandlePing(rw, r) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| @ -109,6 +109,7 @@ func (ps *ProxyServer) Handle(rw http.ResponseWriter, r *http.Request) { | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		ps.log.WithField("headers", r.Header).Trace("tracing headers for no hostname match") | ||||||
| 		ps.log.WithField("host", host).Warning("no app for hostname") | 		ps.log.WithField("host", host).Warning("no app for hostname") | ||||||
|  |  | ||||||
| 		rw.Header().Set("Content-Type", "application/json") | 		rw.Header().Set("Content-Type", "application/json") | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ var ( | |||||||
| func RunServer() { | func RunServer() { | ||||||
| 	m := mux.NewRouter() | 	m := mux.NewRouter() | ||||||
| 	l := log.WithField("logger", "authentik.outpost.metrics") | 	l := log.WithField("logger", "authentik.outpost.metrics") | ||||||
| 	m.HandleFunc("/akprox/ping", func(rw http.ResponseWriter, r *http.Request) { | 	m.HandleFunc("/outpost.goauthentik.io/ping", func(rw http.ResponseWriter, r *http.Request) { | ||||||
| 		rw.WriteHeader(204) | 		rw.WriteHeader(204) | ||||||
| 	}) | 	}) | ||||||
| 	m.Path("/metrics").Handler(promhttp.Handler()) | 	m.Path("/metrics").Handler(promhttp.Handler()) | ||||||
|  | |||||||
| @ -64,8 +64,8 @@ func NewProxyServer(ac *ak.APIController, portOffset int) *ProxyServer { | |||||||
| 		akAPI:       ac, | 		akAPI:       ac, | ||||||
| 		defaultCert: defaultCert, | 		defaultCert: defaultCert, | ||||||
| 	} | 	} | ||||||
| 	globalMux.PathPrefix("/akprox/static").HandlerFunc(s.HandleStatic) | 	globalMux.PathPrefix("/outpost.goauthentik.io/static").HandlerFunc(s.HandleStatic) | ||||||
| 	globalMux.Path("/akprox/ping").HandlerFunc(s.HandlePing) | 	globalMux.Path("/outpost.goauthentik.io/ping").HandlerFunc(s.HandlePing) | ||||||
| 	rootMux.PathPrefix("/").HandlerFunc(s.Handle) | 	rootMux.PathPrefix("/").HandlerFunc(s.Handle) | ||||||
| 	return s | 	return s | ||||||
| } | } | ||||||
| @ -102,7 +102,11 @@ func (ps *ProxyServer) GetCertificate(serverName string) *tls.Certificate { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (ps *ProxyServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) { | func (ps *ProxyServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) { | ||||||
| 	appCert := ps.GetCertificate(info.ServerName) | 	sn := info.ServerName | ||||||
|  | 	if sn == "" { | ||||||
|  | 		return &ps.defaultCert, nil | ||||||
|  | 	} | ||||||
|  | 	appCert := ps.GetCertificate(sn) | ||||||
| 	if appCert == nil { | 	if appCert == nil { | ||||||
| 		return &ps.defaultCert, nil | 		return &ps.defaultCert, nil | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -5,12 +5,13 @@ | |||||||
|         <meta charset="UTF-8"> |         <meta charset="UTF-8"> | ||||||
|         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> |         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||||
|         <title>{{.Title}}</title> |         <title>{{.Title}}</title> | ||||||
|         <link rel="shortcut icon" type="image/png" href="/akprox/static/dist/assets/icons/icon.png"> |         <link rel="shortcut icon" type="image/png" href="/outpost.goauthentik.io/static/dist/assets/icons/icon.png"> | ||||||
|         <link rel="stylesheet" type="text/css" href="/akprox/static/dist/patternfly.min.css"> |         <link rel="stylesheet" type="text/css" href="/outpost.goauthentik.io/static/dist/patternfly.min.css"> | ||||||
|         <link rel="stylesheet" type="text/css" href="/akprox/static/dist/authentik.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"> | ||||||
|         <style> |         <style> | ||||||
|             .pf-c-background-image::before { |             .pf-c-background-image::before { | ||||||
|                 --ak-flow-background: url("/akprox/static/dist/assets/images/flow_background.jpg"); |                 --ak-flow-background: url("/outpost.goauthentik.io/static/dist/assets/images/flow_background.jpg"); | ||||||
|             } |             } | ||||||
|         </style> |         </style> | ||||||
|     </head> |     </head> | ||||||
| @ -32,7 +33,7 @@ | |||||||
|             <div class="ak-login-container"> |             <div class="ak-login-container"> | ||||||
|                 <header class="pf-c-login__header"> |                 <header class="pf-c-login__header"> | ||||||
|                     <div class="pf-c-brand ak-brand"> |                     <div class="pf-c-brand ak-brand"> | ||||||
|                         <img src="/akprox/static/dist/assets/icons/icon_left_brand.svg" alt="authentik icon" /> |                         <img src="/outpost.goauthentik.io/static/dist/assets/icons/icon_left_brand.svg" alt="authentik icon" /> | ||||||
|                     </div> |                     </div> | ||||||
|                 </header> |                 </header> | ||||||
|                 <main class="pf-c-login__main"> |                 <main class="pf-c-login__main"> | ||||||
|  | |||||||
| @ -3,7 +3,8 @@ package templates | |||||||
| import ( | import ( | ||||||
| 	_ "embed" | 	_ "embed" | ||||||
| 	"html/template" | 	"html/template" | ||||||
| 	"log" |  | ||||||
|  | 	log "github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| //go:embed error.html | //go:embed error.html | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| package web | package web | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httputil" | 	"net/http/httputil" | ||||||
| @ -24,11 +25,12 @@ func (ws *WebServer) configureProxy() { | |||||||
| 		if req.TLS != nil { | 		if req.TLS != nil { | ||||||
| 			req.Header.Set("X-Forwarded-Proto", "https") | 			req.Header.Set("X-Forwarded-Proto", "https") | ||||||
| 		} | 		} | ||||||
|  | 		ws.log.WithField("url", req.URL.String()).WithField("headers", req.Header).Trace("tracing request to backend") | ||||||
| 	} | 	} | ||||||
| 	rp := &httputil.ReverseProxy{Director: director} | 	rp := &httputil.ReverseProxy{Director: director} | ||||||
| 	rp.ErrorHandler = ws.proxyErrorHandler | 	rp.ErrorHandler = ws.proxyErrorHandler | ||||||
| 	rp.ModifyResponse = ws.proxyModifyResponse | 	rp.ModifyResponse = ws.proxyModifyResponse | ||||||
| 	ws.m.PathPrefix("/akprox").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | 	ws.m.PathPrefix("/outpost.goauthentik.io").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||||||
| 		if ws.ProxyServer != nil { | 		if ws.ProxyServer != nil { | ||||||
| 			before := time.Now() | 			before := time.Now() | ||||||
| 			ws.ProxyServer.Handle(rw, r) | 			ws.ProxyServer.Handle(rw, r) | ||||||
| @ -65,9 +67,20 @@ func (ws *WebServer) configureProxy() { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) { | func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request, err error) { | ||||||
| 	ws.log.Warning(err.Error()) | 	ws.log.WithError(err).Warning("failed to proxy to backend") | ||||||
| 	rw.WriteHeader(http.StatusBadGateway) | 	rw.WriteHeader(http.StatusBadGateway) | ||||||
| 	_, err = rw.Write([]byte("authentik starting...")) | 	em := fmt.Sprintf("failed to connect to authentik backend: %v", err) | ||||||
|  | 	if !ws.p.IsRunning() { | ||||||
|  | 		em = "authentik starting..." | ||||||
|  | 	} | ||||||
|  | 	// return json if the client asks for json | ||||||
|  | 	if req.Header.Get("Accept") == "application/json" { | ||||||
|  | 		eem, _ := json.Marshal(map[string]string{ | ||||||
|  | 			"error": em, | ||||||
|  | 		}) | ||||||
|  | 		em = string(eem) | ||||||
|  | 	} | ||||||
|  | 	_, err = rw.Write([]byte(em)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		ws.log.WithError(err).Warning("failed to write error message") | 		ws.log.WithError(err).Warning("failed to write error message") | ||||||
| 	} | 	} | ||||||
| @ -75,5 +88,6 @@ func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request | |||||||
|  |  | ||||||
| func (ws *WebServer) proxyModifyResponse(r *http.Response) error { | func (ws *WebServer) proxyModifyResponse(r *http.Response) error { | ||||||
| 	r.Header.Set("X-Powered-By", "authentik") | 	r.Header.Set("X-Powered-By", "authentik") | ||||||
|  | 	r.Header.Del("Server") | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -16,6 +16,9 @@ func (ws *WebServer) GetCertificate() func(ch *tls.ClientHelloInfo) (*tls.Certif | |||||||
| 		ws.log.WithError(err).Error("failed to generate default cert") | 		ws.log.WithError(err).Error("failed to generate default cert") | ||||||
| 	} | 	} | ||||||
| 	return func(ch *tls.ClientHelloInfo) (*tls.Certificate, error) { | 	return func(ch *tls.ClientHelloInfo) (*tls.Certificate, error) { | ||||||
|  | 		if ch.ServerName == "" { | ||||||
|  | 			return &cert, nil | ||||||
|  | 		} | ||||||
| 		if ws.ProxyServer != nil { | 		if ws.ProxyServer != nil { | ||||||
| 			appCert := ws.ProxyServer.GetCertificate(ch.ServerName) | 			appCert := ws.ProxyServer.GetCertificate(ch.ServerName) | ||||||
| 			if appCert != nil { | 			if appCert != nil { | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| # Stage 1: Build | # Stage 1: Build | ||||||
| FROM docker.io/golang:1.17.6-bullseye AS builder | FROM docker.io/golang:1.17.7-bullseye AS builder | ||||||
|  |  | ||||||
| WORKDIR /go/src/goauthentik.io | WORKDIR /go/src/goauthentik.io | ||||||
|  |  | ||||||
| @ -19,7 +19,7 @@ ENV GIT_BUILD_HASH=$GIT_BUILD_HASH | |||||||
|  |  | ||||||
| COPY --from=builder /go/ldap / | COPY --from=builder /go/ldap / | ||||||
|  |  | ||||||
| HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/akprox/ping" ] | HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/outpost.goauthentik.io/ping" ] | ||||||
|  |  | ||||||
| EXPOSE 3389 6636 9300 | EXPOSE 3389 6636 9300 | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										30
									
								
								lifecycle/ak
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								lifecycle/ak
									
									
									
									
									
								
							| @ -32,30 +32,6 @@ function check_if_root { | |||||||
|     chpst -u authentik:$GROUP env HOME=/authentik $1 |     chpst -u authentik:$GROUP env HOME=/authentik $1 | ||||||
| } | } | ||||||
|  |  | ||||||
| function prefixwith { |  | ||||||
|     local prefix="$1" |  | ||||||
|     shift |  | ||||||
|     "$@" > >(sed "s/^/$prefix: /") 2> >(sed "s/^/$prefix (err): /" >&2) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function restore { |  | ||||||
|     PG_HOST=$(python -m authentik.lib.config postgresql.host 2> /dev/null) |  | ||||||
|     PG_NAME=$(python -m authentik.lib.config postgresql.name 2> /dev/null) |  | ||||||
|     PG_USER=$(python -m authentik.lib.config postgresql.user 2> /dev/null) |  | ||||||
|     PG_PORT=$(python -m authentik.lib.config postgresql.port 2> /dev/null) |  | ||||||
|     export PGPASSWORD=$(python -m authentik.lib.config postgresql.password 2> /dev/null) |  | ||||||
|     log "Ensuring no one can connect to the database" |  | ||||||
|     prefixwith "psql" psql -h"${PG_HOST}" -U"${PG_USER}" -c"UPDATE pg_database SET datallowconn = 'false' WHERE datname = '${PG_NAME}';" "postgres" |  | ||||||
|     prefixwith "psql" psql -h"${PG_HOST}" -U"${PG_USER}" -c"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${PG_NAME}';" "postgres" |  | ||||||
|     log "deleting and re-creating database" |  | ||||||
|     prefixwith "psql" dropdb -h"${PG_HOST}" -U"${PG_USER}" "${PG_NAME}" || trueacku |  | ||||||
|     prefixwith "psql" createdb -h"${PG_HOST}" -U"${PG_USER}" "${PG_NAME}" |  | ||||||
|     log "running initial migrations" |  | ||||||
|     prefixwith "migrate" python -m lifecycle.migrate 2> /dev/null |  | ||||||
|     log "restoring database" |  | ||||||
|     prefixwith "restore" python -m manage dbrestore -i ${@:2} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| MODE_FILE="/tmp/authentik-mode" | MODE_FILE="/tmp/authentik-mode" | ||||||
|  |  | ||||||
| if [[ "$1" == "server" ]]; then | if [[ "$1" == "server" ]]; then | ||||||
| @ -75,12 +51,6 @@ elif [[ "$1" == "worker" ]]; then | |||||||
| elif [[ "$1" == "flower" ]]; then | elif [[ "$1" == "flower" ]]; then | ||||||
|     echo "flower" > $MODE_FILE |     echo "flower" > $MODE_FILE | ||||||
|     celery -A authentik.root.celery flower |     celery -A authentik.root.celery flower | ||||||
| elif [[ "$1" == "backup" ]]; then |  | ||||||
|     wait_for_db |  | ||||||
|     python -m manage dbbackup --clean |  | ||||||
| elif [[ "$1" == "restore" ]]; then |  | ||||||
|     wait_for_db |  | ||||||
|     restore $@ |  | ||||||
| elif [[ "$1" == "bash" ]]; then | elif [[ "$1" == "bash" ]]; then | ||||||
|     /bin/bash |     /bin/bash | ||||||
| elif [[ "$1" == "test" ]]; then | elif [[ "$1" == "test" ]]; then | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								locale/zh-Hans/LC_MESSAGES/django.mo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								locale/zh-Hans/LC_MESSAGES/django.mo
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1749
									
								
								locale/zh-Hans/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1749
									
								
								locale/zh-Hans/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								locale/zh-Hant/LC_MESSAGES/django.mo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								locale/zh-Hant/LC_MESSAGES/django.mo
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1749
									
								
								locale/zh-Hant/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1749
									
								
								locale/zh-Hant/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								locale/zh_TW/LC_MESSAGES/django.mo
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								locale/zh_TW/LC_MESSAGES/django.mo
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1749
									
								
								locale/zh_TW/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1749
									
								
								locale/zh_TW/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -22,13 +22,6 @@ warnings.filterwarnings( | |||||||
|         "efault_app_config." |         "efault_app_config." | ||||||
|     ), |     ), | ||||||
| ) | ) | ||||||
| warnings.filterwarnings( |  | ||||||
|     "ignore", |  | ||||||
|     message=( |  | ||||||
|         "'dbbackup' defines default_app_config = 'dbbackup.apps.DbbackupConfig'. Django now det" |  | ||||||
|         "ects this configuration automatically. You can remove default_app_config." |  | ||||||
|     ), |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| defuse_stdlib() | defuse_stdlib() | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										631
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										631
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -7,7 +7,7 @@ ENV NODE_ENV=production | |||||||
| RUN cd /static && npm i && npm run build-proxy | RUN cd /static && npm i && npm run build-proxy | ||||||
|  |  | ||||||
| # Stage 2: Build | # Stage 2: Build | ||||||
| FROM docker.io/golang:1.17.6-bullseye AS builder | FROM docker.io/golang:1.17.7-bullseye AS builder | ||||||
|  |  | ||||||
| WORKDIR /go/src/goauthentik.io | WORKDIR /go/src/goauthentik.io | ||||||
|  |  | ||||||
| @ -32,7 +32,7 @@ COPY --from=web-builder /static/security.txt /web/security.txt | |||||||
| COPY --from=web-builder /static/dist/ /web/dist/ | COPY --from=web-builder /static/dist/ /web/dist/ | ||||||
| COPY --from=web-builder /static/authentik/ /web/authentik/ | COPY --from=web-builder /static/authentik/ /web/authentik/ | ||||||
|  |  | ||||||
| HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/akprox/ping" ] | HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:9300/outpost.goauthentik.io/ping" ] | ||||||
|  |  | ||||||
| EXPOSE 9000 9300 9443 | EXPOSE 9000 9300 9443 | ||||||
|  |  | ||||||
|  | |||||||
| @ -92,12 +92,11 @@ addopts = "-p no:celery --junitxml=unittest.xml" | |||||||
|  |  | ||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2022.1.4" | version = "2022.2.1" | ||||||
| description = "" | description = "" | ||||||
| authors = ["Jens Langhammer <jens.langhammer@beryju.org>"] | authors = ["Jens Langhammer <jens.langhammer@beryju.org>"] | ||||||
|  |  | ||||||
| [tool.poetry.dependencies] | [tool.poetry.dependencies] | ||||||
| boto3 = "*" |  | ||||||
| celery = "*" | celery = "*" | ||||||
| channels = "*" | channels = "*" | ||||||
| channels-redis = "*" | channels-redis = "*" | ||||||
| @ -107,14 +106,12 @@ dacite = "*" | |||||||
| deepmerge = "*" | deepmerge = "*" | ||||||
| defusedxml = "*" | defusedxml = "*" | ||||||
| django = "*" | django = "*" | ||||||
| django-dbbackup = "=4.0.0b0" |  | ||||||
| django-filter = "*" | django-filter = "*" | ||||||
| django-guardian = "*" | django-guardian = "*" | ||||||
| django-model-utils = "*" | django-model-utils = "*" | ||||||
| django-otp = "*" | django-otp = "*" | ||||||
| django-prometheus = "*" | django-prometheus = "*" | ||||||
| django-redis = "*" | django-redis = "*" | ||||||
| django-storages = "*" |  | ||||||
| djangorestframework = "*" | djangorestframework = "*" | ||||||
| djangorestframework-guardian = "*" | djangorestframework-guardian = "*" | ||||||
| docker = "*" | docker = "*" | ||||||
|  | |||||||
							
								
								
									
										56
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								schema.yml
									
									
									
									
									
								
							| @ -1,7 +1,7 @@ | |||||||
| openapi: 3.0.3 | openapi: 3.0.3 | ||||||
| info: | info: | ||||||
|   title: authentik |   title: authentik | ||||||
|   version: 2022.1.4 |   version: 2022.2.1 | ||||||
|   description: Making authentication simple. |   description: Making authentication simple. | ||||||
|   contact: |   contact: | ||||||
|     email: hello@beryju.org |     email: hello@beryju.org | ||||||
| @ -15045,10 +15045,14 @@ paths: | |||||||
|       description: AuthenticatorValidateStage Viewset |       description: AuthenticatorValidateStage Viewset | ||||||
|       parameters: |       parameters: | ||||||
|       - in: query |       - in: query | ||||||
|         name: configuration_stage |         name: configuration_stages | ||||||
|         schema: |         schema: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|  |         explode: true | ||||||
|  |         style: form | ||||||
|       - in: query |       - in: query | ||||||
|         name: name |         name: name | ||||||
|         schema: |         schema: | ||||||
| @ -19826,11 +19830,12 @@ components: | |||||||
|           items: |           items: | ||||||
|             $ref: '#/components/schemas/DeviceClassesEnum' |             $ref: '#/components/schemas/DeviceClassesEnum' | ||||||
|           description: Device classes which can be used to authenticate |           description: Device classes which can be used to authenticate | ||||||
|         configuration_stage: |         configuration_stages: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|           nullable: true |           description: Stages used to configure Authenticator when user doesn't have | ||||||
|           description: Stage used to configure Authenticator when user doesn't have |  | ||||||
|             any compatible devices. After this configuration Stage passes, the user |             any compatible devices. After this configuration Stage passes, the user | ||||||
|             is not prompted again. |             is not prompted again. | ||||||
|       required: |       required: | ||||||
| @ -19858,11 +19863,12 @@ components: | |||||||
|           items: |           items: | ||||||
|             $ref: '#/components/schemas/DeviceClassesEnum' |             $ref: '#/components/schemas/DeviceClassesEnum' | ||||||
|           description: Device classes which can be used to authenticate |           description: Device classes which can be used to authenticate | ||||||
|         configuration_stage: |         configuration_stages: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|           nullable: true |           description: Stages used to configure Authenticator when user doesn't have | ||||||
|           description: Stage used to configure Authenticator when user doesn't have |  | ||||||
|             any compatible devices. After this configuration Stage passes, the user |             any compatible devices. After this configuration Stage passes, the user | ||||||
|             is not prompted again. |             is not prompted again. | ||||||
|       required: |       required: | ||||||
| @ -19892,7 +19898,12 @@ components: | |||||||
|           type: array |           type: array | ||||||
|           items: |           items: | ||||||
|             $ref: '#/components/schemas/DeviceChallenge' |             $ref: '#/components/schemas/DeviceChallenge' | ||||||
|  |         configuration_stages: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|  |             $ref: '#/components/schemas/SelectableStage' | ||||||
|       required: |       required: | ||||||
|  |       - configuration_stages | ||||||
|       - device_challenges |       - device_challenges | ||||||
|       - pending_user |       - pending_user | ||||||
|       - pending_user_avatar |       - pending_user_avatar | ||||||
| @ -19907,6 +19918,9 @@ components: | |||||||
|           default: ak-stage-authenticator-validate |           default: ak-stage-authenticator-validate | ||||||
|         selected_challenge: |         selected_challenge: | ||||||
|           $ref: '#/components/schemas/DeviceChallengeRequest' |           $ref: '#/components/schemas/DeviceChallengeRequest' | ||||||
|  |         selected_stage: | ||||||
|  |           type: string | ||||||
|  |           minLength: 1 | ||||||
|         code: |         code: | ||||||
|           type: string |           type: string | ||||||
|           minLength: 1 |           minLength: 1 | ||||||
| @ -20017,7 +20031,6 @@ components: | |||||||
|       enum: |       enum: | ||||||
|       - can_save_media |       - can_save_media | ||||||
|       - can_geo_ip |       - can_geo_ip | ||||||
|       - can_backup |  | ||||||
|       type: string |       type: string | ||||||
|     CaptchaChallenge: |     CaptchaChallenge: | ||||||
|       type: object |       type: object | ||||||
| @ -26678,11 +26691,12 @@ components: | |||||||
|           items: |           items: | ||||||
|             $ref: '#/components/schemas/DeviceClassesEnum' |             $ref: '#/components/schemas/DeviceClassesEnum' | ||||||
|           description: Device classes which can be used to authenticate |           description: Device classes which can be used to authenticate | ||||||
|         configuration_stage: |         configuration_stages: | ||||||
|  |           type: array | ||||||
|  |           items: | ||||||
|             type: string |             type: string | ||||||
|             format: uuid |             format: uuid | ||||||
|           nullable: true |           description: Stages used to configure Authenticator when user doesn't have | ||||||
|           description: Stage used to configure Authenticator when user doesn't have |  | ||||||
|             any compatible devices. After this configuration Stage passes, the user |             any compatible devices. After this configuration Stage passes, the user | ||||||
|             is not prompted again. |             is not prompted again. | ||||||
|     PatchedCaptchaStageRequest: |     PatchedCaptchaStageRequest: | ||||||
| @ -30018,6 +30032,24 @@ components: | |||||||
|       - direct |       - direct | ||||||
|       - cached |       - cached | ||||||
|       type: string |       type: string | ||||||
|  |     SelectableStage: | ||||||
|  |       type: object | ||||||
|  |       description: Serializer for stages which can be selected by users | ||||||
|  |       properties: | ||||||
|  |         pk: | ||||||
|  |           type: string | ||||||
|  |           format: uuid | ||||||
|  |         name: | ||||||
|  |           type: string | ||||||
|  |         verbose_name: | ||||||
|  |           type: string | ||||||
|  |         meta_model_name: | ||||||
|  |           type: string | ||||||
|  |       required: | ||||||
|  |       - meta_model_name | ||||||
|  |       - name | ||||||
|  |       - pk | ||||||
|  |       - verbose_name | ||||||
|     ServiceConnection: |     ServiceConnection: | ||||||
|       type: object |       type: object | ||||||
|       description: ServiceConnection Serializer |       description: ServiceConnection Serializer | ||||||
|  | |||||||
| @ -105,7 +105,7 @@ class TestProviderProxy(SeleniumTestCase): | |||||||
|         self.assertIn(f"X-Authentik-Username: {self.user.username}", full_body_text) |         self.assertIn(f"X-Authentik-Username: {self.user.username}", full_body_text) | ||||||
|         self.assertIn("X-Foo: bar", full_body_text) |         self.assertIn("X-Foo: bar", full_body_text) | ||||||
|  |  | ||||||
|         self.driver.get("http://localhost:9000/akprox/sign_out") |         self.driver.get("http://localhost:9000/outpost.goauthentik.io/sign_out") | ||||||
|         sleep(2) |         sleep(2) | ||||||
|         full_body_text = self.driver.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text |         full_body_text = self.driver.find_element(By.CSS_SELECTOR, ".pf-c-title.pf-m-3xl").text | ||||||
|         self.assertIn("You've logged out of proxy.", full_body_text) |         self.assertIn("You've logged out of proxy.", full_body_text) | ||||||
|  | |||||||
							
								
								
									
										873
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										873
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -22,7 +22,11 @@ | |||||||
|             "fr_FR", |             "fr_FR", | ||||||
|             "tr", |             "tr", | ||||||
|             "es", |             "es", | ||||||
|             "pl" |             "pl", | ||||||
|  |             "zh_TW", | ||||||
|  |             "zh-Hans", | ||||||
|  |             "zh-Hant", | ||||||
|  |             "de" | ||||||
|         ], |         ], | ||||||
|         "formatOptions": { |         "formatOptions": { | ||||||
|             "lineNumbers": false |             "lineNumbers": false | ||||||
| @ -48,14 +52,14 @@ | |||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@babel/core": "^7.16.12", |         "@babel/core": "^7.17.4", | ||||||
|         "@babel/plugin-proposal-decorators": "^7.16.7", |         "@babel/plugin-proposal-decorators": "^7.17.2", | ||||||
|         "@babel/plugin-transform-runtime": "^7.16.10", |         "@babel/plugin-transform-runtime": "^7.17.0", | ||||||
|         "@babel/preset-env": "^7.16.11", |         "@babel/preset-env": "^7.16.11", | ||||||
|         "@babel/preset-typescript": "^7.16.7", |         "@babel/preset-typescript": "^7.16.7", | ||||||
|         "@formatjs/intl-listformat": "^6.5.1", |         "@formatjs/intl-listformat": "^6.5.2", | ||||||
|         "@fortawesome/fontawesome-free": "^5.15.4", |         "@fortawesome/fontawesome-free": "^6.0.0", | ||||||
|         "@goauthentik/api": "^2022.1.3-1643236150", |         "@goauthentik/api": "^2022.1.5-1644681372", | ||||||
|         "@jackfranklin/rollup-plugin-markdown": "^0.3.0", |         "@jackfranklin/rollup-plugin-markdown": "^0.3.0", | ||||||
|         "@lingui/cli": "^3.13.2", |         "@lingui/cli": "^3.13.2", | ||||||
|         "@lingui/core": "^3.13.2", |         "@lingui/core": "^3.13.2", | ||||||
| @ -67,36 +71,36 @@ | |||||||
|         "@rollup/plugin-babel": "^5.3.0", |         "@rollup/plugin-babel": "^5.3.0", | ||||||
|         "@rollup/plugin-commonjs": "^21.0.1", |         "@rollup/plugin-commonjs": "^21.0.1", | ||||||
|         "@rollup/plugin-node-resolve": "^13.1.3", |         "@rollup/plugin-node-resolve": "^13.1.3", | ||||||
|         "@rollup/plugin-replace": "^3.0.1", |         "@rollup/plugin-replace": "^3.1.0", | ||||||
|         "@rollup/plugin-typescript": "^8.3.0", |         "@rollup/plugin-typescript": "^8.3.0", | ||||||
|         "@sentry/browser": "^6.17.3", |         "@sentry/browser": "^6.17.8", | ||||||
|         "@sentry/tracing": "^6.17.3", |         "@sentry/tracing": "^6.17.8", | ||||||
|         "@squoosh/cli": "^0.7.2", |         "@squoosh/cli": "^0.7.2", | ||||||
|         "@trivago/prettier-plugin-sort-imports": "^3.1.1", |         "@trivago/prettier-plugin-sort-imports": "^3.2.0", | ||||||
|         "@types/chart.js": "^2.9.35", |         "@types/chart.js": "^2.9.35", | ||||||
|         "@types/codemirror": "5.60.5", |         "@types/codemirror": "5.60.5", | ||||||
|         "@types/grecaptcha": "^3.0.3", |         "@types/grecaptcha": "^3.0.3", | ||||||
|         "@typescript-eslint/eslint-plugin": "^5.10.2", |         "@typescript-eslint/eslint-plugin": "^5.12.0", | ||||||
|         "@typescript-eslint/parser": "^5.10.2", |         "@typescript-eslint/parser": "^5.12.0", | ||||||
|         "@webcomponents/webcomponentsjs": "^2.6.0", |         "@webcomponents/webcomponentsjs": "^2.6.0", | ||||||
|         "babel-plugin-macros": "^3.1.0", |         "babel-plugin-macros": "^3.1.0", | ||||||
|         "base64-js": "^1.5.1", |         "base64-js": "^1.5.1", | ||||||
|         "chart.js": "^3.7.0", |         "chart.js": "^3.7.1", | ||||||
|         "chartjs-adapter-moment": "^1.0.0", |         "chartjs-adapter-moment": "^1.0.0", | ||||||
|         "codemirror": "^5.65.1", |         "codemirror": "^5.65.1", | ||||||
|         "construct-style-sheets-polyfill": "^3.1.0", |         "construct-style-sheets-polyfill": "^3.1.0", | ||||||
|         "country-flag-icons": "^1.4.20", |         "country-flag-icons": "^1.4.21", | ||||||
|         "eslint": "^8.8.0", |         "eslint": "^8.9.0", | ||||||
|         "eslint-config-google": "^0.14.0", |         "eslint-config-google": "^0.14.0", | ||||||
|         "eslint-plugin-custom-elements": "0.0.4", |         "eslint-plugin-custom-elements": "0.0.4", | ||||||
|         "eslint-plugin-lit": "^1.6.1", |         "eslint-plugin-lit": "^1.6.1", | ||||||
|         "flowchart.js": "^1.17.0", |         "flowchart.js": "^1.17.1", | ||||||
|         "fuse.js": "^6.5.3", |         "fuse.js": "^6.5.3", | ||||||
|         "lit": "^2.1.2", |         "lit": "^2.1.4", | ||||||
|         "moment": "^2.29.1", |         "moment": "^2.29.1", | ||||||
|         "prettier": "^2.5.1", |         "prettier": "^2.5.1", | ||||||
|         "rapidoc": "^9.1.4", |         "rapidoc": "^9.1.4", | ||||||
|         "rollup": "^2.66.1", |         "rollup": "^2.67.2", | ||||||
|         "rollup-plugin-copy": "^3.4.0", |         "rollup-plugin-copy": "^3.4.0", | ||||||
|         "rollup-plugin-cssimport": "^1.0.2", |         "rollup-plugin-cssimport": "^1.0.2", | ||||||
|         "rollup-plugin-minify-html-literals": "^1.2.6", |         "rollup-plugin-minify-html-literals": "^1.2.6", | ||||||
|  | |||||||
| @ -34,6 +34,7 @@ export const resources = [ | |||||||
|         dest: "dist/", |         dest: "dist/", | ||||||
|     }, |     }, | ||||||
|     { src: "src/authentik.css", dest: "dist/" }, |     { src: "src/authentik.css", dest: "dist/" }, | ||||||
|  |     { src: "src/custom.css", dest: "dist/" }, | ||||||
|  |  | ||||||
|     { |     { | ||||||
|         src: "node_modules/@patternfly/patternfly/assets/*", |         src: "node_modules/@patternfly/patternfly/assets/*", | ||||||
|  | |||||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 704 KiB After Width: | Height: | Size: 580 KiB | 
| @ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success"; | |||||||
| export const ERROR_CLASS = "pf-m-danger"; | export const ERROR_CLASS = "pf-m-danger"; | ||||||
| export const PROGRESS_CLASS = "pf-m-in-progress"; | export const PROGRESS_CLASS = "pf-m-in-progress"; | ||||||
| export const CURRENT_CLASS = "pf-m-current"; | export const CURRENT_CLASS = "pf-m-current"; | ||||||
| export const VERSION = "2022.1.4"; | export const VERSION = "2022.2.1"; | ||||||
| export const TITLE_DEFAULT = "authentik"; | export const TITLE_DEFAULT = "authentik"; | ||||||
| export const ROUTE_SEPARATOR = ";"; | export const ROUTE_SEPARATOR = ";"; | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										1
									
								
								web/src/custom.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/src/custom.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | /* User customisable */ | ||||||
| @ -433,8 +433,7 @@ export class FlowExecutor extends LitElement implements StageHost { | |||||||
|                                                 ) |                                                 ) | ||||||
|                                                     ? html` |                                                     ? html` | ||||||
|                                                           <li> |                                                           <li> | ||||||
|                                                               <a |                                                               <a href="https://unsplash.com/@trime" | ||||||
|                                                                   href="https://unsplash.com/@kimonmaritz" |  | ||||||
|                                                                   >${t`Background image`}</a |                                                                   >${t`Background image`}</a | ||||||
|                                                               > |                                                               > | ||||||
|                                                           </li> |                                                           </li> | ||||||
|  | |||||||
| @ -67,7 +67,7 @@ export class AuthenticatorValidateStage | |||||||
|         return this._selectedDeviceChallenge; |         return this._selectedDeviceChallenge; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<void> { |     submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<boolean> { | ||||||
|         return this.host?.submit(payload) || Promise.resolve(); |         return this.host?.submit(payload) || Promise.resolve(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @ -140,7 +140,7 @@ export class AuthenticatorValidateStage | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     renderDevicePicker(): TemplateResult { |     renderDevicePicker(): TemplateResult { | ||||||
|         return html` <ul> |         return html`<ul> | ||||||
|             ${this.challenge?.deviceChallenges.map((challenges) => { |             ${this.challenge?.deviceChallenges.map((challenges) => { | ||||||
|                 return html`<li> |                 return html`<li> | ||||||
|                     <button |                     <button | ||||||
| @ -157,6 +157,30 @@ export class AuthenticatorValidateStage | |||||||
|         </ul>`; |         </ul>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     renderStagePicker(): TemplateResult { | ||||||
|  |         return html`<ul> | ||||||
|  |             ${this.challenge?.configurationStages.map((stage) => { | ||||||
|  |                 return html`<li> | ||||||
|  |                     <button | ||||||
|  |                         class="pf-c-button authenticator-button" | ||||||
|  |                         type="button" | ||||||
|  |                         @click=${() => { | ||||||
|  |                             this.submit({ | ||||||
|  |                                 component: this.challenge.component || "", | ||||||
|  |                                 selectedStage: stage.pk, | ||||||
|  |                             }); | ||||||
|  |                         }} | ||||||
|  |                     > | ||||||
|  |                         <div class="right"> | ||||||
|  |                             <p>${stage.name}</p> | ||||||
|  |                             <small>${stage.verboseName}</small> | ||||||
|  |                         </div> | ||||||
|  |                     </button> | ||||||
|  |                 </li>`; | ||||||
|  |             })} | ||||||
|  |         </ul>`; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     renderDeviceChallenge(): TemplateResult { |     renderDeviceChallenge(): TemplateResult { | ||||||
|         if (!this.selectedDeviceChallenge) { |         if (!this.selectedDeviceChallenge) { | ||||||
|             return html``; |             return html``; | ||||||
| @ -242,6 +266,9 @@ export class AuthenticatorValidateStage | |||||||
|                               ${this.selectedDeviceChallenge |                               ${this.selectedDeviceChallenge | ||||||
|                                   ? "" |                                   ? "" | ||||||
|                                   : html`<p>${t`Select an authentication method.`}</p>`} |                                   : html`<p>${t`Select an authentication method.`}</p>`} | ||||||
|  |                               ${this.challenge.configurationStages.length > 0 | ||||||
|  |                                   ? this.renderStagePicker() | ||||||
|  |                                   : html``} | ||||||
|                           </form> |                           </form> | ||||||
|                           ${this.renderDevicePicker()} |                           ${this.renderDevicePicker()} | ||||||
|                       </div> |                       </div> | ||||||
|  | |||||||
| @ -1,15 +1,19 @@ | |||||||
| import { en, es, fr, pl, tr } from "make-plural/plurals"; | import { de, en, es, fr, pl, tr, zh } from "make-plural/plurals"; | ||||||
|  |  | ||||||
| import { Messages, i18n } from "@lingui/core"; | import { Messages, i18n } from "@lingui/core"; | ||||||
| import { detect, fromNavigator, fromStorage, fromUrl } from "@lingui/detect-locale"; | import { detect, fromNavigator, fromStorage, fromUrl } from "@lingui/detect-locale"; | ||||||
| import { t } from "@lingui/macro"; | import { t } from "@lingui/macro"; | ||||||
|  |  | ||||||
|  | import { messages as localeDE } from "../locales/de"; | ||||||
| import { messages as localeEN } from "../locales/en"; | import { messages as localeEN } from "../locales/en"; | ||||||
| import { messages as localeES } from "../locales/es"; | import { messages as localeES } from "../locales/es"; | ||||||
| import { messages as localeFR_FR } from "../locales/fr_FR"; | import { messages as localeFR_FR } from "../locales/fr_FR"; | ||||||
| import { messages as localePL } from "../locales/pl"; | import { messages as localePL } from "../locales/pl"; | ||||||
| import { messages as localeDEBUG } from "../locales/pseudo-LOCALE"; | import { messages as localeDEBUG } from "../locales/pseudo-LOCALE"; | ||||||
| import { messages as localeTR } from "../locales/tr"; | import { messages as localeTR } from "../locales/tr"; | ||||||
|  | import { messages as localeZH_Hans } from "../locales/zh-Hans"; | ||||||
|  | import { messages as localeZH_Hant } from "../locales/zh-Hant"; | ||||||
|  | import { messages as localeZH_TW } from "../locales/zh_TW"; | ||||||
|  |  | ||||||
| export const LOCALES: { | export const LOCALES: { | ||||||
|     code: string; |     code: string; | ||||||
| @ -54,6 +58,30 @@ export const LOCALES: { | |||||||
|         label: t`Polish`, |         label: t`Polish`, | ||||||
|         locale: localePL, |         locale: localePL, | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |         code: "zh_TW", | ||||||
|  |         plurals: zh, | ||||||
|  |         label: t`Taiwanese Mandarin`, | ||||||
|  |         locale: localeZH_TW, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         code: "zh-Hans", | ||||||
|  |         plurals: zh, | ||||||
|  |         label: t`Chinese (simplified)`, | ||||||
|  |         locale: localeZH_Hans, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         code: "zh-Hant", | ||||||
|  |         plurals: zh, | ||||||
|  |         label: t`Chinese (traditional)`, | ||||||
|  |         locale: localeZH_Hant, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         code: "de", | ||||||
|  |         plurals: de, | ||||||
|  |         label: t`German`, | ||||||
|  |         locale: localeDE, | ||||||
|  |     }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
| LOCALES.forEach((locale) => { | LOCALES.forEach((locale) => { | ||||||
|  | |||||||
							
								
								
									
										6125
									
								
								web/src/locales/de.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6125
									
								
								web/src/locales/de.po
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -563,16 +563,16 @@ msgid "Background shown during execution." | |||||||
| msgstr "Background shown during execution." | msgstr "Background shown during execution." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with errors." | #~ msgid "Backup finished with errors." | ||||||
| msgstr "Backup finished with errors." | #~ msgstr "Backup finished with errors." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with warnings/backup not supported." | #~ msgid "Backup finished with warnings/backup not supported." | ||||||
| msgstr "Backup finished with warnings/backup not supported." | #~ msgstr "Backup finished with warnings/backup not supported." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/AdminOverviewPage.ts | #: src/pages/admin-overview/AdminOverviewPage.ts | ||||||
| msgid "Backup status" | #~ msgid "Backup status" | ||||||
| msgstr "Backup status" | #~ msgstr "Backup status" | ||||||
|  |  | ||||||
| #: src/pages/providers/ldap/LDAPProviderForm.ts | #: src/pages/providers/ldap/LDAPProviderForm.ts | ||||||
| #: src/pages/providers/ldap/LDAPProviderViewPage.ts | #: src/pages/providers/ldap/LDAPProviderViewPage.ts | ||||||
| @ -844,6 +844,14 @@ msgstr "Checks if the request's user's password has been changed in the last x d | |||||||
| msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | ||||||
| msgstr "Checks the value from the policy request against several rules, mostly used to ensure password strength." | msgstr "Checks the value from the policy request against several rules, mostly used to ensure password strength." | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (simplified)" | ||||||
|  | msgstr "Chinese (simplified)" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (traditional)" | ||||||
|  | msgstr "Chinese (traditional)" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowListPage.ts | #: src/pages/flows/FlowListPage.ts | ||||||
| msgid "Clear Flow cache" | msgid "Clear Flow cache" | ||||||
| msgstr "Clear Flow cache" | msgstr "Clear Flow cache" | ||||||
| @ -948,8 +956,12 @@ msgid "Configuration flow" | |||||||
| msgstr "Configuration flow" | msgstr "Configuration flow" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Configuration stage" | #~ msgid "Configuration stage" | ||||||
| msgstr "Configuration stage" | #~ msgstr "Configuration stage" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Configuration stages" | ||||||
|  | msgstr "Configuration stages" | ||||||
|  |  | ||||||
| #:  | #:  | ||||||
| #~ msgid "Configure WebAuthn" | #~ msgid "Configure WebAuthn" | ||||||
| @ -2205,6 +2217,10 @@ msgstr "Generic" | |||||||
| msgid "Generic OpenID Connect" | msgid "Generic OpenID Connect" | ||||||
| msgstr "Generic OpenID Connect" | msgstr "Generic OpenID Connect" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "German" | ||||||
|  | msgstr "German" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| msgid "Get this value from https://console.twilio.com" | msgid "Get this value from https://console.twilio.com" | ||||||
| @ -4483,8 +4499,8 @@ msgid "Stage type" | |||||||
| msgstr "Stage type" | msgstr "Stage type" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | #~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
| msgstr "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | #~ msgstr "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | ||||||
| msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | ||||||
| @ -4543,6 +4559,10 @@ msgstr "Stages" | |||||||
| msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | ||||||
| msgstr "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | msgstr "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
|  | msgstr "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
|  |  | ||||||
| #: src/pages/outposts/ServiceConnectionListPage.ts | #: src/pages/outposts/ServiceConnectionListPage.ts | ||||||
| msgid "State" | msgid "State" | ||||||
| msgstr "State" | msgstr "State" | ||||||
| @ -5014,6 +5034,10 @@ msgstr "TOTP Device" | |||||||
| msgid "TOTP authenticator" | msgid "TOTP authenticator" | ||||||
| msgstr "TOTP authenticator" | msgstr "TOTP authenticator" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Taiwanese Mandarin" | ||||||
|  | msgstr "Taiwanese Mandarin" | ||||||
|  |  | ||||||
| #: src/pages/flows/StageBindingForm.ts | #: src/pages/flows/StageBindingForm.ts | ||||||
| msgid "Target" | msgid "Target" | ||||||
| msgstr "Target" | msgstr "Target" | ||||||
| @ -5673,8 +5697,8 @@ msgid "Use the username and password below to authenticate. The password can be | |||||||
| msgstr "Use the username and password below to authenticate. The password can be retrieved later on the Tokens page." | msgstr "Use the username and password below to authenticate. The password can be retrieved later on the Tokens page." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)." | ||||||
| msgstr "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)." | msgstr "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | ||||||
| @ -6036,6 +6060,10 @@ msgstr "When enabled, the invitation will be deleted after usage." | |||||||
| msgid "When enabled, user fields are matched regardless of their casing." | msgid "When enabled, user fields are matched regardless of their casing." | ||||||
| msgstr "When enabled, user fields are matched regardless of their casing." | msgstr "When enabled, user fields are matched regardless of their casing." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "When multiple stages are selected, the user can choose which one they want to enroll." | ||||||
|  | msgstr "When multiple stages are selected, the user can choose which one they want to enroll." | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts | #: src/pages/stages/identification/IdentificationStageForm.ts | ||||||
| msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | ||||||
| msgstr "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | msgstr "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | ||||||
|  | |||||||
| @ -561,16 +561,16 @@ msgid "Background shown during execution." | |||||||
| msgstr "Se muestra el fondo durante la ejecución." | msgstr "Se muestra el fondo durante la ejecución." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with errors." | #~ msgid "Backup finished with errors." | ||||||
| msgstr "La copia de seguridad terminó con errores." | #~ msgstr "La copia de seguridad terminó con errores." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with warnings/backup not supported." | #~ msgid "Backup finished with warnings/backup not supported." | ||||||
| msgstr "La copia de seguridad finalizó con advertencias o copias de seguridad no compatibles" | #~ msgstr "La copia de seguridad finalizó con advertencias o copias de seguridad no compatibles" | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/AdminOverviewPage.ts | #: src/pages/admin-overview/AdminOverviewPage.ts | ||||||
| msgid "Backup status" | #~ msgid "Backup status" | ||||||
| msgstr "Estado de respaldo" | #~ msgstr "Estado de respaldo" | ||||||
|  |  | ||||||
| #: src/pages/providers/ldap/LDAPProviderForm.ts | #: src/pages/providers/ldap/LDAPProviderForm.ts | ||||||
| #: src/pages/providers/ldap/LDAPProviderViewPage.ts | #: src/pages/providers/ldap/LDAPProviderViewPage.ts | ||||||
| @ -838,6 +838,14 @@ msgstr "Comprueba si la contraseña del usuario de la solicitud se ha cambiado e | |||||||
| msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | ||||||
| msgstr "Comprueba el valor de la solicitud de política en relación con varias reglas, que se utilizan principalmente para garantizar la seguridad de la contraseña." | msgstr "Comprueba el valor de la solicitud de política en relación con varias reglas, que se utilizan principalmente para garantizar la seguridad de la contraseña." | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (simplified)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (traditional)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowListPage.ts | #: src/pages/flows/FlowListPage.ts | ||||||
| msgid "Clear Flow cache" | msgid "Clear Flow cache" | ||||||
| msgstr "Borrar caché de flujo" | msgstr "Borrar caché de flujo" | ||||||
| @ -942,8 +950,12 @@ msgid "Configuration flow" | |||||||
| msgstr "Flujo de configuración" | msgstr "Flujo de configuración" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Configuration stage" | #~ msgid "Configuration stage" | ||||||
| msgstr "Etapa de configuración" | #~ msgstr "Etapa de configuración" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Configuration stages" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #~ msgid "Configure WebAuthn" | #~ msgid "Configure WebAuthn" | ||||||
| #~ msgstr "Configurar WebAuthn" | #~ msgstr "Configurar WebAuthn" | ||||||
| @ -2166,6 +2178,10 @@ msgstr "Genérico" | |||||||
| msgid "Generic OpenID Connect" | msgid "Generic OpenID Connect" | ||||||
| msgstr "Conexión OpenID genérica" | msgstr "Conexión OpenID genérica" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "German" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| msgid "Get this value from https://console.twilio.com" | msgid "Get this value from https://console.twilio.com" | ||||||
| @ -4391,8 +4407,8 @@ msgid "Stage type" | |||||||
| msgstr "Tipo de escenario" | msgstr "Tipo de escenario" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | #~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
| msgstr "Etapa utilizada para configurar Authenticator cuando el usuario no tiene ningún dispositivo compatible. Una vez superada esta etapa de configuración, no se volverá a preguntar al usuario." | #~ msgstr "Etapa utilizada para configurar Authenticator cuando el usuario no tiene ningún dispositivo compatible. Una vez superada esta etapa de configuración, no se volverá a preguntar al usuario." | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | ||||||
| msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | ||||||
| @ -4451,6 +4467,10 @@ msgstr "Etapas" | |||||||
| msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | ||||||
| msgstr "Las etapas son pasos individuales de un flujo por los que se guía al usuario. Una etapa solo se puede ejecutar desde dentro de un flujo." | msgstr "Las etapas son pasos individuales de un flujo por los que se guía al usuario. Una etapa solo se puede ejecutar desde dentro de un flujo." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/outposts/ServiceConnectionListPage.ts | #: src/pages/outposts/ServiceConnectionListPage.ts | ||||||
| msgid "State" | msgid "State" | ||||||
| msgstr "Estado" | msgstr "Estado" | ||||||
| @ -4905,6 +4925,10 @@ msgstr "Dispositivo TOTP" | |||||||
| msgid "TOTP authenticator" | msgid "TOTP authenticator" | ||||||
| msgstr "Autenticador TOTP" | msgstr "Autenticador TOTP" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Taiwanese Mandarin" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/StageBindingForm.ts | #: src/pages/flows/StageBindingForm.ts | ||||||
| msgid "Target" | msgid "Target" | ||||||
| msgstr "Objetivo" | msgstr "Objetivo" | ||||||
| @ -5556,8 +5580,8 @@ msgid "Use the username and password below to authenticate. The password can be | |||||||
| msgstr "Use el nombre de usuario y la contraseña a continuación para autenticarse. La contraseña se puede recuperar más adelante en la página Tokens." | msgstr "Use el nombre de usuario y la contraseña a continuación para autenticarse. La contraseña se puede recuperar más adelante en la página Tokens." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)." | ||||||
| msgstr "Use este proveedor con auth_request de nginx o ForwardAuth de traefik. Cada aplicación/dominio necesita su propio proveedor. Además, en cada dominio, /akprox debe enrutarse al puesto avanzado (cuando se usa un puesto avanzado administrado, esto se hace por usted)." | msgstr "Use este proveedor con auth_request de nginx o ForwardAuth de traefik. Cada aplicación/dominio necesita su propio proveedor. Además, en cada dominio, /outpost.goauthentik.io debe enrutarse al puesto avanzado (cuando se usa un puesto avanzado administrado, esto se hace por usted)." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | ||||||
| @ -5912,6 +5936,10 @@ msgstr "Cuando se habilita, la invitación se eliminará después de su uso." | |||||||
| msgid "When enabled, user fields are matched regardless of their casing." | msgid "When enabled, user fields are matched regardless of their casing." | ||||||
| msgstr "Cuando se habilita, los campos de usuario coinciden independientemente de su carcasa." | msgstr "Cuando se habilita, los campos de usuario coinciden independientemente de su carcasa." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "When multiple stages are selected, the user can choose which one they want to enroll." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts | #: src/pages/stages/identification/IdentificationStageForm.ts | ||||||
| msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | ||||||
| msgstr "Cuando se selecciona, se muestra un campo de contraseña en la misma página en lugar de en una página separada. Esto evita ataques de enumeración de nombres de usuario." | msgstr "Cuando se selecciona, se muestra un campo de contraseña en la misma página en lugar de en una página separada. Esto evita ataques de enumeración de nombres de usuario." | ||||||
|  | |||||||
| @ -567,16 +567,16 @@ msgid "Background shown during execution." | |||||||
| msgstr "Arrière-plan utilisé durant l'exécution." | msgstr "Arrière-plan utilisé durant l'exécution." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with errors." | #~ msgid "Backup finished with errors." | ||||||
| msgstr "Sauvegarde terminée avec des erreurs." | #~ msgstr "Sauvegarde terminée avec des erreurs." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with warnings/backup not supported." | #~ msgid "Backup finished with warnings/backup not supported." | ||||||
| msgstr "Sauvegarde terminée avec avertissements/sauvegarde non supportée." | #~ msgstr "Sauvegarde terminée avec avertissements/sauvegarde non supportée." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/AdminOverviewPage.ts | #: src/pages/admin-overview/AdminOverviewPage.ts | ||||||
| msgid "Backup status" | #~ msgid "Backup status" | ||||||
| msgstr "État de la sauvegarde" | #~ msgstr "État de la sauvegarde" | ||||||
|  |  | ||||||
| #: src/pages/providers/ldap/LDAPProviderForm.ts | #: src/pages/providers/ldap/LDAPProviderForm.ts | ||||||
| #: src/pages/providers/ldap/LDAPProviderViewPage.ts | #: src/pages/providers/ldap/LDAPProviderViewPage.ts | ||||||
| @ -846,6 +846,14 @@ msgstr "Vérifie si le mot de passe de l'usager a été changé dans les X derni | |||||||
| msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | ||||||
| msgstr "Vérifie la valeur de la requête via plusieurs règles, principalement utilisé pour  s'assurer de la robustesse des mots de passe." | msgstr "Vérifie la valeur de la requête via plusieurs règles, principalement utilisé pour  s'assurer de la robustesse des mots de passe." | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (simplified)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (traditional)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowListPage.ts | #: src/pages/flows/FlowListPage.ts | ||||||
| msgid "Clear Flow cache" | msgid "Clear Flow cache" | ||||||
| msgstr "Vider le cache des flux" | msgstr "Vider le cache des flux" | ||||||
| @ -950,8 +958,12 @@ msgid "Configuration flow" | |||||||
| msgstr "Flux de configuration" | msgstr "Flux de configuration" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Configuration stage" | #~ msgid "Configuration stage" | ||||||
| msgstr "Étape de configuration" | #~ msgstr "Étape de configuration" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Configuration stages" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #:  | #:  | ||||||
| #~ msgid "Configure WebAuthn" | #~ msgid "Configure WebAuthn" | ||||||
| @ -2191,6 +2203,10 @@ msgstr "" | |||||||
| msgid "Generic OpenID Connect" | msgid "Generic OpenID Connect" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "German" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| msgid "Get this value from https://console.twilio.com" | msgid "Get this value from https://console.twilio.com" | ||||||
| @ -4444,8 +4460,8 @@ msgid "Stage type" | |||||||
| msgstr "Type d'étape" | msgstr "Type d'étape" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | #~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
| msgstr "Étape utilisée pour configurer l'Authenticator lorsqu'un utilisateur n'a pas d'appareil compatible. Une fois cette étape franchie, l'utilisateur ne sera plus sollicité." | #~ msgstr "Étape utilisée pour configurer l'Authenticator lorsqu'un utilisateur n'a pas d'appareil compatible. Une fois cette étape franchie, l'utilisateur ne sera plus sollicité." | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | ||||||
| msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | ||||||
| @ -4504,6 +4520,10 @@ msgstr "Étapes" | |||||||
| msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | ||||||
| msgstr "Les étapes sont des étapes simples d'un flux au travers duquel un utilisateur est guidé. Une étape peut être uniquement exécutée à l'intérieur d'un flux." | msgstr "Les étapes sont des étapes simples d'un flux au travers duquel un utilisateur est guidé. Une étape peut être uniquement exécutée à l'intérieur d'un flux." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/outposts/ServiceConnectionListPage.ts | #: src/pages/outposts/ServiceConnectionListPage.ts | ||||||
| msgid "State" | msgid "State" | ||||||
| msgstr "État" | msgstr "État" | ||||||
| @ -4969,6 +4989,10 @@ msgstr "" | |||||||
| msgid "TOTP authenticator" | msgid "TOTP authenticator" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Taiwanese Mandarin" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/StageBindingForm.ts | #: src/pages/flows/StageBindingForm.ts | ||||||
| msgid "Target" | msgid "Target" | ||||||
| msgstr "Cible" | msgstr "Cible" | ||||||
| @ -5614,8 +5638,8 @@ msgid "Use the username and password below to authenticate. The password can be | |||||||
| msgstr "Utilisez le nom d'utilisateur et le mot de passe ci-dessous pour vous authentifier. Le mot de passe peut être récupéré plus tard sur la page Jetons." | msgstr "Utilisez le nom d'utilisateur et le mot de passe ci-dessous pour vous authentifier. Le mot de passe peut être récupéré plus tard sur la page Jetons." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)." | ||||||
| msgstr "Utilisez ce fournisseur avec auth_request de nginx ou forwardAuth de traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, /akprox doit être routé vers l'avant-poste (si vous utilisez un avant-poste géré, cela est fait pour vous)." | msgstr "Utilisez ce fournisseur avec auth_request de nginx ou forwardAuth de traefik. Chaque application/domaine a besoin de son propre fournisseur. De plus, sur chaque domaine, /outpost.goauthentik.io doit être routé vers l'avant-poste (si vous utilisez un avant-poste géré, cela est fait pour vous)." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | ||||||
| @ -5975,6 +5999,10 @@ msgstr "Si activée, l'invitation sera supprimée après utilisation." | |||||||
| msgid "When enabled, user fields are matched regardless of their casing." | msgid "When enabled, user fields are matched regardless of their casing." | ||||||
| msgstr "Si activé, les champs de l'utilisateur sont mis en correspondance en ignorant leur casse." | msgstr "Si activé, les champs de l'utilisateur sont mis en correspondance en ignorant leur casse." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "When multiple stages are selected, the user can choose which one they want to enroll." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts | #: src/pages/stages/identification/IdentificationStageForm.ts | ||||||
| msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | ||||||
| msgstr "Si activée, un champ de mot de passe est affiché sur la même page au lieu d'une page séparée. Cela permet d'éviter les attaques par énumération de noms d'utilisateur." | msgstr "Si activée, un champ de mot de passe est affiché sur la même page au lieu d'une page séparée. Cela permet d'éviter les attaques par énumération de noms d'utilisateur." | ||||||
|  | |||||||
| @ -560,17 +560,14 @@ msgstr "Obraz tła" | |||||||
| msgid "Background shown during execution." | msgid "Background shown during execution." | ||||||
| msgstr "Tło pokazywane podczas wykonywania." | msgstr "Tło pokazywane podczas wykonywania." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #~ msgid "Backup finished with errors." | ||||||
| msgid "Backup finished with errors." | #~ msgstr "Kopia zapasowa zakończona z błędami." | ||||||
| msgstr "Kopia zapasowa zakończona z błędami." |  | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #~ msgid "Backup finished with warnings/backup not supported." | ||||||
| msgid "Backup finished with warnings/backup not supported." | #~ msgstr "Tworzenie kopii zapasowej zakończone z ostrzeżeniami/kopia zapasowa nie jest obsługiwana." | ||||||
| msgstr "Tworzenie kopii zapasowej zakończone z ostrzeżeniami/kopia zapasowa nie jest obsługiwana." |  | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/AdminOverviewPage.ts | #~ msgid "Backup status" | ||||||
| msgid "Backup status" | #~ msgstr "Stan kopii zapasowej" | ||||||
| msgstr "Stan kopii zapasowej" |  | ||||||
|  |  | ||||||
| #: src/pages/providers/ldap/LDAPProviderForm.ts | #: src/pages/providers/ldap/LDAPProviderForm.ts | ||||||
| #: src/pages/providers/ldap/LDAPProviderViewPage.ts | #: src/pages/providers/ldap/LDAPProviderViewPage.ts | ||||||
| @ -838,6 +835,14 @@ msgstr "Sprawdza, czy żądanego użytkownika hasło zostało zmienione w ciągu | |||||||
| msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | ||||||
| msgstr "Sprawdza wartość z żądania zasad pod kątem kilku reguł, używanych głównie w celu zapewnienia siły hasła." | msgstr "Sprawdza wartość z żądania zasad pod kątem kilku reguł, używanych głównie w celu zapewnienia siły hasła." | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (simplified)" | ||||||
|  | msgstr "Chiński (uproszczony)" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (traditional)" | ||||||
|  | msgstr "Chiński (tradycyjny)" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowListPage.ts | #: src/pages/flows/FlowListPage.ts | ||||||
| msgid "Clear Flow cache" | msgid "Clear Flow cache" | ||||||
| msgstr "Wyczyść pamięć podręczną przepływu" | msgstr "Wyczyść pamięć podręczną przepływu" | ||||||
| @ -942,8 +947,12 @@ msgid "Configuration flow" | |||||||
| msgstr "Przepływ konfiguracji" | msgstr "Przepływ konfiguracji" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Configuration stage" | #~ msgid "Configuration stage" | ||||||
| msgstr "Etap konfiguracji" | #~ msgstr "Etap konfiguracji" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Configuration stages" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #~ msgid "Configure WebAuthn" | #~ msgid "Configure WebAuthn" | ||||||
| #~ msgstr "Skonfiguruj WebAuthn" | #~ msgstr "Skonfiguruj WebAuthn" | ||||||
| @ -2166,6 +2175,10 @@ msgstr "Ogólny" | |||||||
| msgid "Generic OpenID Connect" | msgid "Generic OpenID Connect" | ||||||
| msgstr "Ogólny OpenID Connect" | msgstr "Ogólny OpenID Connect" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "German" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| msgid "Get this value from https://console.twilio.com" | msgid "Get this value from https://console.twilio.com" | ||||||
| @ -4391,8 +4404,8 @@ msgid "Stage type" | |||||||
| msgstr "Typ etapu" | msgstr "Typ etapu" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | #~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
| msgstr "Etap używany do konfiguracji uwierzytelniacza, gdy użytkownik nie ma żadnych kompatybilnych urządzeń. Po zakończeniu tego etapu konfiguracji użytkownik nie jest ponownie pytany." | #~ msgstr "Etap używany do konfiguracji uwierzytelniacza, gdy użytkownik nie ma żadnych kompatybilnych urządzeń. Po zakończeniu tego etapu konfiguracji użytkownik nie jest ponownie pytany." | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | ||||||
| msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | ||||||
| @ -4451,6 +4464,10 @@ msgstr "Etapy" | |||||||
| msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | ||||||
| msgstr "Etapy to pojedyncze kroki przepływu, przez które prowadzony jest użytkownik. Etap można wykonać tylko z przepływu." | msgstr "Etapy to pojedyncze kroki przepływu, przez które prowadzony jest użytkownik. Etap można wykonać tylko z przepływu." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/outposts/ServiceConnectionListPage.ts | #: src/pages/outposts/ServiceConnectionListPage.ts | ||||||
| msgid "State" | msgid "State" | ||||||
| msgstr "Stan" | msgstr "Stan" | ||||||
| @ -4905,6 +4922,10 @@ msgstr "Urządzenie TOTP" | |||||||
| msgid "TOTP authenticator" | msgid "TOTP authenticator" | ||||||
| msgstr "Uwierzytelniacz TOTP" | msgstr "Uwierzytelniacz TOTP" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Taiwanese Mandarin" | ||||||
|  | msgstr "Tajwański mandaryński" | ||||||
|  |  | ||||||
| #: src/pages/flows/StageBindingForm.ts | #: src/pages/flows/StageBindingForm.ts | ||||||
| msgid "Target" | msgid "Target" | ||||||
| msgstr "Cel" | msgstr "Cel" | ||||||
| @ -5556,8 +5577,8 @@ msgid "Use the username and password below to authenticate. The password can be | |||||||
| msgstr "Użyj poniższej nazwy użytkownika i hasła do uwierzytelnienia. Hasło można później odzyskać na stronie Tokeny." | msgstr "Użyj poniższej nazwy użytkownika i hasła do uwierzytelnienia. Hasło można później odzyskać na stronie Tokeny." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)." | ||||||
| msgstr "Użyj tego dostawcy z auth_request nginx lub forwardAuth traefik. Każda aplikacja/domena potrzebuje własnego dostawcy. Dodatkowo w każdej domenie /akprox musi być przekierowany do placówki (w przypadku korzystania z zarządzanej placówki jest to zrobione za Ciebie)." | msgstr "Użyj tego dostawcy z auth_request nginx lub forwardAuth traefik. Każda aplikacja/domena potrzebuje własnego dostawcy. Dodatkowo w każdej domenie /outpost.goauthentik.io musi być przekierowany do placówki (w przypadku korzystania z zarządzanej placówki jest to zrobione za Ciebie)." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | ||||||
| @ -5912,6 +5933,10 @@ msgstr "Po włączeniu zaproszenie zostanie usunięte po użyciu." | |||||||
| msgid "When enabled, user fields are matched regardless of their casing." | msgid "When enabled, user fields are matched regardless of their casing." | ||||||
| msgstr "Po włączeniu pola użytkownika są dopasowywane niezależnie od wielkości liter." | msgstr "Po włączeniu pola użytkownika są dopasowywane niezależnie od wielkości liter." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "When multiple stages are selected, the user can choose which one they want to enroll." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts | #: src/pages/stages/identification/IdentificationStageForm.ts | ||||||
| msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | ||||||
| msgstr "Po wybraniu pole hasła jest wyświetlane na tej samej stronie zamiast na osobnej stronie. Zapobiega to atakom polegającym na wyliczaniu nazw użytkowników." | msgstr "Po wybraniu pole hasła jest wyświetlane na tej samej stronie zamiast na osobnej stronie. Zapobiega to atakom polegającym na wyliczaniu nazw użytkowników." | ||||||
|  | |||||||
							
								
								
									
										6648
									
								
								web/src/locales/pl_PL.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6648
									
								
								web/src/locales/pl_PL.po
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -559,16 +559,16 @@ msgid "Background shown during execution." | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with errors." | #~ msgid "Backup finished with errors." | ||||||
| msgstr "" | #~ msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with warnings/backup not supported." | #~ msgid "Backup finished with warnings/backup not supported." | ||||||
| msgstr "" | #~ msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/AdminOverviewPage.ts | #: src/pages/admin-overview/AdminOverviewPage.ts | ||||||
| msgid "Backup status" | #~ msgid "Backup status" | ||||||
| msgstr "" | #~ msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/providers/ldap/LDAPProviderForm.ts | #: src/pages/providers/ldap/LDAPProviderForm.ts | ||||||
| #: src/pages/providers/ldap/LDAPProviderViewPage.ts | #: src/pages/providers/ldap/LDAPProviderViewPage.ts | ||||||
| @ -838,6 +838,14 @@ msgstr "" | |||||||
| msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (simplified)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (traditional)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowListPage.ts | #: src/pages/flows/FlowListPage.ts | ||||||
| msgid "Clear Flow cache" | msgid "Clear Flow cache" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -942,7 +950,11 @@ msgid "Configuration flow" | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Configuration stage" | #~ msgid "Configuration stage" | ||||||
|  | #~ msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Configuration stages" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #:  | #:  | ||||||
| @ -2197,6 +2209,10 @@ msgstr "" | |||||||
| msgid "Generic OpenID Connect" | msgid "Generic OpenID Connect" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "German" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| msgid "Get this value from https://console.twilio.com" | msgid "Get this value from https://console.twilio.com" | ||||||
| @ -4473,8 +4489,8 @@ msgid "Stage type" | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | #~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
| msgstr "" | #~ msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | ||||||
| msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | ||||||
| @ -4533,6 +4549,10 @@ msgstr "" | |||||||
| msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/outposts/ServiceConnectionListPage.ts | #: src/pages/outposts/ServiceConnectionListPage.ts | ||||||
| msgid "State" | msgid "State" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -5004,6 +5024,10 @@ msgstr "" | |||||||
| msgid "TOTP authenticator" | msgid "TOTP authenticator" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Taiwanese Mandarin" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/StageBindingForm.ts | #: src/pages/flows/StageBindingForm.ts | ||||||
| msgid "Target" | msgid "Target" | ||||||
| msgstr "" | msgstr "" | ||||||
| @ -5653,7 +5677,7 @@ msgid "Use the username and password below to authenticate. The password can be | |||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| @ -6016,6 +6040,10 @@ msgstr "" | |||||||
| msgid "When enabled, user fields are matched regardless of their casing." | msgid "When enabled, user fields are matched regardless of their casing." | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "When multiple stages are selected, the user can choose which one they want to enroll." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts | #: src/pages/stages/identification/IdentificationStageForm.ts | ||||||
| msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | ||||||
| msgstr "" | msgstr "" | ||||||
|  | |||||||
| @ -561,16 +561,16 @@ msgid "Background shown during execution." | |||||||
| msgstr "Yürütme sırasında arka plan gösterilir." | msgstr "Yürütme sırasında arka plan gösterilir." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with errors." | #~ msgid "Backup finished with errors." | ||||||
| msgstr "Yedekleme hatalarla tamamlandı." | #~ msgstr "Yedekleme hatalarla tamamlandı." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/cards/BackupStatusCard.ts | #: src/pages/admin-overview/cards/BackupStatusCard.ts | ||||||
| msgid "Backup finished with warnings/backup not supported." | #~ msgid "Backup finished with warnings/backup not supported." | ||||||
| msgstr "Yedekleme desteklenmeyen uyarılar/yedekleme ile tamamlandı." | #~ msgstr "Yedekleme desteklenmeyen uyarılar/yedekleme ile tamamlandı." | ||||||
|  |  | ||||||
| #: src/pages/admin-overview/AdminOverviewPage.ts | #: src/pages/admin-overview/AdminOverviewPage.ts | ||||||
| msgid "Backup status" | #~ msgid "Backup status" | ||||||
| msgstr "Yedekleme durumu" | #~ msgstr "Yedekleme durumu" | ||||||
|  |  | ||||||
| #: src/pages/providers/ldap/LDAPProviderForm.ts | #: src/pages/providers/ldap/LDAPProviderForm.ts | ||||||
| #: src/pages/providers/ldap/LDAPProviderViewPage.ts | #: src/pages/providers/ldap/LDAPProviderViewPage.ts | ||||||
| @ -838,6 +838,14 @@ msgstr "İsteğin kullanıcı parolasının son x gün içinde değiştirilip de | |||||||
| msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | msgid "Checks the value from the policy request against several rules, mostly used to ensure password strength." | ||||||
| msgstr "İlke isteğindeki değeri, çoğunlukla parola gücünü sağlamak için kullanılan çeşitli kurallara göre denetler." | msgstr "İlke isteğindeki değeri, çoğunlukla parola gücünü sağlamak için kullanılan çeşitli kurallara göre denetler." | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (simplified)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Chinese (traditional)" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/FlowListPage.ts | #: src/pages/flows/FlowListPage.ts | ||||||
| msgid "Clear Flow cache" | msgid "Clear Flow cache" | ||||||
| msgstr "Akış önbelleğini temizleme" | msgstr "Akış önbelleğini temizleme" | ||||||
| @ -942,8 +950,12 @@ msgid "Configuration flow" | |||||||
| msgstr "Yapılandırma akışı" | msgstr "Yapılandırma akışı" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Configuration stage" | #~ msgid "Configuration stage" | ||||||
| msgstr "Yapılandırma aşamasında" | #~ msgstr "Yapılandırma aşamasında" | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Configuration stages" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #~ msgid "Configure WebAuthn" | #~ msgid "Configure WebAuthn" | ||||||
| #~ msgstr "WebAuthn'i Yapılandır" | #~ msgstr "WebAuthn'i Yapılandır" | ||||||
| @ -2166,6 +2178,10 @@ msgstr "Jenerik" | |||||||
| msgid "Generic OpenID Connect" | msgid "Generic OpenID Connect" | ||||||
| msgstr "Genel OpenID Connect" | msgstr "Genel OpenID Connect" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "German" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | #: src/pages/stages/authenticator_sms/AuthenticatorSMSStageForm.ts | ||||||
| msgid "Get this value from https://console.twilio.com" | msgid "Get this value from https://console.twilio.com" | ||||||
| @ -4393,8 +4409,8 @@ msgid "Stage type" | |||||||
| msgstr "Aşama türü" | msgstr "Aşama türü" | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
| msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | #~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
| msgstr "Kullanıcının uyumlu bir aygıtı olmadığında Kimlik Doğrulayıcısı'nı yapılandırmak için kullanılan Aşama. Bu yapılandırma Stage geçtikten sonra kullanıcıya yeniden istenmez." | #~ msgstr "Kullanıcının uyumlu bir aygıtı olmadığında Kimlik Doğrulayıcısı'nı yapılandırmak için kullanılan Aşama. Bu yapılandırma Stage geçtikten sonra kullanıcıya yeniden istenmez." | ||||||
|  |  | ||||||
| #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts | ||||||
| msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." | ||||||
| @ -4453,6 +4469,10 @@ msgstr "Aşamalar" | |||||||
| msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." | ||||||
| msgstr "Aşamalar, bir Akış'ın kullanıcının yönlendirildiği tek adımlardır. Bir aşama yalnızca bir akış içinden yürütülebilir." | msgstr "Aşamalar, bir Akış'ın kullanıcının yönlendirildiği tek adımlardır. Bir aşama yalnızca bir akış içinden yürütülebilir." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/outposts/ServiceConnectionListPage.ts | #: src/pages/outposts/ServiceConnectionListPage.ts | ||||||
| msgid "State" | msgid "State" | ||||||
| msgstr "Eyalet" | msgstr "Eyalet" | ||||||
| @ -4907,6 +4927,10 @@ msgstr "TOTP Cihazı" | |||||||
| msgid "TOTP authenticator" | msgid "TOTP authenticator" | ||||||
| msgstr "TOTP kimlik doğrulayıcı" | msgstr "TOTP kimlik doğrulayıcı" | ||||||
|  |  | ||||||
|  | #: src/interfaces/locale.ts | ||||||
|  | msgid "Taiwanese Mandarin" | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/flows/StageBindingForm.ts | #: src/pages/flows/StageBindingForm.ts | ||||||
| msgid "Target" | msgid "Target" | ||||||
| msgstr "Hedef" | msgstr "Hedef" | ||||||
| @ -5558,8 +5582,8 @@ msgid "Use the username and password below to authenticate. The password can be | |||||||
| msgstr "Kimlik doğrulaması için aşağıdaki kullanıcı adı ve parolayı kullanın. Parola daha sonra Belirteçler sayfasından alınabilir." | msgstr "Kimlik doğrulaması için aşağıdaki kullanıcı adı ve parolayı kullanın. Parola daha sonra Belirteçler sayfasından alınabilir." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /akprox must be routed to the outpost (when using a manged outpost, this is done for you)." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a manged outpost, this is done for you)." | ||||||
| msgstr "Bu sağlayıcıyı nginx'in auth_request veya traefik's forwardAuth ile kullanın. Her uygulama/etki alanının kendi sağlayıcısına ihtiyacı vardır. Ayrıca, her etki alanında /akprox üsse yönlendirilmelidir (manged bir üs kullanırken, bu sizin için yapılır)." | msgstr "Bu sağlayıcıyı nginx'in auth_request veya traefik's forwardAuth ile kullanın. Her uygulama/etki alanının kendi sağlayıcısına ihtiyacı vardır. Ayrıca, her etki alanında /outpost.goauthentik.io üsse yönlendirilmelidir (manged bir üs kullanırken, bu sizin için yapılır)." | ||||||
|  |  | ||||||
| #: src/pages/providers/proxy/ProxyProviderForm.ts | #: src/pages/providers/proxy/ProxyProviderForm.ts | ||||||
| msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | msgid "Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application." | ||||||
| @ -5914,6 +5938,10 @@ msgstr "Etkinleştirildiğinde, davetiye kullanımdan sonra silinir." | |||||||
| msgid "When enabled, user fields are matched regardless of their casing." | msgid "When enabled, user fields are matched regardless of their casing." | ||||||
| msgstr "Etkinleştirildiğinde, kullanıcı alanları muhafazası ne olursa olsun eşleştirilir." | msgstr "Etkinleştirildiğinde, kullanıcı alanları muhafazası ne olursa olsun eşleştirilir." | ||||||
|  |  | ||||||
|  | #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts | ||||||
|  | msgid "When multiple stages are selected, the user can choose which one they want to enroll." | ||||||
|  | msgstr "" | ||||||
|  |  | ||||||
| #: src/pages/stages/identification/IdentificationStageForm.ts | #: src/pages/stages/identification/IdentificationStageForm.ts | ||||||
| msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." | ||||||
| msgstr "Seçildiğinde, ayrı bir sayfa yerine aynı sayfada bir parola alanı gösterilir. Bu, kullanıcı adı numaralandırma saldırılarını engeller." | msgstr "Seçildiğinde, ayrı bir sayfa yerine aynı sayfada bir parola alanı gösterilir. Bu, kullanıcı adı numaralandırma saldırılarını engeller." | ||||||
|  | |||||||
							
								
								
									
										6117
									
								
								web/src/locales/zh-Hans.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6117
									
								
								web/src/locales/zh-Hans.po
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										6117
									
								
								web/src/locales/zh-Hant.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6117
									
								
								web/src/locales/zh-Hant.po
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	