Keep demo runtime profile changes out of signed billing state

The helper could repersist legacy entitlement secrets and clear billing integrity while granting demo fixtures. Require the existing demo capability before mutation, leave all billing/key material untouched, and keep verification and encrypted legacy migration with the canonical application. Preserve full-state recovery, service ownership and the existing native route; seed only non-secret admitted fresh-runner fixtures. Add real canonical encrypted/legacy/HMAC connected controls and malformed or unprovisioned refusal-before-mutation coverage.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-03 20:25:40 +01:00
parent 091f987798
commit 7d73969882
6 changed files with 296 additions and 16 deletions
+53 -14
View File
@@ -349,17 +349,61 @@ class Host:
values["PULSE_MOCK_" + key.upper()] = value
return values
def profile_matches(self, request):
text = (PATHS["data"] / ".env").read_text().splitlines()
values = self.profile_values(request)
return (all(text.count(key + "=" + value) == 1 for key, value in values.items())
and "demo_fixtures" in json.loads((PATHS["data"] / "billing.json").read_text()).get("capabilities", []))
def profile_admission(self):
"""Read-only prerequisite; this transaction never grants entitlements.
def profile(self, request, unhealthy):
The running application owns billing HMAC verification, encrypted
secrets and legacy migration. Do not decrypt, clear integrity, re-sign
or rewrite billing/key material to enable a demo profile.
"""
env_file = PATHS["data"] / ".env"
billing_file = PATHS["data"] / "billing.json"
if env_file.is_symlink() or billing_file.is_symlink():
raise Failure("configuration-symlink")
if not env_file.is_file():
raise Failure("configuration-shape")
def unique_fields(pairs):
result = {}
for key, value in pairs:
if key in result:
raise Failure("billing-shape")
result[key] = value
return result
def invalid_constant(_value):
raise Failure("billing-shape")
try:
fd = os.open(billing_file, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
with os.fdopen(fd, "rb") as stream:
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
raise Failure("billing-shape")
raw = stream.read(1024 * 1024 + 1)
if len(raw) > 1024 * 1024:
raise Failure("billing-shape")
billing = json.loads(raw, object_pairs_hook=unique_fields,
parse_constant=invalid_constant)
except (OSError, ValueError, UnicodeError, RecursionError) as exc:
raise Failure("billing-observation") from exc
if (not isinstance(billing, dict)
or not isinstance(billing.get("capabilities"), list)
or not all(isinstance(capability, str) for capability in billing["capabilities"])):
raise Failure("billing-shape")
if "demo_fixtures" not in billing["capabilities"]:
raise Failure("billing-demo-capability-required")
def profile_matches(self, request):
self.profile_admission()
text = (PATHS["data"] / ".env").read_text().splitlines()
values = self.profile_values(request)
return all(text.count(key + "=" + value) == 1 for key, value in values.items())
def profile(self, request, unhealthy):
env_file = PATHS["data"] / ".env"
# Recheck after quiescent capture too; a failed prerequisite must not
# partially change the profile or bypass the application's licence gate.
self.profile_admission()
values = self.profile_values(request)
text = env_file.read_text()
for key, value in values.items():
@@ -369,14 +413,6 @@ class Host:
data_owner = PATHS["data"].stat()
os.chown(env_file, data_owner.st_uid, data_owner.st_gid)
os.chmod(env_file, 0o600)
billing = json.loads(billing_file.read_text())
if not isinstance(billing, dict) or not isinstance(billing.get("capabilities", []), list):
raise Failure("billing-shape")
billing["capabilities"] = sorted(set(billing.get("capabilities", []) + ["demo_fixtures"]))
billing.pop("integrity", None)
billing_file.write_text(json.dumps(billing))
os.chown(billing_file, data_owner.st_uid, data_owner.st_gid)
os.chmod(billing_file, 0o600)
if unhealthy:
for relative in DEMO_HISTORY:
path = PATHS["data"] / relative
@@ -546,6 +582,9 @@ class Transaction:
if (prior.get("status") not in TERMINAL
or (prior.get("child_result_required") and not self.host.prior_closed(entry, prior["status"]))):
raise Failure("prior-recovery-required")
# Refuse absent/malformed or unprovisioned billing before stopping
# a service, installing a binary or changing any estate file.
self.host.profile_admission()
states = self.host.state()
if not all(active(states[name]) for name in ("pulse-relay", "caddy")) or self.host.status(RELAY_HEALTH) != "200":
raise Failure("cohost-unhealthy")
+1 -1
View File
@@ -92,7 +92,7 @@ def main():
Path("/etc/pulse").mkdir(mode=0o755)
os.chown("/etc/pulse", owner.pw_uid, owner.pw_gid)
Path("/etc/pulse/.env").write_text("DEMO_MODE=true\n")
Path("/etc/pulse/billing.json").write_text('{"capabilities":[]}')
Path("/etc/pulse/billing.json").write_text('{"capabilities":["demo_fixtures"],"limits":{},"meters_enabled":[],"plan_version":"community","subscription_state":"active"}')
Path("/etc/pulse/persistent-marker").write_text("original data")
for path in Path("/etc/pulse").iterdir():
os.chown(path, owner.pw_uid, owner.pw_gid)
@@ -156,7 +156,7 @@ class TransactionTest(unittest.TestCase):
path.chmod(0o755 if name == "binary" else 0o644)
(self.paths["dropins"] / "limits.conf").write_bytes(b"old limits")
(self.paths["data"] / ".env").write_text("PRIVATE_TOKEN=not-logged\n")
(self.paths["data"] / "billing.json").write_text('{"capabilities":[],"integrity":"private"}')
(self.paths["data"] / "billing.json").write_text('{"capabilities":["demo_fixtures"],"integrity":"private"}')
(self.paths["data"] / "persisted-history").write_bytes(b"original history")
(self.paths["data"] / "alerts").mkdir()
(self.paths["data"] / "alerts/events.db").write_bytes(b"synthetic opaque persistent file")
@@ -188,6 +188,105 @@ class TransactionTest(unittest.TestCase):
self.assertEqual(tx.receipt, json.loads((self.attempt / "receipt.json").read_text()))
return result, tx.receipt
def test_profile_preserves_encrypted_and_legacy_billing_bytes_key_and_metadata(self):
billing_path = self.paths["data"] / "billing.json"
key_path = self.paths["data"] / ".encryption.key"
key_path.write_bytes(b"synthetic-original-key")
key_path.chmod(0o600)
for representation in ("synthetic-legacy-plaintext", "synthetic-encrypted-ciphertext"):
with self.subTest(representation=representation):
billing = {"capabilities": ["other-feature", "demo_fixtures"],
"integrity": "original-synthetic-signature",
"entitlement_jwt": representation + "-jwt",
"entitlement_refresh_token": representation + "-refresh",
"limits": {"resources": 100}, "subscription_state": "active",
"other_field": {"preserve": [1, 2]}}
billing_path.write_text(json.dumps(billing, indent=2) + "\n")
billing_path.chmod(0o640)
before = engine.estate_hash({"billing": billing_path, "key": key_path})
self.host.profile(request(), False)
self.host.profile(request(), False)
self.assertEqual(engine.estate_hash({"billing": billing_path, "key": key_path}), before)
self.assertTrue(self.host.profile_matches(request()))
def test_missing_demo_capability_refuses_before_any_service_or_estate_mutation(self):
(self.paths["data"] / "billing.json").write_text('{"capabilities":[],"integrity":"unchanged"}')
before = engine.estate_hash(self.paths)
code, receipt = self.run_transaction("update", "v6.4.6")
self.assertEqual((code, receipt["status"], receipt["failure"]),
(1, "refused", "billing-demo-capability-required"))
self.assertFalse(receipt["mutated"])
self.assertEqual((self.host.stops, self.host.starts, self.host.install_count), (0, 0, 0))
self.assertEqual(engine.estate_hash(self.paths), before)
self.assertFalse((self.attempt / "snapshot").exists())
def test_malformed_billing_refuses_before_mutation_with_fixed_diagnostics(self):
path = self.paths["data"] / "billing.json"
cases = (b"private-malformed-input", b"[]", b"null", b"{}",
b'{"capabilities":"demo_fixtures"}',
b'{"capabilities":["demo_fixtures",{}]}',
b'{"capabilities":[],"capabilities":["demo_fixtures"]}',
b'{"capabilities":["demo_fixtures"],"other":NaN}',
b'{"capabilities":["demo_fixtures"],"other":"\xff"}',
b" " * (1024 * 1024 + 1))
for value in cases:
with self.subTest(length=len(value)):
path.write_bytes(value)
before = engine.estate_hash(self.paths)
code, receipt = self.run_transaction()
self.assertEqual((code, receipt["status"]), (1, "refused"))
self.assertIn(receipt["failure"], {"billing-shape", "billing-observation"})
self.assertEqual(self.host.stops, 0)
self.assertFalse(receipt["mutated"])
self.assertEqual(engine.estate_hash(self.paths), before)
self.assertNotIn("private-malformed-input", json.dumps(receipt))
def test_missing_symlink_and_nonregular_billing_refuse_without_touching_target(self):
path = self.paths["data"] / "billing.json"
target = self.paths["data"] / "other-billing"
target.write_text('{"capabilities":["demo_fixtures"]}')
path.unlink()
for kind in ("missing", "symlink", "directory"):
with self.subTest(kind=kind):
if kind == "symlink":
path.symlink_to(target)
elif kind == "directory":
path.mkdir()
before = engine.estate_hash(self.paths)
code, receipt = self.run_transaction()
self.assertEqual((code, receipt["status"]), (1, "refused"))
self.assertEqual(self.host.stops, 0)
self.assertEqual(engine.estate_hash(self.paths), before)
if kind == "symlink":
path.unlink()
elif kind == "directory":
path.rmdir()
def test_profile_rechecks_capability_before_changing_environment(self):
path = self.paths["data"] / "billing.json"
self.host.profile_admission()
path.write_text('{"capabilities":[]}')
before = engine.estate_hash(self.paths)
with self.assertRaisesRegex(engine.Failure, "^billing-demo-capability-required$"):
self.host.profile(request(), True)
self.assertEqual(engine.estate_hash(self.paths), before)
def test_late_update_failure_preserves_full_billing_and_key_through_recovery(self):
path = self.paths["data"] / "billing.json"
path.write_text(json.dumps({"capabilities": ["demo_fixtures"],
"integrity": "synthetic-original-signature",
"entitlement_jwt": "synthetic-original-secret",
"other": {"keep": True}}))
(self.paths["data"] / ".encryption.key").write_text("synthetic-original-key")
before = engine.estate_hash(self.paths)
self.host.fail_at = 55
code, receipt = self.run_transaction("update", "v6.4.6")
self.assertEqual((code, receipt["status"]), (1, "rolled_back"))
self.assertEqual(engine.estate_hash(self.paths), before)
self.assertEqual(receipt["recovery"]["elapsed_seconds"], 300)
for secret in ("synthetic-original-signature", "synthetic-original-secret", "synthetic-original-key"):
self.assertNotIn(secret, json.dumps(receipt))
def test_healthy_profile_change_keeps_data_and_watches_complete_window(self):
code, receipt = self.run_transaction()
self.assertEqual(code, 0)