lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

mod.rs (12056B)


      1 //! Versioned schema authority for `private.sqlite`.
      2 //!
      3 //! The public descriptor surface exposes version and integrity metadata only.
      4 //! Embedded SQL remains an implementation detail of this backend.
      5 
      6 /// Lowest private schema version this package can recognize.
      7 pub const MINIMUM_VERSION: u32 = 1;
      8 /// Current private schema version created by this package.
      9 pub const CURRENT_VERSION: u32 = 4;
     10 
     11 const PRIVATE_V1_SQL: &str = include_str!("0001_private.up.sql");
     12 const LEGACY_PRIVATE_STAGING_V2_SQL: &str = include_str!("0002_legacy_private_staging.up.sql");
     13 const LEGACY_IMPORT_COMMITS_V3_SQL: &str = include_str!("0003_legacy_import_commits.up.sql");
     14 const CONTEXT_BOUND_ENVELOPES_V4_SQL: &str = include_str!("0004_context_bound_envelopes.up.sql");
     15 
     16 /// Stable, non-SQL description of one forward private migration.
     17 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
     18 pub struct MigrationDescriptor {
     19     version: u32,
     20     name: &'static str,
     21     up_sha256: &'static str,
     22     owned_objects: &'static [&'static str],
     23 }
     24 
     25 impl MigrationDescriptor {
     26     pub const fn version(self) -> u32 {
     27         self.version
     28     }
     29 
     30     pub const fn name(self) -> &'static str {
     31         self.name
     32     }
     33 
     34     pub const fn up_sha256(self) -> &'static str {
     35         self.up_sha256
     36     }
     37 
     38     pub const fn owned_objects(self) -> &'static [&'static str] {
     39         self.owned_objects
     40     }
     41 }
     42 
     43 const PRIVATE_V1_OBJECTS: &[&str] = &[
     44     "radroots_private_artifacts",
     45     "radroots_private_artifacts_delete_guard",
     46     "radroots_private_artifacts_envelope_guard",
     47     "radroots_private_artifacts_expiry_idx",
     48     "radroots_private_artifacts_identity_guard",
     49     "radroots_private_artifacts_key_version_idx",
     50     "radroots_private_artifacts_kind_idx",
     51 ];
     52 
     53 const PRIVATE_V2_OBJECTS: &[&str] = &[
     54     "radroots_private_artifacts",
     55     "radroots_private_artifacts_delete_guard",
     56     "radroots_private_artifacts_envelope_guard",
     57     "radroots_private_artifacts_expiry_idx",
     58     "radroots_private_artifacts_identity_guard",
     59     "radroots_private_artifacts_key_version_idx",
     60     "radroots_private_artifacts_kind_idx",
     61     "radroots_private_legacy_import_staging",
     62     "radroots_private_legacy_import_staging_delete_guard",
     63     "radroots_private_legacy_import_staging_insert_guard",
     64     "radroots_private_legacy_import_staging_parent_idx",
     65     "radroots_private_legacy_import_staging_update_guard",
     66 ];
     67 
     68 const PRIVATE_V3_OBJECTS: &[&str] = &[
     69     "radroots_private_artifacts",
     70     "radroots_private_artifacts_delete_guard",
     71     "radroots_private_artifacts_envelope_guard",
     72     "radroots_private_artifacts_expiry_idx",
     73     "radroots_private_artifacts_identity_guard",
     74     "radroots_private_artifacts_key_version_idx",
     75     "radroots_private_artifacts_kind_idx",
     76     "radroots_private_legacy_import_commit_delete_guard",
     77     "radroots_private_legacy_import_commit_update_guard",
     78     "radroots_private_legacy_import_commits",
     79     "radroots_private_legacy_import_staging",
     80     "radroots_private_legacy_import_staging_delete_guard",
     81     "radroots_private_legacy_import_staging_insert_guard",
     82     "radroots_private_legacy_import_staging_parent_idx",
     83     "radroots_private_legacy_import_staging_update_guard",
     84 ];
     85 
     86 const PRIVATE_V4_OBJECTS: &[&str] = &[
     87     "radroots_private_artifacts",
     88     "radroots_private_artifacts_delete_guard",
     89     "radroots_private_artifacts_envelope_guard",
     90     "radroots_private_artifacts_expiry_idx",
     91     "radroots_private_artifacts_identity_guard",
     92     "radroots_private_artifacts_insert_envelope_guard",
     93     "radroots_private_artifacts_key_version_idx",
     94     "radroots_private_artifacts_kind_idx",
     95     "radroots_private_artifacts_reseal_audit",
     96     "radroots_private_envelope_reseals",
     97     "radroots_private_envelope_reseals_artifact_idx",
     98     "radroots_private_envelope_reseals_delete_guard",
     99     "radroots_private_envelope_reseals_update_guard",
    100     "radroots_private_legacy_import_commit_delete_guard",
    101     "radroots_private_legacy_import_commit_update_guard",
    102     "radroots_private_legacy_import_commits",
    103     "radroots_private_legacy_import_staging",
    104     "radroots_private_legacy_import_staging_delete_guard",
    105     "radroots_private_legacy_import_staging_insert_guard",
    106     "radroots_private_legacy_import_staging_parent_idx",
    107     "radroots_private_legacy_import_staging_update_guard",
    108 ];
    109 
    110 /// Ordered, immutable private migration plan.
    111 pub const MIGRATIONS: &[MigrationDescriptor] = &[
    112     MigrationDescriptor {
    113         version: 1,
    114         name: "private_artifacts",
    115         up_sha256: "07050386292ff8ce9ec0e756c9ac88e458a249d53e8654e1102caa2e361f11ab",
    116         owned_objects: PRIVATE_V1_OBJECTS,
    117     },
    118     MigrationDescriptor {
    119         version: 2,
    120         name: "legacy_private_staging",
    121         up_sha256: "299ec0c476b2f5ab995f245d36603969af345827c9ecdf9490cfe0b0dbe4b9f9",
    122         owned_objects: PRIVATE_V2_OBJECTS,
    123     },
    124     MigrationDescriptor {
    125         version: 3,
    126         name: "legacy_import_commits",
    127         up_sha256: "9377f0af8f070d977a5237e2a1294e6977f5b704e7a8434d97a3dc5f4ae75e86",
    128         owned_objects: PRIVATE_V3_OBJECTS,
    129     },
    130     MigrationDescriptor {
    131         version: 4,
    132         name: "context_bound_envelopes",
    133         up_sha256: "dd6bb42471db47fcd9c62f8d59b66ddba381776d879cfbffd94993f7ef7409de",
    134         owned_objects: PRIVATE_V4_OBJECTS,
    135     },
    136 ];
    137 
    138 pub(crate) const fn migration_sql(version: u32) -> Option<&'static str> {
    139     match version {
    140         1 => Some(PRIVATE_V1_SQL),
    141         2 => Some(LEGACY_PRIVATE_STAGING_V2_SQL),
    142         3 => Some(LEGACY_IMPORT_COMMITS_V3_SQL),
    143         4 => Some(CONTEXT_BOUND_ENVELOPES_V4_SQL),
    144         _ => None,
    145     }
    146 }
    147 
    148 #[cfg(test)]
    149 #[cfg_attr(coverage_nightly, coverage(off))]
    150 mod tests {
    151     use super::{CURRENT_VERSION, MIGRATIONS, MINIMUM_VERSION, migration_sql};
    152     use serde::Deserialize;
    153     use sha2::{Digest, Sha256};
    154     use sqlx::{Connection, Row, SqliteConnection};
    155 
    156     const PLAN_SNAPSHOT: &str =
    157         include_str!("../../../../../contracts/storage/private_schema_v1.toml");
    158 
    159     #[derive(Debug, Deserialize)]
    160     struct PlanSnapshot {
    161         schema_version: u32,
    162         database: String,
    163         application_id: u32,
    164         minimum_version: u32,
    165         current_version: u32,
    166         migration_name: String,
    167         migration_sha256: String,
    168         forward_only: bool,
    169         raw_sql_public: bool,
    170         encrypted_envelopes: bool,
    171         authorities: Vec<String>,
    172         forbidden_tables: Vec<String>,
    173         migrations: Vec<MigrationSnapshot>,
    174     }
    175 
    176     #[derive(Debug, Deserialize)]
    177     struct MigrationSnapshot {
    178         version: u32,
    179         name: String,
    180         sha256: String,
    181         owned_objects: Vec<String>,
    182     }
    183 
    184     #[test]
    185     fn migration_plan_matches_governed_snapshot() {
    186         let snapshot = toml::from_str::<PlanSnapshot>(PLAN_SNAPSHOT).expect("valid snapshot");
    187         let migration = MIGRATIONS[3];
    188         assert_eq!(snapshot.schema_version, 1);
    189         assert_eq!(snapshot.database, "private.sqlite");
    190         assert_eq!(snapshot.application_id, 1_380_208_722);
    191         assert_eq!(snapshot.minimum_version, MINIMUM_VERSION);
    192         assert_eq!(snapshot.current_version, CURRENT_VERSION);
    193         assert_eq!(snapshot.migration_name, migration.name());
    194         assert_eq!(snapshot.migration_sha256, migration.up_sha256());
    195         assert!(snapshot.forward_only);
    196         assert!(!snapshot.raw_sql_public);
    197         assert!(snapshot.encrypted_envelopes);
    198         assert_eq!(snapshot.authorities.len(), 7);
    199         assert_eq!(snapshot.forbidden_tables, ["studio", "ui_state"]);
    200         assert_eq!(snapshot.migrations.len(), MIGRATIONS.len());
    201         for (expected, actual) in snapshot.migrations.iter().zip(MIGRATIONS) {
    202             assert_eq!(expected.version, actual.version());
    203             assert_eq!(expected.name, actual.name());
    204             assert_eq!(expected.sha256, actual.up_sha256());
    205             assert_eq!(expected.owned_objects, actual.owned_objects());
    206         }
    207     }
    208 
    209     #[test]
    210     fn embedded_migration_checksum_is_pinned() {
    211         for migration in MIGRATIONS {
    212             let sql = migration_sql(migration.version()).expect("registered SQL");
    213             assert_eq!(format!("{:x}", Sha256::digest(sql)), migration.up_sha256());
    214         }
    215         assert_eq!(migration_sql(5), None);
    216     }
    217 
    218     #[tokio::test]
    219     async fn fresh_database_has_exact_private_schema_and_no_studio_authority() {
    220         let mut connection = SqliteConnection::connect("sqlite::memory:")
    221             .await
    222             .expect("open memory SQLite");
    223         for migration in MIGRATIONS {
    224             sqlx::raw_sql(migration_sql(migration.version()).expect("registered SQL"))
    225                 .execute(&mut connection)
    226                 .await
    227                 .expect("apply private schema");
    228         }
    229         let rows = sqlx::query(
    230             "SELECT name FROM sqlite_schema
    231              WHERE name LIKE 'radroots_private_%'
    232              ORDER BY name",
    233         )
    234         .fetch_all(&mut connection)
    235         .await
    236         .expect("inspect private schema");
    237         let actual = rows
    238             .iter()
    239             .map(|row| row.get::<String, _>("name"))
    240             .collect::<Vec<_>>();
    241         assert_eq!(actual, MIGRATIONS[3].owned_objects());
    242         let forbidden = sqlx::query_scalar::<_, i64>(
    243             "SELECT COUNT(*) FROM sqlite_schema
    244              WHERE lower(name) LIKE '%studio%' OR lower(name) LIKE '%ui_state%'",
    245         )
    246         .fetch_one(&mut connection)
    247         .await
    248         .expect("inspect forbidden tables");
    249         assert_eq!(forbidden, 0);
    250         assert!(
    251             sqlx::query("DELETE FROM radroots_private_artifacts")
    252                 .execute(&mut connection)
    253                 .await
    254                 .is_ok()
    255         );
    256         let integrity = sqlx::query_scalar::<_, String>("PRAGMA integrity_check")
    257             .fetch_one(&mut connection)
    258             .await
    259             .expect("inspect integrity");
    260         assert_eq!(integrity, "ok");
    261     }
    262 
    263     #[tokio::test]
    264     async fn v4_preflight_rejects_unfingerprinted_v2_without_partial_schema() {
    265         let mut connection = SqliteConnection::connect("sqlite::memory:")
    266             .await
    267             .expect("open memory SQLite");
    268         for migration in &MIGRATIONS[..3] {
    269             sqlx::raw_sql(migration_sql(migration.version()).expect("registered SQL"))
    270                 .execute(&mut connection)
    271                 .await
    272                 .expect("apply private schema");
    273         }
    274         sqlx::query(
    275             "INSERT INTO radroots_private_artifacts (
    276                artifact_id, artifact_kind, schema_id, commitment, protected_size_bytes,
    277                secret_provider, secret_reference, key_version, envelope_version,
    278                encrypted_envelope, revision, stage, created_at_unix_ms, updated_at_unix_ms
    279              ) VALUES (?, 'trade.private_terms', 'trade.private_terms.v1', ?, 1,
    280                'memory', 'key', 1, 2, ?, 1, 'active', 1, 1)",
    281         )
    282         .bind([1_u8; 16].as_slice())
    283         .bind([2_u8; 32].as_slice())
    284         .bind([3_u8].as_slice())
    285         .execute(&mut connection)
    286         .await
    287         .expect("unfingerprinted v2 fixture");
    288 
    289         let mut transaction = connection.begin().await.expect("migration transaction");
    290         assert!(
    291             sqlx::raw_sql(migration_sql(4).expect("v4 SQL"))
    292                 .execute(&mut *transaction)
    293                 .await
    294                 .is_err()
    295         );
    296         transaction.rollback().await.expect("rollback preflight");
    297         let context_column = sqlx::query_scalar::<_, i64>(
    298             "SELECT COUNT(*) FROM pragma_table_info('radroots_private_artifacts')
    299              WHERE name = 'context_fingerprint'",
    300         )
    301         .fetch_one(&mut connection)
    302         .await
    303         .expect("inspect columns");
    304         let preflight_table = sqlx::query_scalar::<_, i64>(
    305             "SELECT COUNT(*) FROM sqlite_schema
    306              WHERE name = 'radroots_private_envelope_v2_preflight'",
    307         )
    308         .fetch_one(&mut connection)
    309         .await
    310         .expect("inspect catalog");
    311         assert_eq!(context_column, 0);
    312         assert_eq!(preflight_table, 0);
    313     }
    314 }