arthexis 0.1.9__py3-none-any.whl → 0.1.26__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.

Files changed (112) hide show
  1. arthexis-0.1.26.dist-info/METADATA +272 -0
  2. arthexis-0.1.26.dist-info/RECORD +111 -0
  3. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
  4. config/__init__.py +5 -5
  5. config/active_app.py +15 -15
  6. config/asgi.py +29 -29
  7. config/auth_app.py +7 -7
  8. config/celery.py +32 -25
  9. config/context_processors.py +67 -68
  10. config/horologia_app.py +7 -7
  11. config/loadenv.py +11 -11
  12. config/logging.py +59 -48
  13. config/middleware.py +71 -25
  14. config/offline.py +49 -49
  15. config/settings.py +676 -492
  16. config/settings_helpers.py +109 -0
  17. config/urls.py +228 -159
  18. config/wsgi.py +17 -17
  19. core/admin.py +4052 -2066
  20. core/admin_history.py +50 -50
  21. core/admindocs.py +192 -151
  22. core/apps.py +350 -223
  23. core/auto_upgrade.py +72 -0
  24. core/backends.py +311 -124
  25. core/changelog.py +403 -0
  26. core/entity.py +149 -133
  27. core/environment.py +60 -43
  28. core/fields.py +168 -75
  29. core/form_fields.py +75 -0
  30. core/github_helper.py +188 -25
  31. core/github_issues.py +183 -172
  32. core/github_repos.py +72 -0
  33. core/lcd_screen.py +78 -78
  34. core/liveupdate.py +25 -25
  35. core/log_paths.py +114 -100
  36. core/mailer.py +89 -83
  37. core/middleware.py +91 -91
  38. core/models.py +5041 -2195
  39. core/notifications.py +105 -105
  40. core/public_wifi.py +267 -227
  41. core/reference_utils.py +107 -0
  42. core/release.py +940 -346
  43. core/rfid_import_export.py +113 -0
  44. core/sigil_builder.py +149 -131
  45. core/sigil_context.py +20 -20
  46. core/sigil_resolver.py +250 -284
  47. core/system.py +1425 -230
  48. core/tasks.py +538 -199
  49. core/temp_passwords.py +181 -0
  50. core/test_system_info.py +202 -43
  51. core/tests.py +2673 -1069
  52. core/tests_liveupdate.py +17 -17
  53. core/urls.py +11 -11
  54. core/user_data.py +681 -495
  55. core/views.py +2484 -789
  56. core/widgets.py +213 -51
  57. nodes/admin.py +2236 -445
  58. nodes/apps.py +98 -70
  59. nodes/backends.py +160 -53
  60. nodes/dns.py +203 -0
  61. nodes/feature_checks.py +133 -0
  62. nodes/lcd.py +165 -165
  63. nodes/models.py +2375 -870
  64. nodes/reports.py +411 -0
  65. nodes/rfid_sync.py +210 -0
  66. nodes/signals.py +18 -0
  67. nodes/tasks.py +141 -46
  68. nodes/tests.py +5045 -1489
  69. nodes/urls.py +29 -13
  70. nodes/utils.py +172 -73
  71. nodes/views.py +1768 -304
  72. ocpp/admin.py +1775 -481
  73. ocpp/apps.py +25 -25
  74. ocpp/consumers.py +1843 -630
  75. ocpp/evcs.py +844 -928
  76. ocpp/evcs_discovery.py +158 -0
  77. ocpp/models.py +1417 -640
  78. ocpp/network.py +398 -0
  79. ocpp/reference_utils.py +42 -0
  80. ocpp/routing.py +11 -9
  81. ocpp/simulator.py +745 -368
  82. ocpp/status_display.py +26 -0
  83. ocpp/store.py +603 -403
  84. ocpp/tasks.py +479 -31
  85. ocpp/test_export_import.py +131 -130
  86. ocpp/test_rfid.py +1072 -540
  87. ocpp/tests.py +5494 -2296
  88. ocpp/transactions_io.py +197 -165
  89. ocpp/urls.py +50 -50
  90. ocpp/views.py +2024 -912
  91. pages/admin.py +1123 -396
  92. pages/apps.py +45 -10
  93. pages/checks.py +40 -40
  94. pages/context_processors.py +151 -85
  95. pages/defaults.py +13 -0
  96. pages/forms.py +221 -0
  97. pages/middleware.py +213 -153
  98. pages/models.py +720 -252
  99. pages/module_defaults.py +156 -0
  100. pages/site_config.py +137 -0
  101. pages/tasks.py +74 -0
  102. pages/tests.py +4009 -1389
  103. pages/urls.py +38 -20
  104. pages/utils.py +93 -12
  105. pages/views.py +1736 -762
  106. arthexis-0.1.9.dist-info/METADATA +0 -168
  107. arthexis-0.1.9.dist-info/RECORD +0 -92
  108. core/workgroup_urls.py +0 -17
  109. core/workgroup_views.py +0 -94
  110. nodes/actions.py +0 -70
  111. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
  112. {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/top_level.txt +0 -0
core/backends.py CHANGED
@@ -1,124 +1,311 @@
1
- """Custom authentication backends for the core app."""
2
-
3
- import contextlib
4
- import ipaddress
5
- import socket
6
-
7
- from django.contrib.auth import get_user_model
8
- from django.contrib.auth.backends import ModelBackend
9
-
10
- from .models import EnergyAccount
11
-
12
-
13
- class RFIDBackend:
14
- """Authenticate using a user's RFID."""
15
-
16
- def authenticate(self, request, rfid=None, **kwargs):
17
- if not rfid:
18
- return None
19
- account = (
20
- EnergyAccount.objects.filter(
21
- rfids__rfid=rfid.upper(), rfids__allowed=True, user__isnull=False
22
- )
23
- .select_related("user")
24
- .first()
25
- )
26
- if account:
27
- return account.user
28
- return None
29
-
30
- def get_user(self, user_id):
31
- User = get_user_model()
32
- try:
33
- return User.objects.get(pk=user_id)
34
- except User.DoesNotExist:
35
- return None
36
-
37
-
38
- def _collect_local_ip_addresses():
39
- """Return IP addresses assigned to the current machine."""
40
-
41
- hosts = {socket.gethostname().strip()}
42
- with contextlib.suppress(Exception):
43
- hosts.add(socket.getfqdn().strip())
44
-
45
- addresses = set()
46
- for host in filter(None, hosts):
47
- with contextlib.suppress(OSError):
48
- _, _, ip_list = socket.gethostbyname_ex(host)
49
- for candidate in ip_list:
50
- with contextlib.suppress(ValueError):
51
- addresses.add(ipaddress.ip_address(candidate))
52
- with contextlib.suppress(OSError):
53
- for info in socket.getaddrinfo(host, None, family=socket.AF_UNSPEC):
54
- sockaddr = info[-1]
55
- if not sockaddr:
56
- continue
57
- raw_address = sockaddr[0]
58
- if isinstance(raw_address, bytes):
59
- with contextlib.suppress(UnicodeDecodeError):
60
- raw_address = raw_address.decode()
61
- if isinstance(raw_address, str):
62
- if "%" in raw_address:
63
- raw_address = raw_address.split("%", 1)[0]
64
- with contextlib.suppress(ValueError):
65
- addresses.add(ipaddress.ip_address(raw_address))
66
- return tuple(sorted(addresses, key=str))
67
-
68
-
69
- class LocalhostAdminBackend(ModelBackend):
70
- """Allow default admin credentials only from local networks."""
71
-
72
- _ALLOWED_NETWORKS = [
73
- ipaddress.ip_network("::1/128"),
74
- ipaddress.ip_network("127.0.0.0/8"),
75
- ipaddress.ip_network("192.168.0.0/16"),
76
- ]
77
- _LOCAL_IPS = _collect_local_ip_addresses()
78
-
79
- def authenticate(self, request, username=None, password=None, **kwargs):
80
- if username == "admin" and password == "admin" and request is not None:
81
- forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
82
- if forwarded:
83
- remote = forwarded.split(",")[0].strip()
84
- else:
85
- remote = request.META.get("REMOTE_ADDR", "")
86
- try:
87
- ip = ipaddress.ip_address(remote)
88
- except ValueError:
89
- return None
90
- allowed = any(ip in net for net in self._ALLOWED_NETWORKS)
91
- if not allowed and ip in self._LOCAL_IPS:
92
- allowed = True
93
- if not allowed:
94
- return None
95
- User = get_user_model()
96
- user, created = User.all_objects.get_or_create(
97
- username="admin",
98
- defaults={
99
- "is_staff": True,
100
- "is_superuser": True,
101
- },
102
- )
103
- arthexis_user = (
104
- User.all_objects.filter(username="arthexis").exclude(pk=user.pk).first()
105
- )
106
- if created:
107
- if arthexis_user and user.operate_as_id is None:
108
- user.operate_as = arthexis_user
109
- user.set_password("admin")
110
- user.save()
111
- elif not user.check_password("admin"):
112
- return None
113
- elif arthexis_user and user.operate_as_id is None:
114
- user.operate_as = arthexis_user
115
- user.save(update_fields=["operate_as"])
116
- return user
117
- return super().authenticate(request, username, password, **kwargs)
118
-
119
- def get_user(self, user_id):
120
- User = get_user_model()
121
- try:
122
- return User.all_objects.get(pk=user_id)
123
- except User.DoesNotExist:
124
- return None
1
+ """Custom authentication backends for the core app."""
2
+
3
+ import contextlib
4
+ import ipaddress
5
+ import os
6
+ import socket
7
+ import subprocess
8
+ import sys
9
+
10
+ from django.conf import settings
11
+ from django.contrib.auth import get_user_model
12
+ from django.contrib.auth.backends import ModelBackend
13
+ from django.core.exceptions import DisallowedHost
14
+ from django.http.request import split_domain_port
15
+ from django_otp.plugins.otp_totp.models import TOTPDevice
16
+
17
+ from .models import EnergyAccount, RFID
18
+ from . import temp_passwords
19
+
20
+
21
+ TOTP_DEVICE_NAME = "authenticator"
22
+
23
+
24
+ class TOTPBackend(ModelBackend):
25
+ """Authenticate using a TOTP code from an enrolled authenticator app."""
26
+
27
+ def authenticate(self, request, username=None, otp_token=None, **kwargs):
28
+ if not username or otp_token in (None, ""):
29
+ return None
30
+
31
+ token = str(otp_token).strip().replace(" ", "")
32
+ if not token:
33
+ return None
34
+
35
+ UserModel = get_user_model()
36
+ try:
37
+ user = UserModel._default_manager.get_by_natural_key(username)
38
+ except UserModel.DoesNotExist:
39
+ return None
40
+
41
+ if not user.is_active:
42
+ return None
43
+
44
+ device_qs = TOTPDevice.objects.filter(user=user, confirmed=True)
45
+ if TOTP_DEVICE_NAME:
46
+ device = device_qs.filter(name=TOTP_DEVICE_NAME).order_by("-id").first()
47
+ else:
48
+ device = None
49
+
50
+ if device is None:
51
+ device = device_qs.order_by("-id").first()
52
+ if device is None:
53
+ return None
54
+
55
+ try:
56
+ verified = device.verify_token(token)
57
+ except Exception:
58
+ return None
59
+
60
+ if not verified:
61
+ return None
62
+
63
+ user.otp_device = device
64
+ return user
65
+
66
+ def get_user(self, user_id):
67
+ UserModel = get_user_model()
68
+ try:
69
+ return UserModel._default_manager.get(pk=user_id)
70
+ except UserModel.DoesNotExist:
71
+ return None
72
+
73
+
74
+ class RFIDBackend:
75
+ """Authenticate using a user's RFID."""
76
+
77
+ def authenticate(self, request, rfid=None, **kwargs):
78
+ if not rfid:
79
+ return None
80
+ rfid_value = str(rfid).strip().upper()
81
+ if not rfid_value:
82
+ return None
83
+
84
+ tag = RFID.matching_queryset(rfid_value).filter(allowed=True).first()
85
+ if not tag:
86
+ return None
87
+
88
+ update_fields: list[str] = []
89
+ if tag.adopt_rfid(rfid_value):
90
+ update_fields.append("rfid")
91
+ if update_fields:
92
+ tag.save(update_fields=update_fields)
93
+
94
+ command = (tag.external_command or "").strip()
95
+ if command:
96
+ env = os.environ.copy()
97
+ env["RFID_VALUE"] = rfid_value
98
+ env["RFID_LABEL_ID"] = str(tag.pk)
99
+ env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
100
+ try:
101
+ completed = subprocess.run(
102
+ command,
103
+ shell=True,
104
+ check=False,
105
+ capture_output=True,
106
+ text=True,
107
+ env=env,
108
+ )
109
+ except Exception:
110
+ return None
111
+ if completed.returncode != 0:
112
+ return None
113
+
114
+ account = (
115
+ EnergyAccount.objects.filter(
116
+ rfids__pk=tag.pk, rfids__allowed=True, user__isnull=False
117
+ )
118
+ .select_related("user")
119
+ .first()
120
+ )
121
+ if account:
122
+ post_command = (getattr(tag, "post_auth_command", "") or "").strip()
123
+ if post_command:
124
+ env = os.environ.copy()
125
+ env["RFID_VALUE"] = rfid_value
126
+ env["RFID_LABEL_ID"] = str(tag.pk)
127
+ env["RFID_ENDIANNESS"] = getattr(tag, "endianness", RFID.BIG_ENDIAN)
128
+ with contextlib.suppress(Exception):
129
+ subprocess.Popen(
130
+ post_command,
131
+ shell=True,
132
+ env=env,
133
+ stdout=subprocess.DEVNULL,
134
+ stderr=subprocess.DEVNULL,
135
+ )
136
+ return account.user
137
+ return None
138
+
139
+ def get_user(self, user_id):
140
+ User = get_user_model()
141
+ try:
142
+ return User.objects.get(pk=user_id)
143
+ except User.DoesNotExist:
144
+ return None
145
+
146
+
147
+ def _collect_local_ip_addresses():
148
+ """Return IP addresses assigned to the current machine."""
149
+
150
+ hosts = {socket.gethostname().strip()}
151
+ with contextlib.suppress(Exception):
152
+ hosts.add(socket.getfqdn().strip())
153
+
154
+ addresses = set()
155
+ for host in filter(None, hosts):
156
+ with contextlib.suppress(OSError):
157
+ _, _, ip_list = socket.gethostbyname_ex(host)
158
+ for candidate in ip_list:
159
+ with contextlib.suppress(ValueError):
160
+ addresses.add(ipaddress.ip_address(candidate))
161
+ with contextlib.suppress(OSError):
162
+ for info in socket.getaddrinfo(host, None, family=socket.AF_UNSPEC):
163
+ sockaddr = info[-1]
164
+ if not sockaddr:
165
+ continue
166
+ raw_address = sockaddr[0]
167
+ if isinstance(raw_address, bytes):
168
+ with contextlib.suppress(UnicodeDecodeError):
169
+ raw_address = raw_address.decode()
170
+ if isinstance(raw_address, str):
171
+ if "%" in raw_address:
172
+ raw_address = raw_address.split("%", 1)[0]
173
+ with contextlib.suppress(ValueError):
174
+ addresses.add(ipaddress.ip_address(raw_address))
175
+ return tuple(sorted(addresses, key=str))
176
+
177
+
178
+ class LocalhostAdminBackend(ModelBackend):
179
+ """Allow default admin credentials only from local networks."""
180
+
181
+ _ALLOWED_NETWORKS = (
182
+ ipaddress.ip_network("::1/128"),
183
+ ipaddress.ip_network("127.0.0.0/8"),
184
+ ipaddress.ip_network("10.42.0.0/16"),
185
+ ipaddress.ip_network("192.168.0.0/16"),
186
+ )
187
+ _CONTROL_ALLOWED_NETWORKS = (ipaddress.ip_network("10.0.0.0/8"),)
188
+ _LOCAL_IPS = _collect_local_ip_addresses()
189
+
190
+ def _iter_allowed_networks(self):
191
+ yield from self._ALLOWED_NETWORKS
192
+ if getattr(settings, "NODE_ROLE", "") == "Control":
193
+ yield from self._CONTROL_ALLOWED_NETWORKS
194
+
195
+ def _is_test_environment(self, request) -> bool:
196
+ if os.environ.get("PYTEST_CURRENT_TEST"):
197
+ return True
198
+ if any(arg == "test" for arg in sys.argv):
199
+ return True
200
+ executable = os.path.basename(sys.argv[0]) if sys.argv else ""
201
+ if executable in {"pytest", "py.test"}:
202
+ return True
203
+ server_name = ""
204
+ if request is not None:
205
+ server_name = request.META.get("SERVER_NAME", "")
206
+ return server_name.lower() == "testserver"
207
+
208
+ def authenticate(self, request, username=None, password=None, **kwargs):
209
+ if username == "admin" and password == "admin" and request is not None:
210
+ try:
211
+ host = request.get_host()
212
+ except DisallowedHost:
213
+ return None
214
+ host, _port = split_domain_port(host)
215
+ if host.startswith("[") and host.endswith("]"):
216
+ host = host[1:-1]
217
+ try:
218
+ ipaddress.ip_address(host)
219
+ except ValueError:
220
+ if host.lower() == "localhost":
221
+ host = "127.0.0.1"
222
+ elif not self._is_test_environment(request):
223
+ return None
224
+ forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
225
+ if forwarded:
226
+ remote = forwarded.split(",")[0].strip()
227
+ else:
228
+ remote = request.META.get("REMOTE_ADDR", "")
229
+ try:
230
+ ip = ipaddress.ip_address(remote)
231
+ except ValueError:
232
+ return None
233
+ allowed = any(ip in net for net in self._iter_allowed_networks())
234
+ if not allowed and ip in self._LOCAL_IPS:
235
+ allowed = True
236
+ if not allowed:
237
+ return None
238
+ User = get_user_model()
239
+ user, created = User.all_objects.get_or_create(
240
+ username="admin",
241
+ defaults={
242
+ "is_staff": True,
243
+ "is_superuser": True,
244
+ },
245
+ )
246
+ if not created and not user.is_active:
247
+ return None
248
+ arthexis_user = (
249
+ User.all_objects.filter(username="arthexis").exclude(pk=user.pk).first()
250
+ )
251
+ if created:
252
+ if arthexis_user and user.operate_as_id is None:
253
+ user.operate_as = arthexis_user
254
+ user.set_password("admin")
255
+ user.save()
256
+ else:
257
+ if not user.check_password("admin"):
258
+ if not user.password or not user.has_usable_password():
259
+ user.set_password("admin")
260
+ user.save(update_fields=["password"])
261
+ else:
262
+ return None
263
+ if arthexis_user and user.operate_as_id is None:
264
+ user.operate_as = arthexis_user
265
+ user.save(update_fields=["operate_as"])
266
+ return user
267
+ return super().authenticate(request, username, password, **kwargs)
268
+
269
+ def get_user(self, user_id):
270
+ User = get_user_model()
271
+ try:
272
+ return User.all_objects.get(pk=user_id)
273
+ except User.DoesNotExist:
274
+ return None
275
+
276
+
277
+ class TempPasswordBackend(ModelBackend):
278
+ """Authenticate using a temporary password stored in a lockfile."""
279
+
280
+ def authenticate(self, request, username=None, password=None, **kwargs):
281
+ if not username or not password:
282
+ return None
283
+
284
+ UserModel = get_user_model()
285
+ manager = getattr(UserModel, "all_objects", UserModel._default_manager)
286
+ try:
287
+ user = manager.get_by_natural_key(username)
288
+ except UserModel.DoesNotExist:
289
+ return None
290
+
291
+ entry = temp_passwords.load_temp_password(user.username)
292
+ if entry is None:
293
+ return None
294
+ if entry.is_expired:
295
+ temp_passwords.discard_temp_password(user.username)
296
+ return None
297
+ if not entry.check_password(password):
298
+ return None
299
+
300
+ if not user.is_active:
301
+ user.is_active = True
302
+ user.save(update_fields=["is_active"])
303
+ return user
304
+
305
+ def get_user(self, user_id):
306
+ UserModel = get_user_model()
307
+ manager = getattr(UserModel, "all_objects", UserModel._default_manager)
308
+ try:
309
+ return manager.get(pk=user_id)
310
+ except UserModel.DoesNotExist:
311
+ return None