feat: encrypt completed Filed review history (Closes #1252)
This commit is contained in:
1 parent
a4498fdbbc
commit
4b645d8460
5 files changed
+205
-55
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import sqlite3
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
@@ -21,6 +23,30 @@ def test_receipts_merge_without_lost_updates_and_remain_account_scoped(tmp_path)
|
||||
assert store.get("alexander") == {"receipts": []}
|
||||
|
||||
|
||||
def test_receipt_history_is_encrypted_and_bound_to_its_account(tmp_path):
|
||||
database = tmp_path / "completed-filed.sqlite3"
|
||||
store = CompletedFiledReviewStore(database, encryption_key=b"f" * 32)
|
||||
timmy = receipt("private-timmy/repository", 912)
|
||||
alexander = receipt("private-alexander/repository", 427)
|
||||
|
||||
store.merge("timmy-private-login", [timmy])
|
||||
store.merge("alexander-private-login", [alexander])
|
||||
|
||||
raw = database.read_bytes()
|
||||
assert b"private-timmy/repository" not in raw
|
||||
assert b"private-alexander/repository" not in raw
|
||||
with sqlite3.connect(database) as connection:
|
||||
rows = connection.execute(
|
||||
"SELECT login, receipts FROM completed_filed_review_collections ORDER BY login"
|
||||
).fetchall()
|
||||
connection.execute(
|
||||
"UPDATE completed_filed_review_collections SET receipts = ? WHERE login = ?",
|
||||
(rows[1][1], rows[0][0]),
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="private state could not be decrypted"):
|
||||
store.get("alexander-private-login")
|
||||
|
||||
|
||||
def test_receipts_keep_newest_revision_and_bound_each_account(tmp_path):
|
||||
store = CompletedFiledReviewStore(tmp_path / "completed-filed.sqlite3", limit=2)
|
||||
newer = receipt(updated_at="2026-08-16T12:00:00Z")
|
||||
@@ -32,6 +58,42 @@ def test_receipts_keep_newest_revision_and_bound_each_account(tmp_path):
|
||||
assert bounded == {"receipts": [receipt("stackchain/web", 2), receipt("stackchain/docs", 3)]}
|
||||
|
||||
|
||||
def test_legacy_plaintext_receipts_migrate_for_every_account_without_loss(tmp_path):
|
||||
database = tmp_path / "completed-filed.sqlite3"
|
||||
with sqlite3.connect(database) as connection:
|
||||
connection.execute(
|
||||
"CREATE TABLE completed_filed_reviews ("
|
||||
"login TEXT NOT NULL, repository TEXT NOT NULL, issue_number INTEGER NOT NULL, "
|
||||
"updated_at TEXT NOT NULL, touched_at INTEGER NOT NULL, "
|
||||
"PRIMARY KEY (login, repository, issue_number))"
|
||||
)
|
||||
connection.executemany(
|
||||
"INSERT INTO completed_filed_reviews VALUES (?, ?, ?, ?, ?)",
|
||||
[
|
||||
("timmy", "legacy-private/first", 1, "2026-08-14T12:00:00Z", 1),
|
||||
("timmy", "legacy-private/second", 2, "2026-08-15T12:00:00Z", 2),
|
||||
("alexander", "legacy-private/other", 3, "2026-08-16T12:00:00Z", 1),
|
||||
],
|
||||
)
|
||||
|
||||
store = CompletedFiledReviewStore(database, encryption_key=b"m" * 32)
|
||||
|
||||
assert store.get("timmy") == {"receipts": [
|
||||
receipt("legacy-private/first", 1, "2026-08-14T12:00:00Z"),
|
||||
receipt("legacy-private/second", 2, "2026-08-15T12:00:00Z"),
|
||||
]}
|
||||
assert store.get("alexander") == {"receipts": [
|
||||
receipt("legacy-private/other", 3, "2026-08-16T12:00:00Z")
|
||||
]}
|
||||
raw = database.read_bytes()
|
||||
assert b"legacy-private/first" not in raw
|
||||
assert b"legacy-private/other" not in raw
|
||||
with sqlite3.connect(database) as connection:
|
||||
assert connection.execute(
|
||||
"SELECT count(*) FROM sqlite_master WHERE name = 'completed_filed_reviews'"
|
||||
).fetchone()[0] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("candidate", [
|
||||
{"repository": "bad", "number": 1, "updated_at": "2026-08-15T12:00:00Z"},
|
||||
{"repository": "stackchain/api", "number": 0, "updated_at": "2026-08-15T12:00:00Z"},
|
||||
|
||||
@@ -6,6 +6,7 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from src.completed_filed_review_store import CompletedFiledReviewStore
|
||||
from src.saved_search_store import SavedSearchStore
|
||||
|
||||
|
||||
@@ -81,3 +82,32 @@ def test_rotation_command_fails_closed_and_never_prints_private_content(tmp_path
|
||||
assert secret not in completed.stdout
|
||||
assert "timmy" not in completed.stdout
|
||||
assert completed.stderr == ""
|
||||
|
||||
|
||||
def test_rotation_command_rewraps_completed_filed_history_without_printing_it(tmp_path):
|
||||
state = tmp_path / "state"
|
||||
path = state / "completed-filed-reviews.sqlite3"
|
||||
private_receipt = {
|
||||
"repository": "private-rotation/canary",
|
||||
"number": 1252,
|
||||
"updated_at": "2026-08-22T12:00:00Z",
|
||||
}
|
||||
CompletedFiledReviewStore(path, encryption_key=b"o" * 32).merge("timmy", [private_receipt])
|
||||
|
||||
completed = run_rotation(state)
|
||||
|
||||
assert completed.returncode == 0, completed.stderr
|
||||
report = json.loads(completed.stdout)
|
||||
assert report["completed-filed-reviews"] == {
|
||||
"current": 0, "failed": 0, "migrated": 1, "total": 1
|
||||
}
|
||||
assert "private-rotation" not in completed.stdout
|
||||
assert "timmy" not in completed.stdout
|
||||
with sqlite3.connect(path) as connection:
|
||||
payload = connection.execute(
|
||||
"SELECT receipts FROM completed_filed_review_collections WHERE login = 'timmy'"
|
||||
).fetchone()[0]
|
||||
assert payload.startswith("v2:next:")
|
||||
assert CompletedFiledReviewStore(
|
||||
path, encryption_key=({"next": b"n" * 32}, "next")
|
||||
).get("timmy") == {"receipts": [private_receipt]}
|
||||
Reference in new issue
Block a user