arthexis 0.1.22__py3-none-any.whl → 0.1.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arthexis might be problematic. Click here for more details.

nodes/models.py CHANGED
@@ -4,6 +4,7 @@ from collections.abc import Iterable
4
4
  from copy import deepcopy
5
5
  from dataclasses import dataclass
6
6
  from django.db import models
7
+ from django.db.models import Q
7
8
  from django.db.utils import DatabaseError
8
9
  from django.db.models.signals import post_delete
9
10
  from django.dispatch import Signal, receiver
@@ -294,7 +295,14 @@ class Node(Entity):
294
295
  """Return the node representing the current host if it exists."""
295
296
  mac = cls.get_current_mac()
296
297
  try:
297
- return cls.objects.filter(mac_address=mac).first()
298
+ node = cls.objects.filter(mac_address__iexact=mac).first()
299
+ if node:
300
+ return node
301
+ return (
302
+ cls.objects.filter(current_relation=cls.Relation.SELF)
303
+ .filter(Q(mac_address__isnull=True) | Q(mac_address=""))
304
+ .first()
305
+ )
298
306
  except DatabaseError:
299
307
  logger.debug("nodes.Node.get_local skipped: database unavailable", exc_info=True)
300
308
  return None
nodes/tests.py CHANGED
@@ -66,6 +66,7 @@ from .models import (
66
66
  )
67
67
  from .backends import OutboxEmailBackend
68
68
  from .tasks import capture_node_screenshot, poll_unreachable_upstream, sample_clipboard
69
+ from ocpp.models import Charger
69
70
  from cryptography.hazmat.primitives.asymmetric import rsa, padding
70
71
  from cryptography.hazmat.primitives import serialization, hashes
71
72
  from core.models import Package, PackageRelease, SecurityGroup, RFID, EnergyAccount, Todo
@@ -1816,84 +1817,6 @@ class NodeAdminTests(TestCase):
1816
1817
  self.assertEqual(response.status_code, 200)
1817
1818
  self.assertContains(response, "data:image/png;base64")
1818
1819
 
1819
- @patch("nodes.admin.requests.post")
1820
- def test_proxy_view_uses_remote_login_url(self, mock_post):
1821
- self.client.get(reverse("admin:nodes_node_register_current"))
1822
- local_node = Node.objects.get()
1823
- remote = Node.objects.create(
1824
- hostname="remote",
1825
- address="192.0.2.10",
1826
- port=8443,
1827
- mac_address="aa:bb:cc:dd:ee:ff",
1828
- )
1829
- mock_post.return_value = SimpleNamespace(
1830
- ok=True,
1831
- json=lambda: {
1832
- "login_url": "https://remote.example/nodes/proxy/login/token",
1833
- "expires": "2025-01-01T00:00:00",
1834
- },
1835
- status_code=200,
1836
- text="ok",
1837
- )
1838
- response = self.client.get(
1839
- reverse("admin:nodes_node_proxy", args=[remote.pk])
1840
- )
1841
- self.assertEqual(response.status_code, 200)
1842
- self.assertTemplateUsed(response, "admin/nodes/node/proxy.html")
1843
- self.assertContains(response, "<iframe", html=False)
1844
- mock_post.assert_called()
1845
- payload = json.loads(mock_post.call_args[1]["data"])
1846
- self.assertEqual(payload.get("requester"), str(local_node.uuid))
1847
-
1848
- @patch("nodes.admin.requests.post")
1849
- def test_proxy_view_falls_back_to_http_after_ssl_error(self, mock_post):
1850
- self.client.get(reverse("admin:nodes_node_register_current"))
1851
- remote = Node.objects.create(
1852
- hostname="remote-https",
1853
- address="198.51.100.20",
1854
- port=443,
1855
- mac_address="aa:bb:cc:dd:ee:10",
1856
- )
1857
- local_node = Node.get_local()
1858
- success_response = SimpleNamespace(
1859
- ok=True,
1860
- json=lambda: {
1861
- "login_url": "http://remote.example/nodes/proxy/login/token",
1862
- "expires": "2025-01-01T00:00:00",
1863
- },
1864
- status_code=200,
1865
- text="ok",
1866
- )
1867
- mock_post.side_effect = [
1868
- SSLError("wrong version number"),
1869
- success_response,
1870
- ]
1871
-
1872
- response = self.client.get(
1873
- reverse("admin:nodes_node_proxy", args=[remote.pk])
1874
- )
1875
-
1876
- self.assertEqual(response.status_code, 200)
1877
- self.assertEqual(mock_post.call_count, 2)
1878
- first_url = mock_post.call_args_list[0].args[0]
1879
- second_url = mock_post.call_args_list[1].args[0]
1880
- self.assertTrue(first_url.startswith("https://"))
1881
- self.assertTrue(second_url.startswith("http://"))
1882
- self.assertIn("/nodes/proxy/session/", second_url)
1883
- payload = json.loads(mock_post.call_args_list[-1].kwargs["data"])
1884
- self.assertEqual(payload.get("requester"), str(local_node.uuid))
1885
-
1886
- def test_proxy_link_displayed_for_remote_nodes(self):
1887
- Node.objects.create(
1888
- hostname="remote",
1889
- address="203.0.113.1",
1890
- port=8000,
1891
- mac_address="aa:aa:aa:aa:aa:01",
1892
- )
1893
- response = self.client.get(reverse("admin:nodes_node_changelist"))
1894
- proxy_url = reverse("admin:nodes_node_proxy", args=[1])
1895
- self.assertContains(response, proxy_url)
1896
-
1897
1820
  def test_visit_link_uses_local_admin_dashboard_for_local_node(self):
1898
1821
  node_admin = admin.site._registry[Node]
1899
1822
  local_node = self._create_local_node()
@@ -1926,14 +1849,14 @@ class NodeAdminTests(TestCase):
1926
1849
  port=8443,
1927
1850
  )
1928
1851
 
1929
- urls = list(node_admin._iter_remote_urls(remote, "/nodes/proxy/session/"))
1852
+ urls = list(node_admin._iter_remote_urls(remote, "/nodes/info/"))
1930
1853
 
1931
1854
  self.assertIn(
1932
- "https://example.com:8443/interface/nodes/proxy/session/",
1855
+ "https://example.com:8443/interface/nodes/info/",
1933
1856
  urls,
1934
1857
  )
1935
1858
  self.assertIn(
1936
- "http://example.com:8443/interface/nodes/proxy/session/",
1859
+ "http://example.com:8443/interface/nodes/info/",
1937
1860
  urls,
1938
1861
  )
1939
1862
  combined = "".join(urls)
@@ -2629,6 +2552,32 @@ class NodeProxyGatewayTests(TestCase):
2629
2552
  second = self.client.get(parsed.path)
2630
2553
  self.assertEqual(second.status_code, 410)
2631
2554
 
2555
+ def test_proxy_session_accepts_mac_hint_when_uuid_unknown(self):
2556
+ payload = {
2557
+ "requester": str(uuid.uuid4()),
2558
+ "requester_mac": self.node.mac_address,
2559
+ "requester_public_key": self.node.public_key,
2560
+ "user": {
2561
+ "username": "proxy-user",
2562
+ "email": "proxy@example.com",
2563
+ "first_name": "Proxy",
2564
+ "last_name": "User",
2565
+ "is_staff": True,
2566
+ "is_superuser": True,
2567
+ "groups": [],
2568
+ "permissions": [],
2569
+ },
2570
+ "target": "/admin/",
2571
+ }
2572
+ body, signature = self._sign(payload)
2573
+ response = self.client.post(
2574
+ reverse("node-proxy-session"),
2575
+ data=body,
2576
+ content_type="application/json",
2577
+ HTTP_X_SIGNATURE=signature,
2578
+ )
2579
+ self.assertEqual(response.status_code, 200)
2580
+
2632
2581
  def test_proxy_execute_lists_nodes(self):
2633
2582
  Node.objects.create(
2634
2583
  hostname="target",
@@ -3531,6 +3480,82 @@ class NetMessageSignatureTests(TestCase):
3531
3480
  self.assertTrue(signature_one)
3532
3481
  self.assertTrue(signature_two)
3533
3482
  self.assertNotEqual(signature_one, signature_two)
3483
+
3484
+
3485
+ class NetworkChargerActionSecurityTests(TestCase):
3486
+ def setUp(self):
3487
+ self.client = Client()
3488
+ self.local_node = Node.objects.create(
3489
+ hostname="local-node",
3490
+ address="127.0.0.1",
3491
+ port=8000,
3492
+ mac_address="00:aa:bb:cc:dd:10",
3493
+ public_endpoint="local-endpoint",
3494
+ )
3495
+ self.authorized_node = Node.objects.create(
3496
+ hostname="authorized-node",
3497
+ address="127.0.0.2",
3498
+ port=8001,
3499
+ mac_address="00:aa:bb:cc:dd:11",
3500
+ public_endpoint="authorized-endpoint",
3501
+ )
3502
+ self.unauthorized_node, self.unauthorized_key = self._create_signed_node(
3503
+ "unauthorized-node",
3504
+ mac_suffix=0x12,
3505
+ )
3506
+ self.charger = Charger.objects.create(
3507
+ charger_id="SECURE-TEST-1",
3508
+ allow_remote=True,
3509
+ manager_node=self.authorized_node,
3510
+ node_origin=self.local_node,
3511
+ )
3512
+
3513
+ def _create_signed_node(self, hostname: str, *, mac_suffix: int):
3514
+ key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
3515
+ public_bytes = key.public_key().public_bytes(
3516
+ encoding=serialization.Encoding.PEM,
3517
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
3518
+ )
3519
+ node = Node.objects.create(
3520
+ hostname=hostname,
3521
+ address="10.0.0.{:d}".format(mac_suffix),
3522
+ port=8020,
3523
+ mac_address="00:aa:bb:cc:dd:{:02x}".format(mac_suffix),
3524
+ public_key=public_bytes.decode(),
3525
+ public_endpoint=f"{hostname}-endpoint",
3526
+ )
3527
+ return node, key
3528
+
3529
+ def test_rejects_requests_from_unmanaged_nodes(self):
3530
+ url = reverse("node-network-charger-action")
3531
+ payload = {
3532
+ "requester": str(self.unauthorized_node.uuid),
3533
+ "charger_id": self.charger.charger_id,
3534
+ "action": "reset",
3535
+ }
3536
+ body = json.dumps(payload).encode()
3537
+ signature = self.unauthorized_key.sign(
3538
+ body,
3539
+ padding.PKCS1v15(),
3540
+ hashes.SHA256(),
3541
+ )
3542
+ headers = {"HTTP_X_SIGNATURE": base64.b64encode(signature).decode()}
3543
+
3544
+ with patch.object(Node, "get_local", return_value=self.local_node):
3545
+ response = self.client.post(
3546
+ url,
3547
+ data=body,
3548
+ content_type="application/json",
3549
+ **headers,
3550
+ )
3551
+
3552
+ self.assertEqual(response.status_code, 403)
3553
+ self.assertEqual(
3554
+ response.json().get("detail"),
3555
+ "requester does not manage this charger",
3556
+ )
3557
+
3558
+
3534
3559
  class StartupNotificationTests(TestCase):
3535
3560
  def test_startup_notification_uses_hostname_and_revision(self):
3536
3561
  from nodes.apps import _startup_notification
nodes/urls.py CHANGED
@@ -11,6 +11,12 @@ urlpatterns = [
11
11
  path("net-message/pull/", views.net_message_pull, name="net-message-pull"),
12
12
  path("rfid/export/", views.export_rfids, name="node-rfid-export"),
13
13
  path("rfid/import/", views.import_rfids, name="node-rfid-import"),
14
+ path("network/chargers/", views.network_chargers, name="node-network-chargers"),
15
+ path(
16
+ "network/chargers/action/",
17
+ views.network_charger_action,
18
+ name="node-network-charger-action",
19
+ ),
14
20
  path("proxy/session/", views.proxy_session, name="node-proxy-session"),
15
21
  path("proxy/login/<str:token>/", views.proxy_login, name="node-proxy-login"),
16
22
  path("proxy/execute/", views.proxy_execute, name="node-proxy-execute"),