diff --git a/app_gateway/forms.py b/app_gateway/forms.py index 977d0ba..06fec33 100644 --- a/app_gateway/forms.py +++ b/app_gateway/forms.py @@ -13,10 +13,9 @@ from app_gateway.models import ( class ApplicationForm(forms.ModelForm): class Meta: model = Application - fields = ['name', 'display_name', 'upstream', 'allow_invalid_cert'] + fields = ['display_name', 'upstream', 'allow_invalid_cert'] labels = { - 'name': _('Name'), - 'display_name': _('Display Name'), + 'display_name': _('Name'), 'upstream': _('Upstream'), 'allow_invalid_cert': _('Allow invalid/self-signed certificate'), } @@ -24,12 +23,12 @@ class ApplicationForm(forms.ModelForm): def __init__(self, *args, **kwargs): cancel_url = kwargs.pop('cancel_url', '#') super().__init__(*args, **kwargs) + self.fields['display_name'].required = True self.helper = FormHelper() self.helper.layout = Layout( Div( - Div('name', css_class='col-md-6'), - Div('display_name', css_class='col-md-6'), + Div('display_name', css_class='col-md-12'), css_class='row' ), Div( @@ -49,12 +48,8 @@ class ApplicationForm(forms.ModelForm): def clean(self): cleaned_data = super().clean() - name = cleaned_data.get("name") upstream = (cleaned_data.get("upstream") or "").strip() - if name == "wireguard_webadmin": - self.add_error("name", _("This is a reserved system name.")) - if upstream: if "wireguard-webadmin:8000" in upstream: self.add_error("upstream", _("This upstream is reserved by the system.")) @@ -103,9 +98,9 @@ class ApplicationHostForm(forms.ModelForm): class AccessPolicyForm(forms.ModelForm): class Meta: model = AccessPolicy - fields = ['name', 'policy_type', 'groups', 'methods'] + fields = ['display_name', 'policy_type', 'groups', 'methods'] labels = { - 'name': _('Name'), + 'display_name': _('Name'), 'policy_type': _('Policy Type'), 'groups': _('Allowed Groups'), 'methods': _('Authentication Methods'), @@ -115,6 +110,7 @@ class AccessPolicyForm(forms.ModelForm): cancel_url = kwargs.pop('cancel_url', '#') policy_type = kwargs.pop('policy_type', None) super().__init__(*args, **kwargs) + self.fields['display_name'].required = True if self.instance and self.instance.pk: policy_type = self.instance.policy_type @@ -125,11 +121,11 @@ class AccessPolicyForm(forms.ModelForm): self.fields['policy_type'].widget = forms.HiddenInput() self.helper = FormHelper() - + if policy_type in ['public', 'deny']: self.helper.layout = Layout( Div( - Div('name', css_class='col-md-12'), + Div('display_name', css_class='col-md-12'), 'policy_type', css_class='row' ), @@ -145,7 +141,7 @@ class AccessPolicyForm(forms.ModelForm): else: self.helper.layout = Layout( Div( - Div('name', css_class='col-md-12'), + Div('display_name', css_class='col-md-12'), 'policy_type', css_class='row' ), @@ -244,9 +240,9 @@ class ApplicationPolicyForm(forms.ModelForm): class ApplicationRouteForm(forms.ModelForm): class Meta: model = ApplicationRoute - fields = ['name', 'path_prefix', 'policy', 'order'] + fields = ['display_name', 'path_prefix', 'policy', 'order'] labels = { - 'name': _('Route Name'), + 'display_name': _('Route Name'), 'path_prefix': _('Path Prefix'), 'policy': _('Policy'), 'order': _('Order'), @@ -255,11 +251,12 @@ class ApplicationRouteForm(forms.ModelForm): def __init__(self, *args, **kwargs): cancel_url = kwargs.pop('cancel_url', '#') super().__init__(*args, **kwargs) + self.fields['display_name'].required = True self.helper = FormHelper() self.helper.layout = Layout( Div( - Div('name', css_class='col-md-12'), + Div('display_name', css_class='col-md-12'), css_class='row' ), Div( diff --git a/app_gateway/migrations/0008_accesspolicy_display_name_applicationroute_display_name.py b/app_gateway/migrations/0008_accesspolicy_display_name_applicationroute_display_name.py new file mode 100644 index 0000000..cf47b48 --- /dev/null +++ b/app_gateway/migrations/0008_accesspolicy_display_name_applicationroute_display_name.py @@ -0,0 +1,26 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('app_gateway', '0007_application_allow_invalid_cert'), + ] + + operations = [ + migrations.AddField( + model_name='accesspolicy', + name='display_name', + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name='applicationroute', + name='display_name', + field=models.CharField(blank=True, max_length=128), + ), + migrations.AlterField( + model_name='applicationroute', + name='name', + field=models.SlugField(max_length=64), + ), + ] diff --git a/app_gateway/models.py b/app_gateway/models.py index 024a6b1..a654b7e 100644 --- a/app_gateway/models.py +++ b/app_gateway/models.py @@ -3,7 +3,7 @@ import uuid from django.db import models from django.utils.translation import gettext_lazy as _ -from gatekeeper.models import GatekeeperGroup, AuthMethod +from gatekeeper.models import GatekeeperGroup, AuthMethod, _unique_slug class Application(models.Model): @@ -16,11 +16,13 @@ class Application(models.Model): updated = models.DateTimeField(auto_now=True) uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) - def __str__(self): + def save(self, *args, **kwargs): if self.display_name: - return f"{self.display_name} ({self.name})" - else: - return self.name + self.name = _unique_slug(Application, self.display_name, exclude_pk=self.pk) + super().save(*args, **kwargs) + + def __str__(self): + return self.display_name or self.name class Meta: ordering = ['name'] @@ -43,6 +45,7 @@ class ApplicationHost(models.Model): class AccessPolicy(models.Model): name = models.SlugField(max_length=64, unique=True) + display_name = models.CharField(max_length=128, blank=True) policy_type = models.CharField(max_length=32, choices=(('public', _('Public')), ('protected', _('Protected')), ('deny', _('Deny')))) groups = models.ManyToManyField(GatekeeperGroup, blank=True, related_name='policies') methods = models.ManyToManyField(AuthMethod, blank=True, related_name='policies') @@ -51,8 +54,13 @@ class AccessPolicy(models.Model): updated = models.DateTimeField(auto_now=True) uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + def save(self, *args, **kwargs): + if self.display_name: + self.name = _unique_slug(AccessPolicy, self.display_name, exclude_pk=self.pk) + super().save(*args, **kwargs) + def __str__(self): - return f"{self.name} ({self.get_policy_type_display()})" + return f"{self.display_name or self.name} ({self.get_policy_type_display()})" class Meta: ordering = ['name'] @@ -78,7 +86,8 @@ class ApplicationPolicy(models.Model): class ApplicationRoute(models.Model): application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name='routes') - name = models.SlugField(max_length=64, help_text=_("Route identifier, used in export (e.g.: public_area)")) + name = models.SlugField(max_length=64) + display_name = models.CharField(max_length=128, blank=True) path_prefix = models.CharField(max_length=255) policy = models.ForeignKey(AccessPolicy, on_delete=models.PROTECT, related_name='routes') order = models.PositiveIntegerField(default=0) @@ -87,8 +96,16 @@ class ApplicationRoute(models.Model): updated = models.DateTimeField(auto_now=True) uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + def save(self, *args, **kwargs): + if self.display_name: + self.name = _unique_slug( + ApplicationRoute, self.display_name, exclude_pk=self.pk, + filter_kwargs={'application': self.application}, + ) + super().save(*args, **kwargs) + def __str__(self): - return f"{self.application} {self.path_prefix} → {self.policy}" + return f"{self.application} — {self.display_name or self.name} ({self.path_prefix})" class Meta: ordering = ['application', 'order', 'path_prefix'] diff --git a/gatekeeper/forms.py b/gatekeeper/forms.py index 09dca10..6d81bdc 100644 --- a/gatekeeper/forms.py +++ b/gatekeeper/forms.py @@ -125,20 +125,21 @@ class GatekeeperUserForm(forms.ModelForm): class GatekeeperGroupForm(forms.ModelForm): class Meta: model = GatekeeperGroup - fields = ['name', 'users'] + fields = ['display_name', 'users'] labels = { - 'name': _('Group Name'), + 'display_name': _('Group Name'), 'users': _('Members'), } def __init__(self, *args, **kwargs): cancel_url = kwargs.pop('cancel_url', '#') super().__init__(*args, **kwargs) + self.fields['display_name'].required = True self.helper = FormHelper() self.helper.layout = Layout( Div( - Div('name', css_class='col-xl-12'), + Div('display_name', css_class='col-xl-12'), css_class='row' ), Div( @@ -179,11 +180,11 @@ class AuthMethodForm(forms.ModelForm): class Meta: model = AuthMethod fields = [ - 'name', 'auth_type', 'totp_secret', + 'display_name', 'auth_type', 'totp_secret', 'oidc_provider', 'oidc_client_id', 'oidc_client_secret' ] labels = { - 'name': _('Name'), + 'display_name': _('Name'), 'auth_type': _('Authentication Type'), 'totp_secret': _('Global TOTP Secret'), 'oidc_provider': _('OIDC Provider URL'), @@ -195,6 +196,7 @@ class AuthMethodForm(forms.ModelForm): cancel_url = kwargs.pop('cancel_url', '#') super().__init__(*args, **kwargs) + self.fields['display_name'].required = True if self.instance and self.instance.pk: self.fields['auth_type'].disabled = True exp_min = self.instance.session_expiration_minutes @@ -208,7 +210,7 @@ class AuthMethodForm(forms.ModelForm): self.helper = FormHelper() self.helper.layout = Layout( Div( - Div('name', css_class='col-xl-6'), + Div('display_name', css_class='col-xl-6'), Div('auth_type', css_class='col-xl-6'), css_class='row auth-type-group' ), diff --git a/gatekeeper/migrations/0011_authmethod_display_name_gatekeepergroup_display_name.py b/gatekeeper/migrations/0011_authmethod_display_name_gatekeepergroup_display_name.py new file mode 100644 index 0000000..9d7ba18 --- /dev/null +++ b/gatekeeper/migrations/0011_authmethod_display_name_gatekeepergroup_display_name.py @@ -0,0 +1,21 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gatekeeper', '0010_alter_gatekeeperuser_email'), + ] + + operations = [ + migrations.AddField( + model_name='authmethod', + name='display_name', + field=models.CharField(blank=True, max_length=128), + ), + migrations.AddField( + model_name='gatekeepergroup', + name='display_name', + field=models.CharField(blank=True, max_length=128), + ), + ] diff --git a/gatekeeper/models.py b/gatekeeper/models.py index e8b534a..ca26423 100644 --- a/gatekeeper/models.py +++ b/gatekeeper/models.py @@ -1,11 +1,28 @@ import uuid from django.db import models +from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ +def _unique_slug(model_class, display_name, exclude_pk=None, slug_field='name', filter_kwargs=None): + base = slugify(display_name) or 'item' + slug = base + counter = 1 + qs = model_class.objects.all() + if exclude_pk: + qs = qs.exclude(pk=exclude_pk) + if filter_kwargs: + qs = qs.filter(**filter_kwargs) + while qs.filter(**{slug_field: slug}).exists(): + slug = f"{base}-{counter}" + counter += 1 + return slug + + class AuthMethod(models.Model): name = models.SlugField(max_length=64, unique=True) + display_name = models.CharField(max_length=128, blank=True) auth_type = models.CharField(max_length=32, choices=( ('local_password', _('Local Password')), ('totp', _('One-Time Password (TOTP)')), @@ -31,8 +48,13 @@ class AuthMethod(models.Model): updated = models.DateTimeField(auto_now=True) uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + def save(self, *args, **kwargs): + if self.display_name: + self.name = _unique_slug(AuthMethod, self.display_name, exclude_pk=self.pk) + super().save(*args, **kwargs) + def __str__(self): - return f"{self.name} ({self.get_auth_type_display()})" + return f"{self.display_name or self.name} ({self.get_auth_type_display()})" class Meta: ordering = ['name'] @@ -89,14 +111,20 @@ class GatekeeperUser(models.Model): class GatekeeperGroup(models.Model): name = models.SlugField(max_length=64, unique=True) + display_name = models.CharField(max_length=128, blank=True) users = models.ManyToManyField(GatekeeperUser, blank=True, related_name='groups') created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + def save(self, *args, **kwargs): + if self.display_name: + self.name = _unique_slug(GatekeeperGroup, self.display_name, exclude_pk=self.pk) + super().save(*args, **kwargs) + def __str__(self): - return self.name + return self.display_name or self.name class Meta: ordering = ['name'] diff --git a/templates/app_gateway/app_gateway_list.html b/templates/app_gateway/app_gateway_list.html index db2d793..c93a80a 100644 --- a/templates/app_gateway/app_gateway_list.html +++ b/templates/app_gateway/app_gateway_list.html @@ -75,7 +75,7 @@
{{ application.upstream }}{{ route.path_prefix }}