use std::io::Write;
use sqlx::Database;
use crate::core::SqlValue;
use crate::sql::connect_diagnosis::{redact, SEALED_SCHEME};
use crate::sql::FetcherPool as _;
use crate::tenancy::db_server::{self, Resealed};
use crate::tenancy::error::TenancyError;
use crate::tenancy::org_edit::apply_values_if;
use crate::tenancy::pools::TenantPools;
use crate::tenancy::{Org, SecretsResolver as _};
pub(super) async fn reseal_tenant_secrets<W: Write + Send, DB: Database>(
pools: &TenantPools<DB>,
args: &[String],
w: &mut W,
) -> Result<(), TenancyError>
where
crate::sql::Pool: From<sqlx::Pool<DB>>,
{
let apply = match args {
[] => false,
[flag] if flag == "--apply" => true,
_ => {
return Err(TenancyError::Validation(
"usage: reseal-tenant-secrets [--apply]".into(),
))
}
};
if crate::aead::key_status() == crate::aead::KeyStatus::Missing {
return Err(TenancyError::Validation(format!(
"{} is not set or blank, so nothing can be sealed",
crate::aead::KEY_VAR
)));
}
let secrets = pools.secrets().clone().require_sealed();
let registry = pools.registry_pool();
let mut orgs: Vec<Org> = Org::objects().fetch(®istry).await?;
orgs.sort_by(|a, b| a.slug.cmp(&b.slug));
let (mut todo, mut done, mut skipped, mut failed) = (0usize, 0usize, 0usize, 0usize);
for org in orgs {
let Some(stored) = org.database_url.as_deref() else {
continue;
};
let sealed = stored.starts_with(SEALED_SCHEME);
let resolved = match secrets.resolve(stored).await {
Ok(url) => url,
Err(e) if sealed => {
failed += 1;
writeln!(w, " {}: cannot open: {e}", org.slug)?;
continue;
}
Err(_) => continue,
};
let has_password = redact(&resolved) != resolved;
let literal_with_password = resolved == stored && has_password;
if (sealed && !has_password) || (!sealed && !literal_with_password) {
continue;
}
todo += 1;
let action = if sealed { "re-seal" } else { "seal" };
if !apply {
writeln!(w, " {}: would {action}", org.slug)?;
continue;
}
let values = vec![("database_url", SqlValue::String(resolved))];
match apply_values_if(®istry, &secrets, &org.slug, values, Some(stored)).await {
Ok(Some(_)) => {
done += 1;
writeln!(w, " {}: {action}ed", org.slug)?;
}
Ok(None) => {
skipped += 1;
writeln!(w, " {}: skipped, changed meanwhile", org.slug)?;
}
Err(e) => {
failed += 1;
writeln!(w, " {}: failed: {e}", org.slug)?;
}
}
}
for server in db_server::list(®istry).await? {
if !server.has_password() {
continue;
}
todo += 1;
let name = &server.name;
match db_server::reseal_password(®istry, &server, apply).await {
Resealed::Would => writeln!(w, " server {name}: would re-seal")?,
Resealed::Done => {
done += 1;
writeln!(w, " server {name}: re-sealed")?;
}
Resealed::Skipped => {
skipped += 1;
writeln!(w, " server {name}: skipped, changed meanwhile")?;
}
Resealed::Failed(e) => {
failed += 1;
writeln!(w, " server {name}: failed: {e}")?;
}
}
}
#[cfg(feature = "sso")]
{
let mut tables: Vec<(String, Result<Vec<SsoSecret>, String>, crate::sql::Pool)> =
Vec::new();
#[cfg(feature = "admin-sso")]
{
use crate::tenancy::sso::SharedSsoProvider as P;
let rows = SsoSecret::list(
P::objects().values_list(SsoSecret::COLUMNS),
<P as crate::core::Model>::SCHEMA,
®istry,
)
.await;
tables.push(("shared".into(), rows, registry.clone()));
}
let mut orgs: Vec<Org> = Org::objects().fetch(®istry).await?;
orgs.sort_by(|a, b| a.slug.cmp(&b.slug));
for org in orgs {
use crate::sso::SsoProvider as P;
let Ok(pool) = pools.scoped_pool_dyn(&org).await else {
failed += 1;
writeln!(w, " sso {}: cannot reach the tenant", org.slug)?;
continue;
};
let rows = SsoSecret::list(
P::objects().values_list(SsoSecret::COLUMNS),
<P as crate::core::Model>::SCHEMA,
&pool,
)
.await;
tables.push((org.slug, rows, pool));
}
for (owner, rows, pool) in tables {
let rows = match rows {
Ok(r) => r,
Err(e)
if e.contains("does not exist")
|| e.contains("no such table")
|| e.contains("doesn't exist") =>
{
continue
}
Err(e) => {
failed += 1;
writeln!(w, " sso {owner}: cannot open: {e}")?;
continue;
}
};
for row in rows {
todo += 1;
let what = format!("sso {owner}/{}", row.slug);
if !apply {
writeln!(w, " {what}: would re-seal")?;
continue;
}
match row.reseal(&pool).await {
Ok(true) => {
done += 1;
writeln!(w, " {what}: re-sealed")?;
}
Ok(false) => {
skipped += 1;
writeln!(w, " {what}: skipped, changed meanwhile")?;
}
Err(e) => {
failed += 1;
writeln!(w, " {what}: failed: {e}")?;
}
}
}
}
}
if apply {
writeln!(
w,
"reseal-tenant-secrets: {done} done, {skipped} skipped, {failed} failed"
)?;
} else {
writeln!(
w,
"reseal-tenant-secrets: {todo} to change (dry run; pass --apply)"
)?;
}
if failed > 0 {
return Err(TenancyError::Validation(format!(
"{failed} secret(s) could not be resealed"
)));
}
Ok(())
}
#[cfg(feature = "sso")]
struct SsoSecret {
id: i64,
slug: String,
stored: String,
model: &'static crate::core::ModelSchema,
}
#[cfg(feature = "sso")]
impl SsoSecret {
const COLUMNS: &'static [&'static str] = &["id", "slug", "client_secret"];
async fn list<T: crate::core::Model>(
rows: crate::query::ValuesListQuerySet<T>,
model: &'static crate::core::ModelSchema,
pool: &crate::sql::Pool,
) -> Result<Vec<Self>, String> {
let rows = rows.fetch(pool).await.map_err(|e| e.to_string())?;
Ok(rows
.into_iter()
.filter_map(|r| match r.as_slice() {
[SqlValue::I64(id), SqlValue::String(slug), SqlValue::String(stored)] => {
Some(Self {
id: *id,
slug: slug.clone(),
stored: stored.clone(),
model,
})
}
_ => None,
})
.collect())
}
async fn reseal(&self, pool: &crate::sql::Pool) -> Result<bool, String> {
use crate::core::{Assignment, Filter, Op, UpdateQuery, WhereExpr};
let plain =
crate::aead::decrypt(&self.stored, b"").map_err(|e| format!("cannot open: {e:?}"))?;
let sealed =
crate::aead::encrypt(&plain, b"").map_err(|e| format!("cannot seal: {e:?}"))?;
let seen = Filter {
column: "client_secret",
op: Op::Eq,
value: SqlValue::String(self.stored.clone()),
};
let query = UpdateQuery {
model: self.model,
set: vec![Assignment {
column: "client_secret",
value: SqlValue::String(sealed).into(),
}],
where_clause: WhereExpr::and_predicates(vec![
Filter {
column: "id",
op: Op::Eq,
value: SqlValue::I64(self.id),
},
seen,
]),
};
crate::sql::update_pool(pool, &query)
.await
.map(|n| n > 0)
.map_err(|e| e.to_string())
}
}