Compare commits
	
		
			21 Commits
		
	
	
		
			version/20
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| eaad564e23 | |||
| 511a94975b | |||
| 015810a2fd | |||
| e70e6b84c2 | |||
| d0b9c9a26f | |||
| 3e403fa348 | |||
| 48f4a971ef | |||
| 6314be14ad | |||
| 1a072c6c39 | |||
| ef2eed0bdf | |||
| 91227b1e96 | |||
| 67d68629da | |||
| e875db8f66 | |||
| 055a76393d | |||
| 0754821628 | |||
| fca88d9896 | |||
| dfe0404c51 | |||
| fa61696b46 | |||
| e5773738f4 | |||
| cac8539d79 | |||
| cf600f6f26 | 
| @ -1,5 +1,5 @@ | |||||||
| [bumpversion] | [bumpversion] | ||||||
| current_version = 2022.1.4 | current_version = 2022.1.5 | ||||||
| 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>.*) | ||||||
|  | |||||||
							
								
								
									
										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.1.5, | ||||||
|             beryju/authentik:latest, |             beryju/authentik:latest, | ||||||
|             ghcr.io/goauthentik/server:2022.1.4, |             ghcr.io/goauthentik/server:2022.1.5, | ||||||
|             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.1.5', '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.1.5, | ||||||
|             beryju/authentik-${{ matrix.type }}:latest, |             beryju/authentik-${{ matrix.type }}:latest, | ||||||
|             ghcr.io/goauthentik/${{ matrix.type }}:2022.1.4, |             ghcr.io/goauthentik/${{ matrix.type }}:2022.1.5, | ||||||
|             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.1.5', '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.1.5 | ||||||
|           environment: beryjuorg-prod |           environment: beryjuorg-prod | ||||||
|           sourcemaps: './web/dist' |           sourcemaps: './web/dist' | ||||||
|           url_prefix: '~/static/dist' |           url_prefix: '~/static/dist' | ||||||
|  | |||||||
							
								
								
									
										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, | ||||||
|  | |||||||
| @ -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.1.5" | ||||||
| ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ENV_GIT_HASH_KEY = "GIT_BUILD_HASH" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -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,22 @@ 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 | ||||||
|  |         return url % user.__dict__ | ||||||
|  |  | ||||||
|     class Meta: |     class Meta: | ||||||
|  |  | ||||||
|         model = Application |         model = Application | ||||||
|  | |||||||
| @ -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": "", | ||||||
|  | |||||||
| @ -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() | ||||||
|  | |||||||
| @ -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( | ||||||
|  | |||||||
| @ -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()) | ||||||
|  | |||||||
| @ -196,7 +196,10 @@ class AuthenticatorValidateStageView(ChallengeStageView): | |||||||
|         return super().get(request, *args, **kwargs) |         return super().get(request, *args, **kwargs) | ||||||
|  |  | ||||||
|     def get_challenge(self) -> AuthenticatorValidationChallenge: |     def get_challenge(self) -> AuthenticatorValidationChallenge: | ||||||
|         challenges = self.request.session["device_challenges"] |         challenges = self.request.session.get("device_challenges") | ||||||
|  |         if not challenges: | ||||||
|  |             LOGGER.debug("Authenticator Validation stage ran without challenges") | ||||||
|  |             return self.executor.stage_invalid() | ||||||
|         return AuthenticatorValidationChallenge( |         return AuthenticatorValidationChallenge( | ||||||
|             data={ |             data={ | ||||||
|                 "type": ChallengeTypes.NATIVE.value, |                 "type": ChallengeTypes.NATIVE.value, | ||||||
|  | |||||||
| @ -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.1.5} | ||||||
|     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.1.5} | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     command: worker |     command: worker | ||||||
|     environment: |     environment: | ||||||
|  | |||||||
| @ -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.1.5" | ||||||
|  | |||||||
| @ -6,14 +6,14 @@ type ProxyClaims struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| type Claims struct { | type Claims struct { | ||||||
| 	Sub               string      `json:"sub"` | 	Sub               string       `json:"sub"` | ||||||
| 	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"` | ||||||
|  |  | ||||||
| 	RawToken string | 	RawToken string | ||||||
| } | } | ||||||
|  | |||||||
| @ -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", | ||||||
|  | |||||||
| @ -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", | ||||||
|  | |||||||
| @ -74,17 +74,20 @@ 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) { | ||||||
| 		claims, _ := a.getClaims(r) | 		claims, _ := a.getClaims(r) | ||||||
| 		if claims.Proxy.BackendOverride != "" { | 		r.URL.Scheme = ou.Scheme | ||||||
|  | 		r.URL.Host = ou.Host | ||||||
|  | 		r.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.Host = u.Host | ||||||
|  | 				r.Host = u.Host | ||||||
| 			} | 			} | ||||||
| 			r.URL.Scheme = u.Scheme |  | ||||||
| 			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") | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | |||||||
							
								
								
									
										81
									
								
								internal/outpost/proxyv2/application/mode_proxy_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								internal/outpost/proxyv2/application/mode_proxy_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | |||||||
|  | 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, "/foo", req.URL.Path) | ||||||
|  | 	assert.Equal(t, "backend:8012", req.URL.Host) | ||||||
|  | 	assert.Equal(t, "backend:8012", 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, "other-backend:8123", 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, "backend:8012", req.Host) | ||||||
|  | } | ||||||
| @ -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 | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -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,6 +25,7 @@ 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 | ||||||
| @ -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 { | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								poetry.lock
									
									
									
										generated
									
									
									
								
							| @ -1898,7 +1898,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "uvicorn" | name = "uvicorn" | ||||||
| version = "0.17.1" | version = "0.17.3" | ||||||
| description = "The lightning-fast ASGI server." | description = "The lightning-fast ASGI server." | ||||||
| category = "main" | category = "main" | ||||||
| optional = false | optional = false | ||||||
| @ -3406,8 +3406,8 @@ urllib3 = [ | |||||||
|     {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, |     {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, | ||||||
| ] | ] | ||||||
| uvicorn = [ | uvicorn = [ | ||||||
|     {file = "uvicorn-0.17.1-py3-none-any.whl", hash = "sha256:8b16d9ecb76500f7804184f182835fe8a2b54716d3b0b6bb2da0b2b192f62c73"}, |     {file = "uvicorn-0.17.3-py3-none-any.whl", hash = "sha256:3ab1bf48aa512692db93a91c514576a0739a9d3522827e1656a172ee87118fa5"}, | ||||||
|     {file = "uvicorn-0.17.1.tar.gz", hash = "sha256:dffbacb8cc25d924d68d231d2c478c4fe6727c36537d8de21e5de591b37afc41"}, |     {file = "uvicorn-0.17.3.tar.gz", hash = "sha256:3cebddac78c7dd6bfce2f8f838c2c9b0cfcf3dddf417315ba62378ebe7e8fae2"}, | ||||||
| ] | ] | ||||||
| uvloop = [ | uvloop = [ | ||||||
|     {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"}, |     {file = "uvloop-0.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6224f1401025b748ffecb7a6e2652b17768f30b1a6a3f7b44660e5b5b690b12d"}, | ||||||
|  | |||||||
| @ -92,7 +92,7 @@ addopts = "-p no:celery --junitxml=unittest.xml" | |||||||
|  |  | ||||||
| [tool.poetry] | [tool.poetry] | ||||||
| name = "authentik" | name = "authentik" | ||||||
| version = "2022.1.4" | version = "2022.1.5" | ||||||
| description = "" | description = "" | ||||||
| authors = ["Jens Langhammer <jens.langhammer@beryju.org>"] | authors = ["Jens Langhammer <jens.langhammer@beryju.org>"] | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| openapi: 3.0.3 | openapi: 3.0.3 | ||||||
| info: | info: | ||||||
|   title: authentik |   title: authentik | ||||||
|   version: 2022.1.4 |   version: 2022.1.5 | ||||||
|   description: Making authentication simple. |   description: Making authentication simple. | ||||||
|   contact: |   contact: | ||||||
|     email: hello@beryju.org |     email: hello@beryju.org | ||||||
|  | |||||||
| @ -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.1.5"; | ||||||
| export const TITLE_DEFAULT = "authentik"; | export const TITLE_DEFAULT = "authentik"; | ||||||
| export const ROUTE_SEPARATOR = ";"; | export const ROUTE_SEPARATOR = ";"; | ||||||
|  |  | ||||||
|  | |||||||
| @ -110,7 +110,7 @@ export class AdminOverviewPage extends LitElement { | |||||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" |                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||||
|                     > |                     > | ||||||
|                         <ak-aggregate-card |                         <ak-aggregate-card | ||||||
|                             icon="pf-icon pf-icon-server" |                             icon="pf-icon pf-icon-process-automation" | ||||||
|                             header=${t`Flows`} |                             header=${t`Flows`} | ||||||
|                             headerLink="#/flow/flows" |                             headerLink="#/flow/flows" | ||||||
|                         > |                         > | ||||||
| @ -121,7 +121,7 @@ export class AdminOverviewPage extends LitElement { | |||||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" |                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||||
|                     > |                     > | ||||||
|                         <ak-aggregate-card |                         <ak-aggregate-card | ||||||
|                             icon="fa fa-sync-alt" |                             icon="pf-icon pf-icon-zone" | ||||||
|                             header=${t`Outpost status`} |                             header=${t`Outpost status`} | ||||||
|                             headerLink="#/outpost/outposts" |                             headerLink="#/outpost/outposts" | ||||||
|                         > |                         > | ||||||
| @ -132,7 +132,7 @@ export class AdminOverviewPage extends LitElement { | |||||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" |                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||||
|                     > |                     > | ||||||
|                         <ak-aggregate-card |                         <ak-aggregate-card | ||||||
|                             icon="fa fa-sync-alt" |                             icon="pf-icon pf-icon-user" | ||||||
|                             header=${t`Users`} |                             header=${t`Users`} | ||||||
|                             headerLink="#/identity/users" |                             headerLink="#/identity/users" | ||||||
|                         > |                         > | ||||||
| @ -143,7 +143,7 @@ export class AdminOverviewPage extends LitElement { | |||||||
|                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" |                         class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container" | ||||||
|                     > |                     > | ||||||
|                         <ak-aggregate-card |                         <ak-aggregate-card | ||||||
|                             icon="fa fa-sync-alt" |                             icon="pf-icon pf-icon-users" | ||||||
|                             header=${t`Groups`} |                             header=${t`Groups`} | ||||||
|                             headerLink="#/identity/groups" |                             headerLink="#/identity/groups" | ||||||
|                         > |                         > | ||||||
|  | |||||||
| @ -24,6 +24,9 @@ The following aspects can be configured: | |||||||
|  |  | ||||||
| - *Name*: This is the name shown for the application card | - *Name*: This is the name shown for the application card | ||||||
| - *Launch URL*: The URL that is opened when a user clicks on the application. When left empty, authentik tries to guess it based on the provider | - *Launch URL*: The URL that is opened when a user clicks on the application. When left empty, authentik tries to guess it based on the provider | ||||||
|  |  | ||||||
|  |     Starting with authentik 2022.2, you can use placeholders in the launch url to build them dynamically based on logged in user. For example, you can set the Launch URL to `https://goauthentik.io/%(username)s`, which will be replaced with the currently logged in user's username. | ||||||
|  |  | ||||||
| - *Icon (URL)*: Optionally configure an Icon for the application | - *Icon (URL)*: Optionally configure an Icon for the application | ||||||
| - *Publisher*: Text shown below the application | - *Publisher*: Text shown below the application | ||||||
| - *Description*: Subtext shown on the application card below the publisher | - *Description*: Subtext shown on the application card below the publisher | ||||||
|  | |||||||
| @ -102,6 +102,28 @@ This release mostly removes legacy fields and features that have been deprecated | |||||||
| - web/flows: fix width on flow container | - web/flows: fix width on flow container | ||||||
| - web/user: include locale code in locale selection | - web/user: include locale code in locale selection | ||||||
|  |  | ||||||
|  | ## Fixed in 2022.1.5 | ||||||
|  |  | ||||||
|  | - build(deps): bump uvicorn from 0.17.1 to 0.17.3 (#2229) | ||||||
|  | - core: allow formatting strings to be used for applications' launch URLs | ||||||
|  | - internal: don't attempt to lookup SNI Certificate if no SNI is sent | ||||||
|  | - internal: fix CSRF error caused by Host header | ||||||
|  | - internal: improve error handling for internal reverse proxy | ||||||
|  | - internal: remove uvicorn server header | ||||||
|  | - internal: trace headers and url for backend requests | ||||||
|  | - outposts: fix channel not always having a logger attribute | ||||||
|  | - outposts: fix compare_ports to support both service and container ports | ||||||
|  | - outposts: fix service reconciler re-creating services | ||||||
|  | - outposts: remove node_port on V1ServicePort checks to prevent service creation loops | ||||||
|  | - providers/proxy: fix Host/:Authority not being modified | ||||||
|  | - providers/proxy: fix nil error in claims | ||||||
|  | - providers/proxy: improve error handling for invalid backend_override | ||||||
|  | - sources/ldap: log entire exception | ||||||
|  | - sources/saml: fix incorrect ProtocolBinding being sent | ||||||
|  | - sources/saml: fix server error | ||||||
|  | - stages/authenticator_validate: handle non-existent device_challenges | ||||||
|  | - web/admin: fix mismatched icons in overview and lists | ||||||
|  |  | ||||||
| ## Upgrading | ## Upgrading | ||||||
|  |  | ||||||
| This release does not introduce any new requirements. | This release does not introduce any new requirements. | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	