use std::path::Path;
use rusqlite::Connection;
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use super::{Migration, db_err};
pub(crate) struct DropAmbiguousEmptyCapabilities;
impl Migration for DropAmbiguousEmptyCapabilities {
fn source_version(&self) -> i32 {
9
}
fn target_version(&self) -> i32 {
10
}
fn description(&self) -> &str {
"Drop the now-ambiguous empty capability policy from legacy box configs"
}
fn run(&self, conn: &Connection, _home_dir: Option<&Path>) -> BoxliteResult<()> {
let configs = {
let mut statement = db_err!(conn.prepare("SELECT id, json FROM box_config"))?;
let rows = db_err!(statement.query_map([], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
}))?;
db_err!(rows.collect::<Result<Vec<_>, _>>())?
};
let mut updates = Vec::new();
for (id, json) in configs {
let mut config: serde_json::Value = serde_json::from_str(&json).map_err(|error| {
BoxliteError::Database(format!(
"parse box_config {id} while migrating capability policy: {error}"
))
})?;
let Some(advanced) = config.pointer_mut("/options/advanced") else {
continue;
};
let Some(advanced_obj) = advanced.as_object_mut() else {
continue;
};
let is_empty_policy = advanced_obj
.get("capabilities")
.and_then(|c| c.as_object())
.is_some_and(|c| {
c.get("add")
.is_none_or(|v| v.as_array().is_some_and(Vec::is_empty))
&& c.get("drop")
.is_none_or(|v| v.as_array().is_some_and(Vec::is_empty))
});
if !is_empty_policy {
continue;
}
advanced_obj.remove("capabilities");
let json = serde_json::to_string(&config).map_err(|error| {
BoxliteError::Database(format!(
"serialize box_config {id} while migrating capability policy: {error}"
))
})?;
updates.push((id, json));
}
let transaction = db_err!(conn.unchecked_transaction())?;
for (id, json) in &updates {
db_err!(transaction.execute(
"UPDATE box_config SET json = ?1 WHERE id = ?2",
rusqlite::params![json, id],
))?;
}
db_err!(transaction.commit())?;
if !updates.is_empty() {
tracing::info!(
boxes = updates.len(),
"Cleared ambiguous empty capability policy on legacy box configs"
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn open_with_box_config(rows: &[(&str, &str)]) -> Connection {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE box_config (
id TEXT PRIMARY KEY,
name TEXT,
created_at INTEGER,
json TEXT NOT NULL
)",
)
.unwrap();
for (id, json) in rows {
conn.execute(
"INSERT INTO box_config (id, name, created_at, json) VALUES (?1, NULL, 0, ?2)",
rusqlite::params![id, json],
)
.unwrap();
}
conn
}
fn load_json(conn: &Connection, id: &str) -> serde_json::Value {
let json: String = conn
.query_row("SELECT json FROM box_config WHERE id = ?1", [id], |row| {
row.get(0)
})
.unwrap();
serde_json::from_str(&json).unwrap()
}
#[test]
fn drops_the_legacy_always_present_empty_policy() {
let conn = open_with_box_config(&[(
"ordinary",
r#"{"options":{"advanced":{"capabilities":{"add":[],"drop":[]},"privileged":false}}}"#,
)]);
DropAmbiguousEmptyCapabilities.run(&conn, None).unwrap();
let value = load_json(&conn, "ordinary");
assert!(value.pointer("/options/advanced/capabilities").is_none());
}
#[test]
fn keeps_a_real_non_empty_policy_untouched() {
let conn = open_with_box_config(&[(
"customized",
r#"{"options":{"advanced":{"capabilities":{"add":["SYS_ADMIN"],"drop":[]},"privileged":false}}}"#,
)]);
DropAmbiguousEmptyCapabilities.run(&conn, None).unwrap();
let value = load_json(&conn, "customized");
assert_eq!(
value.pointer("/options/advanced/capabilities/add"),
Some(&serde_json::json!(["SYS_ADMIN"]))
);
}
#[test]
fn keeps_a_legacy_privileged_boxs_all_shape_untouched() {
let conn = open_with_box_config(&[(
"legacy_privileged",
r#"{"options":{"advanced":{"capabilities":{"add":["ALL"],"drop":[]},"privileged":true}}}"#,
)]);
DropAmbiguousEmptyCapabilities.run(&conn, None).unwrap();
let value = load_json(&conn, "legacy_privileged");
assert_eq!(
value.pointer("/options/advanced/capabilities/add"),
Some(&serde_json::json!(["ALL"]))
);
}
#[test]
fn leaves_a_config_with_no_advanced_section_alone() {
let conn = open_with_box_config(&[("bare", r#"{"options":{}}"#)]);
DropAmbiguousEmptyCapabilities.run(&conn, None).unwrap();
let value = load_json(&conn, "bare");
assert!(value.pointer("/options/advanced").is_none());
}
}