Create the archive readable by its owner only
An archive holds a whole mailbox, its contacts and calendars, and it was created with SQLite's default mode, readable by every user on the machine. On Unix a new archive is now created with mode 0600 before SQLite opens it; SQLite gives the -wal and -shm files the database file's mode, so they follow, which a test confirms. An existing archive that others can read is left as it is, with a warning naming it and the chmod that fixes it. Nothing changes on Windows.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||||
|
* SPDX-FileCopyrightText: 2026 John Coffey <[email protected]>
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||||
*/
|
*/
|
||||||
@@ -9,6 +10,7 @@ use rusqlite::Connection;
|
|||||||
pub const SCHEMA_SQL: &str = include_str!("schema.sql");
|
pub const SCHEMA_SQL: &str = include_str!("schema.sql");
|
||||||
|
|
||||||
pub fn open(path: &std::path::Path) -> Result<Connection, OpenError> {
|
pub fn open(path: &std::path::Path) -> Result<Connection, OpenError> {
|
||||||
|
private_archive(path)?;
|
||||||
let conn = Connection::open(path)?;
|
let conn = Connection::open(path)?;
|
||||||
apply_pragmas(&conn)?;
|
apply_pragmas(&conn)?;
|
||||||
apply_schema(&conn)?;
|
apply_schema(&conn)?;
|
||||||
@@ -72,6 +74,50 @@ fn ensure_graph_ids_accept_file_nodes(conn: &Connection) -> Result<(), OpenError
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An archive holds a whole mailbox, so it is created readable by its owner
|
||||||
|
/// only. SQLite gives its `-wal` and `-shm` files the database file's mode,
|
||||||
|
/// so they follow. An existing archive others can read is left as it is,
|
||||||
|
/// with a warning and the command that fixes it.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn private_archive(path: &std::path::Path) -> Result<(), OpenError> {
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
match std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.mode(0o600)
|
||||||
|
.open(path)
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||||
|
if let Some(warning) = permission_warning(path) {
|
||||||
|
eprintln!("warning: {warning}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(OpenError::Create(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn private_archive(_path: &std::path::Path) -> Result<(), OpenError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The warning for an archive that someone other than its owner can read.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn permission_warning(path: &std::path::Path) -> Option<String> {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let mode = std::fs::metadata(path).ok()?.permissions().mode();
|
||||||
|
(mode & 0o077 != 0).then(|| {
|
||||||
|
format!(
|
||||||
|
"archive {} can be read by other users (mode {:o}); run: chmod 600 {}",
|
||||||
|
path.display(),
|
||||||
|
mode & 0o777,
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_pragmas(conn: &Connection) -> Result<(), OpenError> {
|
fn apply_pragmas(conn: &Connection) -> Result<(), OpenError> {
|
||||||
conn.pragma_update(None, "journal_mode", "WAL")?;
|
conn.pragma_update(None, "journal_mode", "WAL")?;
|
||||||
conn.pragma_update(None, "foreign_keys", "ON")?;
|
conn.pragma_update(None, "foreign_keys", "ON")?;
|
||||||
@@ -83,4 +129,53 @@ fn apply_pragmas(conn: &Connection) -> Result<(), OpenError> {
|
|||||||
pub enum OpenError {
|
pub enum OpenError {
|
||||||
#[error("sqlite error: {0}")]
|
#[error("sqlite error: {0}")]
|
||||||
Sqlite(#[from] rusqlite::Error),
|
Sqlite(#[from] rusqlite::Error),
|
||||||
|
#[error("cannot create the archive: {0}")]
|
||||||
|
Create(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, unix))]
|
||||||
|
mod permission_tests {
|
||||||
|
use super::*;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
fn mode(p: &std::path::Path) -> u32 {
|
||||||
|
std::fs::metadata(p).unwrap().permissions().mode() & 0o777
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scratch(name: &str) -> std::path::PathBuf {
|
||||||
|
let dir =
|
||||||
|
std::env::temp_dir().join(format!("inbuxa-migrate-perm-{}-{name}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
dir.join("a.sqlite")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_new_archive_and_its_wal_are_private() {
|
||||||
|
let path = scratch("new");
|
||||||
|
let conn = open(&path).unwrap();
|
||||||
|
conn.execute_batch("CREATE TABLE t(x); INSERT INTO t VALUES (1);")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(mode(&path), 0o600);
|
||||||
|
let wal = path.with_extension("sqlite-wal");
|
||||||
|
assert!(wal.exists(), "WAL mode writes a -wal file");
|
||||||
|
assert_eq!(mode(&wal), 0o600);
|
||||||
|
drop(conn);
|
||||||
|
let _ = std::fs::remove_dir_all(path.parent().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_existing_readable_archive_is_warned_about_not_changed() {
|
||||||
|
let path = scratch("existing");
|
||||||
|
drop(open(&path).unwrap());
|
||||||
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
|
||||||
|
let warning = permission_warning(&path).expect("warns");
|
||||||
|
assert!(warning.contains("chmod 600"), "{warning}");
|
||||||
|
assert!(warning.contains("644"), "{warning}");
|
||||||
|
drop(open(&path).unwrap());
|
||||||
|
assert_eq!(mode(&path), 0o644, "left as it is");
|
||||||
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
|
||||||
|
assert!(permission_warning(&path).is_none());
|
||||||
|
let _ = std::fs::remove_dir_all(path.parent().unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user