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:
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))) => {
|
||||
|
||||
Reference in New Issue
Block a user