"""Read only the explicitly named synthetic owner's committed SQLite rows.""" import json from contextlib import closing from pathlib import Path import sqlite3 import sys import tempfile CHILDREN = ("CareerExperiences", "CareerEducations", "CareerSkills", "CareerProjects", "CareerCertifications", "CareerLanguages") def connect_readonly(filename): path = Path(filename).resolve(strict=True) connection = sqlite3.connect(path.as_uri() + "?mode=ro", uri=True) connection.row_factory = sqlite3.Row return connection def snapshot(connection, owner, job_id=None): def rows(sql, parameters): return [dict(row) for row in connection.execute(sql, parameters)] identities = rows('SELECT "Id", "ProfileCvText", "ProfileCvStructureJson", ' '"CurrentCvProfileVersion" FROM "AspNetUsers" WHERE "Id" = ?', (owner,)) if len(identities) != 1: raise ValueError("Expected exactly one explicitly owned synthetic Identity row") careers = rows('SELECT * FROM "CareerProfiles" WHERE "OwnerUserId" = ?', (owner,)) if len(careers) > 1: raise ValueError("More than one canonical Career Profile for owner") result = { "identity": identities[0], "career": careers[0] if careers else None, "careerHistory": rows('SELECT * FROM "CareerProfileVersions" WHERE "OwnerUserId" = ? ORDER BY "Version", "Id"', (owner,)), "children": {table: rows(f'SELECT * FROM "{table}" WHERE "OwnerUserId" = ? ORDER BY "SortOrder", "Id"', (owner,)) for table in CHILDREN}, "job": None, "opportunity": None, "events": [], } if job_id is not None: jobs = rows('SELECT * FROM "JobApplications" WHERE "OwnerUserId" = ? AND "Id" = ?', (owner, job_id)) if len(jobs) != 1: raise ValueError("Expected exactly one explicitly owned synthetic job") result["job"] = jobs[0] opportunities = rows('SELECT * FROM "Jobs" WHERE "OwnerUserId" = ? AND "Id" = ?', (owner, jobs[0]["JobId"])) if len(opportunities) != 1: raise ValueError("Expected the job's explicitly owned opportunity") result["opportunity"] = opportunities[0] result["events"] = rows('SELECT e.* FROM "JobEvents" e JOIN "JobApplications" j ON e."JobApplicationId" = j."Id" ' 'WHERE j."OwnerUserId" = ? AND j."Id" = ? ORDER BY e."Id"', (owner, job_id)) return result def self_test(): with tempfile.TemporaryDirectory(prefix="rd21-readonly-proof-") as directory: filename = Path(directory) / "jobtracker.db" with closing(sqlite3.connect(filename)) as writer, writer: writer.executescript(''' CREATE TABLE AspNetUsers (Id TEXT, ProfileCvText TEXT, ProfileCvStructureJson TEXT, CurrentCvProfileVersion INTEGER); INSERT INTO AspNetUsers VALUES ('owner', 'raw', '{}', 1), ('other', 'private', '{}', 1); CREATE TABLE CareerProfiles (Id INTEGER, OwnerUserId TEXT, Version INTEGER, ProfileJson TEXT); INSERT INTO CareerProfiles VALUES (1, 'owner', 1, '{}'), (2, 'other', 1, '{}'); CREATE TABLE CareerProfileVersions (Id INTEGER, OwnerUserId TEXT, Version INTEGER); INSERT INTO CareerProfileVersions VALUES (1, 'owner', 1), (2, 'other', 1); CREATE TABLE JobApplications (Id INTEGER, OwnerUserId TEXT, JobId INTEGER, Version INTEGER); INSERT INTO JobApplications VALUES (1, 'owner', 10, 2), (2, 'other', 20, 3); CREATE TABLE Jobs (Id INTEGER, OwnerUserId TEXT); INSERT INTO Jobs VALUES (10, 'owner'), (20, 'other'); CREATE TABLE JobEvents (Id INTEGER, JobApplicationId INTEGER); INSERT INTO JobEvents VALUES (1, 1), (2, 2); ''') for table in CHILDREN: writer.execute(f'CREATE TABLE "{table}" (Id INTEGER, OwnerUserId TEXT, SortOrder INTEGER)') writer.executemany(f'INSERT INTO "{table}" VALUES (?, ?, ?)', [(1, "owner", 0), (2, "other", 0)]) with closing(connect_readonly(filename)) as reader: before = snapshot(reader, "owner", 1) assert before["career"]["Version"] == 1 assert before["job"]["Version"] == 2 assert before["opportunity"]["Id"] == 10 assert before["events"] == [{"Id": 1, "JobApplicationId": 1}] assert all(len(rows) == 1 for rows in before["children"].values()) assert "private" not in json.dumps(before) try: reader.execute('UPDATE CareerProfiles SET Version = 99') except sqlite3.OperationalError as error: assert "readonly" in str(error).lower() else: raise AssertionError("Read-only URI allowed mutation") assert snapshot(reader, "owner", 1) == before try: snapshot(reader, "owner", 2) except ValueError: pass else: raise AssertionError("Cross-owner job was returned") print("readonly-db-proof self-test passed") if __name__ == "__main__": if sys.argv[1:] == ["--self-test"]: self_test() else: if len(sys.argv) not in (3, 4): raise SystemExit("Usage: readonly-db-proof.py DATABASE OWNER_ID [JOB_ID] | --self-test") with closing(connect_readonly(sys.argv[1])) as connection: connection.execute("BEGIN") print(json.dumps(snapshot(connection, sys.argv[2], int(sys.argv[3]) if len(sys.argv) == 4 else None)))