use anyhow::{Context, Result};
use diesel::prelude::*;
use diesel::result::Error;
pub mod models;
mod schema;
use models::*;
use schema::*;
use crate::pgp;
pub(crate) struct OcaDb {
url: String,
conn: SqliteConnection,
}
impl OcaDb {
pub(crate) fn new(db_url: &str) -> Result<Self> {
let conn =
SqliteConnection::establish(db_url).context(format!("Error connecting to {db_url}"))?;
diesel::sql_query("PRAGMA foreign_keys=1;")
.execute(&conn)
.context("Couldn't set 'PRAGMA foreign_keys=1;'")?;
Ok(OcaDb {
conn,
url: db_url.to_string(),
})
}
pub(crate) fn url(&self) -> &str {
&self.url
}
pub(crate) fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
where
F: FnOnce() -> Result<T, E>,
E: From<Error>,
{
self.conn.transaction(f)
}
pub(crate) fn vacuum(&self) -> Result<()> {
diesel::sql_query("VACUUM;")
.execute(&self.conn)
.context("Error while running 'VACUUM;'")?;
Ok(())
}
fn user_insert(&self, user: NewUser) -> Result<User> {
let inserted_count = diesel::insert_into(users::table)
.values(&user)
.execute(&self.conn)?;
if inserted_count != 1 {
return Err(anyhow::anyhow!(
"insert_user: insert should return count '1'"
));
}
let u: Vec<User> = users::table
.order(users::id.desc())
.limit(inserted_count as i64)
.load(&self.conn)?
.into_iter()
.rev()
.collect();
if u.len() == 1 {
Ok(u[0].clone())
} else {
Err(anyhow::anyhow!("insert_user: unexpected insert failure"))
}
}
fn cert_insert(&self, cert: NewCert) -> Result<Cert> {
let inserted_count = diesel::insert_into(certs::table)
.values(&cert)
.execute(&self.conn)?;
if inserted_count != 1 {
return Err(anyhow::anyhow!(
"insert_cert: insert should return count '1'"
));
}
let c: Vec<Cert> = certs::table
.order(certs::id.desc())
.limit(inserted_count as i64)
.load(&self.conn)?
.into_iter()
.rev()
.collect();
if c.len() == 1 {
Ok(c[0].clone())
} else {
Err(anyhow::anyhow!("insert_cert: unexpected insert failure"))
}
}
fn revocation_insert(&self, revoc: NewRevocation) -> Result<Revocation> {
let inserted_count = diesel::insert_into(revocations::table)
.values(&revoc)
.execute(&self.conn)?;
if inserted_count != 1 {
return Err(anyhow::anyhow!(
"insert_revocation: insert should return count '1'"
));
}
let r: Vec<Revocation> = revocations::table
.order(revocations::id.desc())
.limit(inserted_count as i64)
.load(&self.conn)?
.into_iter()
.rev()
.collect();
if r.len() == 1 {
Ok(r[0].clone())
} else {
Err(anyhow::anyhow!(
"insert_revocation: unexpected insert failure"
))
}
}
fn email_insert(&self, email: NewCertEmail) -> Result<CertEmail> {
let inserted_count = diesel::insert_into(certs_emails::table)
.values(&email)
.execute(&self.conn)
.context("Error saving new email")?;
if inserted_count != 1 {
return Err(anyhow::anyhow!(
"insert_email: insert should return count '1'"
));
}
let e: Vec<CertEmail> = certs_emails::table
.order(certs_emails::id.desc())
.limit(inserted_count as i64)
.load(&self.conn)?
.into_iter()
.collect();
if e.len() != 1 {
return Err(anyhow::anyhow!(
"insert_email: unexpected insert failure [emails]"
));
}
Ok(e[0].clone())
}
pub(crate) fn queue_by_id(&self, id: i32) -> Result<Option<Queue>> {
let mut db: Vec<Queue> = queue::table
.filter(queue::id.eq(id))
.load::<Queue>(&self.conn)
.context("Error loading Queue by id")?;
match db.len() {
0 => Ok(None),
1 => Ok(Some(db.pop().unwrap())),
_ => Err(anyhow::anyhow!("queue_by_id: expected 0 or 1 entries")),
}
}
pub(crate) fn queue_insert(&self, q: NewQueue) -> Result<()> {
let inserted_count = diesel::insert_into(queue::table)
.values(&q)
.execute(&self.conn)
.context("Error saving new queue entry")?;
if inserted_count != 1 {
return Err(anyhow::anyhow!(
"queue_insert: insert should return count '1'"
));
}
Ok(())
}
pub(crate) fn queue_not_done(&self) -> Result<Vec<Queue>> {
queue::table
.filter(queue::done.eq(false))
.order(queue::id)
.load::<Queue>(&self.conn)
.context("Error loading queue entries")
}
pub(crate) fn queue_update(&self, queue: &Queue) -> Result<()> {
diesel::update(queue)
.set(queue)
.execute(&self.conn)
.context("Error updating Queue")?;
Ok(())
}
pub(crate) fn is_ca_initialized(&self) -> Result<bool> {
let cas = cas::table
.load::<Ca>(&self.conn)
.context("Error loading CAs")?;
Ok(cas.len() == 1)
}
pub(crate) fn get_ca(&self) -> Result<(Ca, Cacert)> {
let cas = cas::table
.load::<Ca>(&self.conn)
.context("Error loading CAs")?;
match cas.len() {
0 => Err(anyhow::anyhow!("CA is not initialized")),
1 => {
let ca = cas[0].clone();
let ca_certs: Vec<_> = cacerts::table
.filter(cacerts::ca_id.eq(ca.id))
.filter(cacerts::active)
.load::<Cacert>(&self.conn)
.context("Error loading CA Certs")?;
match ca_certs.len() {
0 => Err(anyhow::anyhow!("No CA cert found")),
1 => Ok((ca, ca_certs[0].to_owned())),
_ => {
Err(anyhow::anyhow!(
"More than one active cacert in DB, illegal state."
))
}
}
}
_ => Err(anyhow::anyhow!(
"More than one CA in database, this should never happen."
)),
}
}
pub(crate) fn ca_insert(
&self,
domainname: &str,
ca_key: &str,
fingerprint: &str,
backend: Option<&str>,
) -> Result<()> {
let ca = NewCa { domainname };
diesel::insert_into(cas::table)
.values(&ca)
.execute(&self.conn)
.context("Error saving new CA")?;
let cas = cas::table
.load::<Ca>(&self.conn)
.context("Error loading CAs")?;
let ca = cas.first().unwrap();
let ca_cert = NewCacert {
fingerprint,
ca_id: ca.id,
priv_cert: ca_key.to_string(),
backend,
active: true,
};
self.cacert_insert(&ca_cert)?;
Ok(())
}
pub(crate) fn cacerts_delete(&self) -> Result<()> {
diesel::delete(cacerts::table)
.execute(&self.conn)
.context("Error while deleting cacerts entries")?;
Ok(())
}
pub(crate) fn cacert_insert(&self, ca_cert: &NewCacert) -> Result<()> {
diesel::insert_into(cacerts::table)
.values(ca_cert)
.execute(&self.conn)
.context("Error saving new CA Cert")?;
Ok(())
}
pub(crate) fn cacert_update(&self, cacert: &Cacert) -> Result<()> {
diesel::update(cacert)
.set(cacert)
.execute(&self.conn)
.context("Error updating CaCert")?;
Ok(())
}
pub(crate) fn ca_import_tsig(&self, cert: &[u8]) -> Result<()> {
let (_, mut ca_cert) = self
.get_ca()
.context("Failed to load CA cert from database")?;
let ca = pgp::to_cert(ca_cert.priv_cert.as_bytes())?;
let cert_import = pgp::to_cert(cert)?;
let joined = pgp::merge_in_tsigs(ca, cert_import)?;
ca_cert.priv_cert =
pgp::cert_to_armored_private_key(&joined).context("Failed to re-armor CA Cert")?;
self.cacert_update(&ca_cert)
.context("Update of CA Cert in DB failed")
}
pub(crate) fn users_sorted_by_name(&self) -> Result<Vec<User>> {
users::table
.order((users::name, users::id))
.load::<User>(&self.conn)
.context("Error loading users")
}
pub(crate) fn user_by_cert(&self, cert: &Cert) -> Result<Option<User>> {
match cert.user_id {
None => Ok(None),
Some(search_id) => {
let users = users::table
.filter(users::id.eq(search_id))
.load::<User>(&self.conn)?;
match users.len() {
0 => Ok(None),
1 => Ok(Some(users[0].clone())),
_ => {
Err(anyhow::anyhow!(
"get_user_by_cert: Found more than one user for cert"
))
}
}
}
}
}
pub(crate) fn user_add(
&self,
name: Option<&str>,
(pub_cert, fingerprint): (&str, &str),
emails: &[&str],
revocation_certs: &[String],
) -> Result<User> {
let (ca, _) = self.get_ca().context("Couldn't find CA")?;
let user = self.user_insert(NewUser { name, ca_id: ca.id })?;
let cert = self.cert_add(pub_cert, fingerprint, Some(user.id))?;
for revocation in revocation_certs {
let hash = &pgp::revocation_to_hash(revocation.as_bytes())?;
self.revocation_insert(NewRevocation {
hash,
revocation,
cert_id: cert.id,
published: false,
})?;
}
for &addr in emails {
self.email_insert(NewCertEmail {
addr: addr.to_owned(),
cert_id: cert.id,
})?;
}
Ok(user)
}
#[allow(dead_code)]
pub(crate) fn user_update(&self, user: &User) -> Result<()> {
diesel::update(user)
.set(user)
.execute(&self.conn)
.context("Error updating User")?;
Ok(())
}
pub(crate) fn cert_add(
&self,
pub_cert: &str,
fingerprint: &str,
user_id: Option<i32>,
) -> Result<Cert> {
let cert = NewCert {
pub_cert,
fingerprint,
delisted: false,
inactive: false,
user_id,
};
self.cert_insert(cert)
}
pub fn cert_update(&self, cert: &Cert) -> Result<()> {
diesel::update(cert)
.set(cert)
.execute(&self.conn)
.context("Error updating Cert")?;
Ok(())
}
pub fn cert_by_id(&self, id: i32) -> Result<Option<Cert>> {
let db: Vec<Cert> = certs::table
.filter(certs::id.eq(id))
.load::<Cert>(&self.conn)
.context("Error loading Cert by id")?;
Ok(db.first().cloned())
}
pub(crate) fn cert_by_fp(&self, fingerprint: &str) -> Result<Option<Cert>> {
let c = certs::table
.filter(certs::fingerprint.eq(fingerprint))
.load::<Cert>(&self.conn)
.context("Error loading Cert by fingerprint")?;
match c.len() {
0 => Ok(None),
1 => Ok(Some(c[0].clone())),
_ => Err(anyhow::anyhow!("get_cert: expected 0 or 1 cert")),
}
}
pub(crate) fn certs_by_email(&self, email: &str) -> Result<Vec<Cert>> {
let cert_ids = certs_emails::table
.filter(certs_emails::addr.eq(email))
.select(certs_emails::cert_id);
certs::table
.filter(certs::id.eq_any(cert_ids))
.load::<Cert>(&self.conn)
.context("could not load certs")
}
pub(crate) fn certs_by_user(&self, user: &User) -> Result<Vec<Cert>> {
Ok(Cert::belonging_to(user)
.order(certs::id)
.load::<Cert>(&self.conn)?)
}
pub(crate) fn certs(&self) -> Result<Vec<Cert>> {
certs::table
.load::<Cert>(&self.conn)
.context("Error loading certs")
}
pub(crate) fn revocations_by_cert(&self, cert: &Cert) -> Result<Vec<Revocation>> {
Ok(Revocation::belonging_to(cert).load::<Revocation>(&self.conn)?)
}
pub(crate) fn revocation_add(&self, revocation: &str, cert: &Cert) -> Result<Revocation> {
let hash = &pgp::revocation_to_hash(revocation.as_bytes())?;
self.revocation_insert(NewRevocation {
hash,
revocation,
cert_id: cert.id,
published: false,
})
}
pub(crate) fn revocation_exists(&self, revocation: &[u8]) -> Result<bool> {
let hash = &pgp::revocation_to_hash(revocation)?;
Ok(self.revocation_by_hash(hash)?.is_some())
}
pub(crate) fn revocation_by_hash(&self, hash: &str) -> Result<Option<Revocation>> {
let db: Vec<Revocation> = revocations::table
.filter(revocations::hash.eq(hash))
.load::<Revocation>(&self.conn)
.context("Error loading Revocation by hash")?;
assert!(
db.len() <= 1,
"unexpected duplicate hash in revocations table"
);
Ok(db.first().cloned())
}
pub(crate) fn revocation_update(&self, revocation: &Revocation) -> Result<()> {
diesel::update(revocation)
.set(revocation)
.execute(&self.conn)
.context("Error updating Revocation")?;
Ok(())
}
pub(crate) fn emails_by_cert(&self, cert: &Cert) -> Result<Vec<CertEmail>> {
certs_emails::table
.filter(certs_emails::cert_id.eq(cert.id))
.load(&self.conn)
.context("could not load emails")
}
pub(crate) fn emails(&self) -> Result<Vec<CertEmail>> {
certs_emails::table
.load(&self.conn)
.context("could not load emails")
}
pub(crate) fn bridge_insert(&self, bridge: NewBridge) -> Result<Bridge> {
let inserted_count = diesel::insert_into(bridges::table)
.values(&bridge)
.execute(&self.conn)
.context("Error saving new bridge")?;
if inserted_count != 1 {
return Err(anyhow::anyhow!(
"insert_user: insert should return count '1'"
));
}
let b: Vec<Bridge> = bridges::table
.order(bridges::id.desc())
.limit(inserted_count as i64)
.load(&self.conn)?
.into_iter()
.rev()
.collect();
if b.len() == 1 {
Ok(b[0].clone())
} else {
Err(anyhow::anyhow!("insert_user: unexpected insert failure"))
}
}
pub(crate) fn bridge_by_email(&self, email: &str) -> Result<Option<Bridge>> {
let res = bridges::table
.filter(bridges::email.eq(email))
.load::<Bridge>(&self.conn)
.context("Error loading bridge")?;
match res.len() {
0 => Ok(None),
1 => Ok(Some(res[0].clone())),
_ => Err(anyhow::anyhow!(format!(
"search_bridge for {} found {} results, expected <=1. \
(Database constraints should make this impossible)",
email,
res.len()
))),
}
}
pub(crate) fn list_bridges(&self) -> Result<Vec<Bridge>> {
bridges::table
.load::<Bridge>(&self.conn)
.context("Error loading bridges")
}
pub(crate) fn diesel_migrations_run(&self) {
embed_migrations!();
embedded_migrations::run(&self.conn).unwrap_or_else(|e| {
panic!("failed to configure database, error {e}");
});
}
}