jmri_server.auth.models
Auth data models — Role enum, User record, env constant.
Kept in their own module so tests + downstream code can import them
without pulling bcrypt / YAML transitively. Password hashing lives
in ~jmri_server.auth.passwords; the YAML file layer lives in
~jmri_server.auth.store.
1"""Auth data models — Role enum, User record, env constant. 2 3Kept in their own module so tests + downstream code can import them 4without pulling bcrypt / YAML transitively. Password hashing lives 5in :mod:`~jmri_server.auth.passwords`; the YAML file layer lives in 6:mod:`~jmri_server.auth.store`. 7""" 8from __future__ import annotations 9 10from enum import StrEnum 11 12from pydantic import BaseModel, ConfigDict 13 14 15# Env var name kept as a module constant so tests can flip it via 16# ``monkeypatch.setenv`` without importing anything heavy. 17AUTH_ENABLED_ENV = "JMRI_AUTH_ENABLED" 18 19 20class Role(StrEnum): 21 """Access role for one user. 22 23 Ordered by privilege (viewer < operator < admin). The 24 comparison operators are defined via :meth:`_priority` so 25 ``request.state.role >= Role.OPERATOR`` reads naturally on the 26 dispatch path. 27 """ 28 29 VIEWER = "viewer" 30 OPERATOR = "operator" 31 ADMIN = "admin" 32 33 def _priority(self) -> int: 34 return {"viewer": 0, "operator": 1, "admin": 2}[self.value] 35 36 def __ge__(self, other: object) -> bool: # type: ignore[override] 37 if not isinstance(other, Role): 38 return NotImplemented 39 return self._priority() >= other._priority() 40 41 def __gt__(self, other: object) -> bool: # type: ignore[override] 42 if not isinstance(other, Role): 43 return NotImplemented 44 return self._priority() > other._priority() 45 46 def __le__(self, other: object) -> bool: # type: ignore[override] 47 if not isinstance(other, Role): 48 return NotImplemented 49 return self._priority() <= other._priority() 50 51 def __lt__(self, other: object) -> bool: # type: ignore[override] 52 if not isinstance(other, Role): 53 return NotImplemented 54 return self._priority() < other._priority() 55 56 57class User(BaseModel): 58 """One user record — matches one entry in ``users.yaml``. 59 60 ``password_hash`` is bcrypt-encoded; verify via 61 :func:`~jmri_server.auth.passwords.verify_password`. ``token`` 62 is the long-lived personal API token — regenerated on demand, 63 never derived from the password so a password change doesn't 64 invalidate mobile clients. 65 66 ``token`` may be empty for a user that only logs in via the 67 web UI. The bootstrap admin always gets one so JMRI mobile 68 clients can be connected without a browser round-trip. 69 """ 70 71 model_config = ConfigDict(frozen=False, extra="ignore") 72 73 username: str 74 password_hash: str 75 role: Role = Role.VIEWER 76 token: str = ""
21class Role(StrEnum): 22 """Access role for one user. 23 24 Ordered by privilege (viewer < operator < admin). The 25 comparison operators are defined via :meth:`_priority` so 26 ``request.state.role >= Role.OPERATOR`` reads naturally on the 27 dispatch path. 28 """ 29 30 VIEWER = "viewer" 31 OPERATOR = "operator" 32 ADMIN = "admin" 33 34 def _priority(self) -> int: 35 return {"viewer": 0, "operator": 1, "admin": 2}[self.value] 36 37 def __ge__(self, other: object) -> bool: # type: ignore[override] 38 if not isinstance(other, Role): 39 return NotImplemented 40 return self._priority() >= other._priority() 41 42 def __gt__(self, other: object) -> bool: # type: ignore[override] 43 if not isinstance(other, Role): 44 return NotImplemented 45 return self._priority() > other._priority() 46 47 def __le__(self, other: object) -> bool: # type: ignore[override] 48 if not isinstance(other, Role): 49 return NotImplemented 50 return self._priority() <= other._priority() 51 52 def __lt__(self, other: object) -> bool: # type: ignore[override] 53 if not isinstance(other, Role): 54 return NotImplemented 55 return self._priority() < other._priority()
Access role for one user.
Ordered by privilege (viewer < operator < admin). The
comparison operators are defined via _priority() so
request.state.role >= Role.OPERATOR reads naturally on the
dispatch path.
58class User(BaseModel): 59 """One user record — matches one entry in ``users.yaml``. 60 61 ``password_hash`` is bcrypt-encoded; verify via 62 :func:`~jmri_server.auth.passwords.verify_password`. ``token`` 63 is the long-lived personal API token — regenerated on demand, 64 never derived from the password so a password change doesn't 65 invalidate mobile clients. 66 67 ``token`` may be empty for a user that only logs in via the 68 web UI. The bootstrap admin always gets one so JMRI mobile 69 clients can be connected without a browser round-trip. 70 """ 71 72 model_config = ConfigDict(frozen=False, extra="ignore") 73 74 username: str 75 password_hash: str 76 role: Role = Role.VIEWER 77 token: str = ""
One user record — matches one entry in users.yaml.
password_hash is bcrypt-encoded; verify via
~jmri_server.auth.passwords.verify_password(). token
is the long-lived personal API token — regenerated on demand,
never derived from the password so a password change doesn't
invalidate mobile clients.
token may be empty for a user that only logs in via the
web UI. The bootstrap admin always gets one so JMRI mobile
clients can be connected without a browser round-trip.