From 6737a8dbd916dea46491919d9dc2a6c39513d374 Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 1 Nov 2023 12:10:10 +0100 Subject: [PATCH 1/9] Add new protocol type for SNMPv3 SNMPv3 attributes are very distinct from either SNMPv1 or SNMPv2, so i deserves a new type. --- python/nav/models/manage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python/nav/models/manage.py b/python/nav/models/manage.py index 50de8d1a4a..b87df8602c 100644 --- a/python/nav/models/manage.py +++ b/python/nav/models/manage.py @@ -131,9 +131,11 @@ class ManagementProfile(models.Model): PROTOCOL_DEBUG = 0 PROTOCOL_SNMP = 1 PROTOCOL_NAPALM = 2 + PROTOCOL_SNMPV3 = 3 PROTOCOL_CHOICES = [ (PROTOCOL_SNMP, "SNMP"), (PROTOCOL_NAPALM, "NAPALM"), + (PROTOCOL_SNMPV3, "SNMPv3"), ] if settings.DEBUG: PROTOCOL_CHOICES.insert(0, (PROTOCOL_DEBUG, 'debug')) From 5ee8f537676539e5e30aa607ba7ef10cb2877598 Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 1 Nov 2023 12:45:35 +0100 Subject: [PATCH 2/9] Add SNMPV3Form with initial fields --- .../seeddb/page/management_profile/forms.py | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/python/nav/web/seeddb/page/management_profile/forms.py b/python/nav/web/seeddb/page/management_profile/forms.py index caec7494fd..407324742a 100644 --- a/python/nav/web/seeddb/page/management_profile/forms.py +++ b/python/nav/web/seeddb/page/management_profile/forms.py @@ -94,6 +94,79 @@ class Meta(object): ) +class SnmpV3Form(ProtocolSpecificMixIn, forms.ModelForm): + PROTOCOL = ManagementProfile.PROTOCOL_SNMPV3 + PROTOCOL_NAME = PROTOCOL_CHOICES.get(PROTOCOL) + + class Meta(object): + model = ManagementProfile + configuration_fields = [ + "sec_level", + "auth_protocol", + "sec_name", + "auth_password", + "priv_protocol", + "priv_password", + ] + fields = [] + + sec_level = forms.ChoiceField( + label="Security level", + choices=( + ("noAuthNoPriv", "noAuthNoPriv"), + ("authNoPriv", "authNoPriv"), + ("authPriv", "authPriv"), + ), + help_text="The required SNMPv3 security level", + ) + auth_protocol = forms.ChoiceField( + label="Authentication protocol", + choices=( + ("MD5", "MD5"), + ("SHA", "SHA"), + ("SHA-512", "SHA-512"), + ("SHA-384", "SHA-384"), + ("SHA-256", "SHA-256"), + ("SHA-224", "SHA-224"), + ), + help_text="Authentication protocol to use", + ) + sec_name = forms.CharField( + label="Security name", + help_text=( + "The username to authenticate as. This is required even if noAuthPriv " + "security mode is selected." + ), + ) + auth_password = forms.CharField( + widget=forms.PasswordInput(), + label="Authentication password", + help_text=( + "The password to authenticate the user. Required for authNoPriv or " + "authPriv security levels." + ), + required=False, + ) + priv_protocol = forms.ChoiceField( + label="Privacy protocol", + choices=( + ("DES", "DES"), + ("AES", "AES"), + ), + help_text="Privacy protocol to use. Required for authPriv security level.", + required=False, + ) + priv_password = forms.CharField( + widget=forms.PasswordInput(), + label="Privacy password", + help_text=( + "The password to use for DES or AES encryption. Required for authPriv " + "security level." + ), + required=False, + ) + + class NapalmForm(ProtocolSpecificMixIn, forms.ModelForm): PROTOCOL = ManagementProfile.PROTOCOL_NAPALM PROTOCOL_NAME = PROTOCOL_CHOICES.get(PROTOCOL) From bcd268e9996519731c5822545b603c68c613adb5 Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 1 Nov 2023 13:00:26 +0100 Subject: [PATCH 3/9] Signal no autocompletion for SNMPv3 form fields My browser tries to input my NAV username and password in these unrelated profile fields. This is not some kind of login form, so this suggests to the browser to eschew autocompletion of usernames and passwords. --- python/nav/web/seeddb/page/management_profile/forms.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/nav/web/seeddb/page/management_profile/forms.py b/python/nav/web/seeddb/page/management_profile/forms.py index 407324742a..b4747a3aab 100644 --- a/python/nav/web/seeddb/page/management_profile/forms.py +++ b/python/nav/web/seeddb/page/management_profile/forms.py @@ -132,6 +132,7 @@ class Meta(object): help_text="Authentication protocol to use", ) sec_name = forms.CharField( + widget=forms.TextInput(attrs={"autocomplete": "off"}), label="Security name", help_text=( "The username to authenticate as. This is required even if noAuthPriv " @@ -139,7 +140,7 @@ class Meta(object): ), ) auth_password = forms.CharField( - widget=forms.PasswordInput(), + widget=forms.PasswordInput(render_value=True, attrs={"autocomplete": "off"}), label="Authentication password", help_text=( "The password to authenticate the user. Required for authNoPriv or " @@ -157,7 +158,7 @@ class Meta(object): required=False, ) priv_password = forms.CharField( - widget=forms.PasswordInput(), + widget=forms.PasswordInput(render_value=True, attrs={"autocomplete": "off"}), label="Privacy password", help_text=( "The password to use for DES or AES encryption. Required for authPriv " From b03028201a7a8789bc6558e6b3572f382fa213bd Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 1 Nov 2023 13:52:09 +0100 Subject: [PATCH 4/9] Clean SNMPv3 password fields The two password are only required dependent upon which security level has been chosen for this profile. --- .../seeddb/page/management_profile/forms.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/python/nav/web/seeddb/page/management_profile/forms.py b/python/nav/web/seeddb/page/management_profile/forms.py index b4747a3aab..a63b072971 100644 --- a/python/nav/web/seeddb/page/management_profile/forms.py +++ b/python/nav/web/seeddb/page/management_profile/forms.py @@ -167,6 +167,24 @@ class Meta(object): required=False, ) + def clean_auth_password(self): + level = self.cleaned_data.get("sec_level") + password = self.cleaned_data.get("auth_password").strip() + if level.startswith("auth") and not password: + raise forms.ValidationError( + f"Authentication password must be set for security level {level}" + ) + return password + + def clean_priv_password(self): + level = self.cleaned_data.get("sec_level") + password = self.cleaned_data.get("priv_password").strip() + if level == "authPriv" and not password: + raise forms.ValidationError( + f"Privacy password must be set for security level {level}" + ) + return password + class NapalmForm(ProtocolSpecificMixIn, forms.ModelForm): PROTOCOL = ManagementProfile.PROTOCOL_NAPALM From 7e71add315cd3f130b828fc9005090fe30ab535f Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 1 Nov 2023 14:05:12 +0100 Subject: [PATCH 5/9] Add option for form notabene warnings This is used to signal to end users that while SNMPv3 profiles are available, the functionality in NAV is not yet complete and cannot be expected to work. This notabene can be removed from `SNMPV3Form` once SNMPv3 implementation is considered complete. --- python/nav/web/seeddb/page/management_profile/forms.py | 1 + python/nav/web/templates/seeddb/management-profile/edit.html | 3 +++ 2 files changed, 4 insertions(+) diff --git a/python/nav/web/seeddb/page/management_profile/forms.py b/python/nav/web/seeddb/page/management_profile/forms.py index a63b072971..fd6174702e 100644 --- a/python/nav/web/seeddb/page/management_profile/forms.py +++ b/python/nav/web/seeddb/page/management_profile/forms.py @@ -97,6 +97,7 @@ class Meta(object): class SnmpV3Form(ProtocolSpecificMixIn, forms.ModelForm): PROTOCOL = ManagementProfile.PROTOCOL_SNMPV3 PROTOCOL_NAME = PROTOCOL_CHOICES.get(PROTOCOL) + NOTABENE = "SNMPv3 is not yet fully supported in NAV" class Meta(object): model = ManagementProfile diff --git a/python/nav/web/templates/seeddb/management-profile/edit.html b/python/nav/web/templates/seeddb/management-profile/edit.html index f8aa5f33b3..634e1ca69e 100644 --- a/python/nav/web/templates/seeddb/management-profile/edit.html +++ b/python/nav/web/templates/seeddb/management-profile/edit.html @@ -35,6 +35,9 @@

Add new management profile

{% for form in protocol_forms %}
{{ form.PROTOCOL_NAME }} configuration + {% if form.NOTABENE %} +
{{ form.NOTABENE }}
+ {% endif %} {{ form | crispy }}
{% endfor %} From 7f9b82fd0cc965085fdc36c34a347a54ad53aa8b Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Thu, 2 Nov 2023 11:57:34 +0100 Subject: [PATCH 6/9] Update is_snmp and snmp_version properties These need to reflect the existence of the new SNMPv3 profile type. --- python/nav/models/manage.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/nav/models/manage.py b/python/nav/models/manage.py index b87df8602c..385c8d322e 100644 --- a/python/nav/models/manage.py +++ b/python/nav/models/manage.py @@ -154,16 +154,18 @@ def __str__(self): @property def is_snmp(self): - return self.protocol == self.PROTOCOL_SNMP + return self.protocol in (self.PROTOCOL_SNMP, self.PROTOCOL_SNMPV3) @property def snmp_version(self): """Returns the configured SNMP version as an integer""" - if self.is_snmp: + if self.protocol == self.PROTOCOL_SNMP: value = self.configuration['version'] if value == "2c": return 2 return int(value) + elif self.protocol == self.PROTOCOL_SNMPV3: + return 3 raise ValueError( "Getting snmp protocol version for non-snmp management profile" From 75d62b1dc55b4a9eb617bc960f23d4702410aae3 Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Thu, 9 Nov 2023 10:50:28 +0100 Subject: [PATCH 7/9] Add write flag to SNMPv3 profiles As with SNMP v1/v2 profiles, it is good to know whether a profile represents write access to a device or not, something PortAdmin or Arnold will need to know in order to select the correct profile if there are several. --- python/nav/web/seeddb/page/management_profile/forms.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/python/nav/web/seeddb/page/management_profile/forms.py b/python/nav/web/seeddb/page/management_profile/forms.py index fd6174702e..7f2f5cfc7c 100644 --- a/python/nav/web/seeddb/page/management_profile/forms.py +++ b/python/nav/web/seeddb/page/management_profile/forms.py @@ -108,6 +108,7 @@ class Meta(object): "auth_password", "priv_protocol", "priv_password", + "write", ] fields = [] @@ -167,6 +168,12 @@ class Meta(object): ), required=False, ) + write = forms.BooleanField( + initial=False, + required=False, + label="Enables write access", + help_text="Check this if this profile enables write access", + ) def clean_auth_password(self): level = self.cleaned_data.get("sec_level") From 2bcd8d04905f3eb5159e52b63ddfd5eed3c738f6 Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Thu, 9 Nov 2023 11:07:03 +0100 Subject: [PATCH 8/9] Test SnmpV3Form --- .../seeddb/management_profile_test.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/unittests/seeddb/management_profile_test.py diff --git a/tests/unittests/seeddb/management_profile_test.py b/tests/unittests/seeddb/management_profile_test.py new file mode 100644 index 0000000000..03937302b6 --- /dev/null +++ b/tests/unittests/seeddb/management_profile_test.py @@ -0,0 +1,48 @@ +from nav.web.seeddb.page.management_profile.forms import SnmpV3Form + + +class TestSnmpv3Form: + def test_when_seclevel_is_noauth_then_it_should_not_require_auth_password(self): + form = SnmpV3Form( + dict( + sec_level="noAuthNoPriv", + auth_protocol="MD5", + sec_name="foo", + auth_password="", + ) + ) + assert form.is_valid() + + def test_when_seclevel_is_auth_then_it_should_require_auth_password(self): + form = SnmpV3Form( + dict( + sec_level="authNoPriv", + auth_protocol="MD5", + sec_name="foo", + auth_password="", + ) + ) + assert not form.is_valid() + + def test_when_seclevel_is_priv_then_it_should_require_priv_password(self): + form = SnmpV3Form( + dict( + sec_level="authPriv", + auth_protocol="MD5", + sec_name="foo", + auth_password="bar", + ) + ) + assert not form.is_valid() + + def test_when_seclevel_is_priv_then_it_should_accept_priv_password(self): + form = SnmpV3Form( + dict( + sec_level="authPriv", + auth_protocol="MD5", + sec_name="foo", + auth_password="bar", + priv_password="cromulent", + ) + ) + assert form.is_valid() From bca69aea066d8486c1dd6be06a03dd55617bf39b Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Thu, 9 Nov 2023 11:34:29 +0100 Subject: [PATCH 9/9] Add SNMPv3 support to preferred profile fetch Netbox.get_preferred_snmp_management_profile() also needs to be able to look at SNMPv3 profiles and return those if they fit - as they would take priority before SNMPv2 profiles, if there are any. --- python/nav/models/manage.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/python/nav/models/manage.py b/python/nav/models/manage.py index 385c8d322e..e0fcf3dff6 100644 --- a/python/nav/models/manage.py +++ b/python/nav/models/manage.py @@ -363,7 +363,12 @@ def get_preferred_snmp_management_profile(self, writeable=None): Returns the snmp management profile with the highest available SNMP version. """ - query = Q(protocol=ManagementProfile.PROTOCOL_SNMP) + query = Q( + protocol__in=( + ManagementProfile.PROTOCOL_SNMP, + ManagementProfile.PROTOCOL_SNMPV3, + ) + ) if writeable: query = query & Q(configuration__write=True) elif writeable is not None: @@ -372,7 +377,7 @@ def get_preferred_snmp_management_profile(self, writeable=None): ) profiles = sorted( self.profiles.filter(query), - key=lambda p: str(p.configuration.get('version') or 0), + key=lambda p: p.snmp_version or 0, reverse=True, ) if profiles: