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.
- {arthexis-0.1.22.dist-info → arthexis-0.1.24.dist-info}/METADATA +6 -5
- {arthexis-0.1.22.dist-info → arthexis-0.1.24.dist-info}/RECORD +26 -26
- config/settings.py +4 -0
- core/admin.py +200 -16
- core/models.py +878 -118
- core/release.py +0 -5
- core/tasks.py +25 -0
- core/tests.py +29 -1
- core/user_data.py +42 -2
- core/views.py +33 -26
- nodes/admin.py +153 -132
- nodes/models.py +9 -1
- nodes/tests.py +106 -81
- nodes/urls.py +6 -0
- nodes/views.py +620 -48
- ocpp/admin.py +543 -166
- ocpp/models.py +57 -2
- ocpp/tasks.py +336 -1
- ocpp/tests.py +123 -0
- ocpp/views.py +19 -3
- pages/tests.py +25 -6
- pages/urls.py +5 -0
- pages/views.py +117 -11
- {arthexis-0.1.22.dist-info → arthexis-0.1.24.dist-info}/WHEEL +0 -0
- {arthexis-0.1.22.dist-info → arthexis-0.1.24.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.22.dist-info → arthexis-0.1.24.dist-info}/top_level.txt +0 -0
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
|
-
|
|
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/
|
|
1852
|
+
urls = list(node_admin._iter_remote_urls(remote, "/nodes/info/"))
|
|
1930
1853
|
|
|
1931
1854
|
self.assertIn(
|
|
1932
|
-
"https://example.com:8443/interface/nodes/
|
|
1855
|
+
"https://example.com:8443/interface/nodes/info/",
|
|
1933
1856
|
urls,
|
|
1934
1857
|
)
|
|
1935
1858
|
self.assertIn(
|
|
1936
|
-
"http://example.com:8443/interface/nodes/
|
|
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"),
|