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.
- arthexis-0.1.26.dist-info/METADATA +272 -0
- arthexis-0.1.26.dist-info/RECORD +111 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/licenses/LICENSE +674 -674
- config/__init__.py +5 -5
- config/active_app.py +15 -15
- config/asgi.py +29 -29
- config/auth_app.py +7 -7
- config/celery.py +32 -25
- config/context_processors.py +67 -68
- config/horologia_app.py +7 -7
- config/loadenv.py +11 -11
- config/logging.py +59 -48
- config/middleware.py +71 -25
- config/offline.py +49 -49
- config/settings.py +676 -492
- config/settings_helpers.py +109 -0
- config/urls.py +228 -159
- config/wsgi.py +17 -17
- core/admin.py +4052 -2066
- core/admin_history.py +50 -50
- core/admindocs.py +192 -151
- core/apps.py +350 -223
- core/auto_upgrade.py +72 -0
- core/backends.py +311 -124
- core/changelog.py +403 -0
- core/entity.py +149 -133
- core/environment.py +60 -43
- core/fields.py +168 -75
- core/form_fields.py +75 -0
- core/github_helper.py +188 -25
- core/github_issues.py +183 -172
- core/github_repos.py +72 -0
- core/lcd_screen.py +78 -78
- core/liveupdate.py +25 -25
- core/log_paths.py +114 -100
- core/mailer.py +89 -83
- core/middleware.py +91 -91
- core/models.py +5041 -2195
- core/notifications.py +105 -105
- core/public_wifi.py +267 -227
- core/reference_utils.py +107 -0
- core/release.py +940 -346
- core/rfid_import_export.py +113 -0
- core/sigil_builder.py +149 -131
- core/sigil_context.py +20 -20
- core/sigil_resolver.py +250 -284
- core/system.py +1425 -230
- core/tasks.py +538 -199
- core/temp_passwords.py +181 -0
- core/test_system_info.py +202 -43
- core/tests.py +2673 -1069
- core/tests_liveupdate.py +17 -17
- core/urls.py +11 -11
- core/user_data.py +681 -495
- core/views.py +2484 -789
- core/widgets.py +213 -51
- nodes/admin.py +2236 -445
- nodes/apps.py +98 -70
- nodes/backends.py +160 -53
- nodes/dns.py +203 -0
- nodes/feature_checks.py +133 -0
- nodes/lcd.py +165 -165
- nodes/models.py +2375 -870
- nodes/reports.py +411 -0
- nodes/rfid_sync.py +210 -0
- nodes/signals.py +18 -0
- nodes/tasks.py +141 -46
- nodes/tests.py +5045 -1489
- nodes/urls.py +29 -13
- nodes/utils.py +172 -73
- nodes/views.py +1768 -304
- ocpp/admin.py +1775 -481
- ocpp/apps.py +25 -25
- ocpp/consumers.py +1843 -630
- ocpp/evcs.py +844 -928
- ocpp/evcs_discovery.py +158 -0
- ocpp/models.py +1417 -640
- ocpp/network.py +398 -0
- ocpp/reference_utils.py +42 -0
- ocpp/routing.py +11 -9
- ocpp/simulator.py +745 -368
- ocpp/status_display.py +26 -0
- ocpp/store.py +603 -403
- ocpp/tasks.py +479 -31
- ocpp/test_export_import.py +131 -130
- ocpp/test_rfid.py +1072 -540
- ocpp/tests.py +5494 -2296
- ocpp/transactions_io.py +197 -165
- ocpp/urls.py +50 -50
- ocpp/views.py +2024 -912
- pages/admin.py +1123 -396
- pages/apps.py +45 -10
- pages/checks.py +40 -40
- pages/context_processors.py +151 -85
- pages/defaults.py +13 -0
- pages/forms.py +221 -0
- pages/middleware.py +213 -153
- pages/models.py +720 -252
- pages/module_defaults.py +156 -0
- pages/site_config.py +137 -0
- pages/tasks.py +74 -0
- pages/tests.py +4009 -1389
- pages/urls.py +38 -20
- pages/utils.py +93 -12
- pages/views.py +1736 -762
- arthexis-0.1.9.dist-info/METADATA +0 -168
- arthexis-0.1.9.dist-info/RECORD +0 -92
- core/workgroup_urls.py +0 -17
- core/workgroup_views.py +0 -94
- nodes/actions.py +0 -70
- {arthexis-0.1.9.dist-info → arthexis-0.1.26.dist-info}/WHEEL +0 -0
- {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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from .
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return None
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|