pray-core 1.10.0

Core library for Prayfile, the package manager for the language placed before inference
Documentation
#![cfg(feature = "auth")]

use pray_core::auth::{AuthSessionKind, RegistryAuthStore, PUBLISH_SCOPE};
use pray_core::trust::EmailConfirmationPolicy;
use rusqlite::Connection;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

#[test]
fn issued_tokens_are_unique_and_stored_as_hashes() {
    let root = temporary_root("random");
    let store = verified_store(&root);

    let first_session = store
        .issue_session("bob@example.com", AuthSessionKind::Email)
        .expect("first session");
    let second_session = store
        .issue_session("bob@example.com", AuthSessionKind::Email)
        .expect("second session");
    let first_publish = store
        .issue_publish_token("bob@example.com", &[PUBLISH_SCOPE.to_string()])
        .expect("first publish token");
    let second_publish = store
        .issue_publish_token("bob@example.com", &[PUBLISH_SCOPE.to_string()])
        .expect("second publish token");

    assert_ne!(first_session.token, second_session.token);
    assert_ne!(first_publish.token, second_publish.token);
    let connection = Connection::open(root.join(".pray/auth.db")).expect("database");
    let stored_session: String = connection
        .query_row("SELECT token FROM sessions LIMIT 1", [], |row| row.get(0))
        .expect("stored session");
    assert_ne!(stored_session, first_session.token);
    assert!(stored_session.starts_with("sha256:"));
}

#[test]
fn expired_sessions_and_verification_codes_are_rejected() {
    let root = temporary_root("expiry");
    let store = RegistryAuthStore::open(&root).expect("store");
    let registration = store
        .register_email("alice@example.com", EmailConfirmationPolicy::Required)
        .expect("registration");
    let code = registration.verification_code.expect("code");
    let connection = Connection::open(root.join(".pray/auth.db")).expect("database");
    connection
        .execute("UPDATE email_verification_codes SET created_at = 0", [])
        .expect("expire code");
    assert!(store.verify_email("alice@example.com", &code).is_err());

    let store = verified_store(&root);
    let session = store
        .issue_session("bob@example.com", AuthSessionKind::Email)
        .expect("session");
    connection
        .execute("UPDATE sessions SET created_at = 0", [])
        .expect("expire session");
    assert!(store
        .resolve_session(&session.token)
        .expect("resolve")
        .is_none());
}

#[test]
fn verification_codes_are_hashed_and_guessing_is_uniform() {
    let root = temporary_root("verify");
    let store = RegistryAuthStore::open(&root).expect("store");
    let registration = store
        .register_email("alice@example.com", EmailConfirmationPolicy::Required)
        .expect("registration");
    let code = registration.verification_code.expect("code");
    let connection = Connection::open(root.join(".pray/auth.db")).expect("database");
    let stored_code: String = connection
        .query_row(
            "SELECT code FROM email_verification_codes WHERE email = ?1",
            ["alice@example.com"],
            |row| row.get(0),
        )
        .expect("stored code");
    assert_ne!(stored_code, code);
    assert!(stored_code.starts_with("sha256:"));

    let missing = store
        .verify_email("missing@example.com", "guess")
        .expect_err("missing");
    let mismatch = store
        .verify_email("alice@example.com", "guess")
        .expect_err("mismatch");
    assert_eq!(missing.to_string(), mismatch.to_string());
    assert!(missing.to_string().contains("verification failed"));

    for _ in 0..5 {
        let _ = store.verify_email("alice@example.com", "guess");
    }
    assert!(store.verify_email("alice@example.com", &code).is_err());
}

#[cfg(unix)]
#[test]
fn auth_database_uses_owner_only_permissions() {
    use std::os::unix::fs::PermissionsExt;
    let root = temporary_root("mode");
    let _store = RegistryAuthStore::open(&root).expect("store");
    let database_mode = fs::metadata(root.join(".pray/auth.db"))
        .expect("db")
        .permissions()
        .mode()
        & 0o777;
    let directory_mode = fs::metadata(root.join(".pray"))
        .expect("dir")
        .permissions()
        .mode()
        & 0o777;
    assert_eq!(database_mode, 0o600);
    assert_eq!(directory_mode, 0o700);
}

fn verified_store(root: &Path) -> RegistryAuthStore {
    let store = RegistryAuthStore::open(root).expect("store");
    store
        .register_email("bob@example.com", EmailConfirmationPolicy::Disabled)
        .expect("registration");
    store
}

fn temporary_root(name: &str) -> PathBuf {
    let root = std::env::temp_dir().join(format!(
        "pray-auth-token-{name}-{}-{}",
        std::process::id(),
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("clock")
            .as_nanos()
    ));
    fs::create_dir_all(&root).expect("root");
    root
}