core: add validator which allows for URLs with formatting (#4890)
This commit is contained in:
		@ -182,7 +182,9 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            model_name="application",
 | 
					            model_name="application",
 | 
				
			||||||
            name="meta_launch_url",
 | 
					            name="meta_launch_url",
 | 
				
			||||||
            field=models.TextField(
 | 
					            field=models.TextField(
 | 
				
			||||||
                blank=True, default="", validators=[authentik.lib.models.DomainlessURLValidator()]
 | 
					                blank=True,
 | 
				
			||||||
 | 
					                default="",
 | 
				
			||||||
 | 
					                validators=[authentik.lib.models.DomainlessFormattedURLValidator()],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        migrations.RunPython(
 | 
					        migrations.RunPython(
 | 
				
			||||||
 | 
				
			|||||||
@ -26,7 +26,11 @@ from authentik.core.types import UILoginButton, UserSettingSerializer
 | 
				
			|||||||
from authentik.lib.avatars import get_avatar
 | 
					from authentik.lib.avatars import get_avatar
 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
from authentik.lib.generators import generate_id
 | 
					from authentik.lib.generators import generate_id
 | 
				
			||||||
from authentik.lib.models import CreatedUpdatedModel, DomainlessURLValidator, SerializerModel
 | 
					from authentik.lib.models import (
 | 
				
			||||||
 | 
					    CreatedUpdatedModel,
 | 
				
			||||||
 | 
					    DomainlessFormattedURLValidator,
 | 
				
			||||||
 | 
					    SerializerModel,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from authentik.lib.utils.http import get_client_ip
 | 
					from authentik.lib.utils.http import get_client_ip
 | 
				
			||||||
from authentik.policies.models import PolicyBindingModel
 | 
					from authentik.policies.models import PolicyBindingModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -291,7 +295,7 @@ class Application(SerializerModel, PolicyBindingModel):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    meta_launch_url = models.TextField(
 | 
					    meta_launch_url = models.TextField(
 | 
				
			||||||
        default="", blank=True, validators=[DomainlessURLValidator()]
 | 
					        default="", blank=True, validators=[DomainlessFormattedURLValidator()]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    open_in_new_tab = models.BooleanField(
 | 
					    open_in_new_tab = models.BooleanField(
 | 
				
			||||||
 | 
				
			|||||||
@ -37,6 +37,22 @@ class TestApplicationsAPI(APITestCase):
 | 
				
			|||||||
            order=0,
 | 
					            order=0,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_formatted_launch_url(self):
 | 
				
			||||||
 | 
					        """Test formatted launch URL"""
 | 
				
			||||||
 | 
					        self.client.force_login(self.user)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            self.client.patch(
 | 
				
			||||||
 | 
					                reverse("authentik_api:application-detail", kwargs={"slug": self.allowed.slug}),
 | 
				
			||||||
 | 
					                {"meta_launch_url": "https://%(username)s.test.goauthentik.io/%(username)s"},
 | 
				
			||||||
 | 
					            ).status_code,
 | 
				
			||||||
 | 
					            200,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.allowed.refresh_from_db()
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            self.allowed.get_launch_url(self.user),
 | 
				
			||||||
 | 
					            f"https://{self.user.username}.test.goauthentik.io/{self.user.username}",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_set_icon(self):
 | 
					    def test_set_icon(self):
 | 
				
			||||||
        """Test set_icon"""
 | 
					        """Test set_icon"""
 | 
				
			||||||
        file = ContentFile(b"text", "name")
 | 
					        file = ContentFile(b"text", "name")
 | 
				
			||||||
 | 
				
			|||||||
@ -74,3 +74,21 @@ class DomainlessURLValidator(URLValidator):
 | 
				
			|||||||
        if scheme not in self.schemes:
 | 
					        if scheme not in self.schemes:
 | 
				
			||||||
            value = "default" + value
 | 
					            value = "default" + value
 | 
				
			||||||
        super().__call__(value)
 | 
					        super().__call__(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DomainlessFormattedURLValidator(DomainlessURLValidator):
 | 
				
			||||||
 | 
					    """URL validator which allows for python format strings"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs) -> None:
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self.host_re = r"([%\(\)a-zA-Z])+" + self.domain_re + self.domain_re
 | 
				
			||||||
 | 
					        self.regex = _lazy_re_compile(
 | 
				
			||||||
 | 
					            r"^(?:[a-z0-9.+-]*)://"  # scheme is validated separately
 | 
				
			||||||
 | 
					            r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?"  # user:pass authentication
 | 
				
			||||||
 | 
					            r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
 | 
				
			||||||
 | 
					            r"(?::\d{2,5})?"  # port
 | 
				
			||||||
 | 
					            r"(?:[/?#][^\s]*)?"  # resource path
 | 
				
			||||||
 | 
					            r"\Z",
 | 
				
			||||||
 | 
					            re.IGNORECASE,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.schemes = ["http", "https", "blank"] + list(self.schemes)
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user