Compare commits

..
4 Commits
Author SHA1 Message Date
naturallaw777 87aa9b741b chore(release): prepare v1.2.2 2026-10-21 14:23:35 -05:00
naturallaw777 ed9e7ce065 updated nixpkgs 2026-10-21 12:30:07 -05:00
Arena Agent e060ec96eb fix(hub): make Hub login verification loadable and diagnosable
Correction to the patch-0002 report: after a reboot the Hub icon asks for a password AND the correct free (desktop) password is rejected there, while desktop sign-in accepts it. Patch 0002/0003 explain the icon prompt (legacy hub_manual_logout cookie; fixed in 0003), but the rejection itself means verify_system_password() never gets a conclusive crypt answer in the Hub unit and its PAM fallback cannot load either: on NixOS a bare libcrypt.so.1/libpam.so.0 resolves through LD_LIBRARY_PATH or not at all (or to glibc's failure-token stub), and until now every stage failed silently into a generic 'Incorrect password'.

password-model.nix now puts pkgs.libxcrypt and pkgs.pam on sovran-hub-web's LD_LIBRARY_PATH so both the crypt fallbacks and PAM can actually load. verify_system_password() now logs, journal-visible and never containing a password or hash: the shadow hash scheme, every libcrypt source tried and whether it loaded, and PAM's service + return code when it has to decide.
2026-10-18 21:39:41 -05:00
Arena Agent 761a7d76e9 fix(hub): trusted Hub icon, truthful root hint, no Default-keyring prompt
1) Clicking the desktop Hub icon demanded the Hub password after any sign-out: api_logout set a hub_manual_logout cookie for 400 days and /auto-login honoured it. The icon is a trusted local entry point (its user is already signed in to the OS as free), so /auto-login now ignores and clears that legacy cookie and logout no longer sets it. Sign-out still destroys the session, and LAN clients -- who never get auto-login -- must still enter the password.

2) The Administrator (root) row in System Passwords showed the free/Hub hint verbatim. Root has no desktop or Hub sign-in and nothing refreshes its display copy, so it now gets a root-specific hint (sudo passwd root to set it; Hub Security Reset to make the tile show one again).

3) Brave prompted for a locked 'Default keyring' on Hub launch: the password cleanup removed configuration.nix's keyrings/default pointer and password-model.nix never re-seeded it, so gnome-keyring fell back to an encrypted keyring named Default. password-model.nix now seeds default -> login (the blank keyring) via tmpfiles, and its activation migration restores the pointer and moves any encrypted default.keyring aside (kept, never deleted).
2026-10-18 21:39:25 -05:00
9 changed files with 206 additions and 59 deletions

No files matched your search

+14
View File
@@ -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
+11 -11
View File
@@ -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>
+1 -1
View File
@@ -1 +1 @@
1.2.1
1.2.2
+25 -16
View File
@@ -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
+31 -3
View File
@@ -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
View File
@@ -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": {
+33
View File
@@ -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
View File
@@ -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
+57 -1
View File
@@ -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)