diff --git a/app_gateway/__init__.py b/app_gateway/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app_gateway/admin.py b/app_gateway/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/app_gateway/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/app_gateway/apps.py b/app_gateway/apps.py new file mode 100644 index 0000000..ea16f1f --- /dev/null +++ b/app_gateway/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AppGatewayConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'app_gateway' diff --git a/app_gateway/migrations/0001_initial.py b/app_gateway/migrations/0001_initial.py new file mode 100644 index 0000000..7c37b0b --- /dev/null +++ b/app_gateway/migrations/0001_initial.py @@ -0,0 +1,100 @@ +# Generated by Django 5.2.12 on 2026-03-11 19:35 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('gatekeeper', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Application', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(max_length=64, unique=True)), + ('display_name', models.CharField(max_length=128)), + ('upstream', models.CharField(help_text='Upstream address, e.g.: http://10.188.18.27:3000', max_length=255)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='AccessPolicy', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(max_length=64, unique=True)), + ('policy_type', models.CharField(choices=[('bypass', 'Bypass (public)'), ('one_factor', 'One Factor'), ('two_factor', 'Two Factor'), ('deny', 'Deny')], max_length=32)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('groups', models.ManyToManyField(blank=True, related_name='policies', to='gatekeeper.gatekeepergroup')), + ('methods', models.ManyToManyField(blank=True, related_name='policies', to='gatekeeper.authmethod')), + ], + options={ + 'verbose_name': 'Access Policy', + 'verbose_name_plural': 'Access Policies', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='ApplicationHost', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('hostname', models.CharField(max_length=255, unique=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hosts', to='app_gateway.application')), + ], + options={ + 'ordering': ['hostname'], + }, + ), + migrations.CreateModel( + name='ApplicationPolicy', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('application', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='default_policy_config', to='app_gateway.application')), + ('default_policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='application_defaults', to='app_gateway.accesspolicy')), + ], + options={ + 'verbose_name': 'Application Policy', + 'verbose_name_plural': 'Application Policies', + }, + ), + migrations.CreateModel( + name='ApplicationRoute', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(help_text='Route identifier, used in export (e.g.: public_area)', max_length=64)), + ('path_prefix', models.CharField(max_length=255)), + ('order', models.PositiveIntegerField(default=0, help_text='Evaluation order — lower value means higher priority')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('application', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='routes', to='app_gateway.application')), + ('policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='routes', to='app_gateway.accesspolicy')), + ], + options={ + 'verbose_name': 'Application Route', + 'verbose_name_plural': 'Application Routes', + 'ordering': ['application', 'order', 'path_prefix'], + 'unique_together': {('application', 'name'), ('application', 'path_prefix')}, + }, + ), + ] diff --git a/app_gateway/migrations/__init__.py b/app_gateway/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app_gateway/models.py b/app_gateway/models.py new file mode 100644 index 0000000..0ae00de --- /dev/null +++ b/app_gateway/models.py @@ -0,0 +1,100 @@ +import uuid + +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from gatekeeper.models import GatekeeperGroup, AuthMethod + + +class Application(models.Model): + name = models.SlugField(max_length=64, unique=True) + display_name = models.CharField(max_length=128) + upstream = models.CharField(max_length=255, help_text=_("Upstream address, e.g.: http://10.188.18.27:3000")) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return self.display_name + + class Meta: + ordering = ['name'] + + +class ApplicationHost(models.Model): + application = models.ForeignKey(Application, on_delete=models.CASCADE, related_name='hosts') + hostname = models.CharField(max_length=255, unique=True) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return self.hostname + + class Meta: + ordering = ['hostname'] + + +class AccessPolicy(models.Model): + POLICY_TYPE_CHOICES = [ + ('bypass', _('Bypass (public)')), + ('one_factor', _('One Factor')), + ('two_factor', _('Two Factor')), + ('deny', _('Deny')), + ] + + name = models.SlugField(max_length=64, unique=True) + policy_type = models.CharField(max_length=32, choices=POLICY_TYPE_CHOICES) + groups = models.ManyToManyField(GatekeeperGroup, blank=True, related_name='policies') + methods = models.ManyToManyField(AuthMethod, blank=True, related_name='policies') + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return f"{self.name} ({self.get_policy_type_display()})" + + class Meta: + ordering = ['name'] + verbose_name = 'Access Policy' + verbose_name_plural = 'Access Policies' + + +class ApplicationPolicy(models.Model): + application = models.OneToOneField(Application, on_delete=models.CASCADE, related_name='default_policy_config') + default_policy = models.ForeignKey(AccessPolicy, on_delete=models.PROTECT, related_name='application_defaults') + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return f"{self.application} → default: {self.default_policy}" + + class Meta: + verbose_name = 'Application Policy' + verbose_name_plural = 'Application Policies' + + +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)")) + path_prefix = models.CharField(max_length=255) + policy = models.ForeignKey(AccessPolicy, on_delete=models.PROTECT, related_name='routes') + order = models.PositiveIntegerField(default=0, help_text=_("Evaluation order — lower value means higher priority")) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return f"{self.application} {self.path_prefix} → {self.policy}" + + class Meta: + ordering = ['application', 'order', 'path_prefix'] + unique_together = [('application', 'path_prefix'), ('application', 'name')] + verbose_name = 'Application Route' + verbose_name_plural = 'Application Routes' \ No newline at end of file diff --git a/app_gateway/tests.py b/app_gateway/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/app_gateway/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/app_gateway/views.py b/app_gateway/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/app_gateway/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/gatekeeper/__init__.py b/gatekeeper/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gatekeeper/admin.py b/gatekeeper/admin.py new file mode 100644 index 0000000..846f6b4 --- /dev/null +++ b/gatekeeper/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/gatekeeper/apps.py b/gatekeeper/apps.py new file mode 100644 index 0000000..430050f --- /dev/null +++ b/gatekeeper/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class GatekeeperConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'gatekeeper' diff --git a/gatekeeper/migrations/0001_initial.py b/gatekeeper/migrations/0001_initial.py new file mode 100644 index 0000000..e14d91a --- /dev/null +++ b/gatekeeper/migrations/0001_initial.py @@ -0,0 +1,97 @@ +# Generated by Django 5.2.12 on 2026-03-11 19:35 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AuthMethod', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(max_length=64, unique=True)), + ('auth_type', models.CharField(choices=[('local_password', 'Local Password'), ('totp', 'TOTP'), ('oidc', 'OIDC')], max_length=32)), + ('totp_secret', models.CharField(blank=True, help_text='Shared/global TOTP secret key', max_length=255)), + ('oidc_provider', models.CharField(blank=True, max_length=64)), + ('oidc_client_id', models.CharField(blank=True, max_length=255)), + ('oidc_client_secret', models.CharField(blank=True, max_length=255)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='GatekeeperUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.SlugField(max_length=64, unique=True)), + ('email', models.EmailField(max_length=254, unique=True)), + ('password', models.CharField(blank=True, help_text='Password for local authentication (leave blank if not using)', max_length=128)), + ('totp_secret', models.CharField(blank=True, help_text='Per-user TOTP secret key', max_length=255)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ], + options={ + 'verbose_name': 'Gatekeeper User', + 'verbose_name_plural': 'Gatekeeper Users', + 'ordering': ['username'], + }, + ), + migrations.CreateModel( + name='GatekeeperGroup', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(max_length=64, unique=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('users', models.ManyToManyField(blank=True, related_name='groups', to='gatekeeper.gatekeeperuser')), + ], + options={ + 'verbose_name': 'Gatekeeper Group', + 'verbose_name_plural': 'Gatekeeper Groups', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='AuthMethodAllowedDomain', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('domain', models.CharField(max_length=255)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('auth_method', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_domains', to='gatekeeper.authmethod')), + ], + options={ + 'unique_together': {('auth_method', 'domain')}, + }, + ), + migrations.CreateModel( + name='AuthMethodAllowedEmail', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=254)), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('auth_method', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='allowed_emails', to='gatekeeper.authmethod')), + ], + options={ + 'unique_together': {('auth_method', 'email')}, + }, + ), + ] diff --git a/gatekeeper/migrations/__init__.py b/gatekeeper/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gatekeeper/models.py b/gatekeeper/models.py new file mode 100644 index 0000000..04241fa --- /dev/null +++ b/gatekeeper/models.py @@ -0,0 +1,97 @@ +import uuid + +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class AuthMethod(models.Model): + name = models.SlugField(max_length=64, unique=True) + auth_type = models.CharField(max_length=32, choices=(('local_password', _('Local Password')), ('totp', _('TOTP')), ('oidc', _('OIDC')))) + + # TOTP-specific fields + totp_secret = models.CharField( + max_length=255, blank=True, + help_text="Shared/global TOTP secret key" + ) + + # OIDC-specific fields + oidc_provider = models.CharField(max_length=64, blank=True) + oidc_client_id = models.CharField(max_length=255, blank=True) + oidc_client_secret = models.CharField(max_length=255, blank=True) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return f"{self.name} ({self.get_auth_type_display()})" + + class Meta: + ordering = ['name'] + + +class AuthMethodAllowedDomain(models.Model): + auth_method = models.ForeignKey(AuthMethod, on_delete=models.CASCADE, related_name='allowed_domains') + domain = models.CharField(max_length=255) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return self.domain + + class Meta: + unique_together = [('auth_method', 'domain')] + + +class AuthMethodAllowedEmail(models.Model): + auth_method = models.ForeignKey(AuthMethod, on_delete=models.CASCADE, related_name='allowed_emails') + email = models.EmailField() + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return self.email + + class Meta: + unique_together = [('auth_method', 'email')] + + +class GatekeeperUser(models.Model): + username = models.SlugField(max_length=64, unique=True) + email = models.EmailField(unique=True) + password = models.CharField(blank=True, max_length=128, help_text=_("Password for local authentication (leave blank if not using)")) + totp_secret = models.CharField(max_length=255, blank=True, help_text=_("Per-user TOTP secret key")) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + uuid = models.UUIDField(unique=True, default=uuid.uuid4, editable=False) + + def __str__(self): + return self.username + + class Meta: + ordering = ['username'] + verbose_name = 'Gatekeeper User' + verbose_name_plural = 'Gatekeeper Users' + + +class GatekeeperGroup(models.Model): + name = models.SlugField(max_length=64, unique=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 __str__(self): + return self.name + + class Meta: + ordering = ['name'] + verbose_name = 'Gatekeeper Group' + verbose_name_plural = 'Gatekeeper Groups' + diff --git a/gatekeeper/tests.py b/gatekeeper/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/gatekeeper/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/gatekeeper/views.py b/gatekeeper/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/gatekeeper/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/wireguard_webadmin/settings.py b/wireguard_webadmin/settings.py index 2874d4e..a96ed97 100644 --- a/wireguard_webadmin/settings.py +++ b/wireguard_webadmin/settings.py @@ -50,7 +50,9 @@ INSTALLED_APPS = [ 'api', 'routing_templates', 'scheduler.apps.SchedulerConfig', - 'api_v2' + 'api_v2', + 'gatekeeper', + 'app_gateway', ] MIDDLEWARE = [