2026-03-12 14:15:14 -03:00
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
|
|
from crispy_forms.helper import FormHelper
|
|
|
|
|
from crispy_forms.layout import Layout, Submit, HTML, Div
|
|
|
|
|
from django import forms
|
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
|
|
|
|
from app_gateway.models import (
|
|
|
|
|
Application, ApplicationHost, AccessPolicy, ApplicationPolicy, ApplicationRoute
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApplicationForm(forms.ModelForm):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = Application
|
|
|
|
|
fields = ['name', 'display_name', 'upstream']
|
|
|
|
|
labels = {
|
|
|
|
|
'name': _('Name'),
|
|
|
|
|
'display_name': _('Display Name'),
|
|
|
|
|
'upstream': _('Upstream'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
|
|
|
|
Div('name', css_class='col-md-6'),
|
|
|
|
|
Div('display_name', css_class='col-md-6'),
|
|
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div('upstream', css_class='col-md-12'),
|
|
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-12 17:28:41 -03:00
|
|
|
css_class='col-md-12'
|
2026-03-12 14:15:14 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
|
cleaned_data = super().clean()
|
2026-03-14 10:14:30 -03:00
|
|
|
name = cleaned_data.get("name")
|
2026-03-12 14:15:14 -03:00
|
|
|
upstream = (cleaned_data.get("upstream") or "").strip()
|
|
|
|
|
|
2026-03-14 10:14:30 -03:00
|
|
|
if name == "wireguard_webadmin":
|
|
|
|
|
self.add_error("name", _("This is a reserved system name."))
|
|
|
|
|
|
2026-03-12 14:15:14 -03:00
|
|
|
if upstream:
|
2026-03-14 10:14:30 -03:00
|
|
|
if "wireguard-webadmin:8000" in upstream:
|
|
|
|
|
self.add_error("upstream", _("This upstream is reserved by the system."))
|
|
|
|
|
|
2026-03-12 14:15:14 -03:00
|
|
|
if " " in upstream:
|
|
|
|
|
self.add_error("upstream", _("Upstream URL cannot contain spaces."))
|
|
|
|
|
|
|
|
|
|
parsed = urlparse(upstream)
|
|
|
|
|
is_valid = (parsed.scheme in {"http", "https"} and bool(parsed.netloc))
|
|
|
|
|
|
|
|
|
|
if not is_valid:
|
|
|
|
|
self.add_error("upstream", _("Enter a valid upstream URL starting with http:// or https://"))
|
|
|
|
|
|
|
|
|
|
return cleaned_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApplicationHostForm(forms.ModelForm):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = ApplicationHost
|
2026-03-12 18:41:21 -03:00
|
|
|
fields = ['hostname']
|
2026-03-12 14:15:14 -03:00
|
|
|
labels = {
|
|
|
|
|
'hostname': _('Hostname'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
2026-03-12 18:41:21 -03:00
|
|
|
Div('hostname', css_class='col-md-12'),
|
2026-03-12 14:15:14 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-12 17:28:41 -03:00
|
|
|
css_class='col-md-12'
|
2026-03-12 14:15:14 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AccessPolicyForm(forms.ModelForm):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = AccessPolicy
|
|
|
|
|
fields = ['name', 'policy_type', 'groups', 'methods']
|
|
|
|
|
labels = {
|
|
|
|
|
'name': _('Name'),
|
|
|
|
|
'policy_type': _('Policy Type'),
|
|
|
|
|
'groups': _('Allowed Groups'),
|
|
|
|
|
'methods': _('Authentication Methods'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
2026-03-13 20:12:29 -03:00
|
|
|
policy_type = kwargs.pop('policy_type', None)
|
2026-03-12 14:15:14 -03:00
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
2026-03-13 20:12:29 -03:00
|
|
|
if self.instance and self.instance.pk:
|
|
|
|
|
policy_type = self.instance.policy_type
|
|
|
|
|
|
|
|
|
|
if policy_type and not self.initial.get('policy_type'):
|
|
|
|
|
self.initial['policy_type'] = policy_type
|
|
|
|
|
|
|
|
|
|
self.fields['policy_type'].widget = forms.HiddenInput()
|
|
|
|
|
|
2026-03-12 14:15:14 -03:00
|
|
|
self.helper = FormHelper()
|
2026-03-13 20:12:29 -03:00
|
|
|
|
|
|
|
|
if policy_type in ['public', 'deny']:
|
|
|
|
|
self.helper.layout = Layout(
|
2026-03-12 14:15:14 -03:00
|
|
|
Div(
|
2026-03-13 20:12:29 -03:00
|
|
|
Div('name', css_class='col-md-12'),
|
|
|
|
|
'policy_type',
|
|
|
|
|
css_class='row'
|
2026-03-12 14:15:14 -03:00
|
|
|
),
|
2026-03-13 20:12:29 -03:00
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
|
|
|
|
css_class='col-md-12'
|
|
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
2026-03-12 14:15:14 -03:00
|
|
|
)
|
2026-03-13 20:12:29 -03:00
|
|
|
else:
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
|
|
|
|
Div('name', css_class='col-md-12'),
|
|
|
|
|
'policy_type',
|
|
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(Div('methods', css_class='col-md-12'), css_class='row'),
|
|
|
|
|
Div(Div('groups', css_class='col-md-12'), css_class='row'),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
|
|
|
|
css_class='col-md-12'
|
|
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
policy_type = cleaned_data.get('policy_type')
|
|
|
|
|
groups = cleaned_data.get('groups')
|
|
|
|
|
methods = cleaned_data.get('methods')
|
|
|
|
|
|
|
|
|
|
if policy_type == 'protected':
|
2026-03-14 09:34:59 -03:00
|
|
|
has_local_password = False
|
|
|
|
|
local_password_count = 0
|
|
|
|
|
oidc_count = 0
|
|
|
|
|
has_groups = groups and len(groups) > 0
|
|
|
|
|
|
|
|
|
|
# Count authentication methods
|
|
|
|
|
if methods:
|
|
|
|
|
for method in methods:
|
|
|
|
|
if method.auth_type == 'local_password':
|
|
|
|
|
has_local_password = True
|
|
|
|
|
local_password_count += 1
|
|
|
|
|
elif method.auth_type == 'oidc':
|
|
|
|
|
oidc_count += 1
|
|
|
|
|
|
|
|
|
|
# Rule: Cannot select more than one local password method
|
|
|
|
|
if local_password_count > 1:
|
|
|
|
|
self.add_error('methods', _("Cannot select more than one Local Password authentication method."))
|
|
|
|
|
|
|
|
|
|
# Rule: Cannot select more than one oidc method
|
|
|
|
|
if oidc_count > 1:
|
|
|
|
|
self.add_error('methods', _("Cannot select more than one OpenID Connect (OIDC) authentication method."))
|
|
|
|
|
|
|
|
|
|
# Rule: Cannot mix local password and oidc
|
|
|
|
|
if local_password_count > 0 and oidc_count > 0:
|
|
|
|
|
self.add_error('methods', _("Cannot select both Local Password and OpenID Connect (OIDC) authentication methods."))
|
|
|
|
|
|
|
|
|
|
# Rule: If local password is selected, at least one user group must be selected
|
|
|
|
|
if has_local_password and not has_groups:
|
|
|
|
|
self.add_error('groups', _("At least one user group must be selected when using Local Password authentication."))
|
|
|
|
|
|
|
|
|
|
# Rule: If local password is NOT selected, user groups cannot be selected
|
|
|
|
|
if not has_local_password and has_groups:
|
|
|
|
|
self.add_error('groups', _("User groups can only be used with Local Password authentication."))
|
2026-03-13 20:12:29 -03:00
|
|
|
|
|
|
|
|
return cleaned_data
|
2026-03-12 14:15:14 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApplicationPolicyForm(forms.ModelForm):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = ApplicationPolicy
|
2026-03-12 18:41:21 -03:00
|
|
|
fields = ['default_policy']
|
2026-03-12 14:15:14 -03:00
|
|
|
labels = {
|
|
|
|
|
'default_policy': _('Default Policy'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
2026-03-12 18:41:21 -03:00
|
|
|
Div('default_policy', css_class='col-md-12'),
|
2026-03-12 14:15:14 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-12 17:28:41 -03:00
|
|
|
css_class='col-md-12'
|
2026-03-12 14:15:14 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ApplicationRouteForm(forms.ModelForm):
|
|
|
|
|
class Meta:
|
|
|
|
|
model = ApplicationRoute
|
2026-03-12 18:41:21 -03:00
|
|
|
fields = ['name', 'path_prefix', 'policy', 'order']
|
2026-03-12 14:15:14 -03:00
|
|
|
labels = {
|
|
|
|
|
'name': _('Route Name'),
|
|
|
|
|
'path_prefix': _('Path Prefix'),
|
|
|
|
|
'policy': _('Policy'),
|
2026-03-12 18:41:21 -03:00
|
|
|
'order': _('Order'),
|
2026-03-12 14:15:14 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
|
cancel_url = kwargs.pop('cancel_url', '#')
|
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
self.helper = FormHelper()
|
|
|
|
|
self.helper.layout = Layout(
|
|
|
|
|
Div(
|
2026-03-12 18:41:21 -03:00
|
|
|
Div('name', css_class='col-md-12'),
|
2026-03-12 14:15:14 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
2026-03-12 18:41:21 -03:00
|
|
|
Div('path_prefix', css_class='col-xl-7'),
|
|
|
|
|
Div('order', css_class='col-xl-5'),
|
2026-03-12 14:15:14 -03:00
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div('policy', css_class='col-md-12'),
|
|
|
|
|
css_class='row'
|
|
|
|
|
),
|
|
|
|
|
Div(
|
|
|
|
|
Div(
|
|
|
|
|
Submit('submit', _('Save'), css_class='btn btn-primary'),
|
|
|
|
|
HTML(f'<a href="{cancel_url}" class="btn btn-secondary">{_("Cancel")}</a>'),
|
2026-03-12 17:28:41 -03:00
|
|
|
css_class='col-md-12'
|
2026-03-12 14:15:14 -03:00
|
|
|
),
|
|
|
|
|
css_class='row'
|
|
|
|
|
)
|
|
|
|
|
)
|