Support multiple passwords per breach

Also update password-less links to a breach if we insert a 2nd link that
contains a password between the same breach and email.
This commit is contained in:
kpcyrd
2019-02-21 16:22:09 +01:00
parent b48c8728fd
commit 8150ad9483
9 changed files with 172 additions and 59 deletions

View File

@@ -12,5 +12,5 @@ CREATE TABLE breach_emails (
password VARCHAR,
FOREIGN KEY(breach_id) REFERENCES breaches(id) ON DELETE CASCADE,
FOREIGN KEY(email_id) REFERENCES emails(id) ON DELETE CASCADE,
CONSTRAINT breach_emails_unique UNIQUE (breach_id, email_id)
CONSTRAINT breach_emails_unique UNIQUE (breach_id, email_id, password)
);

View File

@@ -45,6 +45,8 @@ pub enum Family {
Network,
NetworkDevice,
Account,
Breach,
BreachEmail,
}
impl FromStr for Family {
@@ -63,6 +65,8 @@ impl FromStr for Family {
"network" => Family::Network,
"network-device" => Family::NetworkDevice,
"account" => Family::Account,
"breach" => Family::Breach,
"breach-email" => Family::BreachEmail,
_ => bail!("Unknown object family"),
})
}
@@ -197,7 +201,7 @@ impl Database {
Insert::Breach(object) => self.insert_struct(NewBreach {
value: &object.value,
}),
Insert::BreachEmail(object) => self.insert_breach_email_struct(&NewBreachEmail {
Insert::BreachEmail(object) => self.insert_breach_email_struct(NewBreachEmail {
breach_id: object.breach_id,
email_id: object.email_id,
password: object.password.as_ref(),
@@ -251,14 +255,24 @@ impl Database {
}
}
pub fn insert_breach_email_struct(&self, breach_email: &NewBreachEmail) -> Result<Option<(DbChange, i32)>> {
if let Some(breach_email_id) = BreachEmail::get_id_opt(self, &(breach_email.breach_id, breach_email.email_id))? {
Ok(Some((DbChange::None, breach_email_id)))
pub fn insert_breach_email_struct(&self, obj: NewBreachEmail) -> Result<Option<(DbChange, i32)>> {
let password = obj.password.map(|x| x.clone());
if let Some(existing) = BreachEmail::get_opt(self, &(obj.breach_id, obj.email_id, password.clone()))? {
let id = <BreachEmail as Model>::id(&existing);
let update = obj.upsert(&existing);
if update.is_dirty() {
update.apply(&self)?;
Ok(Some((DbChange::Update(update.generic()), id)))
} else {
Ok(Some((DbChange::None, id)))
}
} else {
let value = &(obj.breach_id, obj.email_id, password);
diesel::insert_into(breach_emails::table)
.values(breach_email)
.values(obj)
.execute(&self.db)?;
let id = BreachEmail::get_id(self, &(breach_email.breach_id, breach_email.email_id))?;
let id = BreachEmail::get_id(self, value)?;
Ok(Some((DbChange::Insert, id)))
}
}
@@ -380,6 +394,8 @@ impl Database {
Family::Network => self.get_opt_typed::<Network>(&value),
Family::NetworkDevice => bail!("Unsupported operation"),
Family::Account => self.get_opt_typed::<Account>(&value),
Family::Breach => self.get_opt_typed::<Breach>(&value),
Family::BreachEmail => bail!("Unsupported operation"),
}
}

View File

@@ -116,11 +116,21 @@ impl Scopable for Breach {
}
impl Breach {
fn emails(&self, db: &Database) -> Result<Vec<Email>> {
let email_ids = BreachEmail::belonging_to(self).select(breach_emails::email_id);
emails::table
.filter(emails::id.eq_any(email_ids))
.load::<Email>(db.db())
fn emails(&self, db: &Database) -> Result<Vec<(Email, Option<String>)>> {
use std::result;
let email_id_pws = BreachEmail::belonging_to(self)
.select((breach_emails::email_id, breach_emails::password))
.load::<(i32, Option<String>)>(db.db())?;
email_id_pws.into_iter()
.map(|(email_id, password)| {
emails::table
.filter(emails::id.eq(email_id))
.first::<Email>(db.db())
.map(|email| (email, password))
})
.collect::<result::Result<Vec<_>, _>>()
.map_err(Error::from)
}
}
@@ -143,10 +153,25 @@ impl Printable<PrintableBreach> for Breach {
}
}
pub struct EmailWithPassword {
email: PrintableEmail,
password: Option<String>,
}
impl fmt::Display for EmailWithPassword {
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
write!(w, "{}", self.email)?;
if let Some(password) = &self.password {
write!(w, " ({:?})", password)?;
}
Ok(())
}
}
pub struct DetailedBreach {
id: i32,
value: String,
emails: Vec<PrintableEmail>,
emails: Vec<EmailWithPassword>,
unscoped: bool,
}
@@ -179,7 +204,10 @@ impl Detailed for Breach {
fn detailed(&self, db: &Database) -> Result<Self::T> {
let emails = self.emails(db)?.into_iter()
.map(|sd| sd.printable(db))
.map(|(sd, password)| Ok(EmailWithPassword {
email: sd.printable(db)?,
password,
}))
.collect::<Result<_>>()?;
Ok(DetailedBreach {

View File

@@ -16,7 +16,7 @@ pub struct BreachEmail {
}
impl Model for BreachEmail {
type ID = (i32, i32);
type ID = (i32, i32, Option<String>);
fn to_string(&self) -> String {
unimplemented!("BreachEmail can not be printed")
@@ -71,10 +71,18 @@ impl Model for BreachEmail {
fn get(db: &Database, query: &Self::ID) -> Result<Self> {
use crate::schema::breach_emails::dsl::*;
let (my_breach_id, my_email_id) = query;
let breach_email = breach_emails.filter(breach_id.eq(my_breach_id))
.filter(email_id.eq(my_email_id))
.first::<Self>(db.db())?;
let (my_breach_id, my_email_id, my_password) = query;
let query = breach_emails.filter(breach_id.eq(my_breach_id))
.filter(email_id.eq(my_email_id));
let breach_email = if let Some(my_password) = my_password {
query
.filter(password.is_null().or(password.eq(my_password)))
.first::<Self>(db.db())?
} else {
query
.first::<Self>(db.db())?
};
Ok(breach_email)
}
@@ -82,11 +90,20 @@ impl Model for BreachEmail {
fn get_opt(db: &Database, query: &Self::ID) -> Result<Option<Self>> {
use crate::schema::breach_emails::dsl::*;
let (my_breach_id, my_email_id) = query;
let breach_email = breach_emails.filter(breach_id.eq(my_breach_id))
.filter(email_id.eq(my_email_id))
.first::<Self>(db.db())
.optional()?;
let (my_breach_id, my_email_id, my_password) = query;
let query = breach_emails.filter(breach_id.eq(my_breach_id))
.filter(email_id.eq(my_email_id));
let breach_email = if let Some(my_password) = my_password {
query
.filter(password.is_null().or(password.eq(my_password)))
.first::<Self>(db.db())
.optional()?
} else {
query
.first::<Self>(db.db())
.optional()?
};
Ok(breach_email)
}

View File

@@ -117,11 +117,21 @@ impl Scopable for Email {
}
impl Email {
fn breaches(&self, db: &Database) -> Result<Vec<Breach>> {
let breach_ids = BreachEmail::belonging_to(self).select(breach_emails::breach_id);
breaches::table
.filter(breaches::id.eq_any(breach_ids))
.load::<Breach>(db.db())
fn breaches(&self, db: &Database) -> Result<Vec<(Breach, Option<String>)>> {
use std::result;
let breach_id_pws = BreachEmail::belonging_to(self)
.select((breach_emails::breach_id, breach_emails::password))
.load::<(i32, Option<String>)>(db.db())?;
breach_id_pws.into_iter()
.map(|(breach_id, password)| {
breaches::table
.filter(breaches::id.eq(breach_id))
.first::<Breach>(db.db())
.map(|breach| (breach, password))
})
.collect::<result::Result<Vec<_>, _>>()
.map_err(Error::from)
}
}
@@ -144,10 +154,25 @@ impl Printable<PrintableEmail> for Email {
}
}
pub struct BreachWithPassword {
breach: PrintableBreach,
password: Option<String>,
}
impl fmt::Display for BreachWithPassword {
fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result {
write!(w, "{}", self.breach)?;
if let Some(password) = &self.password {
write!(w, " ({:?})", password)?;
}
Ok(())
}
}
pub struct DetailedEmail {
id: i32,
value: String,
breaches: Vec<PrintableBreach>,
breaches: Vec<BreachWithPassword>,
unscoped: bool,
valid: Option<bool>,
}
@@ -192,7 +217,10 @@ impl Detailed for Email {
fn detailed(&self, db: &Database) -> Result<Self::T> {
let breaches = self.breaches(db)?.into_iter()
.map(|sd| sd.printable(db))
.map(|(sd, password)| Ok(BreachWithPassword {
breach: sd.printable(db)?,
password,
}))
.collect::<Result<_>>()?;
Ok(DetailedEmail {

View File

@@ -22,22 +22,35 @@ pub enum Insert {
}
impl Insert {
pub fn value(&self) -> &str {
match self {
Insert::Domain(x) => &x.value,
Insert::Subdomain(x) => &x.value,
Insert::IpAddr(x) => &x.value,
Insert::SubdomainIpAddr(_x) => unimplemented!("SubdomainIpAddr doesn't have value field"),
Insert::Url(x) => &x.value,
Insert::Email(x) => &x.value,
Insert::PhoneNumber(x) => &x.value,
Insert::Device(x) => &x.value,
Insert::Network(x) => &x.value,
Insert::NetworkDevice(_x) => unimplemented!("NetworkDevice doesn't have value field"),
Insert::Account(x) => &x.value,
Insert::Breach(x) => &x.value,
Insert::BreachEmail(_x) => unimplemented!("BreachEmail doesn't have value field"),
}
pub fn label(&self, db: &Database) -> Result<String> {
let label = match self {
Insert::Domain(x) => format!("{:?}", x.value),
Insert::Subdomain(x) => format!("{:?}", x.value),
Insert::IpAddr(x) => format!("{:?}", x.value),
Insert::SubdomainIpAddr(x) => {
let subdomain = Subdomain::by_id(db, x.subdomain_id)?;
let ipaddr = IpAddr::by_id(db, x.ip_addr_id)?;
format!("{:?}+{:?}", subdomain.value, ipaddr.value)
},
Insert::Url(x) => format!("{:?}", x.value),
Insert::Email(x) => format!("{:?}", x.value),
Insert::PhoneNumber(x) => format!("{:?}", x.value),
Insert::Device(x) => format!("{:?}", x.value),
Insert::Network(x) => format!("{:?}", x.value),
Insert::NetworkDevice(x) => {
let network = Network::by_id(db, x.network_id)?;
let device = Device::by_id(db, x.device_id)?;
format!("{:?}+{:?}", network.value, device.value)
},
Insert::Account(x) => format!("{:?}", x.value),
Insert::Breach(x) => format!("{:?}", x.value),
Insert::BreachEmail(x) => {
let breach = Breach::by_id(db, x.breach_id)?;
let email = Email::by_id(db, x.email_id)?;
format!("{:?}+{:?}", breach.value, email.value)
}
};
Ok(label)
}
pub fn table(&self) -> &str {

View File

@@ -3,7 +3,6 @@ use crate::fmt::colors::*;
use diesel;
use diesel::prelude::*;
use crate::models::*;
use std::result;
#[derive(Identifiable, Queryable, Serialize, Deserialize, PartialEq, Debug)]
@@ -119,16 +118,11 @@ impl Scopable for Network {
impl Network {
fn devices(&self, db: &Database) -> Result<Vec<Device>> {
let device_ids = NetworkDevice::belonging_to(self)
.select(network_devices::device_id)
.load::<i32>(db.db())?;
let device_ids = NetworkDevice::belonging_to(self).select(network_devices::device_id);
device_ids.into_iter()
.map(|device_id| devices::table
.filter(devices::id.eq(device_id))
.first::<Device>(db.db())
)
.collect::<result::Result<_, _>>()
devices::table
.filter(devices::id.eq_any(device_ids))
.load::<Device>(db.db())
.map_err(Error::from)
}
}

View File

@@ -53,6 +53,12 @@ fn into_insert(family: Family, object: LuaJsonValue) -> Result<Insert> {
Family::Account => {
Insert::Account(try_into_new::<InsertAccount>(object)?)
},
Family::Breach => {
Insert::Breach(try_into_new::<InsertBreach>(object)?)
},
Family::BreachEmail => {
Insert::BreachEmail(try_into_new::<InsertBreachEmail>(object)?)
},
};
Ok(obj)
}
@@ -128,7 +134,7 @@ pub fn db_update(lua: &mut hlua::Lua, state: Arc<State>) {
.map(|(id, v, u)| (id, v, Update::Subdomain(u))),
Family::IpAddr => gen_changeset::<IpAddr, IpAddrUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::IpAddr(u))),
Family::SubdomainIpAddr => bail!("Unsupported operation"),
Family::SubdomainIpAddr => bail!("Subdomain-IpAddr doesn't have mutable fields"),
Family::Url => gen_changeset::<Url, UrlUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Url(u))),
Family::Email => gen_changeset::<Email, EmailUpdate>(object, update)
@@ -143,6 +149,9 @@ pub fn db_update(lua: &mut hlua::Lua, state: Arc<State>) {
.map(|(id, v, u)| (id, v, Update::NetworkDevice(u))),
Family::Account => gen_changeset::<Account, AccountUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::Account(u))),
Family::Breach => bail!("Breach doesn't have mutable fields"),
Family::BreachEmail => gen_changeset::<BreachEmail, BreachEmailUpdate>(object, update)
.map(|(id, v, u)| (id, v, Update::BreachEmail(u))),
};
let (id, value, update) = update

View File

@@ -153,7 +153,15 @@ impl DatabaseEvent {
}
// TODO: replace id with actual object(?)
spinner.log(&format!("Updating {:?} ({})", object.value(), update));
match object.label(&db) {
Ok(label) => {
spinner.log(&format!("Updating {} ({})", label, update));
},
Err(err) => {
// TODO: this should be unreachable
spinner.error(&format!("Failed to get label for {:?}: {:?}", object, err));
},
}
Ok(Some(id))
},
Ok(Some((DbChange::None, id))) => {