//! DB-layer contract tests for `db::ssh_keys`, git-over-SSH key CRUD + lookup. //! //! Audit Run 16 flagged `db/ssh_keys.rs:98` (`lookup_user_by_fingerprint`) and //! the module's thin coverage. These pin the ownership-scoped deletes (an IDOR //! guard on the git surface), the per-user duplicate rejection, and the global //! fingerprint uniqueness (migration 161) that keeps `lookup_user_by_fingerprint` //! unambiguous, one account can't register another's key fingerprint. use crate::harness::db::TestDb; use makenotwork::db::{UserId, ssh_keys}; async fn seed_user(pool: &sqlx::PgPool, username: &str) -> UserId { let hash = makenotwork::auth::hash_password("password123").expect("hash"); sqlx::query_scalar::<_, UserId>( "INSERT INTO users (username, email, password_hash, email_verified) VALUES ($1, $2, $3, true) RETURNING id", ) .bind(username) .bind(format!("{username}@test.com")) .bind(&hash) .fetch_one(pool) .await .expect("seed user") } #[tokio::test] async fn add_then_lookup_resolves_the_owner() { let db = TestDb::new().await; let user = seed_user(&db.pool, "ssh_lookup").await; ssh_keys::add_key(&db.pool, user, "ssh-ed25519 AAAA", "SHA256:aaa", "laptop") .await .unwrap(); let found = ssh_keys::lookup_user_by_fingerprint(&db.pool, "SHA256:aaa") .await .unwrap(); assert_eq!(found.expect("known fingerprint resolves").user_id, user); // An unknown fingerprint authenticates nobody. assert!( ssh_keys::lookup_user_by_fingerprint(&db.pool, "SHA256:unknown") .await .unwrap() .is_none() ); } #[tokio::test] async fn duplicate_fingerprint_for_same_user_is_rejected() { let db = TestDb::new().await; let user = seed_user(&db.pool, "ssh_dup").await; ssh_keys::add_key(&db.pool, user, "ssh-ed25519 AAAA", "SHA256:dup", "one") .await .unwrap(); // UNIQUE (user_id, fingerprint), re-registering the same key fails. let dup = ssh_keys::add_key(&db.pool, user, "ssh-ed25519 AAAA", "SHA256:dup", "two").await; assert!( dup.is_err(), "the same fingerprint cannot be registered twice by one user" ); } /// A fingerprint maps to exactly ONE identity, globally. Migration 161 added a /// global `UNIQUE (fingerprint)` on top of the original `(user_id, fingerprint)` /// specifically so one account can't register a public key already tied to /// another account and thereby authenticate as it (Run 15 Auth C1). This guards /// that fix: a second user registering the same fingerprint is rejected, so /// `lookup_user_by_fingerprint` can never be ambiguous. #[tokio::test] async fn fingerprint_is_globally_unique_across_users() { let db = TestDb::new().await; let first = seed_user(&db.pool, "ssh_first").await; let second = seed_user(&db.pool, "ssh_second").await; ssh_keys::add_key( &db.pool, first, "ssh-ed25519 AAAA", "SHA256:shared", "first", ) .await .unwrap(); // A different account cannot claim the same fingerprint. let stolen = ssh_keys::add_key( &db.pool, second, "ssh-ed25519 AAAA", "SHA256:shared", "second", ) .await; assert!( stolen.is_err(), "a fingerprint already registered to one account must not be registrable by another" ); // The lookup remains unambiguous, it resolves to the sole registrant. let resolved = ssh_keys::lookup_user_by_fingerprint(&db.pool, "SHA256:shared") .await .unwrap() .expect("fingerprint resolves"); assert_eq!(resolved.user_id, first); } /// IDOR guard: `delete_key` scopes on `(id, user_id)`, so one user cannot delete /// another's key by id. #[tokio::test] async fn delete_by_id_is_scoped_to_the_owner() { let db = TestDb::new().await; let owner = seed_user(&db.pool, "ssh_owner").await; let attacker = seed_user(&db.pool, "ssh_attacker").await; let key = ssh_keys::add_key(&db.pool, owner, "ssh-ed25519 AAAA", "SHA256:own", "k") .await .unwrap(); assert!( !ssh_keys::delete_key(&db.pool, key.id, attacker) .await .unwrap(), "a non-owner delete-by-id must return false" ); assert_eq!( ssh_keys::list_keys_by_user(&db.pool, owner) .await .unwrap() .len(), 1, "the key survives" ); assert!(ssh_keys::delete_key(&db.pool, key.id, owner).await.unwrap()); assert!( ssh_keys::list_keys_by_user(&db.pool, owner) .await .unwrap() .is_empty() ); } #[tokio::test] async fn delete_by_fingerprint_is_scoped_to_the_owner() { let db = TestDb::new().await; let owner = seed_user(&db.pool, "ssh_fp_owner").await; let attacker = seed_user(&db.pool, "ssh_fp_attacker").await; ssh_keys::add_key(&db.pool, owner, "ssh-ed25519 AAAA", "SHA256:fp", "k") .await .unwrap(); // The attacker knows the fingerprint but not the owner scope: no-op. assert!( !ssh_keys::delete_key_by_fingerprint(&db.pool, attacker, "SHA256:fp") .await .unwrap(), "a non-owner delete-by-fingerprint must return false" ); assert_eq!( ssh_keys::list_keys_by_user(&db.pool, owner) .await .unwrap() .len(), 1 ); assert!( ssh_keys::delete_key_by_fingerprint(&db.pool, owner, "SHA256:fp") .await .unwrap() ); }