diff --git a/device/models.py b/device/models.py index a351a11..4e0778f 100644 --- a/device/models.py +++ b/device/models.py @@ -29,7 +29,7 @@ class Device: self.shortid = self.pk[:6].upper() self.algorithm = None self.owner = None - self.annotations = [] + self.annotations = [] self.hids = [] self.uuids = [] self.evidences = [] @@ -108,7 +108,7 @@ class Device: return annotation = annotations.first() self.last_evidence = Evidence(annotation.uuid) - + def is_eraseserver(self): if not self.uuids: self.get_uuids() @@ -120,7 +120,7 @@ class Device: owner=self.owner, type=Annotation.Type.ERASE_SERVER ).first() - + if annotation: return True return False @@ -129,7 +129,8 @@ class Device: return self.uuids[0] def get_lots(self): - self.lots = [x.lot for x in DeviceLot.objects.filter(device_id=self.id)] + self.lots = [ + x.lot for x in DeviceLot.objects.filter(device_id=self.id)] @classmethod def get_unassigned(cls, institution, offset=0, limit=None): @@ -179,7 +180,6 @@ class Device: count = cls.get_unassigned_count(institution) return devices, count - @classmethod def get_unassigned_count(cls, institution): @@ -279,6 +279,12 @@ class Device: self.get_last_evidence() return self.last_evidence.get_manufacturer() + @property + def serial_number(self): + if not self.last_evidence: + self.get_last_evidence() + return self.last_evidence.get_serial_number() + @property def type(self): if self.last_evidence.doc['type'] == "WebSnapshot": diff --git a/device/templates/details.html b/device/templates/details.html index 8477948..331c857 100644 --- a/device/templates/details.html +++ b/device/templates/details.html @@ -84,7 +84,7 @@
{% trans 'Serial Number' %}
-
{{ object.last_evidence.doc.device.serialNumber|default:'' }}
+
{{ object.serial_number|default:'' }}
{% endif %} diff --git a/device/templates/device_web.html b/device/templates/device_web.html index 21ca570..8c607d3 100644 --- a/device/templates/device_web.html +++ b/device/templates/device_web.html @@ -120,10 +120,12 @@
Model
{{ object.model|default:'' }}
-
-
Serial Number
-
{{ object.last_evidence.doc.device.serialNumber|default:'' }}
-
+ {% if user.is_authenticated %} +
+
Serial Number
+
{{ object.serial_number|default:'' }}
+
+ {% endif %} {% endif %} @@ -136,7 +138,6 @@ {% endfor %} -

Components

{% for component in object.components %} @@ -147,7 +148,9 @@

{% for component_key, component_value in component.items %} {% if component_key not in 'actions,type' %} - {{ component_key }}: {{ component_value }}
+ {% if component_key != 'serialNumber' or user.is_authenticated %} + {{ component_key }}: {{ component_value }}
+ {% endif %} {% endif %} {% endfor %}

@@ -157,10 +160,9 @@ {% endfor %}
- diff --git a/device/tests/test_mock_device.py b/device/tests/test_mock_device.py index a3fe019..732c6ed 100644 --- a/device/tests/test_mock_device.py +++ b/device/tests/test_mock_device.py @@ -3,74 +3,47 @@ from unittest.mock import MagicMock class TestDevice(Device): - """A test subclass of Device that overrides the database-dependent methods""" - # TODO Leaving commented bc not used, but might be useful at some point - # def get_annotations(self): - # """Return empty list instead of querying database""" - # return [] + def __init__(self, id): + super().__init__(id=id) + self.shortid = id[:6].upper() + self.uuids = [] + self.hids = ['hid1', 'hid2'] + self._setup_evidence() - # def get_uuids(self): - # """Set uuids directly instead of querying""" - # self.uuids = ['uuid1', 'uuid2'] - - # def get_hids(self): - # """Set hids directly instead of querying""" - # self.hids = ['hid1', 'hid2'] - - # def get_evidences(self): - # """Set evidences directly instead of querying""" - # self.evidences = [] - - # def get_lots(self): - # """Set lots directly instead of querying""" - # self.lots = [] - - def get_last_evidence(self): - if not hasattr(self, '_evidence'): - self._evidence = MagicMock() - self._evidence.doc = { - 'type': 'Computer', - 'manufacturer': 'Test Manufacturer', - 'model': 'Test Model', - 'device': { - 'serialNumber': 'SN123456', - 'type': 'Computer' - } + def _setup_evidence(self): + self._evidence = MagicMock() + self._evidence.doc = { + 'type': 'Computer', + 'manufacturer': 'Test Manufacturer', + 'model': 'Test Model', + 'device': { + 'serialNumber': 'SN123456', + 'type': 'Computer' } - self._evidence.get_manufacturer = lambda: 'Test Manufacturer' - self._evidence.get_model = lambda: 'Test Model' - self._evidence.get_chassis = lambda: 'Computer' - self._evidence.get_components = lambda: [ - { - 'type': 'CPU', - 'model': 'Intel i7', - 'manufacturer': 'Intel' - }, - { - 'type': 'RAM', - 'size': '8GB', - 'manufacturer': 'Kingston' - } - ] + } + self._evidence.get_manufacturer = lambda: 'Test Manufacturer' + self._evidence.get_model = lambda: 'Test Model' + self._evidence.get_chassis = lambda: 'Computer' + self._evidence.get_components = lambda: [ + { + 'type': 'CPU', + 'model': 'Intel i7', + 'manufacturer': 'Intel', + 'serialNumber': 'SN12345678' + }, + { + 'type': 'RAM', + 'size': '8GB', + 'manufacturer': 'Kingston', + 'serialNumber': 'SN87654321' + } + ] self.last_evidence = self._evidence + @property + def components(self): + return self.last_evidence.get_components() -class TestWebSnapshotDevice(TestDevice): - """A test subclass of Device that simulates a WebSnapshot device""" - - def get_last_evidence(self): - if not hasattr(self, '_evidence'): - self._evidence = MagicMock() - self._evidence.doc = { - 'type': 'WebSnapshot', - 'kv': { - 'URL': 'http://example.com', - 'Title': 'Test Page', - 'Timestamp': '2024-01-01' - }, - 'device': { - 'type': 'Laptop' - } - } - self.last_evidence = self._evidence - return self._evidence + @property + def serial_number(self): + return self.last_evidence.doc['device']['serialNumber'] diff --git a/device/tests/test_public_device_web.py b/device/tests/test_public_device_web.py index 33efe05..84d8bf1 100644 --- a/device/tests/test_public_device_web.py +++ b/device/tests/test_public_device_web.py @@ -2,7 +2,8 @@ from django.test import TestCase, Client from django.urls import reverse from unittest.mock import patch from device.views import PublicDeviceWebView -from device.tests.test_mock_device import TestDevice, TestWebSnapshotDevice +from device.tests.test_mock_device import TestDevice +from user.models import User, Institution class PublicDeviceWebViewTests(TestCase): @@ -11,57 +12,99 @@ class PublicDeviceWebViewTests(TestCase): self.test_id = "test123" self.test_url = reverse('device:device_web', kwargs={'pk': self.test_id}) + self.institution = Institution.objects.create( + name="Test Institution" + ) + self.user = User.objects.create_user( + email='test@example.com', + institution=self.institution, + password='testpass123' + ) def test_url_resolves_correctly(self): - """Test that the URL is constructed correctly""" url = reverse('device:device_web', kwargs={'pk': self.test_id}) self.assertEqual(url, f'/device/{self.test_id}/public/') @patch('device.views.Device') - def test_html_response(self, MockDevice): + def test_html_response_anonymous(self, MockDevice): test_device = TestDevice(id=self.test_id) MockDevice.return_value = test_device response = self.client.get(self.test_url) - self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'device_web.html') - self.assertContains(response, 'Test Manufacturer') self.assertContains(response, 'Test Model') self.assertContains(response, 'Computer') self.assertContains(response, self.test_id) + self.assertNotContains(response, 'Serial Number') + self.assertNotContains(response, 'serialNumber') + @patch('device.views.Device') + def test_html_response_authenticated(self, MockDevice): + test_device = TestDevice(id=self.test_id) + MockDevice.return_value = test_device + self.client.login(username='test@example.com', password='testpass123') + response = self.client.get(self.test_url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'device_web.html') + self.assertContains(response, 'Test Manufacturer') + self.assertContains(response, 'Test Model') + self.assertContains(response, 'Computer') + self.assertContains(response, self.test_id) + self.assertContains(response, 'Serial Number') + self.assertContains(response, 'Components') self.assertContains(response, 'CPU') self.assertContains(response, 'Intel') self.assertContains(response, 'RAM') self.assertContains(response, 'Kingston') @patch('device.views.Device') - def test_json_response(self, MockDevice): + def test_json_response_anonymous(self, MockDevice): test_device = TestDevice(id=self.test_id) MockDevice.return_value = test_device - response = self.client.get( self.test_url, HTTP_ACCEPT='application/json' ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response['Content-Type'], 'application/json') + json_data = response.json() + self.assertEqual(json_data['id'], self.test_id) + self.assertEqual(json_data['shortid'], self.test_id[:6].upper()) + self.assertEqual(json_data['uuids'], []) + self.assertEqual(json_data['hids'], ['hid1', 'hid2']) + self.assertNotIn('serial_number', json_data) + self.assertNotIn('serialNumber', json_data) + @patch('device.views.Device') + def test_json_response_authenticated(self, MockDevice): + test_device = TestDevice(id=self.test_id) + MockDevice.return_value = test_device + self.client.login(username='test@example.com', password='testpass123') + response = self.client.get( + self.test_url, + HTTP_ACCEPT='application/json' + ) self.assertEqual(response.status_code, 200) self.assertEqual(response['Content-Type'], 'application/json') json_data = response.json() self.assertEqual(json_data['id'], self.test_id) self.assertEqual(json_data['shortid'], self.test_id[:6].upper()) - self.assertEqual(json_data['components'], test_device.components) - - @patch('device.views.Device') - def test_websnapshot_device(self, MockDevice): - test_device = TestWebSnapshotDevice(id=self.test_id) - MockDevice.return_value = test_device - response = self.client.get(self.test_url) - - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'device_web.html') - - self.assertContains(response, 'http://example.com') - self.assertContains(response, 'Test Page') + self.assertEqual(json_data['components'], [ + { + 'type': 'CPU', + 'model': 'Intel i7', + 'manufacturer': 'Intel', + 'serialNumber': 'SN12345678' + }, + { + 'type': 'RAM', + 'size': '8GB', + 'manufacturer': 'Kingston', + 'serialNumber': 'SN87654321' + } + ]) + self.assertEqual(json_data['serial_number'], 'SN123456') + self.assertEqual(json_data['uuids'], []) + self.assertEqual(json_data['hids'], ['hid1', 'hid2']) diff --git a/device/views.py b/device/views.py index 90b43e8..319f8cf 100644 --- a/device/views.py +++ b/device/views.py @@ -117,10 +117,10 @@ class PublicDeviceWebView(TemplateView): def get(self, request, *args, **kwargs): self.pk = kwargs['pk'] self.object = Device(id=self.pk) - + if not self.object.last_evidence: raise Http404 - + if self.request.headers.get('Accept') == 'application/json': return self.get_json_response() return super().get(request, *args, **kwargs) @@ -133,15 +133,38 @@ class PublicDeviceWebView(TemplateView): }) return context - def get_json_response(self): - data = { + @property + def public_fields(self): + return { 'id': self.object.id, 'shortid': self.object.shortid, 'uuids': self.object.uuids, 'hids': self.object.hids, - 'components': self.object.components + 'components': self.remove_serial_number_from(self.object.components), } - return JsonResponse(data) + + @property + def authenticated_fields(self): + return { + 'serial_number': self.object.serial_number, + 'components': self.object.components, + } + + def remove_serial_number_from(self, components): + for component in components: + if 'serial_number' in component: + del component['SerialNumber'] + return components + + def get_device_data(self): + data = self.public_fields + if self.request.user.is_authenticated: + data.update(self.authenticated_fields) + return data + + def get_json_response(self): + device_data = self.get_device_data() + return JsonResponse(device_data) class AddAnnotationView(DashboardView, CreateView): diff --git a/evidence/models.py b/evidence/models.py index fef6a92..e9af092 100644 --- a/evidence/models.py +++ b/evidence/models.py @@ -11,7 +11,7 @@ from user.models import User, Institution class Annotation(models.Model): class Type(models.IntegerChoices): - SYSTEM= 0, "System" + SYSTEM = 0, "System" USER = 1, "User" DOCUMENT = 2, "Document" ERASE_SERVER = 3, "EraseServer" @@ -19,14 +19,16 @@ class Annotation(models.Model): created = models.DateTimeField(auto_now_add=True) uuid = models.UUIDField() owner = models.ForeignKey(Institution, on_delete=models.CASCADE) - user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True) - type = models.SmallIntegerField(choices=Type) + user = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, blank=True) + type = models.SmallIntegerField(choices=Type) key = models.CharField(max_length=STR_EXTEND_SIZE) value = models.CharField(max_length=STR_EXTEND_SIZE) class Meta: constraints = [ - models.UniqueConstraint(fields=["type", "key", "uuid"], name="unique_type_key_uuid") + models.UniqueConstraint( + fields=["type", "key", "uuid"], name="unique_type_key_uuid") ] @@ -37,8 +39,8 @@ class Evidence: self.doc = None self.created = None self.dmi = None - self.annotations = [] - self.components = [] + self.annotations = [] + self.components = [] self.default = "n/a" self.get_owner() @@ -87,7 +89,7 @@ class Evidence: return self.components def get_manufacturer(self): - if self.doc.get("type") == "WebSnapshot": + if self.is_web_snapshot(): kv = self.doc.get('kv', {}) if len(kv) < 1: return "" @@ -99,7 +101,7 @@ class Evidence: return self.dmi.manufacturer().strip() def get_model(self): - if self.doc.get("type") == "WebSnapshot": + if self.is_web_snapshot(): kv = self.doc.get('kv', {}) if len(kv) < 2: return "" @@ -122,6 +124,11 @@ class Evidence: return k return "" + def get_serial_number(self): + if self.is_legacy(): + return self.doc['device']['serialNumber'] + return self.dmi.serial_number().strip() + @classmethod def get_all(cls, user): return Annotation.objects.filter( @@ -136,3 +143,6 @@ class Evidence: def is_legacy(self): return self.doc.get("software") != "workbench-script" + + def is_web_snapshot(self): + return self.doc.get("type") == "WebSnapshot"