diff --git a/CHANGELOG.md b/CHANGELOG.md index f936636..bad843f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * Bump the package version so Git/Pip upgrades replace older installations * Make the license type field optional * Show the plugin under Licences in the main menu +* Restrict SMTP notification activation to administrators and explicitly authorized users ## [1.9.0](https://github.com/ICTU/netbox_slm/releases/tag/1.9.0) - 2026-06-25 diff --git a/README.md b/README.md index dc8d615..04974ec 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,10 @@ Die NetBox-Nachricht wird immer erzeugt. Wird an der Lizenz zusätzlich **E-Mail das Plugin auch eine E-Mail an die im Benutzerkonto hinterlegte Adresse. Dafür muss der SMTP-Versand in NetBox konfiguriert sein. +Die Aktivierung des E-Mail-Versands darf nur durch Administratoren oder Benutzer mit der Berechtigung +`netbox_slm.manage_softwarelicense_email_notifications` geändert werden. Die eigentlichen SMTP-Zugangsdaten werden +ausschließlich in der NetBox-Serverkonfiguration gepflegt und sind im Plugin nicht einsehbar. + Die Prüfung läuft einmal täglich als NetBox-Systemjob. Der NetBox-RQ-Worker muss daher mit Scheduler-Unterstützung laufen; dies ist bei der regulären NetBox-Installation mit `rqworker` standardmäßig der Fall. Das interne Versandprotokoll verhindert doppelte Nachrichten. diff --git a/netbox_slm/__init__.py b/netbox_slm/__init__.py index e0530f8..6fa668e 100644 --- a/netbox_slm/__init__.py +++ b/netbox_slm/__init__.py @@ -17,7 +17,7 @@ limitations under the License. from netbox.plugins import PluginConfig from django.utils.translation import gettext_lazy as _ -__version__ = "1.12.0" +__version__ = "1.12.1" class SLMConfig(PluginConfig): diff --git a/netbox_slm/api/serializers.py b/netbox_slm/api/serializers.py index 1327ef7..5ef5a6b 100644 --- a/netbox_slm/api/serializers.py +++ b/netbox_slm/api/serializers.py @@ -54,6 +54,16 @@ class SoftwareLicenseSerializer(NetBoxModelSerializer): return f"{obj}" def validate(self, attrs): + request = self.context.get("request") + if ( + "email_notifications" in attrs + and request is not None + and not request.user.has_perm("netbox_slm.manage_softwarelicense_email_notifications") + ): + raise serializers.ValidationError( + {"email_notifications": _("You do not have permission to change email notifications.")} + ) + tenant_group = attrs.get("tenant_group", getattr(self.instance, "tenant_group", None)) tenant = attrs.get("tenant", getattr(self.instance, "tenant", None)) if tenant_group and tenant and tenant.group_id != tenant_group.pk: diff --git a/netbox_slm/locale/de/LC_MESSAGES/django.mo b/netbox_slm/locale/de/LC_MESSAGES/django.mo index ac4b17c..e807294 100644 Binary files a/netbox_slm/locale/de/LC_MESSAGES/django.mo and b/netbox_slm/locale/de/LC_MESSAGES/django.mo differ diff --git a/netbox_slm/locale/de/LC_MESSAGES/django.po b/netbox_slm/locale/de/LC_MESSAGES/django.po index d331a20..063d15c 100644 --- a/netbox_slm/locale/de/LC_MESSAGES/django.po +++ b/netbox_slm/locale/de/LC_MESSAGES/django.po @@ -408,3 +408,12 @@ msgstr "Lizenzen" msgid "Licence Management NetBox Plugin." msgstr "NetBox-Plugin zur Lizenzverwaltung." + +msgid "Can manage license email notifications" +msgstr "Kann E-Mail-Benachrichtigungen für Lizenzen verwalten" + +msgid "You do not have permission to change email notifications." +msgstr "Sie haben keine Berechtigung, E-Mail-Benachrichtigungen zu ändern." + +msgid "Only administrators or explicitly authorized users can change this setting." +msgstr "Nur Administratoren oder ausdrücklich berechtigte Benutzer können diese Einstellung ändern." diff --git a/netbox_slm/migrations/0014_softwarelicense_email_notification_permission.py b/netbox_slm/migrations/0014_softwarelicense_email_notification_permission.py new file mode 100644 index 0000000..c2b8a1e --- /dev/null +++ b/netbox_slm/migrations/0014_softwarelicense_email_notification_permission.py @@ -0,0 +1,23 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("netbox_slm", "0013_license_expiration_notifications"), + ] + + operations = [ + migrations.AlterModelOptions( + name="softwarelicense", + options={ + "permissions": [ + ( + "manage_softwarelicense_email_notifications", + "Can manage license email notifications", + ) + ], + "verbose_name": "software license", + "verbose_name_plural": "software licenses", + }, + ), + ] diff --git a/netbox_slm/models.py b/netbox_slm/models.py index 5d1b2b0..c98ba00 100644 --- a/netbox_slm/models.py +++ b/netbox_slm/models.py @@ -267,6 +267,9 @@ class SoftwareLicense(NetBoxModel): class Meta: verbose_name = _("software license") verbose_name_plural = _("software licenses") + permissions = [ + ("manage_softwarelicense_email_notifications", _("Can manage license email notifications")), + ] def __str__(self): return self.name diff --git a/netbox_slm/views/software_license.py b/netbox_slm/views/software_license.py index 4eb3716..e5c524b 100644 --- a/netbox_slm/views/software_license.py +++ b/netbox_slm/views/software_license.py @@ -1,9 +1,34 @@ from netbox.views import generic +from django.utils.translation import gettext_lazy as _ from netbox_slm import filtersets, forms, tables from netbox_slm.models import SoftwareLicense from utilities.views import register_model_view +class EmailNotificationPermissionMixin: + """Prevent users without the dedicated permission from changing SMTP delivery.""" + + email_permission = "netbox_slm.manage_softwarelicense_email_notifications" + + def dispatch(self, request, *args, **kwargs): + form_attribute = "model_form" if hasattr(self, "model_form") else "form" + base_form = getattr(self, form_attribute) + can_manage_email = request.user.has_perm(self.email_permission) + + class PermissionAwareForm(base_form): + def __init__(self, *form_args, **form_kwargs): + super().__init__(*form_args, **form_kwargs) + if not can_manage_email and "email_notifications" in self.fields: + field = self.fields["email_notifications"] + field.disabled = True + field.help_text = _( + "Only administrators or explicitly authorized users can change this setting." + ) + + setattr(self, form_attribute, PermissionAwareForm) + return super().dispatch(request, *args, **kwargs) + + @register_model_view(SoftwareLicense, "list", path="", detail=False) class SoftwareLicenseListView(generic.ObjectListView): queryset = SoftwareLicense.objects.all() @@ -19,7 +44,7 @@ class SoftwareLicenseView(generic.ObjectView): @register_model_view(SoftwareLicense, "add", detail=False) @register_model_view(SoftwareLicense, "edit") -class SoftwareLicenseEditView(generic.ObjectEditView): +class SoftwareLicenseEditView(EmailNotificationPermissionMixin, generic.ObjectEditView): queryset = SoftwareLicense.objects.all() form = forms.SoftwareLicenseForm @@ -30,13 +55,13 @@ class SoftwareLicenseDeleteView(generic.ObjectDeleteView): @register_model_view(SoftwareLicense, "bulk_import", detail=False) -class SoftwareLicenseBulkImportView(generic.BulkImportView): +class SoftwareLicenseBulkImportView(EmailNotificationPermissionMixin, generic.BulkImportView): queryset = SoftwareLicense.objects.all() model_form = forms.SoftwareLicenseBulkImportForm @register_model_view(SoftwareLicense, "bulk_edit", path="edit", detail=False) -class SoftwareLicenseBulkEditView(generic.BulkEditView): +class SoftwareLicenseBulkEditView(EmailNotificationPermissionMixin, generic.BulkEditView): queryset = SoftwareLicense.objects.all() filterset = filtersets.SoftwareLicenseFilterSet table = tables.SoftwareLicenseTable