Compare commits
4
Commits
d84e0c4d82
...
v1.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87aa9b741b | ||
|
|
ed9e7ce065 | ||
|
|
e060ec96eb | ||
|
|
761a7d76e9 |
No files matched your search
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
---
|
||||
|
||||
## [1.2.2] - 2026-10-21
|
||||
|
||||
### Changed
|
||||
- Updated nixpkgs
|
||||
- Updated Sovran_Bitcoin flake for a hot fix for NWC
|
||||
|
||||
### Fixed
|
||||
- Make Hub login verification loadable and diagnosable
|
||||
- Trusted Hub icon, truthful root hint, no Default-keyring prompt
|
||||
- Hub login must accept the password the desktop accepts
|
||||
- Don't let password verification crash the System Passwords tile
|
||||
[1.2.2]: https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS/releases/tag/v1.2.2
|
||||
|
||||
|
||||
## [1.2.1] - 2026-10-18
|
||||
|
||||
### Added
|
||||
|
||||
@@ -21,9 +21,9 @@ Lightning infrastructure, private cloud, and communications platform when you
|
||||
are ready.
|
||||
|
||||
[Visit the Website](https://sovransystems.com) ·
|
||||
[Download the ISO](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso) ·
|
||||
[Download the ISO](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.2.iso) ·
|
||||
[Try it safely in a VM](#try-it-first-in-a-virtual-machine) ·
|
||||
[Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso.sha256) ·
|
||||
[Verify the Download](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.2.iso.sha256) ·
|
||||
[Build from Source](#build-from-source)
|
||||
|
||||
<img src="assets/desktop-screenshot.webp" alt="Sovran_SystemsOS private Bitcoin desktop" width="800" />
|
||||
@@ -401,8 +401,8 @@ with an imaging application such as [Balena Etcher](https://etcher.balena.io).
|
||||
|
||||
### 1. Download the ISO and checksum
|
||||
|
||||
- [Download Sovran_SystemsOS-1.2.1.iso](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso)
|
||||
- [Download Sovran_SystemsOS-1.2.1.iso.sha256](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso.sha256)
|
||||
- [Download Sovran_SystemsOS-1.2.2.iso](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.2.iso)
|
||||
- [Download Sovran_SystemsOS-1.2.2.iso.sha256](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.2.iso.sha256)
|
||||
|
||||
The download may take some time. Do not rename or modify the ISO before
|
||||
verifying it, and keep both files in the same folder.
|
||||
@@ -419,13 +419,13 @@ ISO must match the published one exactly.
|
||||
Open a terminal in the download folder and run:
|
||||
|
||||
```bash
|
||||
sha256sum --check Sovran_SystemsOS-1.2.1.iso.sha256
|
||||
sha256sum --check Sovran_SystemsOS-1.2.2.iso.sha256
|
||||
```
|
||||
|
||||
A successful comparison reports:
|
||||
|
||||
```text
|
||||
Sovran_SystemsOS-1.2.1.iso: OK
|
||||
Sovran_SystemsOS-1.2.2.iso: OK
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -436,11 +436,11 @@ Sovran_SystemsOS-1.2.1.iso: OK
|
||||
Open Terminal in the download folder and run:
|
||||
|
||||
```bash
|
||||
shasum -a 256 Sovran_SystemsOS-1.2.1.iso
|
||||
shasum -a 256 Sovran_SystemsOS-1.2.2.iso
|
||||
```
|
||||
|
||||
Compare the value shown in Terminal with the value inside
|
||||
`Sovran_SystemsOS-1.2.1.iso.sha256`.
|
||||
`Sovran_SystemsOS-1.2.2.iso.sha256`.
|
||||
|
||||
</details>
|
||||
|
||||
@@ -450,7 +450,7 @@ Compare the value shown in Terminal with the value inside
|
||||
Open PowerShell in the download folder and run:
|
||||
|
||||
```powershell
|
||||
Get-FileHash .\Sovran_SystemsOS-1.2.1.iso -Algorithm SHA256
|
||||
Get-FileHash .\Sovran_SystemsOS-1.2.2.iso -Algorithm SHA256
|
||||
```
|
||||
|
||||
Compare the value under `Hash` with the published checksum.
|
||||
@@ -465,7 +465,7 @@ match exactly.
|
||||
|
||||
1. Download and install [Balena Etcher](https://etcher.balena.io), then
|
||||
connect the USB drive.
|
||||
2. Choose **Flash from file** and select `Sovran_SystemsOS-1.2.1.iso`.
|
||||
2. Choose **Flash from file** and select `Sovran_SystemsOS-1.2.2.iso`.
|
||||
3. Choose **Select target**, select the USB drive, and review your selection
|
||||
carefully.
|
||||
4. Choose **Flash** and wait for the writing and verification process to
|
||||
@@ -883,7 +883,7 @@ primary location for collaboration. Please read our
|
||||
## Privacy. Sovereignty. Bitcoin.
|
||||
|
||||
[Visit Sovran Systems](https://sovransystems.com) ·
|
||||
[Download Sovran_SystemsOS](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.1.iso) ·
|
||||
[Download Sovran_SystemsOS](https://downloads.sovransystems.com/Sovran_SystemsOS-1.2.2.iso) ·
|
||||
[View the License](LICENSE)
|
||||
|
||||
</div>
|
||||
@@ -1822,9 +1822,14 @@ def _resolve_credential(cred: dict) -> dict | None:
|
||||
logger.warning("Could not resolve %s for display: %s", filepath, exc)
|
||||
pw, migration = None, None
|
||||
if pw is None:
|
||||
hint = ("Changed outside the Hub — finish the password step in onboarding to see it here."
|
||||
if migration else
|
||||
"Changed outside the Hub — the current password is your desktop (free) sign-in password; it will show here after you next sign in to the Hub with it. Do not sign out unless you know that password — the Hub has no separate password.")
|
||||
if filepath == sp.ROOT_DISPLAY_FILE:
|
||||
# Root is NOT the desktop/Hub sign-in: nothing a user signs
|
||||
# in with refreshes this copy, so never show the free text.
|
||||
hint = ("Changed outside the Hub — the Hub's saved copy no longer matches the current root password (set by the installer, 'passwd', or a terminal). Root has no desktop or Hub sign-in, so signing in will not refresh this copy. If you do not know the root password, set one in a terminal with 'sudo passwd root'; to make this tile show a root password again, run a Hub Security Reset, which sets new free + root passwords and saves copies of both.")
|
||||
elif migration:
|
||||
hint = "Changed outside the Hub — finish the password step in onboarding to see it here."
|
||||
else:
|
||||
hint = "Changed outside the Hub — the current password is your desktop (free) sign-in password; it will show here after you next sign in to the Hub with it. Do not sign out unless you know that password — the Hub has no separate password."
|
||||
return {"label": label, "value": "", "unknown": True, "hint": hint}
|
||||
return {"label": label, "value": prefix + pw + suffix, "multiline": multiline}
|
||||
|
||||
@@ -2663,10 +2668,13 @@ async def auto_login_redirect(request: Request):
|
||||
client_ip = request.client.host if request.client else "unknown"
|
||||
if client_ip not in ("127.0.0.1", "::1"):
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
# An explicit logout must take precedence over the desktop launcher's
|
||||
# localhost auto-login, including after the Hub window is closed/reopened.
|
||||
if request.cookies.get(MANUAL_LOGOUT_COOKIE_NAME) == "1":
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
# The desktop Hub icon is a trusted local entry point: whoever can click
|
||||
# it is already signed in to this machine as 'free', so it must never
|
||||
# demand the Hub password again. A legacy hub_manual_logout cookie (set
|
||||
# by older versions for 400 days after any Hub sign-out) used to send
|
||||
# the icon to /login forever -- ignore it and clear it. Sign-out still
|
||||
# destroys the session; clients on the LAN never get auto-login and
|
||||
# must sign in with the password as before.
|
||||
token = _create_session()
|
||||
response = RedirectResponse(url="/", status_code=303)
|
||||
response.set_cookie(
|
||||
@@ -2677,6 +2685,7 @@ async def auto_login_redirect(request: Request):
|
||||
samesite="lax",
|
||||
secure=False, # LAN-only appliance; no TLS on the Hub port
|
||||
)
|
||||
response.delete_cookie(key=MANUAL_LOGOUT_COOKIE_NAME)
|
||||
return response
|
||||
|
||||
|
||||
@@ -2724,20 +2733,20 @@ async def api_login(req: LoginRequest, request: Request):
|
||||
|
||||
@app.post("/api/logout")
|
||||
async def api_logout(request: Request):
|
||||
"""Destroy the session and prevent desktop auto-login until password login."""
|
||||
"""Destroy the session.
|
||||
|
||||
This is a real sign-out for LAN clients (they must enter the password
|
||||
again). On this machine the desktop Hub icon remains a trusted,
|
||||
passwordless entry point -- its user is already signed in to the OS --
|
||||
so no persistent "manual logout" cookie is set any more: it made the
|
||||
Hub icon demand a password for 400 days after a single sign-out.
|
||||
"""
|
||||
token = request.cookies.get(SESSION_COOKIE_NAME)
|
||||
if token:
|
||||
_destroy_session(token)
|
||||
response = JSONResponse({"ok": True})
|
||||
response.delete_cookie(key=SESSION_COOKIE_NAME)
|
||||
response.set_cookie(
|
||||
key=MANUAL_LOGOUT_COOKIE_NAME,
|
||||
value="1",
|
||||
max_age=MANUAL_LOGOUT_MAX_AGE,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
secure=False, # LAN-only appliance; no TLS on the Hub port
|
||||
)
|
||||
response.delete_cookie(key=MANUAL_LOGOUT_COOKIE_NAME)
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@@ -18,12 +18,16 @@ from __future__ import annotations
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import hmac
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
from typing import Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
USER = "free"
|
||||
SECRETS_DIR = "/var/lib/secrets"
|
||||
DISPLAY_FILE = f"{SECRETS_DIR}/free-password"
|
||||
@@ -33,6 +37,7 @@ LIVE_ISO_MARKER = "/iso" # NixOS live media mounts the image here; never start
|
||||
|
||||
_lock = threading.Lock() # crypt() returns a static buffer; serialise calls
|
||||
_libcrypt_libs = None # list of every loadable libcrypt, in preference order
|
||||
_libcrypt_sources: list[str] = [] # where those libraries were loaded from
|
||||
|
||||
|
||||
# ── low level ──────────────────────────────────────────────────────────────
|
||||
@@ -65,12 +70,14 @@ def _crypt_libs() -> list:
|
||||
lib = ctypes.CDLL(path)
|
||||
lib.crypt.restype = ctypes.c_char_p
|
||||
lib.crypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
|
||||
except (OSError, AttributeError):
|
||||
except (OSError, AttributeError) as exc:
|
||||
_libcrypt_sources.append(f"{path} (unloadable: {exc})")
|
||||
continue
|
||||
if lib._handle in seen: # same library reached by two names
|
||||
continue
|
||||
seen.add(lib._handle)
|
||||
libs.append(lib)
|
||||
_libcrypt_sources.append(path)
|
||||
_libcrypt_libs = libs
|
||||
return _libcrypt_libs
|
||||
|
||||
@@ -161,11 +168,21 @@ def _verify_via_pam(password: str, user: str) -> bool | None:
|
||||
ctypes.byref(conv), ctypes.byref(handle)) != 0:
|
||||
continue
|
||||
try:
|
||||
return libpam.pam_authenticate(handle, 0) == 0
|
||||
rc = libpam.pam_authenticate(handle, 0)
|
||||
if rc != 0:
|
||||
# e.g. 7 = auth failure, 9/10 = bad item / user unknown.
|
||||
# Logged (code only, never the password) so a Hub-only
|
||||
# rejection can be told apart from a wrong password.
|
||||
logger.warning(
|
||||
"verify via PAM: service=%s user=%s pam_authenticate rc=%d",
|
||||
service, user, rc)
|
||||
return rc == 0
|
||||
finally:
|
||||
libpam.pam_end(handle, 0)
|
||||
logger.warning("verify via PAM: no PAM service could be started for user=%s", user)
|
||||
return None
|
||||
except Exception:
|
||||
except Exception as exc:
|
||||
logger.warning("verify via PAM unavailable for user=%s: %s", user, exc)
|
||||
return None
|
||||
|
||||
|
||||
@@ -211,7 +228,18 @@ def verify_system_password(password: str, user: str = USER) -> bool:
|
||||
return hmac.compare_digest(out, h.encode())
|
||||
except Exception:
|
||||
pass
|
||||
# No libcrypt could decide. Log *why* (hash scheme only -- never the
|
||||
# password or hash) so a false "wrong password" at Hub login, while the
|
||||
# same password signs in at the desktop, is diagnosable from
|
||||
# `journalctl -u sovran-hub-web` instead of looking like user error.
|
||||
scheme = h[1:].split("$", 1)[0] if h.startswith("$") else "legacy"
|
||||
logger.warning(
|
||||
"verify_system_password: libcrypt inconclusive for user=%s (scheme=%s, "
|
||||
"sources=%r); falling back to PAM",
|
||||
user, scheme, _libcrypt_sources,
|
||||
)
|
||||
pam_result = _verify_via_pam(password, user)
|
||||
logger.warning("verify_system_password: PAM verdict for user=%s: %s", user, pam_result)
|
||||
return bool(pam_result)
|
||||
|
||||
|
||||
|
||||
Generated
+15
-15
@@ -5,11 +5,11 @@
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1792157233,
|
||||
"narHash": "sha256-XQnyEc+Pj0Tg21ckbPMri0fiK/exCzhBw6wNgd1INqw=",
|
||||
"lastModified": 1792500275,
|
||||
"narHash": "sha256-Y86rSoq9EWo6538kYoWZCnH9+bl0jNS1vQrnNvm8znE=",
|
||||
"owner": "emmanuelrosa",
|
||||
"repo": "btc-clients-nix",
|
||||
"rev": "990d29e23437f30b8573f640d3e323167de1dc7b",
|
||||
"rev": "487c7e9ce3978601d37ac10b18610db672850f3b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -56,11 +56,11 @@
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1792030654,
|
||||
"narHash": "sha256-OaQwAtPMTjQiY2sT/2/62UbSPhzP/0aDnWzMm/NCiUw=",
|
||||
"lastModified": 1792503283,
|
||||
"narHash": "sha256-vhe2jyvRCgufWPqDuDq1S3t3ypE+ka46IFutV02xKso=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "26f4107e47e3ad0ffd0db220469ba83b6f6d96e8",
|
||||
"rev": "cd4ea8d57de16f4810bc5f14207bac45fbf19f67",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -72,11 +72,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1792094642,
|
||||
"narHash": "sha256-zp8ToDMnqZcu4o4IH9pdwXFXGp7yLm/M0dEnNWnyd7I=",
|
||||
"lastModified": 1792502884,
|
||||
"narHash": "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fe13242281a67641c0102cdb1087f0ebe586c7df",
|
||||
"rev": "02c087f3fe673c9e317f206a627cb6b910336dbe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -104,11 +104,11 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1791956154,
|
||||
"narHash": "sha256-jBYGPi4udQ9uo4KQp9m2J0546nJVQitG15gvZSaVVAs=",
|
||||
"lastModified": 1792502884,
|
||||
"narHash": "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "55bfb4d7437bd4e09ab7cba58a43bd68eb310044",
|
||||
"rev": "02c087f3fe673c9e317f206a627cb6b910336dbe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -152,11 +152,11 @@
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1792372617,
|
||||
"narHash": "sha256-MLFWEcj6ZyFgFWubNPYK7IEI8NUtjbrhxhXQiI6JbPM=",
|
||||
"lastModified": 1792602201,
|
||||
"narHash": "sha256-5LkFX16SxgXyaUDvFPtHdFGp4w4deqQy9btRmPQSlwU=",
|
||||
"owner": "naturallaw777",
|
||||
"repo": "Sovran_Bitcoin",
|
||||
"rev": "78e4430525069172a8d1a9c08a951f8b87a03bf3",
|
||||
"rev": "63e104940ef4ff21eb2b43977ca6e110e9c5d524",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -36,6 +36,17 @@ let
|
||||
lock-after=false
|
||||
'';
|
||||
|
||||
# Which keyring gnome-keyring treats as the default collection. Without
|
||||
# this pointer the daemon falls back to a keyring literally named
|
||||
# "Default" (default.keyring), which is created encrypted and prompts
|
||||
# "An application wants access to the keyring \"Default keyring\"" --
|
||||
# e.g. Brave on Hub launch -- even though the login keyring is blank.
|
||||
# The pre-cleanup configuration.nix seeded this file; password-model.nix
|
||||
# dropped it by mistake.
|
||||
defaultKeyringPointer = pkgs.writeText "keyring-default" ''
|
||||
login
|
||||
'';
|
||||
|
||||
# Apply the installer's plaintext password ONCE: only while the account still has no
|
||||
# usable hash (nixos-install --no-root-password leaves both accounts locked). After that
|
||||
# the file is display-only and the Hub (system_password.py) is the only thing that
|
||||
@@ -88,6 +99,7 @@ in
|
||||
"d ${home}/.local/share 0700 ${user} users -"
|
||||
"d ${home}/.local/share/keyrings 0700 ${user} users -"
|
||||
"C ${home}/.local/share/keyrings/login.keyring 0600 ${user} users - ${blankLoginKeyring}"
|
||||
"C ${home}/.local/share/keyrings/default 0600 ${user} users - ${defaultKeyringPointer}"
|
||||
];
|
||||
|
||||
# One-time migration for machines that already have a keyring encrypted with the
|
||||
@@ -100,6 +112,19 @@ in
|
||||
mv "$d/login.keyring" "$d/login.keyring.encrypted-$(date +%Y%m%d)"
|
||||
install -o ${user} -g users -m 0600 ${blankLoginKeyring} "$d/login.keyring"
|
||||
fi
|
||||
# Machines that already lost the default pointer may also have picked up
|
||||
# an encrypted "Default keyring" (default.keyring) that prompts on every
|
||||
# application start (Brave/Hub). Point the default at the blank login
|
||||
# keyring again, and move any encrypted Default keyring aside (kept,
|
||||
# never deleted) so the prompt cannot come back.
|
||||
if [ -d "$d" ]; then
|
||||
if [ ! -f "$d/default" ] || ! grep -qx 'login' "$d/default"; then
|
||||
install -o ${user} -g users -m 0600 ${defaultKeyringPointer} "$d/default"
|
||||
fi
|
||||
if [ -f "$d/default.keyring" ] && ! head -c 9 "$d/default.keyring" | grep -q '^\[keyring\]'; then
|
||||
mv "$d/default.keyring" "$d/default.keyring.encrypted-$(date +%Y%m%d)"
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
|
||||
# ── 2. First-boot password hand-off (unit names kept: the Hub's "System Passwords"
|
||||
@@ -124,4 +149,12 @@ in
|
||||
|
||||
# ── 3. The Hub verifies passwords against /etc/shadow via libxcrypt ───────
|
||||
systemd.services.sovran-hub-web.environment.LIBCRYPT = "${pkgs.libxcrypt}/lib/libcrypt.so.1";
|
||||
# ctypes fallback lookups (bare "libcrypt.so.1" / "libpam.so.0") resolve
|
||||
# through LD_LIBRARY_PATH inside the unit. Without this they find nothing
|
||||
# on NixOS (no ld.so.conf paths) or glibc's failure-token libcrypt stub,
|
||||
# whose "*0"/NULL answers read as a wrong password at Hub login even
|
||||
# though the same password signs in at the desktop. Put the real
|
||||
# libxcrypt and PAM libraries on the Hub's library path.
|
||||
systemd.services.sovran-hub-web.environment.LD_LIBRARY_PATH =
|
||||
lib.makeLibraryPath [ pkgs.libxcrypt pkgs.pam ];
|
||||
}
|
||||
+19
-12
@@ -386,7 +386,13 @@ class TestFrontendAuthRecovery(unittest.TestCase):
|
||||
|
||||
|
||||
class TestManualLogoutPersistence(unittest.TestCase):
|
||||
"""Explicit logout must take precedence over desktop auto-login."""
|
||||
"""Hub sign-out must NOT make the desktop Hub icon demand a password.
|
||||
|
||||
The icon is a trusted local entry point (its user is already signed in
|
||||
to the OS), and the old hub_manual_logout cookie blocked it for 400
|
||||
days after a single sign-out -- users read that as "the Hub is locked".
|
||||
Sign-out still destroys the session; LAN clients never get auto-login.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@@ -397,22 +403,24 @@ class TestManualLogoutPersistence(unittest.TestCase):
|
||||
def _between(self, start, end):
|
||||
return self.server.split(start, 1)[1].split(end, 1)[0]
|
||||
|
||||
def test_auto_login_honors_manual_logout_cookie(self):
|
||||
def test_auto_login_ignores_manual_logout_cookie(self):
|
||||
route = self._between(
|
||||
'@app.get("/auto-login")',
|
||||
"class LoginRequest",
|
||||
)
|
||||
self.assertIn("request.cookies.get(MANUAL_LOGOUT_COOKIE_NAME)", route)
|
||||
self.assertIn('RedirectResponse(url="/login"', route)
|
||||
self.assertNotIn("request.cookies.get(MANUAL_LOGOUT_COOKIE_NAME)", route)
|
||||
self.assertNotIn('RedirectResponse(url="/login"', route)
|
||||
# Legacy cookies are cleared so the icon keeps working.
|
||||
self.assertIn("response.delete_cookie(key=MANUAL_LOGOUT_COOKIE_NAME)", route)
|
||||
|
||||
def test_logout_sets_persistent_manual_logout_cookie(self):
|
||||
def test_logout_sets_no_persistent_manual_logout_cookie(self):
|
||||
route = self._between(
|
||||
'@app.post("/api/logout")',
|
||||
"def _get_sovran_version",
|
||||
)
|
||||
self.assertIn("key=MANUAL_LOGOUT_COOKIE_NAME", route)
|
||||
self.assertIn("max_age=MANUAL_LOGOUT_MAX_AGE", route)
|
||||
self.assertIn("httponly=True", route)
|
||||
self.assertNotIn("max_age=MANUAL_LOGOUT_MAX_AGE", route)
|
||||
self.assertIn("response.delete_cookie(key=MANUAL_LOGOUT_COOKIE_NAME)", route)
|
||||
self.assertIn("_destroy_session(token)", route)
|
||||
|
||||
def test_password_login_clears_manual_logout_cookie(self):
|
||||
route = self._between(
|
||||
@@ -427,10 +435,9 @@ class TestManualLogoutPersistence(unittest.TestCase):
|
||||
class TestHubBrowserProfilePersistence(unittest.TestCase):
|
||||
"""The desktop launcher must keep a persistent browser profile.
|
||||
|
||||
The hub_manual_logout marker (and the session cookie) are stored in this
|
||||
profile. If the launcher used an ephemeral /tmp profile that it deleted on
|
||||
exit, closing and reopening the Hub window would wipe the marker and
|
||||
/auto-login would silently log the user back in without a password.
|
||||
The session cookie is stored in this profile. If the launcher used an
|
||||
ephemeral /tmp profile that it deleted on exit, the browser would lose
|
||||
its session (and all site state) every time the Hub window closed.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -140,6 +140,62 @@ class WrongLibraryMustNotLockTheUserOut(unittest.TestCase):
|
||||
self.assertNotIn("sign out and back in to show it here", text)
|
||||
|
||||
|
||||
class HubUnitMustBeAbleToLoadItsVerifiers(unittest.TestCase):
|
||||
"""Patch 0002's PAM fallback could only work if libpam.so.0 is findable
|
||||
inside the Hub unit. On NixOS a bare soname resolves via LD_LIBRARY_PATH
|
||||
or not at all, so the unit must put the real libxcrypt + PAM libraries
|
||||
on it -- otherwise a correct desktop password reads as 'wrong password'
|
||||
at Hub login while the desktop accepts it."""
|
||||
|
||||
def test_hub_unit_gets_real_libcrypt_and_pam_on_library_path(self):
|
||||
nix = _read("modules", "password-model.nix")
|
||||
self.assertIn("LD_LIBRARY_PATH", nix)
|
||||
self.assertIn("pkgs.libxcrypt pkgs.pam", nix)
|
||||
|
||||
def test_no_unix_chkpwd_shortcut(self):
|
||||
# unix_chkpwd refuses (and stalls) when invoked outside pam_unix;
|
||||
# PAM is the only supported route to it.
|
||||
src = _read("app", "sovran_systemsos_web", "system_password.py")
|
||||
self.assertNotIn("_verify_via_unix_chkpwd", src)
|
||||
|
||||
|
||||
class RootHintMustNotClaimItIsTheFreePassword(unittest.TestCase):
|
||||
"""The root row in the System Passwords tile showed the free/hub hint
|
||||
verbatim. Root has no desktop or Hub sign-in, so that text was false
|
||||
and sent users looking for a sign-in that could refresh it."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.server = _read("app", "sovran_systemsos_web", "server.py")
|
||||
|
||||
def test_root_gets_its_own_hint(self):
|
||||
self.assertIn("ROOT_DISPLAY_FILE", self.server)
|
||||
start = self.server.index('if filepath == sp.ROOT_DISPLAY_FILE:')
|
||||
block = self.server[start:start + 1400]
|
||||
self.assertIn("Root has no desktop or Hub sign-in", block)
|
||||
self.assertIn("sudo passwd root", block)
|
||||
self.assertIn("Security Reset", block)
|
||||
|
||||
|
||||
class DefaultKeyringMustStayTheBlankLoginKeyring(unittest.TestCase):
|
||||
"""The password cleanup dropped the keyrings/default pointer, so
|
||||
gnome-keyring fell back to an encrypted keyring literally named
|
||||
"Default" and Brave prompted for it on every Hub launch."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.nix = _read("modules", "password-model.nix")
|
||||
|
||||
def test_default_pointer_is_seeded(self):
|
||||
self.assertIn("keyring-default", self.nix)
|
||||
self.assertIn("/.local/share/keyrings/default ", self.nix)
|
||||
|
||||
def test_encrypted_default_keyring_is_moved_aside_never_deleted(self):
|
||||
self.assertIn("default.keyring.encrypted-", self.nix)
|
||||
self.assertNotIn('rm "$d/default.keyring"', self.nix)
|
||||
self.assertNotIn("rm -f \"$d/default.keyring\"", self.nix)
|
||||
|
||||
|
||||
class ServiceDetailCannotBeKilledByOneCredential(unittest.TestCase):
|
||||
"""Source guards (server.py is not importable without the full Hub deps,
|
||||
same reason the other suites read it as text)."""
|
||||
@@ -150,7 +206,7 @@ class ServiceDetailCannotBeKilledByOneCredential(unittest.TestCase):
|
||||
|
||||
def test_resolve_credential_guards_system_password_calls(self):
|
||||
start = self.server.index("if filepath in (sp.DISPLAY_FILE, sp.ROOT_DISPLAY_FILE):")
|
||||
block = self.server[start:start + 1600]
|
||||
block = self.server[start:start + 3200]
|
||||
self.assertIn("try:", block)
|
||||
self.assertIn("except Exception", block)
|
||||
self.assertIn('"unknown": True', block)
|
||||
|
||||
Reference in new issue
Block a user