Add support for multiple simultaneous database features by using macros.

Diesel requires the following changes:
- Separate connection and pool types per connection, the generate_connections! macro generates an enum with a variant per db type
- Separate migrations and schemas, these were always imported as one type depending on db feature, now they are all imported under different module names
- Separate model objects per connection, the db_object! macro generates one object for each connection with the diesel macros, a generic object, and methods to convert between the connection-specific and the generic ones
- Separate connection queries, the db_run! macro allows writing only one that gets compiled for all databases or multiple ones
pull/1115/head
Daniel García 4 years ago
parent 19889187a5
commit 0365b7c6a4
No known key found for this signature in database
GPG Key ID: FC8A7D14C3CD543A

33
Cargo.lock generated

@ -152,6 +152,7 @@ dependencies = [
"oath", "oath",
"once_cell", "once_cell",
"openssl", "openssl",
"paste",
"percent-encoding 2.1.0", "percent-encoding 2.1.0",
"rand 0.7.3", "rand 0.7.3",
"regex", "regex",
@ -274,9 +275,9 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.13" version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b"
dependencies = [ dependencies = [
"num-integer", "num-integer",
"num-traits", "num-traits",
@ -295,9 +296,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "2.33.2" version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10040cdf04294b565d9e0319955430099ec3813a64c952b86a41200ad714ae48" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"atty", "atty",
@ -781,14 +782,14 @@ dependencies = [
[[package]] [[package]]
name = "handlebars" name = "handlebars"
version = "3.3.0" version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86dbc8a0746b08f363d2e00da48e6c9ceb75c198ac692d2715fcbb5bee74c87d" checksum = "5deefd4816fb852b1ff3cb48f6c41da67be2d0e1d20b26a7a3b076da11f064b1"
dependencies = [ dependencies = [
"log 0.4.11", "log 0.4.11",
"pest", "pest",
"pest_derive", "pest_derive",
"quick-error", "quick-error 2.0.0",
"serde", "serde",
"serde_json", "serde_json",
"walkdir", "walkdir",
@ -1360,7 +1361,7 @@ dependencies = [
"log 0.4.11", "log 0.4.11",
"mime 0.3.16", "mime 0.3.16",
"mime_guess", "mime_guess",
"quick-error", "quick-error 1.2.3",
"rand 0.6.5", "rand 0.6.5",
"safemem", "safemem",
"tempfile", "tempfile",
@ -1519,9 +1520,9 @@ checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.4.0" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
@ -1628,6 +1629,12 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "paste"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ddc8e145de01d9180ac7b78b9676f95a9c2447f6a88b2c2a04702211bc5d71"
[[package]] [[package]]
name = "pear" name = "pear"
version = "0.1.4" version = "0.1.4"
@ -1873,6 +1880,12 @@ version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quick-error"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ac73b1112776fc109b2e61909bc46c7e1bf0d7f690ffb1676553acce16d5cda"
[[package]] [[package]]
name = "quote" name = "quote"
version = "0.6.13" version = "0.6.13"

@ -123,6 +123,9 @@ structopt = "0.3.16"
# Logging panics to logfile instead stderr only # Logging panics to logfile instead stderr only
backtrace = "0.3.50" backtrace = "0.3.50"
# Macro ident concatenation
paste = "1.0"
[patch.crates-io] [patch.crates-io]
# Use newest ring # Use newest ring
rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '1010f6a2a88fac899dec0cd2f642156908038a53' } rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '1010f6a2a88fac899dec0cd2f642156908038a53' }

@ -1,13 +1,14 @@
use std::process::Command; use std::process::Command;
use std::env; use std::env;
fn main() { fn main() {
#[cfg(all(feature = "sqlite", feature = "mysql"))] // This allow using #[cfg(sqlite)] instead of #[cfg(feature = "sqlite")], which helps when trying to add them through macros
compile_error!("Can't enable both sqlite and mysql at the same time"); #[cfg(feature = "sqlite")]
#[cfg(all(feature = "sqlite", feature = "postgresql"))] println!("cargo:rustc-cfg=sqlite");
compile_error!("Can't enable both sqlite and postgresql at the same time"); #[cfg(feature = "mysql")]
#[cfg(all(feature = "mysql", feature = "postgresql"))] println!("cargo:rustc-cfg=mysql");
compile_error!("Can't enable both mysql and postgresql at the same time"); #[cfg(feature = "postgresql")]
println!("cargo:rustc-cfg=postgresql");
#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))] #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
compile_error!("You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite"); compile_error!("You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite");

@ -15,7 +15,7 @@ use crate::{
api::{ApiResult, EmptyResult, JsonResult}, api::{ApiResult, EmptyResult, JsonResult},
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}, auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
config::ConfigBuilder, config::ConfigBuilder,
db::{backup_database, models::*, DbConn}, db::{backup_database, models::*, DbConn, DbConnType},
error::{Error, MapResult}, error::{Error, MapResult},
mail, mail,
util::get_display_size, util::get_display_size,
@ -48,8 +48,12 @@ pub fn routes() -> Vec<Route> {
] ]
} }
static CAN_BACKUP: Lazy<bool> = static CAN_BACKUP: Lazy<bool> = Lazy::new(|| {
Lazy::new(|| cfg!(feature = "sqlite") && Command::new("sqlite3").arg("-version").status().is_ok()); DbConnType::from_url(&CONFIG.database_url())
.map(|t| t == DbConnType::sqlite)
.unwrap_or(false)
&& Command::new("sqlite3").arg("-version").status().is_ok()
});
#[get("/")] #[get("/")]
fn admin_disabled() -> &'static str { fn admin_disabled() -> &'static str {

@ -5,6 +5,7 @@ use once_cell::sync::Lazy;
use reqwest::Url; use reqwest::Url;
use crate::{ use crate::{
db::DbConnType,
error::Error, error::Error,
util::{get_env, get_env_bool}, util::{get_env, get_env_bool},
}; };
@ -421,20 +422,9 @@ make_config! {
} }
fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
let db_url = cfg.database_url.to_lowercase();
if cfg!(feature = "sqlite")
&& (db_url.starts_with("mysql:") || db_url.starts_with("postgresql:") || db_url.starts_with("postgres:"))
{
err!("`DATABASE_URL` is meant for MySQL or Postgres, while this server is meant for SQLite")
}
if cfg!(feature = "mysql") && !db_url.starts_with("mysql:") { // Validate connection URL is valid and DB feature is enabled
err!("`DATABASE_URL` should start with mysql: when using the MySQL server") DbConnType::from_url(&cfg.database_url)?;
}
if cfg!(feature = "postgresql") && !(db_url.starts_with("postgresql:") || db_url.starts_with("postgres:")) {
err!("`DATABASE_URL` should start with postgresql: when using the PostgreSQL server")
}
let dom = cfg.domain.to_lowercase(); let dom = cfg.domain.to_lowercase();
if !dom.starts_with("http://") && !dom.starts_with("https://") { if !dom.starts_with("http://") && !dom.starts_with("https://") {

@ -1,51 +1,203 @@
use std::process::Command; use std::process::Command;
use chrono::prelude::*; use chrono::prelude::*;
use diesel::{r2d2, r2d2::ConnectionManager, Connection as DieselConnection, ConnectionError}; use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use rocket::{ use rocket::{
http::Status, http::Status,
request::{FromRequest, Outcome}, request::{FromRequest, Outcome},
Request, State, Request, State,
}; };
use crate::{error::Error, CONFIG}; use crate::{
error::{Error, MapResult},
/// An alias to the database connection used CONFIG,
#[cfg(feature = "sqlite")] };
type Connection = diesel::sqlite::SqliteConnection;
#[cfg(feature = "mysql")]
type Connection = diesel::mysql::MysqlConnection;
#[cfg(feature = "postgresql")]
type Connection = diesel::pg::PgConnection;
/// An alias to the type for a pool of Diesel connections.
type Pool = r2d2::Pool<ConnectionManager<Connection>>;
/// Connection request guard type: a wrapper around an r2d2 pooled connection.
pub struct DbConn(pub r2d2::PooledConnection<ConnectionManager<Connection>>);
pub mod models; #[cfg(sqlite)]
#[cfg(feature = "sqlite")]
#[path = "schemas/sqlite/schema.rs"] #[path = "schemas/sqlite/schema.rs"]
pub mod schema; pub mod __sqlite_schema;
#[cfg(feature = "mysql")]
#[cfg(mysql)]
#[path = "schemas/mysql/schema.rs"] #[path = "schemas/mysql/schema.rs"]
pub mod schema; pub mod __mysql_schema;
#[cfg(feature = "postgresql")]
#[cfg(postgresql)]
#[path = "schemas/postgresql/schema.rs"] #[path = "schemas/postgresql/schema.rs"]
pub mod schema; pub mod __postgresql_schema;
// This is used to generate the main DbConn and DbPool enums, which contain one variant for each database supported
macro_rules! generate_connections {
( $( $name:ident: $ty:ty ),+ ) => {
#[allow(non_camel_case_types, dead_code)]
#[derive(Eq, PartialEq)]
pub enum DbConnType { $( $name, )+ }
#[allow(non_camel_case_types)]
pub enum DbConn { $( #[cfg($name)] $name(PooledConnection<ConnectionManager< $ty >>), )+ }
#[allow(non_camel_case_types)]
pub enum DbPool { $( #[cfg($name)] $name(Pool<ConnectionManager< $ty >>), )+ }
impl DbPool {
// For the given database URL, guess it's type, run migrations create pool and return it
pub fn from_config() -> Result<Self, Error> {
let url = CONFIG.database_url();
let conn_type = DbConnType::from_url(&url)?;
match conn_type { $(
DbConnType::$name => {
#[cfg($name)]
{
paste::paste!{ [< $name _migrations >]::run_migrations(); }
let manager = ConnectionManager::new(&url);
let pool = Pool::builder().build(manager).map_res("Failed to create pool")?;
return Ok(Self::$name(pool));
}
#[cfg(not($name))]
#[allow(unreachable_code)]
return unreachable!("Trying to use a DB backend when it's feature is disabled");
},
)+ }
}
// Get a connection from the pool
pub fn get(&self) -> Result<DbConn, Error> {
match self { $(
#[cfg($name)]
Self::$name(p) => Ok(DbConn::$name(p.get().map_res("Error retrieving connection from pool")?)),
)+ }
}
}
};
}
/// Initializes a database pool. generate_connections! {
pub fn init_pool() -> Pool { sqlite: diesel::sqlite::SqliteConnection,
let manager = ConnectionManager::new(CONFIG.database_url()); mysql: diesel::mysql::MysqlConnection,
postgresql: diesel::pg::PgConnection
}
impl DbConnType {
pub fn from_url(url: &str) -> Result<DbConnType, Error> {
// Mysql
if url.starts_with("mysql:") {
#[cfg(mysql)]
return Ok(DbConnType::mysql);
#[cfg(not(mysql))]
err!("`DATABASE_URL` is a MySQL URL, but the 'mysql' feature is not enabled")
// Postgres
} else if url.starts_with("postgresql:") || url.starts_with("postgres:") {
#[cfg(postgresql)]
return Ok(DbConnType::postgresql);
r2d2::Pool::builder().build(manager).expect("Failed to create pool") #[cfg(not(postgresql))]
err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled")
//Sqlite
} else {
#[cfg(sqlite)]
return Ok(DbConnType::sqlite);
#[cfg(not(sqlite))]
err!("`DATABASE_URL` looks like a SQLite URL, but 'sqlite' feature is not enabled")
}
}
} }
pub fn get_connection() -> Result<Connection, ConnectionError> {
Connection::establish(&CONFIG.database_url()) #[macro_export]
macro_rules! db_run {
// Same for all dbs
( $conn:ident: $body:block ) => {
db_run! { $conn: sqlite, mysql, postgresql $body }
};
// Different code for each db
( $conn:ident: $( $($db:ident),+ $body:block )+ ) => {
#[allow(unused)] use diesel::prelude::*;
match $conn {
$($(
#[cfg($db)]
crate::db::DbConn::$db(ref $conn) => {
paste::paste! {
#[allow(unused)] use crate::db::[<__ $db _schema>]::{self as schema, *};
#[allow(unused)] use [<__ $db _model>]::*;
#[allow(unused)] use crate::db::FromDb;
}
$body
},
)+)+
}
};
} }
pub trait FromDb {
type Output;
fn from_db(self) -> Self::Output;
}
// For each struct eg. Cipher, we create a CipherDb inside a module named __$db_model (where $db is sqlite, mysql or postgresql),
// to implement the Diesel traits. We also provide methods to convert between them and the basic structs. Later, that module will be auto imported when using db_run!
#[macro_export]
macro_rules! db_object {
( $(
$( #[$attr:meta] )*
pub struct $name:ident {
$( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty ),+
$(,)?
}
)+ ) => {
// Create the normal struct, without attributes
$( pub struct $name { $( /*$( #[$field_attr] )**/ $vis $field : $typ, )+ } )+
#[cfg(sqlite)]
pub mod __sqlite_model { $( db_object! { @db sqlite | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
#[cfg(mysql)]
pub mod __mysql_model { $( db_object! { @db mysql | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
#[cfg(postgresql)]
pub mod __postgresql_model { $( db_object! { @db postgresql | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ }
};
( @db $db:ident | $( #[$attr:meta] )* | $name:ident | $( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty),+) => {
paste::paste! {
#[allow(unused)] use super::*;
#[allow(unused)] use diesel::prelude::*;
#[allow(unused)] use crate::db::[<__ $db _schema>]::*;
$( #[$attr] )*
pub struct [<$name Db>] { $(
$( #[$field_attr] )* $vis $field : $typ,
)+ }
impl [<$name Db>] {
#[inline(always)] pub fn from_db(self) -> super::$name { super::$name { $( $field: self.$field, )+ } }
#[inline(always)] pub fn to_db(x: &super::$name) -> Self { Self { $( $field: x.$field.clone(), )+ } }
}
impl crate::db::FromDb for [<$name Db>] {
type Output = super::$name;
#[inline(always)] fn from_db(self) -> Self::Output { super::$name { $( $field: self.$field, )+ } }
}
impl crate::db::FromDb for Vec<[<$name Db>]> {
type Output = Vec<super::$name>;
#[inline(always)] fn from_db(self) -> Self::Output { self.into_iter().map(crate::db::FromDb::from_db).collect() }
}
impl crate::db::FromDb for Option<[<$name Db>]> {
type Output = Option<super::$name>;
#[inline(always)] fn from_db(self) -> Self::Output { self.map(crate::db::FromDb::from_db) }
}
}
};
}
// Reexport the models, needs to be after the macros are defined so it can access them
pub mod models;
/// Creates a back-up of the database using sqlite3 /// Creates a back-up of the database using sqlite3
pub fn backup_database() -> Result<(), Error> { pub fn backup_database() -> Result<(), Error> {
use std::path::Path; use std::path::Path;
@ -73,18 +225,99 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn {
fn from_request(request: &'a Request<'r>) -> Outcome<DbConn, ()> { fn from_request(request: &'a Request<'r>) -> Outcome<DbConn, ()> {
// https://github.com/SergioBenitez/Rocket/commit/e3c1a4ad3ab9b840482ec6de4200d30df43e357c // https://github.com/SergioBenitez/Rocket/commit/e3c1a4ad3ab9b840482ec6de4200d30df43e357c
let pool = try_outcome!(request.guard::<State<Pool>>()); let pool = try_outcome!(request.guard::<State<DbPool>>());
match pool.get() { match pool.get() {
Ok(conn) => Outcome::Success(DbConn(conn)), Ok(conn) => Outcome::Success(conn),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())), Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
} }
} }
} }
// For the convenience of using an &DbConn as a &Database. // Embed the migrations from the migrations folder into the application
impl std::ops::Deref for DbConn { // This way, the program automatically migrates the database to the latest version
type Target = Connection; // https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
fn deref(&self) -> &Self::Target { #[cfg(sqlite)]
&self.0 mod sqlite_migrations {
#[allow(unused_imports)]
embed_migrations!("migrations/sqlite");
pub fn run_migrations() {
// Make sure the directory exists
let url = crate::CONFIG.database_url();
let path = std::path::Path::new(&url);
if let Some(parent) = path.parent() {
if std::fs::create_dir_all(parent).is_err() {
error!("Error creating database directory");
std::process::exit(1);
}
}
use diesel::{Connection, RunQueryDsl};
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let connection =
diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url()).expect("Can't connect to DB");
// Disable Foreign Key Checks during migration
// Scoped to a connection.
diesel::sql_query("PRAGMA foreign_keys = OFF")
.execute(&connection)
.expect("Failed to disable Foreign Key Checks during migrations");
// Turn on WAL in SQLite
if crate::CONFIG.enable_db_wal() {
diesel::sql_query("PRAGMA journal_mode=wal")
.execute(&connection)
.expect("Failed to turn on WAL");
}
embedded_migrations::run_with_output(&connection, &mut std::io::stdout()).expect("Can't run migrations");
}
}
#[cfg(mysql)]
mod mysql_migrations {
#[allow(unused_imports)]
embed_migrations!("migrations/mysql");
pub fn run_migrations() {
use diesel::{Connection, RunQueryDsl};
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let connection =
diesel::mysql::MysqlConnection::establish(&crate::CONFIG.database_url()).expect("Can't connect to DB");
// Disable Foreign Key Checks during migration
// Scoped to a connection/session.
diesel::sql_query("SET FOREIGN_KEY_CHECKS = 0")
.execute(&connection)
.expect("Failed to disable Foreign Key Checks during migrations");
embedded_migrations::run_with_output(&connection, &mut std::io::stdout()).expect("Can't run migrations");
}
}
#[cfg(postgresql)]
mod postgresql_migrations {
#[allow(unused_imports)]
embed_migrations!("migrations/postgresql");
pub fn run_migrations() {
use diesel::{Connection, RunQueryDsl};
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let connection =
diesel::pg::PgConnection::establish(&crate::CONFIG.database_url()).expect("Can't connect to DB");
// Disable Foreign Key Checks during migration
// FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html,
// "SET CONSTRAINTS sets the behavior of constraint checking within the
// current transaction", so this setting probably won't take effect for
// any of the migrations since it's being run outside of a transaction.
// Migrations that need to disable foreign key checks should run this
// from within the migration script itself.
diesel::sql_query("SET CONSTRAINTS ALL DEFERRED")
.execute(&connection)
.expect("Failed to disable Foreign Key Checks during migrations");
embedded_migrations::run_with_output(&connection, &mut std::io::stdout()).expect("Can't run migrations");
} }
} }

@ -3,17 +3,19 @@ use serde_json::Value;
use super::Cipher; use super::Cipher;
use crate::CONFIG; use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] db_object! {
#[table_name = "attachments"] #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[changeset_options(treat_none_as_null="true")] #[table_name = "attachments"]
#[belongs_to(Cipher, foreign_key = "cipher_uuid")] #[changeset_options(treat_none_as_null="true")]
#[primary_key(id)] #[belongs_to(super::Cipher, foreign_key = "cipher_uuid")]
pub struct Attachment { #[primary_key(id)]
pub id: String, pub struct Attachment {
pub cipher_uuid: String, pub id: String,
pub file_name: String, pub cipher_uuid: String,
pub file_size: i32, pub file_name: String,
pub akey: Option<String>, pub file_size: i32,
pub akey: Option<String>,
}
} }
/// Local methods /// Local methods
@ -50,43 +52,46 @@ impl Attachment {
} }
} }
use crate::db::schema::{attachments, ciphers};
use crate::db::DbConn; use crate::db::DbConn;
use diesel::prelude::*;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::error::MapResult; use crate::error::MapResult;
/// Database methods /// Database methods
impl Attachment { impl Attachment {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::insert_into(attachments::table)
.values(self)
.on_conflict(attachments::id)
.do_update()
.set(self)
.execute(&**conn)
.map_res("Error saving attachment")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(attachments::table) db_run! { conn:
.values(self) sqlite, mysql {
.execute(&**conn) diesel::replace_into(attachments::table)
.map_res("Error saving attachment") .values(AttachmentDb::to_db(self))
.execute(conn)
.map_res("Error saving attachment")
}
postgresql {
let value = AttachmentDb::to_db(self);
diesel::insert_into(attachments::table)
.values(&value)
.on_conflict(attachments::id)
.do_update()
.set(&value)
.execute(conn)
.map_res("Error saving attachment")
}
}
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
crate::util::retry( db_run! { conn: {
|| diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(&**conn), crate::util::retry(
10, || diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(conn),
) 10,
.map_res("Error deleting attachment")?; )
.map_res("Error deleting attachment")?;
crate::util::delete_file(&self.get_file_path())?;
Ok(()) crate::util::delete_file(&self.get_file_path())?;
Ok(())
}}
} }
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
@ -97,67 +102,78 @@ impl Attachment {
} }
pub fn find_by_id(id: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_id(id: &str, conn: &DbConn) -> Option<Self> {
let id = id.to_lowercase(); db_run! { conn: {
attachments::table
attachments::table .filter(attachments::id.eq(id.to_lowercase()))
.filter(attachments::id.eq(id)) .first::<AttachmentDb>(conn)
.first::<Self>(&**conn) .ok()
.ok() .from_db()
}}
} }
pub fn find_by_cipher(cipher_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_cipher(cipher_uuid: &str, conn: &DbConn) -> Vec<Self> {
attachments::table db_run! { conn: {
.filter(attachments::cipher_uuid.eq(cipher_uuid)) attachments::table
.load::<Self>(&**conn) .filter(attachments::cipher_uuid.eq(cipher_uuid))
.expect("Error loading attachments") .load::<AttachmentDb>(conn)
.expect("Error loading attachments")
.from_db()
}}
} }
pub fn find_by_ciphers(cipher_uuids: Vec<String>, conn: &DbConn) -> Vec<Self> { pub fn find_by_ciphers(cipher_uuids: Vec<String>, conn: &DbConn) -> Vec<Self> {
attachments::table db_run! { conn: {
.filter(attachments::cipher_uuid.eq_any(cipher_uuids)) attachments::table
.load::<Self>(&**conn) .filter(attachments::cipher_uuid.eq_any(cipher_uuids))
.expect("Error loading attachments") .load::<AttachmentDb>(conn)
.expect("Error loading attachments")
.from_db()
}}
} }
pub fn size_by_user(user_uuid: &str, conn: &DbConn) -> i64 { pub fn size_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
let result: Option<i64> = attachments::table db_run! { conn: {
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) let result: Option<i64> = attachments::table
.filter(ciphers::user_uuid.eq(user_uuid)) .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
.select(diesel::dsl::sum(attachments::file_size)) .filter(ciphers::user_uuid.eq(user_uuid))
.first(&**conn) .select(diesel::dsl::sum(attachments::file_size))
.expect("Error loading user attachment total size"); .first(conn)
.expect("Error loading user attachment total size");
result.unwrap_or(0) result.unwrap_or(0)
}}
} }
pub fn count_by_user(user_uuid: &str, conn: &DbConn) -> i64 { pub fn count_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
attachments::table db_run! { conn: {
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) attachments::table
.filter(ciphers::user_uuid.eq(user_uuid)) .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
.count() .filter(ciphers::user_uuid.eq(user_uuid))
.first::<i64>(&**conn) .count()
.ok() .first(conn)
.unwrap_or(0) .unwrap_or(0)
}}
} }
pub fn size_by_org(org_uuid: &str, conn: &DbConn) -> i64 { pub fn size_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
let result: Option<i64> = attachments::table db_run! { conn: {
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) let result: Option<i64> = attachments::table
.filter(ciphers::organization_uuid.eq(org_uuid)) .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
.select(diesel::dsl::sum(attachments::file_size)) .filter(ciphers::organization_uuid.eq(org_uuid))
.first(&**conn) .select(diesel::dsl::sum(attachments::file_size))
.expect("Error loading user attachment total size"); .first(conn)
.expect("Error loading user attachment total size");
result.unwrap_or(0) result.unwrap_or(0)
}}
} }
pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 { pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
attachments::table db_run! { conn: {
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) attachments::table
.filter(ciphers::organization_uuid.eq(org_uuid)) .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
.count() .filter(ciphers::organization_uuid.eq(org_uuid))
.first(&**conn) .count()
.ok() .first(conn)
.unwrap_or(0) .unwrap_or(0)
}}
} }
} }

@ -5,35 +5,37 @@ use super::{
Attachment, CollectionCipher, FolderCipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization, Attachment, CollectionCipher, FolderCipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization,
}; };
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] db_object! {
#[table_name = "ciphers"] #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[changeset_options(treat_none_as_null="true")] #[table_name = "ciphers"]
#[belongs_to(User, foreign_key = "user_uuid")] #[changeset_options(treat_none_as_null="true")]
#[belongs_to(Organization, foreign_key = "organization_uuid")] #[belongs_to(User, foreign_key = "user_uuid")]
#[primary_key(uuid)] #[belongs_to(Organization, foreign_key = "organization_uuid")]
pub struct Cipher { #[primary_key(uuid)]
pub uuid: String, pub struct Cipher {
pub created_at: NaiveDateTime, pub uuid: String,
pub updated_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: Option<String>,
pub organization_uuid: Option<String>, pub user_uuid: Option<String>,
pub organization_uuid: Option<String>,
/*
Login = 1, /*
SecureNote = 2, Login = 1,
Card = 3, SecureNote = 2,
Identity = 4 Card = 3,
*/ Identity = 4
pub atype: i32, */
pub name: String, pub atype: i32,
pub notes: Option<String>, pub name: String,
pub fields: Option<String>, pub notes: Option<String>,
pub fields: Option<String>,
pub data: String,
pub data: String,
pub password_history: Option<String>,
pub deleted_at: Option<NaiveDateTime>, pub password_history: Option<String>,
pub deleted_at: Option<NaiveDateTime>,
}
} }
/// Local methods /// Local methods
@ -62,9 +64,7 @@ impl Cipher {
} }
} }
use crate::db::schema::*;
use crate::db::DbConn; use crate::db::DbConn;
use diesel::prelude::*;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::error::MapResult; use crate::error::MapResult;
@ -81,7 +81,7 @@ impl Cipher {
let password_history_json = self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null); let password_history_json = self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
let (read_only, hide_passwords) = let (read_only, hide_passwords) =
match self.get_access_restrictions(&user_uuid, &conn) { match self.get_access_restrictions(&user_uuid, conn) {
Some((ro, hp)) => (ro, hp), Some((ro, hp)) => (ro, hp),
None => { None => {
error!("Cipher ownership assertion failure"); error!("Cipher ownership assertion failure");
@ -125,14 +125,14 @@ impl Cipher {
"Type": self.atype, "Type": self.atype,
"RevisionDate": format_date(&self.updated_at), "RevisionDate": format_date(&self.updated_at),
"DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))), "DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
"FolderId": self.get_folder_uuid(&user_uuid, &conn), "FolderId": self.get_folder_uuid(&user_uuid, conn),
"Favorite": self.is_favorite(&user_uuid, &conn), "Favorite": self.is_favorite(&user_uuid, conn),
"OrganizationId": self.organization_uuid, "OrganizationId": self.organization_uuid,
"Attachments": attachments_json, "Attachments": attachments_json,
"OrganizationUseTotp": true, "OrganizationUseTotp": true,
// This field is specific to the cipherDetails type. // This field is specific to the cipherDetails type.
"CollectionIds": self.get_collections(user_uuid, &conn), "CollectionIds": self.get_collections(user_uuid, conn),
"Name": self.name, "Name": self.name,
"Notes": self.notes, "Notes": self.notes,
@ -183,41 +183,42 @@ impl Cipher {
user_uuids user_uuids
} }
#[cfg(feature = "postgresql")]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn);
self.updated_at = Utc::now().naive_utc();
diesel::insert_into(ciphers::table)
.values(&*self)
.on_conflict(ciphers::uuid)
.do_update()
.set(&*self)
.execute(&**conn)
.map_res("Error saving cipher")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn); self.update_users_revision(conn);
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();
diesel::replace_into(ciphers::table) db_run! { conn:
.values(&*self) sqlite, mysql {
.execute(&**conn) diesel::replace_into(ciphers::table)
.map_res("Error saving cipher") .values(CipherDb::to_db(self))
.execute(conn)
.map_res("Error saving cipher")
}
postgresql {
let value = CipherDb::to_db(self);
diesel::insert_into(ciphers::table)
.values(&value)
.on_conflict(ciphers::uuid)
.do_update()
.set(&value)
.execute(conn)
.map_res("Error saving cipher")
}
}
} }
pub fn delete(&self, conn: &DbConn) -> EmptyResult { pub fn delete(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn); self.update_users_revision(conn);
FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?; FolderCipher::delete_all_by_cipher(&self.uuid, conn)?;
CollectionCipher::delete_all_by_cipher(&self.uuid, &conn)?; CollectionCipher::delete_all_by_cipher(&self.uuid, conn)?;
Attachment::delete_all_by_cipher(&self.uuid, &conn)?; Attachment::delete_all_by_cipher(&self.uuid, conn)?;
diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid)))
.map_res("Error deleting cipher") .execute(conn)
.map_res("Error deleting cipher")
}}
} }
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
@ -235,28 +236,28 @@ impl Cipher {
} }
pub fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(user_uuid, &conn); User::update_uuid_revision(user_uuid, conn);
match (self.get_folder_uuid(&user_uuid, &conn), folder_uuid) { match (self.get_folder_uuid(&user_uuid, conn), folder_uuid) {
// No changes // No changes
(None, None) => Ok(()), (None, None) => Ok(()),
(Some(ref old), Some(ref new)) if old == new => Ok(()), (Some(ref old), Some(ref new)) if old == new => Ok(()),
// Add to folder // Add to folder
(None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(&conn), (None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(conn),
// Remove from folder // Remove from folder
(Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, &conn) { (Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn) {
Some(old) => old.delete(&conn), Some(old) => old.delete(conn),
None => err!("Couldn't move from previous folder"), None => err!("Couldn't move from previous folder"),
}, },
// Move to another folder // Move to another folder
(Some(old), Some(new)) => { (Some(old), Some(new)) => {
if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, &conn) { if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn) {
old.delete(&conn)?; old.delete(conn)?;
} }
FolderCipher::new(&new, &self.uuid).save(&conn) FolderCipher::new(&new, &self.uuid).save(conn)
} }
} }
} }
@ -269,7 +270,7 @@ impl Cipher {
/// Returns whether this cipher is owned by an org in which the user has full access. /// Returns whether this cipher is owned by an org in which the user has full access.
pub fn is_in_full_access_org(&self, user_uuid: &str, conn: &DbConn) -> bool { pub fn is_in_full_access_org(&self, user_uuid: &str, conn: &DbConn) -> bool {
if let Some(ref org_uuid) = self.organization_uuid { if let Some(ref org_uuid) = self.organization_uuid {
if let Some(user_org) = UserOrganization::find_by_user_and_org(&user_uuid, &org_uuid, &conn) { if let Some(user_org) = UserOrganization::find_by_user_and_org(&user_uuid, &org_uuid, conn) {
return user_org.has_full_access(); return user_org.has_full_access();
} }
} }
@ -290,38 +291,40 @@ impl Cipher {
return Some((false, false)); return Some((false, false));
} }
// Check whether this cipher is in any collections accessible to the db_run! {conn: {
// user. If so, retrieve the access flags for each collection. // Check whether this cipher is in any collections accessible to the
let query = ciphers::table // user. If so, retrieve the access flags for each collection.
.filter(ciphers::uuid.eq(&self.uuid)) let query = ciphers::table
.inner_join(ciphers_collections::table.on( .filter(ciphers::uuid.eq(&self.uuid))
ciphers::uuid.eq(ciphers_collections::cipher_uuid))) .inner_join(ciphers_collections::table.on(
.inner_join(users_collections::table.on( ciphers::uuid.eq(ciphers_collections::cipher_uuid)))
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) .inner_join(users_collections::table.on(
.and(users_collections::user_uuid.eq(user_uuid)))) ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
.select((users_collections::read_only, users_collections::hide_passwords)); .and(users_collections::user_uuid.eq(user_uuid))))
.select((users_collections::read_only, users_collections::hide_passwords));
// There's an edge case where a cipher can be in multiple collections
// with inconsistent access flags. For example, a cipher could be in // There's an edge case where a cipher can be in multiple collections
// one collection where the user has read-only access, but also in // with inconsistent access flags. For example, a cipher could be in
// another collection where the user has read/write access. To handle // one collection where the user has read-only access, but also in
// this, we do a boolean OR of all values in each of the `read_only` // another collection where the user has read/write access. To handle
// and `hide_passwords` columns. This could ideally be done as part // this, we do a boolean OR of all values in each of the `read_only`
// of the query, but Diesel doesn't support a max() or bool_or() // and `hide_passwords` columns. This could ideally be done as part
// function on booleans and this behavior isn't portable anyway. // of the query, but Diesel doesn't support a max() or bool_or()
if let Some(vec) = query.load::<(bool, bool)>(&**conn).ok() { // function on booleans and this behavior isn't portable anyway.
let mut read_only = false; if let Some(vec) = query.load::<(bool, bool)>(conn).ok() {
let mut hide_passwords = false; let mut read_only = false;
for (ro, hp) in vec.iter() { let mut hide_passwords = false;
read_only |= ro; for (ro, hp) in vec.iter() {
hide_passwords |= hp; read_only |= ro;
} hide_passwords |= hp;
}
Some((read_only, hide_passwords)) Some((read_only, hide_passwords))
} else { } else {
// This cipher isn't in any collections accessible to the user. // This cipher isn't in any collections accessible to the user.
None None
} }
}}
} }
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
@ -337,12 +340,14 @@ impl Cipher {
// Returns whether this cipher is a favorite of the specified user. // Returns whether this cipher is a favorite of the specified user.
pub fn is_favorite(&self, user_uuid: &str, conn: &DbConn) -> bool { pub fn is_favorite(&self, user_uuid: &str, conn: &DbConn) -> bool {
let query = favorites::table db_run!{ conn: {
.filter(favorites::user_uuid.eq(user_uuid)) let query = favorites::table
.filter(favorites::cipher_uuid.eq(&self.uuid)) .filter(favorites::user_uuid.eq(user_uuid))
.count(); .filter(favorites::cipher_uuid.eq(&self.uuid))
.count();
query.first::<i64>(&**conn).ok().unwrap_or(0) != 0
query.first::<i64>(conn).ok().unwrap_or(0) != 0
}}
} }
// Updates whether this cipher is a favorite of the specified user. // Updates whether this cipher is a favorite of the specified user.
@ -356,23 +361,27 @@ impl Cipher {
match (old, new) { match (old, new) {
(false, true) => { (false, true) => {
User::update_uuid_revision(user_uuid, &conn); User::update_uuid_revision(user_uuid, &conn);
diesel::insert_into(favorites::table) db_run!{ conn: {
.values(( diesel::insert_into(favorites::table)
favorites::user_uuid.eq(user_uuid), .values((
favorites::cipher_uuid.eq(&self.uuid), favorites::user_uuid.eq(user_uuid),
)) favorites::cipher_uuid.eq(&self.uuid),
.execute(&**conn) ))
.map_res("Error adding favorite") .execute(conn)
.map_res("Error adding favorite")
}}
} }
(true, false) => { (true, false) => {
User::update_uuid_revision(user_uuid, &conn); User::update_uuid_revision(user_uuid, &conn);
diesel::delete( db_run!{ conn: {
favorites::table diesel::delete(
.filter(favorites::user_uuid.eq(user_uuid)) favorites::table
.filter(favorites::cipher_uuid.eq(&self.uuid)) .filter(favorites::user_uuid.eq(user_uuid))
) .filter(favorites::cipher_uuid.eq(&self.uuid))
.execute(&**conn) )
.map_res("Error removing favorite") .execute(conn)
.map_res("Error removing favorite")
}}
} }
// Otherwise, the favorite status is already what it should be. // Otherwise, the favorite status is already what it should be.
_ => Ok(()) _ => Ok(())
@ -380,112 +389,131 @@ impl Cipher {
} }
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> { pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
folders_ciphers::table db_run! {conn: {
.inner_join(folders::table) folders_ciphers::table
.filter(folders::user_uuid.eq(&user_uuid)) .inner_join(folders::table)
.filter(folders_ciphers::cipher_uuid.eq(&self.uuid)) .filter(folders::user_uuid.eq(&user_uuid))
.select(folders_ciphers::folder_uuid) .filter(folders_ciphers::cipher_uuid.eq(&self.uuid))
.first::<String>(&**conn) .select(folders_ciphers::folder_uuid)
.ok() .first::<String>(conn)
.ok()
}}
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
ciphers::table db_run! {conn: {
.filter(ciphers::uuid.eq(uuid)) ciphers::table
.first::<Self>(&**conn) .filter(ciphers::uuid.eq(uuid))
.ok() .first::<CipherDb>(conn)
.ok()
.from_db()
}}
} }
// Find all ciphers accessible to user // Find all ciphers accessible to user
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
ciphers::table db_run! {conn: {
.left_join(users_organizations::table.on( ciphers::table
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and( .left_join(users_organizations::table.on(
users_organizations::user_uuid.eq(user_uuid).and( ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32) users_organizations::user_uuid.eq(user_uuid).and(
) users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
) )
))
.left_join(ciphers_collections::table.on(
ciphers::uuid.eq(ciphers_collections::cipher_uuid)
))
.left_join(users_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
))
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
users_organizations::access_all.eq(true).or( // access_all in Organization
users_organizations::atype.le(UserOrgType::Admin as i32).or( // Org admin or owner
users_collections::user_uuid.eq(user_uuid).and( // Access to Collection
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
) )
) ))
) .left_join(ciphers_collections::table.on(
)) ciphers::uuid.eq(ciphers_collections::cipher_uuid)
.select(ciphers::all_columns) ))
.distinct() .left_join(users_collections::table.on(
.load::<Self>(&**conn).expect("Error loading ciphers") ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
))
.filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner
users_organizations::access_all.eq(true).or( // access_all in Organization
users_organizations::atype.le(UserOrgType::Admin as i32).or( // Org admin or owner
users_collections::user_uuid.eq(user_uuid).and( // Access to Collection
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
)
)
)
))
.select(ciphers::all_columns)
.distinct()
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
}}
} }
// Find all ciphers directly owned by user // Find all ciphers directly owned by user
pub fn find_owned_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_owned_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
ciphers::table db_run! {conn: {
.filter(ciphers::user_uuid.eq(user_uuid)) ciphers::table
.load::<Self>(&**conn).expect("Error loading ciphers") .filter(ciphers::user_uuid.eq(user_uuid))
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
}}
} }
pub fn count_owned_by_user(user_uuid: &str, conn: &DbConn) -> i64 { pub fn count_owned_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
ciphers::table db_run! {conn: {
.filter(ciphers::user_uuid.eq(user_uuid)) ciphers::table
.count() .filter(ciphers::user_uuid.eq(user_uuid))
.first::<i64>(&**conn) .count()
.ok() .first::<i64>(conn)
.unwrap_or(0) .ok()
.unwrap_or(0)
}}
} }
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
ciphers::table db_run! {conn: {
.filter(ciphers::organization_uuid.eq(org_uuid)) ciphers::table
.load::<Self>(&**conn).expect("Error loading ciphers") .filter(ciphers::organization_uuid.eq(org_uuid))
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
}}
} }
pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 { pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
ciphers::table db_run! {conn: {
.filter(ciphers::organization_uuid.eq(org_uuid)) ciphers::table
.count() .filter(ciphers::organization_uuid.eq(org_uuid))
.first::<i64>(&**conn) .count()
.ok() .first::<i64>(conn)
.unwrap_or(0) .ok()
.unwrap_or(0)
}}
} }
pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> {
folders_ciphers::table.inner_join(ciphers::table) db_run! {conn: {
.filter(folders_ciphers::folder_uuid.eq(folder_uuid)) folders_ciphers::table.inner_join(ciphers::table)
.select(ciphers::all_columns) .filter(folders_ciphers::folder_uuid.eq(folder_uuid))
.load::<Self>(&**conn).expect("Error loading ciphers") .select(ciphers::all_columns)
.load::<CipherDb>(conn).expect("Error loading ciphers").from_db()
}}
} }
pub fn get_collections(&self, user_id: &str, conn: &DbConn) -> Vec<String> { pub fn get_collections(&self, user_id: &str, conn: &DbConn) -> Vec<String> {
ciphers_collections::table db_run! {conn: {
.inner_join(collections::table.on( ciphers_collections::table
collections::uuid.eq(ciphers_collections::collection_uuid) .inner_join(collections::table.on(
)) collections::uuid.eq(ciphers_collections::collection_uuid)
.inner_join(users_organizations::table.on( ))
users_organizations::org_uuid.eq(collections::org_uuid).and( .inner_join(users_organizations::table.on(
users_organizations::user_uuid.eq(user_id) users_organizations::org_uuid.eq(collections::org_uuid).and(
) users_organizations::user_uuid.eq(user_id)
)) )
.left_join(users_collections::table.on( ))
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and( .left_join(users_collections::table.on(
users_collections::user_uuid.eq(user_id) users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and(
) users_collections::user_uuid.eq(user_id)
)) )
.filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) ))
.filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection .filter(ciphers_collections::cipher_uuid.eq(&self.uuid))
users_organizations::access_all.eq(true).or( // User has access all .filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection
users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner users_organizations::access_all.eq(true).or( // User has access all
) users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner
)) )
.select(ciphers_collections::collection_uuid) ))
.load::<String>(&**conn).unwrap_or_default() .select(ciphers_collections::collection_uuid)
.load::<String>(conn).unwrap_or_default()
}}
} }
} }

@ -1,15 +1,39 @@
use serde_json::Value; use serde_json::Value;
use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization, User, Cipher};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] db_object! {
#[table_name = "collections"] #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[belongs_to(Organization, foreign_key = "org_uuid")] #[table_name = "collections"]
#[primary_key(uuid)] #[belongs_to(Organization, foreign_key = "org_uuid")]
pub struct Collection { #[primary_key(uuid)]
pub uuid: String, pub struct Collection {
pub org_uuid: String, pub uuid: String,
pub name: String, pub org_uuid: String,
pub name: String,
}
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "users_collections"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[belongs_to(Collection, foreign_key = "collection_uuid")]
#[primary_key(user_uuid, collection_uuid)]
pub struct CollectionUser {
pub user_uuid: String,
pub collection_uuid: String,
pub read_only: bool,
pub hide_passwords: bool,
}
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "ciphers_collections"]
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
#[belongs_to(Collection, foreign_key = "collection_uuid")]
#[primary_key(cipher_uuid, collection_uuid)]
pub struct CollectionCipher {
pub cipher_uuid: String,
pub collection_uuid: String,
}
} }
/// Local methods /// Local methods
@ -33,36 +57,34 @@ impl Collection {
} }
} }
use crate::db::schema::*;
use crate::db::DbConn; use crate::db::DbConn;
use diesel::prelude::*;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::error::MapResult; use crate::error::MapResult;
/// Database methods /// Database methods
impl Collection { impl Collection {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
self.update_users_revision(conn); self.update_users_revision(conn);
diesel::insert_into(collections::table) db_run! { conn:
.values(self) sqlite, mysql {
.on_conflict(collections::uuid) diesel::replace_into(collections::table)
.do_update() .values(CollectionDb::to_db(self))
.set(self) .execute(conn)
.execute(&**conn) .map_res("Error saving collection")
.map_res("Error saving collection") }
} postgresql {
let value = CollectionDb::to_db(self);
#[cfg(not(feature = "postgresql"))] diesel::insert_into(collections::table)
pub fn save(&self, conn: &DbConn) -> EmptyResult { .values(&value)
self.update_users_revision(conn); .on_conflict(collections::uuid)
.do_update()
diesel::replace_into(collections::table) .set(&value)
.values(self) .execute(conn)
.execute(&**conn) .map_res("Error saving collection")
.map_res("Error saving collection") }
}
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
@ -70,9 +92,11 @@ impl Collection {
CollectionCipher::delete_all_by_collection(&self.uuid, &conn)?; CollectionCipher::delete_all_by_collection(&self.uuid, &conn)?;
CollectionUser::delete_all_by_collection(&self.uuid, &conn)?; CollectionUser::delete_all_by_collection(&self.uuid, &conn)?;
diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid)))
.map_res("Error deleting collection") .execute(conn)
.map_res("Error deleting collection")
}}
} }
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
@ -91,33 +115,38 @@ impl Collection {
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
collections::table db_run! { conn: {
.filter(collections::uuid.eq(uuid)) collections::table
.first::<Self>(&**conn) .filter(collections::uuid.eq(uuid))
.ok() .first::<CollectionDb>(conn)
.ok()
.from_db()
}}
} }
pub fn find_by_user_uuid(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_user_uuid(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
collections::table db_run! { conn: {
.left_join(users_collections::table.on( collections::table
users_collections::collection_uuid.eq(collections::uuid).and( .left_join(users_collections::table.on(
users_collections::user_uuid.eq(user_uuid) users_collections::collection_uuid.eq(collections::uuid).and(
) users_collections::user_uuid.eq(user_uuid)
)) )
.left_join(users_organizations::table.on( ))
collections::org_uuid.eq(users_organizations::org_uuid).and( .left_join(users_organizations::table.on(
users_organizations::user_uuid.eq(user_uuid) collections::org_uuid.eq(users_organizations::org_uuid).and(
) users_organizations::user_uuid.eq(user_uuid)
)) )
.filter( ))
users_organizations::status.eq(UserOrgStatus::Confirmed as i32) .filter(
) users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
.filter(
users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
users_organizations::access_all.eq(true) // access_all in Organization
) )
).select(collections::all_columns) .filter(
.load::<Self>(&**conn).expect("Error loading collections") users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
users_organizations::access_all.eq(true) // access_all in Organization
)
).select(collections::all_columns)
.load::<CollectionDb>(conn).expect("Error loading collections").from_db()
}}
} }
pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> {
@ -128,42 +157,51 @@ impl Collection {
} }
pub fn find_by_organization(org_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_organization(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
collections::table db_run! { conn: {
.filter(collections::org_uuid.eq(org_uuid)) collections::table
.load::<Self>(&**conn) .filter(collections::org_uuid.eq(org_uuid))
.expect("Error loading collections") .load::<CollectionDb>(conn)
.expect("Error loading collections")
.from_db()
}}
} }
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
collections::table db_run! { conn: {
.filter(collections::uuid.eq(uuid)) collections::table
.filter(collections::org_uuid.eq(org_uuid)) .filter(collections::uuid.eq(uuid))
.select(collections::all_columns) .filter(collections::org_uuid.eq(org_uuid))
.first::<Self>(&**conn) .select(collections::all_columns)
.ok() .first::<CollectionDb>(conn)
.ok()
.from_db()
}}
} }
pub fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
collections::table db_run! { conn: {
.left_join(users_collections::table.on( collections::table
users_collections::collection_uuid.eq(collections::uuid).and( .left_join(users_collections::table.on(
users_collections::user_uuid.eq(user_uuid) users_collections::collection_uuid.eq(collections::uuid).and(
) users_collections::user_uuid.eq(user_uuid)
))
.left_join(users_organizations::table.on(
collections::org_uuid.eq(users_organizations::org_uuid).and(
users_organizations::user_uuid.eq(user_uuid)
)
))
.filter(collections::uuid.eq(uuid))
.filter(
users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection
users_organizations::access_all.eq(true).or( // access_all in Organization
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
) )
) ))
).select(collections::all_columns) .left_join(users_organizations::table.on(
.first::<Self>(&**conn).ok() collections::org_uuid.eq(users_organizations::org_uuid).and(
users_organizations::user_uuid.eq(user_uuid)
)
))
.filter(collections::uuid.eq(uuid))
.filter(
users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection
users_organizations::access_all.eq(true).or( // access_all in Organization
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
)
)
).select(collections::all_columns)
.first::<CollectionDb>(conn).ok()
.from_db()
}}
} }
pub fn is_writable_by_user(&self, user_uuid: &str, conn: &DbConn) -> bool { pub fn is_writable_by_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
@ -173,110 +211,108 @@ impl Collection {
if user_org.access_all { if user_org.access_all {
true true
} else { } else {
users_collections::table db_run! { conn: {
.inner_join(collections::table) users_collections::table
.filter(users_collections::collection_uuid.eq(&self.uuid)) .inner_join(collections::table)
.filter(users_collections::user_uuid.eq(&user_uuid)) .filter(users_collections::collection_uuid.eq(&self.uuid))
.filter(users_collections::read_only.eq(false)) .filter(users_collections::user_uuid.eq(&user_uuid))
.select(collections::all_columns) .filter(users_collections::read_only.eq(false))
.first::<Self>(&**conn) .select(collections::all_columns)
.ok() .first::<CollectionDb>(conn)
.is_some() // Read only or no access to collection .ok()
.is_some() // Read only or no access to collection
}}
} }
} }
} }
} }
} }
use super::User;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "users_collections"]
#[belongs_to(User, foreign_key = "user_uuid")]
#[belongs_to(Collection, foreign_key = "collection_uuid")]
#[primary_key(user_uuid, collection_uuid)]
pub struct CollectionUser {
pub user_uuid: String,
pub collection_uuid: String,
pub read_only: bool,
pub hide_passwords: bool,
}
/// Database methods /// Database methods
impl CollectionUser { impl CollectionUser {
pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_collections::table db_run! { conn: {
.filter(users_collections::user_uuid.eq(user_uuid)) users_collections::table
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) .filter(users_collections::user_uuid.eq(user_uuid))
.filter(collections::org_uuid.eq(org_uuid)) .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
.select(users_collections::all_columns) .filter(collections::org_uuid.eq(org_uuid))
.load::<Self>(&**conn) .select(users_collections::all_columns)
.expect("Error loading users_collections") .load::<CollectionUserDb>(conn)
} .expect("Error loading users_collections")
.from_db()
#[cfg(feature = "postgresql")] }}
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&user_uuid, conn);
diesel::insert_into(users_collections::table)
.values((
users_collections::user_uuid.eq(user_uuid),
users_collections::collection_uuid.eq(collection_uuid),
users_collections::read_only.eq(read_only),
users_collections::hide_passwords.eq(hide_passwords),
))
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
.do_update()
.set((
users_collections::read_only.eq(read_only),
users_collections::hide_passwords.eq(hide_passwords),
))
.execute(&**conn)
.map_res("Error adding user to collection")
} }
#[cfg(not(feature = "postgresql"))]
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult { pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&user_uuid, conn); User::update_uuid_revision(&user_uuid, conn);
diesel::replace_into(users_collections::table) db_run! { conn:
.values(( sqlite, mysql {
users_collections::user_uuid.eq(user_uuid), diesel::replace_into(users_collections::table)
users_collections::collection_uuid.eq(collection_uuid), .values((
users_collections::read_only.eq(read_only), users_collections::user_uuid.eq(user_uuid),
users_collections::hide_passwords.eq(hide_passwords), users_collections::collection_uuid.eq(collection_uuid),
)) users_collections::read_only.eq(read_only),
.execute(&**conn) users_collections::hide_passwords.eq(hide_passwords),
.map_res("Error adding user to collection") ))
.execute(conn)
.map_res("Error adding user to collection")
}
postgresql {
diesel::insert_into(users_collections::table)
.values((
users_collections::user_uuid.eq(user_uuid),
users_collections::collection_uuid.eq(collection_uuid),
users_collections::read_only.eq(read_only),
users_collections::hide_passwords.eq(hide_passwords),
))
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
.do_update()
.set((
users_collections::read_only.eq(read_only),
users_collections::hide_passwords.eq(hide_passwords),
))
.execute(conn)
.map_res("Error adding user to collection")
}
}
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn); User::update_uuid_revision(&self.user_uuid, conn);
diesel::delete( db_run! { conn: {
users_collections::table diesel::delete(
.filter(users_collections::user_uuid.eq(&self.user_uuid)) users_collections::table
.filter(users_collections::collection_uuid.eq(&self.collection_uuid)), .filter(users_collections::user_uuid.eq(&self.user_uuid))
) .filter(users_collections::collection_uuid.eq(&self.collection_uuid)),
.execute(&**conn) )
.map_res("Error removing user from collection") .execute(conn)
.map_res("Error removing user from collection")
}}
} }
pub fn find_by_collection(collection_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_collection(collection_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_collections::table db_run! { conn: {
.filter(users_collections::collection_uuid.eq(collection_uuid)) users_collections::table
.select(users_collections::all_columns) .filter(users_collections::collection_uuid.eq(collection_uuid))
.load::<Self>(&**conn) .select(users_collections::all_columns)
.expect("Error loading users_collections") .load::<CollectionUserDb>(conn)
.expect("Error loading users_collections")
.from_db()
}}
} }
pub fn find_by_collection_and_user(collection_uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_collection_and_user(collection_uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> {
users_collections::table db_run! { conn: {
.filter(users_collections::collection_uuid.eq(collection_uuid)) users_collections::table
.filter(users_collections::user_uuid.eq(user_uuid)) .filter(users_collections::collection_uuid.eq(collection_uuid))
.select(users_collections::all_columns) .filter(users_collections::user_uuid.eq(user_uuid))
.first::<Self>(&**conn) .select(users_collections::all_columns)
.ok() .first::<CollectionUserDb>(conn)
.ok()
.from_db()
}}
} }
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
@ -286,81 +322,81 @@ impl CollectionUser {
User::update_uuid_revision(&collection.user_uuid, conn); User::update_uuid_revision(&collection.user_uuid, conn);
}); });
diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid)))
.map_res("Error deleting users from collection") .execute(conn)
.map_res("Error deleting users from collection")
}}
} }
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&user_uuid, conn); User::update_uuid_revision(&user_uuid, conn);
diesel::delete(users_collections::table.filter(users_collections::user_uuid.eq(user_uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(users_collections::table.filter(users_collections::user_uuid.eq(user_uuid)))
.map_res("Error removing user from collections") .execute(conn)
.map_res("Error removing user from collections")
}}
} }
} }
use super::Cipher;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "ciphers_collections"]
#[belongs_to(Cipher, foreign_key = "cipher_uuid")]
#[belongs_to(Collection, foreign_key = "collection_uuid")]
#[primary_key(cipher_uuid, collection_uuid)]
pub struct CollectionCipher {
pub cipher_uuid: String,
pub collection_uuid: String,
}
/// Database methods /// Database methods
impl CollectionCipher { impl CollectionCipher {
#[cfg(feature = "postgresql")]
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
Self::update_users_revision(&collection_uuid, conn); Self::update_users_revision(&collection_uuid, conn);
diesel::insert_into(ciphers_collections::table)
.values((
ciphers_collections::cipher_uuid.eq(cipher_uuid),
ciphers_collections::collection_uuid.eq(collection_uuid),
))
.on_conflict((ciphers_collections::cipher_uuid, ciphers_collections::collection_uuid))
.do_nothing()
.execute(&**conn)
.map_res("Error adding cipher to collection")
}
#[cfg(not(feature = "postgresql"))] db_run! { conn:
pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult { sqlite, mysql {
Self::update_users_revision(&collection_uuid, conn); diesel::replace_into(ciphers_collections::table)
diesel::replace_into(ciphers_collections::table) .values((
.values(( ciphers_collections::cipher_uuid.eq(cipher_uuid),
ciphers_collections::cipher_uuid.eq(cipher_uuid), ciphers_collections::collection_uuid.eq(collection_uuid),
ciphers_collections::collection_uuid.eq(collection_uuid), ))
)) .execute(conn)
.execute(&**conn) .map_res("Error adding cipher to collection")
.map_res("Error adding cipher to collection") }
postgresql {
diesel::insert_into(ciphers_collections::table)
.values((
ciphers_collections::cipher_uuid.eq(cipher_uuid),
ciphers_collections::collection_uuid.eq(collection_uuid),
))
.on_conflict((ciphers_collections::cipher_uuid, ciphers_collections::collection_uuid))
.do_nothing()
.execute(conn)
.map_res("Error adding cipher to collection")
}
}
} }
pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
Self::update_users_revision(&collection_uuid, conn); Self::update_users_revision(&collection_uuid, conn);
diesel::delete(
ciphers_collections::table db_run! { conn: {
.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)) diesel::delete(
.filter(ciphers_collections::collection_uuid.eq(collection_uuid)), ciphers_collections::table
) .filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))
.execute(&**conn) .filter(ciphers_collections::collection_uuid.eq(collection_uuid)),
.map_res("Error deleting cipher from collection") )
.execute(conn)
.map_res("Error deleting cipher from collection")
}}
} }
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)))
.map_res("Error removing cipher from collections") .execute(conn)
.map_res("Error removing cipher from collections")
}}
} }
pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
.map_res("Error removing ciphers from collection") .execute(conn)
.map_res("Error removing ciphers from collection")
}}
} }
pub fn update_users_revision(collection_uuid: &str, conn: &DbConn) { pub fn update_users_revision(collection_uuid: &str, conn: &DbConn) {

@ -3,26 +3,28 @@ use chrono::{NaiveDateTime, Utc};
use super::User; use super::User;
use crate::CONFIG; use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] db_object! {
#[table_name = "devices"] #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[changeset_options(treat_none_as_null="true")] #[table_name = "devices"]
#[belongs_to(User, foreign_key = "user_uuid")] #[changeset_options(treat_none_as_null="true")]
#[primary_key(uuid)] #[belongs_to(User, foreign_key = "user_uuid")]
pub struct Device { #[primary_key(uuid)]
pub uuid: String, pub struct Device {
pub created_at: NaiveDateTime, pub uuid: String,
pub updated_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: String,
pub user_uuid: String,
pub name: String,
/// https://github.com/bitwarden/core/tree/master/src/Core/Enums pub name: String,
pub atype: i32, // https://github.com/bitwarden/core/tree/master/src/Core/Enums
pub push_token: Option<String>, pub atype: i32,
pub push_token: Option<String>,
pub refresh_token: String,
pub refresh_token: String,
pub twofactor_remember: Option<String>,
pub twofactor_remember: Option<String>,
}
} }
/// Local methods /// Local methods
@ -105,41 +107,39 @@ impl Device {
} }
} }
use crate::db::schema::devices;
use crate::db::DbConn; use crate::db::DbConn;
use diesel::prelude::*;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::error::MapResult; use crate::error::MapResult;
/// Database methods /// Database methods
impl Device { impl Device {
#[cfg(feature = "postgresql")]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();
crate::util::retry( db_run! { conn:
|| diesel::insert_into(devices::table).values(&*self).on_conflict(devices::uuid).do_update().set(&*self).execute(&**conn), sqlite, mysql {
10, crate::util::retry(
) || diesel::replace_into(devices::table).values(DeviceDb::to_db(self)).execute(conn),
.map_res("Error saving device") 10,
} ).map_res("Error saving device")
}
#[cfg(not(feature = "postgresql"))] postgresql {
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { let value = DeviceDb::to_db(self);
self.updated_at = Utc::now().naive_utc(); crate::util::retry(
|| diesel::insert_into(devices::table).values(&value).on_conflict(devices::uuid).do_update().set(&value).execute(conn),
crate::util::retry( 10,
|| diesel::replace_into(devices::table).values(&*self).execute(&**conn), ).map_res("Error saving device")
10, }
) }
.map_res("Error saving device")
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
diesel::delete(devices::table.filter(devices::uuid.eq(self.uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(devices::table.filter(devices::uuid.eq(self.uuid)))
.map_res("Error removing device") .execute(conn)
.map_res("Error removing device")
}}
} }
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
@ -150,23 +150,32 @@ impl Device {
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
devices::table db_run! { conn: {
.filter(devices::uuid.eq(uuid)) devices::table
.first::<Self>(&**conn) .filter(devices::uuid.eq(uuid))
.ok() .first::<DeviceDb>(conn)
.ok()
.from_db()
}}
} }
pub fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Self> {
devices::table db_run! { conn: {
.filter(devices::refresh_token.eq(refresh_token)) devices::table
.first::<Self>(&**conn) .filter(devices::refresh_token.eq(refresh_token))
.ok() .first::<DeviceDb>(conn)
.ok()
.from_db()
}}
} }
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
devices::table db_run! { conn: {
.filter(devices::user_uuid.eq(user_uuid)) devices::table
.load::<Self>(&**conn) .filter(devices::user_uuid.eq(user_uuid))
.expect("Error loading devices") .load::<DeviceDb>(conn)
.expect("Error loading devices")
.from_db()
}}
} }
} }

@ -3,26 +3,28 @@ use serde_json::Value;
use super::{Cipher, User}; use super::{Cipher, User};
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] db_object! {
#[table_name = "folders"] #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[belongs_to(User, foreign_key = "user_uuid")] #[table_name = "folders"]
#[primary_key(uuid)] #[belongs_to(User, foreign_key = "user_uuid")]
pub struct Folder { #[primary_key(uuid)]
pub uuid: String, pub struct Folder {
pub created_at: NaiveDateTime, pub uuid: String,
pub updated_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub user_uuid: String, pub updated_at: NaiveDateTime,
pub name: String, pub user_uuid: String,
} pub name: String,
}
#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[table_name = "folders_ciphers"] #[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
#[belongs_to(Cipher, foreign_key = "cipher_uuid")] #[table_name = "folders_ciphers"]
#[belongs_to(Folder, foreign_key = "folder_uuid")] #[belongs_to(Cipher, foreign_key = "cipher_uuid")]
#[primary_key(cipher_uuid, folder_uuid)] #[belongs_to(Folder, foreign_key = "folder_uuid")]
pub struct FolderCipher { #[primary_key(cipher_uuid, folder_uuid)]
pub cipher_uuid: String, pub struct FolderCipher {
pub folder_uuid: String, pub cipher_uuid: String,
pub folder_uuid: String,
}
} }
/// Local methods /// Local methods
@ -61,47 +63,47 @@ impl FolderCipher {
} }
} }
use crate::db::schema::{folders, folders_ciphers};
use crate::db::DbConn; use crate::db::DbConn;
use diesel::prelude::*;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::error::MapResult; use crate::error::MapResult;
/// Database methods /// Database methods
impl Folder { impl Folder {
#[cfg(feature = "postgresql")]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn);
self.updated_at = Utc::now().naive_utc();
diesel::insert_into(folders::table)
.values(&*self)
.on_conflict(folders::uuid)
.do_update()
.set(&*self)
.execute(&**conn)
.map_res("Error saving folder")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn); User::update_uuid_revision(&self.user_uuid, conn);
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();
diesel::replace_into(folders::table) db_run! { conn:
.values(&*self) sqlite, mysql {
.execute(&**conn) diesel::replace_into(folders::table)
.map_res("Error saving folder") .values(FolderDb::to_db(self))
.execute(conn)
.map_res("Error saving folder")
}
postgresql {
let value = FolderDb::to_db(self);
diesel::insert_into(folders::table)
.values(&value)
.on_conflict(folders::uuid)
.do_update()
.set(&value)
.execute(conn)
.map_res("Error saving folder")
}
}
} }
pub fn delete(&self, conn: &DbConn) -> EmptyResult { pub fn delete(&self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn); User::update_uuid_revision(&self.user_uuid, conn);
FolderCipher::delete_all_by_folder(&self.uuid, &conn)?; FolderCipher::delete_all_by_folder(&self.uuid, &conn)?;
diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid)))
.execute(&**conn) db_run! { conn: {
.map_res("Error deleting folder") diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid)))
.execute(conn)
.map_res("Error deleting folder")
}}
} }
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
@ -112,73 +114,92 @@ impl Folder {
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
folders::table db_run! { conn: {
.filter(folders::uuid.eq(uuid)) folders::table
.first::<Self>(&**conn) .filter(folders::uuid.eq(uuid))
.ok() .first::<FolderDb>(conn)
.ok()
.from_db()
}}
} }
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
folders::table db_run! { conn: {
.filter(folders::user_uuid.eq(user_uuid)) folders::table
.load::<Self>(&**conn) .filter(folders::user_uuid.eq(user_uuid))
.expect("Error loading folders") .load::<FolderDb>(conn)
.expect("Error loading folders")
.from_db()
}}
} }
} }
impl FolderCipher { impl FolderCipher {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::insert_into(folders_ciphers::table)
.values(&*self)
.on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid))
.do_nothing()
.execute(&**conn)
.map_res("Error adding cipher to folder")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(folders_ciphers::table) db_run! { conn:
.values(&*self) sqlite, mysql {
.execute(&**conn) diesel::replace_into(folders_ciphers::table)
.map_res("Error adding cipher to folder") .values(FolderCipherDb::to_db(self))
.execute(conn)
.map_res("Error adding cipher to folder")
}
postgresql {
diesel::insert_into(folders_ciphers::table)
.values(FolderCipherDb::to_db(self))
.on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid))
.do_nothing()
.execute(conn)
.map_res("Error adding cipher to folder")
}
}
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
diesel::delete( db_run! { conn: {
folders_ciphers::table diesel::delete(
.filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid)) folders_ciphers::table
.filter(folders_ciphers::folder_uuid.eq(self.folder_uuid)), .filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid))
) .filter(folders_ciphers::folder_uuid.eq(self.folder_uuid)),
.execute(&**conn) )
.map_res("Error removing cipher from folder") .execute(conn)
.map_res("Error removing cipher from folder")
}}
} }
pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)))
.map_res("Error removing cipher from folders") .execute(conn)
.map_res("Error removing cipher from folders")
}}
} }
pub fn delete_all_by_folder(folder_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_folder(folder_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid)))
.map_res("Error removing ciphers from folder") .execute(conn)
.map_res("Error removing ciphers from folder")
}}
} }
pub fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &DbConn) -> Option<Self> {
folders_ciphers::table db_run! { conn: {
.filter(folders_ciphers::folder_uuid.eq(folder_uuid)) folders_ciphers::table
.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)) .filter(folders_ciphers::folder_uuid.eq(folder_uuid))
.first::<Self>(&**conn) .filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))
.ok() .first::<FolderCipherDb>(conn)
.ok()
.from_db()
}}
} }
pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> {
folders_ciphers::table db_run! { conn: {
.filter(folders_ciphers::folder_uuid.eq(folder_uuid)) folders_ciphers::table
.load::<Self>(&**conn) .filter(folders_ciphers::folder_uuid.eq(folder_uuid))
.expect("Error loading folders") .load::<FolderCipherDb>(conn)
.expect("Error loading folders")
.from_db()
}}
} }
} }

@ -1,23 +1,23 @@
use diesel::prelude::*;
use serde_json::Value; use serde_json::Value;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::db::schema::org_policies;
use crate::db::DbConn; use crate::db::DbConn;
use crate::error::MapResult; use crate::error::MapResult;
use super::Organization; use super::Organization;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] db_object! {
#[table_name = "org_policies"] #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[belongs_to(Organization, foreign_key = "org_uuid")] #[table_name = "org_policies"]
#[primary_key(uuid)] #[belongs_to(Organization, foreign_key = "org_uuid")]
pub struct OrgPolicy { #[primary_key(uuid)]
pub uuid: String, pub struct OrgPolicy {
pub org_uuid: String, pub uuid: String,
pub atype: i32, pub org_uuid: String,
pub enabled: bool, pub atype: i32,
pub data: String, pub enabled: bool,
pub data: String,
}
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -55,87 +55,105 @@ impl OrgPolicy {
/// Database methods /// Database methods
impl OrgPolicy { impl OrgPolicy {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult {
// We need to make sure we're not going to violate the unique constraint on org_uuid and atype.
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
// not support multiple constraints on ON CONFLICT clauses.
diesel::delete(
org_policies::table
.filter(org_policies::org_uuid.eq(&self.org_uuid))
.filter(org_policies::atype.eq(&self.atype)),
)
.execute(&**conn)
.map_res("Error deleting org_policy for insert")?;
diesel::insert_into(org_policies::table)
.values(self)
.on_conflict(org_policies::uuid)
.do_update()
.set(self)
.execute(&**conn)
.map_res("Error saving org_policy")
}
#[cfg(not(feature = "postgresql"))]
pub fn save(&self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
diesel::replace_into(org_policies::table) db_run! { conn:
.values(&*self) sqlite, mysql {
.execute(&**conn) diesel::replace_into(org_policies::table)
.map_res("Error saving org_policy") .values(OrgPolicyDb::to_db(self))
.execute(conn)
.map_res("Error saving org_policy")
}
postgresql {
let value = OrgPolicyDb::to_db(self);
// We need to make sure we're not going to violate the unique constraint on org_uuid and atype.
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
// not support multiple constraints on ON CONFLICT clauses.
diesel::delete(
org_policies::table
.filter(org_policies::org_uuid.eq(&self.org_uuid))
.filter(org_policies::atype.eq(&self.atype)),
)
.execute(conn)
.map_res("Error deleting org_policy for insert")?;
diesel::insert_into(org_policies::table)
.values(&value)
.on_conflict(org_policies::uuid)
.do_update()
.set(&value)
.execute(conn)
.map_res("Error saving org_policy")
}
}
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid)))
.map_res("Error deleting org_policy") .execute(conn)
.map_res("Error deleting org_policy")
}}
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
org_policies::table db_run! { conn: {
.filter(org_policies::uuid.eq(uuid)) org_policies::table
.first::<Self>(&**conn) .filter(org_policies::uuid.eq(uuid))
.ok() .first::<OrgPolicyDb>(conn)
.ok()
.from_db()
}}
} }
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
org_policies::table db_run! { conn: {
.filter(org_policies::org_uuid.eq(org_uuid)) org_policies::table
.load::<Self>(&**conn) .filter(org_policies::org_uuid.eq(org_uuid))
.expect("Error loading org_policy") .load::<OrgPolicyDb>(conn)
.expect("Error loading org_policy")
.from_db()
}}
} }
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
use crate::db::schema::users_organizations; db_run! { conn: {
org_policies::table
org_policies::table .left_join(
.left_join( users_organizations::table.on(
users_organizations::table.on( users_organizations::org_uuid.eq(org_policies::org_uuid)
users_organizations::org_uuid.eq(org_policies::org_uuid) .and(users_organizations::user_uuid.eq(user_uuid)))
.and(users_organizations::user_uuid.eq(user_uuid))) )
) .select(org_policies::all_columns)
.select(org_policies::all_columns) .load::<OrgPolicyDb>(conn)
.load::<Self>(&**conn) .expect("Error loading org_policy")
.expect("Error loading org_policy") .from_db()
}}
} }
pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> { pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> {
org_policies::table db_run! { conn: {
.filter(org_policies::org_uuid.eq(org_uuid)) org_policies::table
.filter(org_policies::atype.eq(atype)) .filter(org_policies::org_uuid.eq(org_uuid))
.first::<Self>(&**conn) .filter(org_policies::atype.eq(atype))
.ok() .first::<OrgPolicyDb>(conn)
.ok()
.from_db()
}}
} }
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid)))
.map_res("Error deleting org_policy") .execute(conn)
.map_res("Error deleting org_policy")
}}
} }
/*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { /*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
.map_res("Error deleting twofactors") .execute(conn)
.map_res("Error deleting twofactors")
}}
}*/ }*/
} }

@ -4,27 +4,29 @@ use num_traits::FromPrimitive;
use super::{CollectionUser, User, OrgPolicy}; use super::{CollectionUser, User, OrgPolicy};
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] db_object! {
#[table_name = "organizations"] #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[primary_key(uuid)] #[table_name = "organizations"]
pub struct Organization { #[primary_key(uuid)]
pub uuid: String, pub struct Organization {
pub name: String, pub uuid: String,
pub billing_email: String, pub name: String,
} pub billing_email: String,
}
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[table_name = "users_organizations"] #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[primary_key(uuid)] #[table_name = "users_organizations"]
pub struct UserOrganization { #[primary_key(uuid)]
pub uuid: String, pub struct UserOrganization {
pub user_uuid: String, pub uuid: String,
pub org_uuid: String, pub user_uuid: String,
pub org_uuid: String,
pub access_all: bool,
pub akey: String, pub access_all: bool,
pub status: i32, pub akey: String,
pub atype: i32, pub status: i32,
pub atype: i32,
}
} }
pub enum UserOrgStatus { pub enum UserOrgStatus {
@ -196,16 +198,13 @@ impl UserOrganization {
} }
} }
use crate::db::schema::{ciphers_collections, organizations, users_collections, users_organizations};
use crate::db::DbConn; use crate::db::DbConn;
use diesel::prelude::*;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::error::MapResult; use crate::error::MapResult;
/// Database methods /// Database methods
impl Organization { impl Organization {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
UserOrganization::find_by_org(&self.uuid, conn) UserOrganization::find_by_org(&self.uuid, conn)
.iter() .iter()
@ -213,27 +212,24 @@ impl Organization {
User::update_uuid_revision(&user_org.user_uuid, conn); User::update_uuid_revision(&user_org.user_uuid, conn);
}); });
diesel::insert_into(organizations::table) db_run! { conn:
.values(self) sqlite, mysql {
.on_conflict(organizations::uuid) diesel::replace_into(organizations::table)
.do_update() .values(OrganizationDb::to_db(self))
.set(self) .execute(conn)
.execute(&**conn) .map_res("Error saving organization")
.map_res("Error saving organization") }
} postgresql {
let value = OrganizationDb::to_db(self);
#[cfg(not(feature = "postgresql"))] diesel::insert_into(organizations::table)
pub fn save(&self, conn: &DbConn) -> EmptyResult { .values(&value)
UserOrganization::find_by_org(&self.uuid, conn) .on_conflict(organizations::uuid)
.iter() .do_update()
.for_each(|user_org| { .set(&value)
User::update_uuid_revision(&user_org.user_uuid, conn); .execute(conn)
}); .map_res("Error saving organization")
}
diesel::replace_into(organizations::table) }
.values(self)
.execute(&**conn)
.map_res("Error saving organization")
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
@ -244,20 +240,27 @@ impl Organization {
UserOrganization::delete_all_by_organization(&self.uuid, &conn)?; UserOrganization::delete_all_by_organization(&self.uuid, &conn)?;
OrgPolicy::delete_all_by_organization(&self.uuid, &conn)?; OrgPolicy::delete_all_by_organization(&self.uuid, &conn)?;
diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid)))
.execute(&**conn) db_run! { conn: {
.map_res("Error saving organization") diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid)))
.execute(conn)
.map_res("Error saving organization")
}}
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
organizations::table db_run! { conn: {
.filter(organizations::uuid.eq(uuid)) organizations::table
.first::<Self>(&**conn) .filter(organizations::uuid.eq(uuid))
.ok() .first::<OrganizationDb>(conn)
.ok().from_db()
}}
} }
pub fn get_all(conn: &DbConn) -> Vec<Self> { pub fn get_all(conn: &DbConn) -> Vec<Self> {
organizations::table.load::<Self>(&**conn).expect("Error loading organizations") db_run! { conn: {
organizations::table.load::<OrganizationDb>(conn).expect("Error loading organizations").from_db()
}}
} }
} }
@ -345,28 +348,27 @@ impl UserOrganization {
"Object": "organizationUserDetails", "Object": "organizationUserDetails",
}) })
} }
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
User::update_uuid_revision(&self.user_uuid, conn); User::update_uuid_revision(&self.user_uuid, conn);
diesel::insert_into(users_organizations::table) db_run! { conn:
.values(self) sqlite, mysql {
.on_conflict(users_organizations::uuid) diesel::replace_into(users_organizations::table)
.do_update() .values(UserOrganizationDb::to_db(self))
.set(self) .execute(conn)
.execute(&**conn) .map_res("Error adding user to organization")
.map_res("Error adding user to organization") }
} postgresql {
let value = UserOrganizationDb::to_db(self);
#[cfg(not(feature = "postgresql"))] diesel::insert_into(users_organizations::table)
pub fn save(&self, conn: &DbConn) -> EmptyResult { .values(&value)
User::update_uuid_revision(&self.user_uuid, conn); .on_conflict(users_organizations::uuid)
.do_update()
diesel::replace_into(users_organizations::table) .set(&value)
.values(self) .execute(conn)
.execute(&**conn) .map_res("Error adding user to organization")
.map_res("Error adding user to organization") }
}
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
@ -374,9 +376,11 @@ impl UserOrganization {
CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?; CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?;
diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
.map_res("Error removing user from organization") .execute(conn)
.map_res("Error removing user from organization")
}}
} }
pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult {
@ -403,107 +407,129 @@ impl UserOrganization {
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::uuid.eq(uuid)) users_organizations::table
.first::<Self>(&**conn) .filter(users_organizations::uuid.eq(uuid))
.ok() .first::<UserOrganizationDb>(conn)
.ok().from_db()
}}
} }
pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::uuid.eq(uuid)) users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::uuid.eq(uuid))
.first::<Self>(&**conn) .filter(users_organizations::org_uuid.eq(org_uuid))
.ok() .first::<UserOrganizationDb>(conn)
.ok().from_db()
}}
} }
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::user_uuid.eq(user_uuid)) users_organizations::table
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) .filter(users_organizations::user_uuid.eq(user_uuid))
.load::<Self>(&**conn) .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.unwrap_or_default() .load::<UserOrganizationDb>(conn)
.unwrap_or_default().from_db()
}}
} }
pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::user_uuid.eq(user_uuid)) users_organizations::table
.filter(users_organizations::status.eq(UserOrgStatus::Invited as i32)) .filter(users_organizations::user_uuid.eq(user_uuid))
.load::<Self>(&**conn) .filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
.unwrap_or_default() .load::<UserOrganizationDb>(conn)
.unwrap_or_default().from_db()
}}
} }
pub fn find_any_state_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_any_state_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::user_uuid.eq(user_uuid)) users_organizations::table
.load::<Self>(&**conn) .filter(users_organizations::user_uuid.eq(user_uuid))
.unwrap_or_default() .load::<UserOrganizationDb>(conn)
.unwrap_or_default().from_db()
}}
} }
pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::org_uuid.eq(org_uuid)) users_organizations::table
.load::<Self>(&**conn) .filter(users_organizations::org_uuid.eq(org_uuid))
.expect("Error loading user organizations") .load::<UserOrganizationDb>(conn)
.expect("Error loading user organizations").from_db()
}}
} }
pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 { pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 {
users_organizations::table db_run! { conn: {
.filter(users_organizations::org_uuid.eq(org_uuid)) users_organizations::table
.count() .filter(users_organizations::org_uuid.eq(org_uuid))
.first::<i64>(&**conn) .count()
.ok() .first::<i64>(conn)
.unwrap_or(0) .ok()
.unwrap_or(0)
}}
} }
pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Vec<Self> { pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Vec<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::org_uuid.eq(org_uuid)) users_organizations::table
.filter(users_organizations::atype.eq(atype)) .filter(users_organizations::org_uuid.eq(org_uuid))
.load::<Self>(&**conn) .filter(users_organizations::atype.eq(atype))
.expect("Error loading user organizations") .load::<UserOrganizationDb>(conn)
.expect("Error loading user organizations").from_db()
}}
} }
pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::user_uuid.eq(user_uuid)) users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid)) .filter(users_organizations::user_uuid.eq(user_uuid))
.first::<Self>(&**conn) .filter(users_organizations::org_uuid.eq(org_uuid))
.ok() .first::<UserOrganizationDb>(conn)
.ok().from_db()
}}
} }
pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::org_uuid.eq(org_uuid)) users_organizations::table
.left_join(users_collections::table.on( .filter(users_organizations::org_uuid.eq(org_uuid))
users_collections::user_uuid.eq(users_organizations::user_uuid) .left_join(users_collections::table.on(
)) users_collections::user_uuid.eq(users_organizations::user_uuid)
.left_join(ciphers_collections::table.on( ))
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and( .left_join(ciphers_collections::table.on(
ciphers_collections::cipher_uuid.eq(&cipher_uuid) ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and(
) ciphers_collections::cipher_uuid.eq(&cipher_uuid)
)) )
.filter( ))
users_organizations::access_all.eq(true).or( // AccessAll.. .filter(
ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher users_organizations::access_all.eq(true).or( // AccessAll..
ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher
)
) )
) .select(users_organizations::all_columns)
.select(users_organizations::all_columns) .load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
.load::<Self>(&**conn).expect("Error loading user organizations") }}
} }
pub fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> {
users_organizations::table db_run! { conn: {
.filter(users_organizations::org_uuid.eq(org_uuid)) users_organizations::table
.left_join(users_collections::table.on( .filter(users_organizations::org_uuid.eq(org_uuid))
users_collections::user_uuid.eq(users_organizations::user_uuid) .left_join(users_collections::table.on(
)) users_collections::user_uuid.eq(users_organizations::user_uuid)
.filter( ))
users_organizations::access_all.eq(true).or( // AccessAll.. .filter(
users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher users_organizations::access_all.eq(true).or( // AccessAll..
users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher
)
) )
) .select(users_organizations::all_columns)
.select(users_organizations::all_columns) .load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
.load::<Self>(&**conn).expect("Error loading user organizations") }}
} }
} }

@ -1,24 +1,24 @@
use diesel::prelude::*;
use serde_json::Value; use serde_json::Value;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::db::schema::twofactor;
use crate::db::DbConn; use crate::db::DbConn;
use crate::error::MapResult; use crate::error::MapResult;
use super::User; use super::User;
#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] db_object! {
#[table_name = "twofactor"] #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
#[belongs_to(User, foreign_key = "user_uuid")] #[table_name = "twofactor"]
#[primary_key(uuid)] #[belongs_to(User, foreign_key = "user_uuid")]
pub struct TwoFactor { #[primary_key(uuid)]
pub uuid: String, pub struct TwoFactor {
pub user_uuid: String, pub uuid: String,
pub atype: i32, pub user_uuid: String,
pub enabled: bool, pub atype: i32,
pub data: String, pub enabled: bool,
pub last_used: i32, pub data: String,
pub last_used: i32,
}
} }
#[allow(dead_code)] #[allow(dead_code)]
@ -70,57 +70,69 @@ impl TwoFactor {
/// Database methods /// Database methods
impl TwoFactor { impl TwoFactor {
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
// We need to make sure we're not going to violate the unique constraint on user_uuid and atype. db_run! { conn:
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does sqlite, mysql {
// not support multiple constraints on ON CONFLICT clauses. diesel::replace_into(twofactor::table)
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype))) .values(TwoFactorDb::to_db(self))
.execute(&**conn) .execute(conn)
.map_res("Error deleting twofactor for insert")?; .map_res("Error saving twofactor")
}
diesel::insert_into(twofactor::table) postgresql {
.values(self) let value = TwoFactorDb::to_db(self);
.on_conflict(twofactor::uuid) // We need to make sure we're not going to violate the unique constraint on user_uuid and atype.
.do_update() // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
.set(self) // not support multiple constraints on ON CONFLICT clauses.
.execute(&**conn) diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype)))
.map_res("Error saving twofactor") .execute(conn)
} .map_res("Error deleting twofactor for insert")?;
#[cfg(not(feature = "postgresql"))] diesel::insert_into(twofactor::table)
pub fn save(&self, conn: &DbConn) -> EmptyResult { .values(&value)
diesel::replace_into(twofactor::table) .on_conflict(twofactor::uuid)
.values(self) .do_update()
.execute(&**conn) .set(&value)
.map_res("Error saving twofactor") .execute(conn)
.map_res("Error saving twofactor")
}
}
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid)))
.map_res("Error deleting twofactor") .execute(conn)
.map_res("Error deleting twofactor")
}}
} }
pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> {
twofactor::table db_run! { conn: {
.filter(twofactor::user_uuid.eq(user_uuid)) twofactor::table
.filter(twofactor::atype.lt(1000)) // Filter implementation types .filter(twofactor::user_uuid.eq(user_uuid))
.load::<Self>(&**conn) .filter(twofactor::atype.lt(1000)) // Filter implementation types
.expect("Error loading twofactor") .load::<TwoFactorDb>(conn)
.expect("Error loading twofactor")
.from_db()
}}
} }
pub fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> { pub fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> {
twofactor::table db_run! { conn: {
.filter(twofactor::user_uuid.eq(user_uuid)) twofactor::table
.filter(twofactor::atype.eq(atype)) .filter(twofactor::user_uuid.eq(user_uuid))
.first::<Self>(&**conn) .filter(twofactor::atype.eq(atype))
.ok() .first::<TwoFactorDb>(conn)
.ok()
.from_db()
}}
} }
pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult {
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) db_run! { conn: {
.execute(&**conn) diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
.map_res("Error deleting twofactors") .execute(conn)
.map_res("Error deleting twofactors")
}}
} }
} }

@ -4,43 +4,53 @@ use serde_json::Value;
use crate::crypto; use crate::crypto;
use crate::CONFIG; use crate::CONFIG;
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] db_object! {
#[table_name = "users"] #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[changeset_options(treat_none_as_null="true")] #[table_name = "users"]
#[primary_key(uuid)] #[changeset_options(treat_none_as_null="true")]
pub struct User { #[primary_key(uuid)]
pub uuid: String, pub struct User {
pub created_at: NaiveDateTime, pub uuid: String,
pub updated_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub verified_at: Option<NaiveDateTime>, pub updated_at: NaiveDateTime,
pub last_verifying_at: Option<NaiveDateTime>, pub verified_at: Option<NaiveDateTime>,
pub login_verify_count: i32, pub last_verifying_at: Option<NaiveDateTime>,
pub login_verify_count: i32,
pub email: String,
pub email_new: Option<String>, pub email: String,
pub email_new_token: Option<String>, pub email_new: Option<String>,
pub name: String, pub email_new_token: Option<String>,
pub name: String,
pub password_hash: Vec<u8>,
pub salt: Vec<u8>, pub password_hash: Vec<u8>,
pub password_iterations: i32, pub salt: Vec<u8>,
pub password_hint: Option<String>, pub password_iterations: i32,
pub password_hint: Option<String>,
pub akey: String,
pub private_key: Option<String>, pub akey: String,
pub public_key: Option<String>, pub private_key: Option<String>,
pub public_key: Option<String>,
#[column_name = "totp_secret"]
_totp_secret: Option<String>, #[column_name = "totp_secret"] // Note, this is only added to the UserDb structs, not to User
pub totp_recover: Option<String>, _totp_secret: Option<String>,
pub totp_recover: Option<String>,
pub security_stamp: String,
pub security_stamp: String,
pub equivalent_domains: String,
pub excluded_globals: String, pub equivalent_domains: String,
pub excluded_globals: String,
pub client_kdf_type: i32,
pub client_kdf_iter: i32, pub client_kdf_type: i32,
pub client_kdf_iter: i32,
}
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "invitations"]
#[primary_key(email)]
pub struct Invitation {
pub email: String,
}
} }
enum UserStatus { enum UserStatus {
@ -119,9 +129,7 @@ impl User {
} }
use super::{Cipher, Device, Folder, TwoFactor, UserOrgType, UserOrganization}; use super::{Cipher, Device, Folder, TwoFactor, UserOrgType, UserOrganization};
use crate::db::schema::{invitations, users};
use crate::db::DbConn; use crate::db::DbConn;
use diesel::prelude::*;
use crate::api::EmptyResult; use crate::api::EmptyResult;
use crate::error::MapResult; use crate::error::MapResult;
@ -158,7 +166,6 @@ impl User {
}) })
} }
#[cfg(feature = "postgresql")]
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
if self.email.trim().is_empty() { if self.email.trim().is_empty() {
err!("User email can't be empty") err!("User email can't be empty")
@ -166,49 +173,48 @@ impl User {
self.updated_at = Utc::now().naive_utc(); self.updated_at = Utc::now().naive_utc();
diesel::insert_into(users::table) // Insert or update db_run! {conn:
.values(&*self) sqlite, mysql {
.on_conflict(users::uuid) diesel::replace_into(users::table) // Insert or update
.do_update() .values(&UserDb::to_db(self))
.set(&*self) .execute(conn)
.execute(&**conn) .map_res("Error saving user")
.map_res("Error saving user") }
} postgresql {
let value = UserDb::to_db(self);
#[cfg(not(feature = "postgresql"))] diesel::insert_into(users::table) // Insert or update
pub fn save(&mut self, conn: &DbConn) -> EmptyResult { .values(&value)
if self.email.trim().is_empty() { .on_conflict(users::uuid)
err!("User email can't be empty") .do_update()
.set(&value)
.execute(conn)
.map_res("Error saving user")
}
} }
self.updated_at = Utc::now().naive_utc();
diesel::replace_into(users::table) // Insert or update
.values(&*self)
.execute(&**conn)
.map_res("Error saving user")
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) { for user_org in UserOrganization::find_by_user(&self.uuid, conn) {
if user_org.atype == UserOrgType::Owner { if user_org.atype == UserOrgType::Owner {
let owner_type = UserOrgType::Owner as i32; let owner_type = UserOrgType::Owner as i32;
if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, &conn).len() <= 1 { if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, conn).len() <= 1 {
err!("Can't delete last owner") err!("Can't delete last owner")
} }
} }
} }
UserOrganization::delete_all_by_user(&self.uuid, &*conn)?; UserOrganization::delete_all_by_user(&self.uuid, conn)?;
Cipher::delete_all_by_user(&self.uuid, &*conn)?; Cipher::delete_all_by_user(&self.uuid, conn)?;
Folder::delete_all_by_user(&self.uuid, &*conn)?; Folder::delete_all_by_user(&self.uuid, conn)?;
Device::delete_all_by_user(&self.uuid, &*conn)?; Device::delete_all_by_user(&self.uuid, conn)?;
TwoFactor::delete_all_by_user(&self.uuid, &*conn)?; TwoFactor::delete_all_by_user(&self.uuid, conn)?;
Invitation::take(&self.email, &*conn); // Delete invitation if any Invitation::take(&self.email, conn); // Delete invitation if any
diesel::delete(users::table.filter(users::uuid.eq(self.uuid))) db_run! {conn: {
.execute(&**conn) diesel::delete(users::table.filter(users::uuid.eq(self.uuid)))
.map_res("Error deleting user") .execute(conn)
.map_res("Error deleting user")
}}
} }
pub fn update_uuid_revision(uuid: &str, conn: &DbConn) { pub fn update_uuid_revision(uuid: &str, conn: &DbConn) {
@ -220,15 +226,14 @@ impl User {
pub fn update_all_revisions(conn: &DbConn) -> EmptyResult { pub fn update_all_revisions(conn: &DbConn) -> EmptyResult {
let updated_at = Utc::now().naive_utc(); let updated_at = Utc::now().naive_utc();
crate::util::retry( db_run! {conn: {
|| { crate::util::retry(|| {
diesel::update(users::table) diesel::update(users::table)
.set(users::updated_at.eq(updated_at)) .set(users::updated_at.eq(updated_at))
.execute(&**conn) .execute(conn)
}, }, 10)
10, .map_res("Error updating revision date for all users")
) }}
.map_res("Error updating revision date for all users")
} }
pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult { pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult {
@ -238,84 +243,85 @@ impl User {
} }
fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult { fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult {
crate::util::retry( db_run! {conn: {
|| { crate::util::retry(|| {
diesel::update(users::table.filter(users::uuid.eq(uuid))) diesel::update(users::table.filter(users::uuid.eq(uuid)))
.set(users::updated_at.eq(date)) .set(users::updated_at.eq(date))
.execute(&**conn) .execute(conn)
}, }, 10)
10, .map_res("Error updating user revision")
) }}
.map_res("Error updating user revision")
} }
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
let lower_mail = mail.to_lowercase(); let lower_mail = mail.to_lowercase();
users::table db_run! {conn: {
.filter(users::email.eq(lower_mail)) users::table
.first::<Self>(&**conn) .filter(users::email.eq(lower_mail))
.ok() .first::<UserDb>(conn)
.ok()
.from_db()
}}
} }
pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> {
users::table.filter(users::uuid.eq(uuid)).first::<Self>(&**conn).ok() db_run! {conn: {
users::table.filter(users::uuid.eq(uuid)).first::<UserDb>(conn).ok().from_db()
}}
} }
pub fn get_all(conn: &DbConn) -> Vec<Self> { pub fn get_all(conn: &DbConn) -> Vec<Self> {
users::table.load::<Self>(&**conn).expect("Error loading users") db_run! {conn: {
users::table.load::<UserDb>(conn).expect("Error loading users").from_db()
}}
} }
} }
#[derive(Debug, Identifiable, Queryable, Insertable)]
#[table_name = "invitations"]
#[primary_key(email)]
pub struct Invitation {
pub email: String,
}
impl Invitation { impl Invitation {
pub const fn new(email: String) -> Self { pub const fn new(email: String) -> Self {
Self { email } Self { email }
} }
#[cfg(feature = "postgresql")]
pub fn save(&self, conn: &DbConn) -> EmptyResult { pub fn save(&self, conn: &DbConn) -> EmptyResult {
if self.email.trim().is_empty() { if self.email.trim().is_empty() {
err!("Invitation email can't be empty") err!("Invitation email can't be empty")
} }
diesel::insert_into(invitations::table) db_run! {conn:
.values(self) sqlite, mysql {
.on_conflict(invitations::email) diesel::replace_into(invitations::table)
.do_nothing() .values(InvitationDb::to_db(self))
.execute(&**conn) .execute(conn)
.map_res("Error saving invitation") .map_res("Error saving invitation")
} }
postgresql {
#[cfg(not(feature = "postgresql"))] diesel::insert_into(invitations::table)
pub fn save(&self, conn: &DbConn) -> EmptyResult { .values(InvitationDb::to_db(self))
if self.email.trim().is_empty() { .on_conflict(invitations::email)
err!("Invitation email can't be empty") .do_nothing()
.execute(conn)
.map_res("Error saving invitation")
}
} }
diesel::replace_into(invitations::table)
.values(self)
.execute(&**conn)
.map_res("Error saving invitation")
} }
pub fn delete(self, conn: &DbConn) -> EmptyResult { pub fn delete(self, conn: &DbConn) -> EmptyResult {
diesel::delete(invitations::table.filter(invitations::email.eq(self.email))) db_run! {conn: {
.execute(&**conn) diesel::delete(invitations::table.filter(invitations::email.eq(self.email)))
.map_res("Error deleting invitation") .execute(conn)
.map_res("Error deleting invitation")
}}
} }
pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> { pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> {
let lower_mail = mail.to_lowercase(); let lower_mail = mail.to_lowercase();
invitations::table db_run! {conn: {
.filter(invitations::email.eq(lower_mail)) invitations::table
.first::<Self>(&**conn) .filter(invitations::email.eq(lower_mail))
.ok() .first::<InvitationDb>(conn)
.ok()
.from_db()
}}
} }
pub fn take(mail: &str, conn: &DbConn) -> bool { pub fn take(mail: &str, conn: &DbConn) -> bool {

@ -34,6 +34,7 @@ macro_rules! make_error {
} }
use diesel::result::Error as DieselErr; use diesel::result::Error as DieselErr;
use diesel::r2d2::PoolError as R2d2Err;
use handlebars::RenderError as HbErr; use handlebars::RenderError as HbErr;
use jsonwebtoken::errors::Error as JWTErr; use jsonwebtoken::errors::Error as JWTErr;
use regex::Error as RegexErr; use regex::Error as RegexErr;
@ -66,6 +67,7 @@ make_error! {
// Used for special return values, like 2FA errors // Used for special return values, like 2FA errors
JsonError(Value): _no_source, _serialize, JsonError(Value): _no_source, _serialize,
DbError(DieselErr): _has_source, _api_error, DbError(DieselErr): _has_source, _api_error,
R2d2Error(R2d2Err): _has_source, _api_error,
U2fError(U2fErr): _has_source, _api_error, U2fError(U2fErr): _has_source, _api_error,
SerdeError(SerdeErr): _has_source, _api_error, SerdeError(SerdeErr): _has_source, _api_error,
JWTError(JWTErr): _has_source, _api_error, JWTError(JWTErr): _has_source, _api_error,

@ -33,6 +33,7 @@ mod api;
mod auth; mod auth;
mod config; mod config;
mod crypto; mod crypto;
#[macro_use]
mod db; mod db;
mod mail; mod mail;
mod util; mod util;
@ -61,10 +62,8 @@ fn main() {
_ => false, _ => false,
}; };
check_db();
check_rsa_keys(); check_rsa_keys();
check_web_vault(); check_web_vault();
migrations::run_migrations();
create_icon_cache_folder(); create_icon_cache_folder();
@ -200,30 +199,6 @@ fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch {
} }
} }
fn check_db() {
if cfg!(feature = "sqlite") {
let url = CONFIG.database_url();
let path = Path::new(&url);
if let Some(parent) = path.parent() {
if create_dir_all(parent).is_err() {
error!("Error creating database directory");
exit(1);
}
}
// Turn on WAL in SQLite
if CONFIG.enable_db_wal() {
use diesel::RunQueryDsl;
let connection = db::get_connection().expect("Can't connect to DB");
diesel::sql_query("PRAGMA journal_mode=wal")
.execute(&connection)
.expect("Failed to turn on WAL");
}
}
db::get_connection().expect("Can't connect to DB");
}
fn create_icon_cache_folder() { fn create_icon_cache_folder() {
// Try to create the icon cache folder, and generate an error if it could not. // Try to create the icon cache folder, and generate an error if it could not.
create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache directory"); create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache directory");
@ -285,57 +260,22 @@ fn check_web_vault() {
let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html"); let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html");
if !index_path.exists() { if !index_path.exists() {
error!("Web vault is not found. To install it, please follow the steps in: "); error!("Web vault is not found at '{}'. To install it, please follow the steps in: ", CONFIG.web_vault_folder());
error!("https://github.com/dani-garcia/bitwarden_rs/wiki/Building-binary#install-the-web-vault"); error!("https://github.com/dani-garcia/bitwarden_rs/wiki/Building-binary#install-the-web-vault");
error!("You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it"); error!("You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it");
exit(1); exit(1);
} }
} }
// Embed the migrations from the migrations folder into the application
// This way, the program automatically migrates the database to the latest version
// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html
#[allow(unused_imports)]
mod migrations {
#[cfg(feature = "sqlite")]
embed_migrations!("migrations/sqlite");
#[cfg(feature = "mysql")]
embed_migrations!("migrations/mysql");
#[cfg(feature = "postgresql")]
embed_migrations!("migrations/postgresql");
pub fn run_migrations() {
// Make sure the database is up to date (create if it doesn't exist, or run the migrations)
let connection = crate::db::get_connection().expect("Can't connect to DB");
use std::io::stdout;
// Disable Foreign Key Checks during migration
use diesel::RunQueryDsl;
// FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html,
// "SET CONSTRAINTS sets the behavior of constraint checking within the
// current transaction", so this setting probably won't take effect for
// any of the migrations since it's being run outside of a transaction.
// Migrations that need to disable foreign key checks should run this
// from within the migration script itself.
#[cfg(feature = "postgres")]
diesel::sql_query("SET CONSTRAINTS ALL DEFERRED").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations");
// Scoped to a connection/session.
#[cfg(feature = "mysql")]
diesel::sql_query("SET FOREIGN_KEY_CHECKS = 0").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations");
// Scoped to a connection.
#[cfg(feature = "sqlite")]
diesel::sql_query("PRAGMA foreign_keys = OFF").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations");
embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations");
}
}
fn launch_rocket(extra_debug: bool) { fn launch_rocket(extra_debug: bool) {
let pool = match db::DbPool::from_config() {
Ok(p) => p,
Err(e) => {
error!("Error creating database pool: {:?}", e);
exit(1);
}
};
let basepath = &CONFIG.domain_path(); let basepath = &CONFIG.domain_path();
// If adding more paths here, consider also adding them to // If adding more paths here, consider also adding them to
@ -347,7 +287,7 @@ fn launch_rocket(extra_debug: bool) {
.mount(&[basepath, "/identity"].concat(), api::identity_routes()) .mount(&[basepath, "/identity"].concat(), api::identity_routes())
.mount(&[basepath, "/icons"].concat(), api::icons_routes()) .mount(&[basepath, "/icons"].concat(), api::icons_routes())
.mount(&[basepath, "/notifications"].concat(), api::notifications_routes()) .mount(&[basepath, "/notifications"].concat(), api::notifications_routes())
.manage(db::init_pool()) .manage(pool)
.manage(api::start_notification_server()) .manage(api::start_notification_server())
.attach(util::AppHeaders()) .attach(util::AppHeaders())
.attach(util::CORS()) .attach(util::CORS())

Loading…
Cancel
Save