rename membership and adopt newtype pattern (#5320)

* rename membership

rename UserOrganization to Membership to clarify the relation
and prevent confusion whether something refers to a member(ship) or user

* use newtype pattern

* implement custom derive macro IdFromParam

* add UuidFromParam macro for UUIDs

* add macros to Docker build

Co-authored-by: dfunkt <dfunkt@users.noreply.github.com>

---------

Co-authored-by: dfunkt <dfunkt@users.noreply.github.com>
This commit is contained in:
Stefan Melmuk 2025-01-09 18:37:23 +01:00 committed by GitHub
parent 10d12676cf
commit 871a3f214a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 2800 additions and 2114 deletions

View File

@ -5,6 +5,7 @@
!.git
!docker/healthcheck.sh
!docker/start.sh
!macros
!migrations
!src

43
Cargo.lock generated
View File

@ -752,6 +752,27 @@ dependencies = [
"syn",
]
[[package]]
name = "derive_more"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"proc-macro2",
"quote",
"syn",
"unicode-xid",
]
[[package]]
name = "devise"
version = "0.4.2"
@ -809,6 +830,17 @@ dependencies = [
"url",
]
[[package]]
name = "diesel-derive-newtype"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5adf688c584fe33726ce0e2898f608a2a92578ac94a4a92fcecf73214fe0716"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "diesel_derives"
version = "2.2.3"
@ -2013,6 +2045,14 @@ dependencies = [
"linked-hash-map",
]
[[package]]
name = "macros"
version = "0.1.0"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "match_cfg"
version = "0.1.0"
@ -4011,7 +4051,9 @@ dependencies = [
"dashmap",
"data-encoding",
"data-url",
"derive_more",
"diesel",
"diesel-derive-newtype",
"diesel_logger",
"diesel_migrations",
"dotenvy",
@ -4028,6 +4070,7 @@ dependencies = [
"lettre",
"libsqlite3-sys",
"log",
"macros",
"mimalloc",
"num-derive",
"num-traits",

View File

@ -1,3 +1,5 @@
workspace = { members = ["macros"] }
[package]
name = "vaultwarden"
version = "1.0.0"
@ -39,6 +41,8 @@ unstable = []
syslog = "7.0.0"
[dependencies]
macros = { path = "./macros" }
# Logging
log = "0.4.22"
fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] }
@ -78,6 +82,9 @@ diesel = { version = "2.2.6", features = ["chrono", "r2d2", "numeric"] }
diesel_migrations = "2.2.0"
diesel_logger = { version = "0.4.0", optional = true }
derive_more = { version = "1.0.0", features = ["from", "into", "as_ref", "deref", "display"] }
diesel-derive-newtype = "2.1.2"
# Bundled/Static SQLite
libsqlite3-sys = { version = "0.30.1", features = ["bundled"], optional = true }

View File

@ -76,6 +76,7 @@ RUN source /env-cargo && \
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
COPY ./macros ./macros
ARG CARGO_PROFILE=release

View File

@ -116,6 +116,7 @@ RUN source /env-cargo && \
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
COPY ./macros ./macros
ARG CARGO_PROFILE=release

View File

@ -143,6 +143,7 @@ RUN source /env-cargo && \
# Copies over *only* your manifests and build files
COPY ./Cargo.* ./rust-toolchain.toml ./build.rs ./
COPY ./macros ./macros
ARG CARGO_PROFILE=release

13
macros/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "macros"
version = "0.1.0"
edition = "2021"
[lib]
name = "macros"
path = "src/lib.rs"
proc-macro = true
[dependencies]
quote = "1.0.38"
syn = "2.0.94"

58
macros/src/lib.rs Normal file
View File

@ -0,0 +1,58 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(UuidFromParam)]
pub fn derive_uuid_from_param(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_derive_uuid_macro(&ast)
}
fn impl_derive_uuid_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
#[automatically_derived]
impl<'r> rocket::request::FromParam<'r> for #name {
type Error = ();
#[inline(always)]
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
if uuid::Uuid::parse_str(param).is_ok() {
Ok(Self(param.to_string()))
} else {
Err(())
}
}
}
};
gen.into()
}
#[proc_macro_derive(IdFromParam)]
pub fn derive_id_from_param(input: TokenStream) -> TokenStream {
let ast = syn::parse(input).unwrap();
impl_derive_safestring_macro(&ast)
}
fn impl_derive_safestring_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
#[automatically_derived]
impl<'r> rocket::request::FromParam<'r> for #name {
type Error = ();
#[inline(always)]
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) {
Ok(Self(param.to_string()))
} else {
Err(())
}
}
}
};
gen.into()
}

View File

@ -50,7 +50,7 @@ pub fn routes() -> Vec<Route> {
disable_user,
enable_user,
remove_2fa,
update_user_org_type,
update_membership_type,
update_revision_users,
post_config,
delete_config,
@ -280,8 +280,8 @@ struct InviteData {
email: String,
}
async fn get_user_or_404(uuid: &str, conn: &mut DbConn) -> ApiResult<User> {
if let Some(user) = User::find_by_uuid(uuid, conn).await {
async fn get_user_or_404(user_id: &UserId, conn: &mut DbConn) -> ApiResult<User> {
if let Some(user) = User::find_by_uuid(user_id, conn).await {
Ok(user)
} else {
err_code!("User doesn't exist", Status::NotFound.code);
@ -381,29 +381,29 @@ async fn get_user_by_mail_json(mail: &str, _token: AdminToken, mut conn: DbConn)
}
}
#[get("/users/<uuid>")]
async fn get_user_json(uuid: &str, _token: AdminToken, mut conn: DbConn) -> JsonResult {
let u = get_user_or_404(uuid, &mut conn).await?;
#[get("/users/<user_id>")]
async fn get_user_json(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> JsonResult {
let u = get_user_or_404(&user_id, &mut conn).await?;
let mut usr = u.to_json(&mut conn).await;
usr["userEnabled"] = json!(u.enabled);
usr["createdAt"] = json!(format_naive_datetime_local(&u.created_at, DT_FMT));
Ok(Json(usr))
}
#[post("/users/<uuid>/delete")]
async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let user = get_user_or_404(uuid, &mut conn).await?;
#[post("/users/<user_id>/delete")]
async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let user = get_user_or_404(&user_id, &mut conn).await?;
// Get the user_org records before deleting the actual user
let user_orgs = UserOrganization::find_any_state_by_user(uuid, &mut conn).await;
// Get the membership records before deleting the actual user
let memberships = Membership::find_any_state_by_user(&user_id, &mut conn).await;
let res = user.delete(&mut conn).await;
for user_org in user_orgs {
for membership in memberships {
log_event(
EventType::OrganizationUserRemoved as i32,
&user_org.uuid,
&user_org.org_uuid,
ACTING_ADMIN_USER,
&membership.uuid,
&membership.org_uuid,
&ACTING_ADMIN_USER.into(),
14, // Use UnknownBrowser type
&token.ip.ip,
&mut conn,
@ -414,9 +414,9 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe
res
}
#[post("/users/<uuid>/deauth")]
async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(uuid, &mut conn).await?;
#[post("/users/<user_id>/deauth")]
async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
nt.send_logout(&user, None).await;
@ -435,9 +435,9 @@ async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notif
user.save(&mut conn).await
}
#[post("/users/<uuid>/disable")]
async fn disable_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(uuid, &mut conn).await?;
#[post("/users/<user_id>/disable")]
async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
user.reset_security_stamp();
user.enabled = false;
@ -449,26 +449,26 @@ async fn disable_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Noti
save_result
}
#[post("/users/<uuid>/enable")]
async fn enable_user(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(uuid, &mut conn).await?;
#[post("/users/<user_id>/enable")]
async fn enable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
user.enabled = true;
user.save(&mut conn).await
}
#[post("/users/<uuid>/remove-2fa")]
async fn remove_2fa(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(uuid, &mut conn).await?;
#[post("/users/<user_id>/remove-2fa")]
async fn remove_2fa(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
two_factor::enforce_2fa_policy(&user, ACTING_ADMIN_USER, 14, &token.ip.ip, &mut conn).await?;
two_factor::enforce_2fa_policy(&user, &ACTING_ADMIN_USER.into(), 14, &token.ip.ip, &mut conn).await?;
user.totp_recover = None;
user.save(&mut conn).await
}
#[post("/users/<uuid>/invite/resend")]
async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
if let Some(user) = User::find_by_uuid(uuid, &mut conn).await {
#[post("/users/<user_id>/invite/resend")]
async fn resend_user_invite(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
if let Some(user) = User::find_by_uuid(&user_id, &mut conn).await {
//TODO: replace this with user.status check when it will be available (PR#3397)
if !user.password_hash.is_empty() {
err_code!("User already accepted invitation", Status::BadRequest.code);
@ -485,42 +485,41 @@ async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) ->
}
#[derive(Debug, Deserialize)]
struct UserOrgTypeData {
struct MembershipTypeData {
user_type: NumberOrString,
user_uuid: String,
org_uuid: String,
user_uuid: UserId,
org_uuid: OrganizationId,
}
#[post("/users/org_type", data = "<data>")]
async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let data: UserOrgTypeData = data.into_inner();
async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let data: MembershipTypeData = data.into_inner();
let Some(mut user_to_edit) =
UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await
let Some(mut member_to_edit) = Membership::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &mut conn).await
else {
err!("The specified user isn't member of the organization")
};
let new_type = match UserOrgType::from_str(&data.user_type.into_string()) {
let new_type = match MembershipType::from_str(&data.user_type.into_string()) {
Some(new_type) => new_type as i32,
None => err!("Invalid type"),
};
if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
if member_to_edit.atype == MembershipType::Owner && new_type != MembershipType::Owner {
// Removing owner permission, check that there is at least one other confirmed owner
if UserOrganization::count_confirmed_by_org_and_type(&data.org_uuid, UserOrgType::Owner, &mut conn).await <= 1 {
if Membership::count_confirmed_by_org_and_type(&data.org_uuid, MembershipType::Owner, &mut conn).await <= 1 {
err!("Can't change the type of the last owner")
}
}
// This check is also done at api::organizations::{accept_invite(), _confirm_invite, _activate_user(), edit_user()}, update_user_org_type
// This check is also done at api::organizations::{accept_invite, _confirm_invite, _activate_member, edit_member}, update_membership_type
// It returns different error messages per function.
if new_type < UserOrgType::Admin {
match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &mut conn).await {
if new_type < MembershipType::Admin {
match OrgPolicy::is_user_allowed(&member_to_edit.user_uuid, &member_to_edit.org_uuid, true, &mut conn).await {
Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => {
if CONFIG.email_2fa_auto_fallback() {
two_factor::email::find_and_activate_email_2fa(&user_to_edit.user_uuid, &mut conn).await?;
two_factor::email::find_and_activate_email_2fa(&member_to_edit.user_uuid, &mut conn).await?;
} else {
err!("You cannot modify this user to this type because they have not setup 2FA");
}
@ -533,17 +532,17 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, token: AdminToken, mu
log_event(
EventType::OrganizationUserUpdated as i32,
&user_to_edit.uuid,
&member_to_edit.uuid,
&data.org_uuid,
ACTING_ADMIN_USER,
&ACTING_ADMIN_USER.into(),
14, // Use UnknownBrowser type
&token.ip.ip,
&mut conn,
)
.await;
user_to_edit.atype = new_type;
user_to_edit.save(&mut conn).await
member_to_edit.atype = new_type;
member_to_edit.save(&mut conn).await
}
#[post("/users/update_revision")]
@ -557,7 +556,7 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
let mut organizations_json = Vec::with_capacity(organizations.len());
for o in organizations {
let mut org = o.to_json();
org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &mut conn).await);
org["user_count"] = json!(Membership::count_by_org(&o.uuid, &mut conn).await);
org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &mut conn).await);
org["collection_count"] = json!(Collection::count_by_org(&o.uuid, &mut conn).await);
org["group_count"] = json!(Group::count_by_org(&o.uuid, &mut conn).await);
@ -571,9 +570,9 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
Ok(Html(text))
}
#[post("/organizations/<uuid>/delete")]
async fn delete_organization(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
let org = Organization::find_by_uuid(uuid, &mut conn).await.map_res("Organization doesn't exist")?;
#[post("/organizations/<org_id>/delete")]
async fn delete_organization(org_id: OrganizationId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
let org = Organization::find_by_uuid(&org_id, &mut conn).await.map_res("Organization doesn't exist")?;
org.delete(&mut conn).await
}

View File

@ -79,7 +79,7 @@ pub struct RegisterData {
name: Option<String>,
token: Option<String>,
#[allow(dead_code)]
organization_user_id: Option<String>,
organization_user_id: Option<MembershipId>,
}
#[derive(Debug, Deserialize)]
@ -106,15 +106,15 @@ fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult
}
Ok(())
}
async fn is_email_2fa_required(org_user_uuid: Option<String>, conn: &mut DbConn) -> bool {
async fn is_email_2fa_required(member_id: Option<MembershipId>, conn: &mut DbConn) -> bool {
if !CONFIG._enable_email_2fa() {
return false;
}
if CONFIG.email_2fa_enforce_on_verified_invite() {
return true;
}
if org_user_uuid.is_some() {
return OrgPolicy::is_enabled_for_member(&org_user_uuid.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
if member_id.is_some() {
return OrgPolicy::is_enabled_for_member(&member_id.unwrap(), OrgPolicyType::TwoFactorAuthentication, conn)
.await;
}
false
@ -161,9 +161,9 @@ pub async fn _register(data: Json<RegisterData>, mut conn: DbConn) -> JsonResult
err!("Registration email does not match invite email")
}
} else if Invitation::take(&email, &mut conn).await {
for user_org in UserOrganization::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
user_org.status = UserOrgStatus::Accepted as i32;
user_org.save(&mut conn).await?;
for membership in Membership::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
membership.status = MembershipStatus::Accepted as i32;
membership.save(&mut conn).await?;
}
user
} else if CONFIG.is_signup_allowed(&email)
@ -305,9 +305,9 @@ async fn put_avatar(data: Json<AvatarData>, headers: Headers, mut conn: DbConn)
Ok(Json(user.to_json(&mut conn).await))
}
#[get("/users/<uuid>/public-key")]
async fn get_public_keys(uuid: &str, _headers: Headers, mut conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(uuid, &mut conn).await {
#[get("/users/<user_id>/public-key")]
async fn get_public_keys(user_id: UserId, _headers: Headers, mut conn: DbConn) -> JsonResult {
let user = match User::find_by_uuid(&user_id, &mut conn).await {
Some(user) if user.public_key.is_some() => user,
Some(_) => err_code!("User has no public_key", Status::NotFound.code),
None => err_code!("User doesn't exist", Status::NotFound.code),
@ -379,7 +379,7 @@ async fn post_password(data: Json<ChangePassData>, headers: Headers, mut conn: D
// Prevent logging out the client where the user requested this endpoint from.
// If you do logout the user it will causes issues at the client side.
// Adding the device uuid will prevent this.
nt.send_logout(&user, Some(headers.device.uuid)).await;
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
save_result
}
@ -439,7 +439,7 @@ async fn post_kdf(data: Json<ChangeKdfData>, headers: Headers, mut conn: DbConn,
user.set_password(&data.new_master_password_hash, Some(data.key), true, None);
let save_result = user.save(&mut conn).await;
nt.send_logout(&user, Some(headers.device.uuid)).await;
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
save_result
}
@ -450,21 +450,21 @@ struct UpdateFolderData {
// There is a bug in 2024.3.x which adds a `null` item.
// To bypass this we allow a Option here, but skip it during the updates
// See: https://github.com/bitwarden/clients/issues/8453
id: Option<String>,
id: Option<FolderId>,
name: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UpdateEmergencyAccessData {
id: String,
id: EmergencyAccessId,
key_encrypted: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UpdateResetPasswordData {
organization_id: String,
organization_id: OrganizationId,
reset_password_key: String,
}
@ -489,48 +489,49 @@ fn validate_keydata(
existing_ciphers: &[Cipher],
existing_folders: &[Folder],
existing_emergency_access: &[EmergencyAccess],
existing_user_orgs: &[UserOrganization],
existing_memberships: &[Membership],
existing_sends: &[Send],
) -> EmptyResult {
// Check that we're correctly rotating all the user's ciphers
let existing_cipher_ids = existing_ciphers.iter().map(|c| c.uuid.as_str()).collect::<HashSet<_>>();
let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::<HashSet<&CipherId>>();
let provided_cipher_ids = data
.ciphers
.iter()
.filter(|c| c.organization_id.is_none())
.filter_map(|c| c.id.as_deref())
.collect::<HashSet<_>>();
.filter_map(|c| c.id.as_ref())
.collect::<HashSet<&CipherId>>();
if !provided_cipher_ids.is_superset(&existing_cipher_ids) {
err!("All existing ciphers must be included in the rotation")
}
// Check that we're correctly rotating all the user's folders
let existing_folder_ids = existing_folders.iter().map(|f| f.uuid.as_str()).collect::<HashSet<_>>();
let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_deref()).collect::<HashSet<_>>();
let existing_folder_ids = existing_folders.iter().map(|f| &f.uuid).collect::<HashSet<&FolderId>>();
let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_ref()).collect::<HashSet<&FolderId>>();
if !provided_folder_ids.is_superset(&existing_folder_ids) {
err!("All existing folders must be included in the rotation")
}
// Check that we're correctly rotating all the user's emergency access keys
let existing_emergency_access_ids =
existing_emergency_access.iter().map(|ea| ea.uuid.as_str()).collect::<HashSet<_>>();
existing_emergency_access.iter().map(|ea| &ea.uuid).collect::<HashSet<&EmergencyAccessId>>();
let provided_emergency_access_ids =
data.emergency_access_keys.iter().map(|ea| ea.id.as_str()).collect::<HashSet<_>>();
data.emergency_access_keys.iter().map(|ea| &ea.id).collect::<HashSet<&EmergencyAccessId>>();
if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) {
err!("All existing emergency access keys must be included in the rotation")
}
// Check that we're correctly rotating all the user's reset password keys
let existing_reset_password_ids = existing_user_orgs.iter().map(|uo| uo.org_uuid.as_str()).collect::<HashSet<_>>();
let existing_reset_password_ids =
existing_memberships.iter().map(|m| &m.org_uuid).collect::<HashSet<&OrganizationId>>();
let provided_reset_password_ids =
data.reset_password_keys.iter().map(|rp| rp.organization_id.as_str()).collect::<HashSet<_>>();
data.reset_password_keys.iter().map(|rp| &rp.organization_id).collect::<HashSet<&OrganizationId>>();
if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) {
err!("All existing reset password keys must be included in the rotation")
}
// Check that we're correctly rotating all the user's sends
let existing_send_ids = existing_sends.iter().map(|s| s.uuid.as_str()).collect::<HashSet<_>>();
let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_deref()).collect::<HashSet<_>>();
let existing_send_ids = existing_sends.iter().map(|s| &s.uuid).collect::<HashSet<&SendId>>();
let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_ref()).collect::<HashSet<&SendId>>();
if !provided_send_ids.is_superset(&existing_send_ids) {
err!("All existing sends must be included in the rotation")
}
@ -553,24 +554,24 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
Cipher::validate_cipher_data(&data.ciphers)?;
let user_uuid = &headers.user.uuid;
let user_id = &headers.user.uuid;
// TODO: Ideally we'd do everything after this point in a single transaction.
let mut existing_ciphers = Cipher::find_owned_by_user(user_uuid, &mut conn).await;
let mut existing_folders = Folder::find_by_user(user_uuid, &mut conn).await;
let mut existing_emergency_access = EmergencyAccess::find_all_by_grantor_uuid(user_uuid, &mut conn).await;
let mut existing_user_orgs = UserOrganization::find_by_user(user_uuid, &mut conn).await;
let mut existing_ciphers = Cipher::find_owned_by_user(user_id, &mut conn).await;
let mut existing_folders = Folder::find_by_user(user_id, &mut conn).await;
let mut existing_emergency_access = EmergencyAccess::find_all_by_grantor_uuid(user_id, &mut conn).await;
let mut existing_memberships = Membership::find_by_user(user_id, &mut conn).await;
// We only rotate the reset password key if it is set.
existing_user_orgs.retain(|uo| uo.reset_password_key.is_some());
let mut existing_sends = Send::find_by_user(user_uuid, &mut conn).await;
existing_memberships.retain(|m| m.reset_password_key.is_some());
let mut existing_sends = Send::find_by_user(user_id, &mut conn).await;
validate_keydata(
&data,
&existing_ciphers,
&existing_folders,
&existing_emergency_access,
&existing_user_orgs,
&existing_memberships,
&existing_sends,
)?;
@ -602,14 +603,14 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Update reset password data
for reset_password_data in data.reset_password_keys {
let Some(user_org) =
existing_user_orgs.iter_mut().find(|uo| uo.org_uuid == reset_password_data.organization_id)
let Some(membership) =
existing_memberships.iter_mut().find(|m| m.org_uuid == reset_password_data.organization_id)
else {
err!("Reset password doesn't exist")
};
user_org.reset_password_key = Some(reset_password_data.reset_password_key);
user_org.save(&mut conn).await?
membership.reset_password_key = Some(reset_password_data.reset_password_key);
membership.save(&mut conn).await?
}
// Update send data
@ -650,7 +651,7 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, mut conn: DbConn,
// Prevent logging out the client where the user requested this endpoint from.
// If you do logout the user it will causes issues at the client side.
// Adding the device uuid will prevent this.
nt.send_logout(&user, Some(headers.device.uuid)).await;
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
save_result
}
@ -797,7 +798,7 @@ async fn post_verify_email(headers: Headers) -> EmptyResult {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct VerifyEmailTokenData {
user_id: String,
user_id: UserId,
token: String,
}
@ -812,7 +813,7 @@ async fn post_verify_email_token(data: Json<VerifyEmailTokenData>, mut conn: DbC
let Ok(claims) = decode_verify_email(&data.token) else {
err!("Invalid claim")
};
if claims.sub != user.uuid {
if claims.sub != *user.uuid {
err!("Invalid claim");
}
user.verified_at = Some(Utc::now().naive_utc());
@ -854,7 +855,7 @@ async fn post_delete_recover(data: Json<DeleteRecoverData>, mut conn: DbConn) ->
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DeleteRecoverTokenData {
user_id: String,
user_id: UserId,
token: String,
}
@ -870,7 +871,7 @@ async fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, mut conn:
err!("User doesn't exist")
};
if claims.sub != user.uuid {
if claims.sub != *user.uuid {
err!("Invalid claim");
}
user.delete(&mut conn).await
@ -1032,7 +1033,7 @@ async fn get_known_device(device: KnownDevice, mut conn: DbConn) -> JsonResult {
struct KnownDevice {
email: String,
uuid: String,
uuid: DeviceId,
}
#[rocket::async_trait]
@ -1055,7 +1056,7 @@ impl<'r> FromRequest<'r> for KnownDevice {
};
let uuid = if let Some(uuid) = req.headers().get_one("X-Device-Identifier") {
uuid.to_string()
uuid.to_string().into()
} else {
return Outcome::Error((Status::BadRequest, "X-Device-Identifier value is required"));
};
@ -1073,26 +1074,31 @@ struct PushToken {
push_token: String,
}
#[post("/devices/identifier/<uuid>/token", data = "<data>")]
async fn post_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
put_device_token(uuid, data, headers, conn).await
#[post("/devices/identifier/<device_id>/token", data = "<data>")]
async fn post_device_token(device_id: DeviceId, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
put_device_token(device_id, data, headers, conn).await
}
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
async fn put_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, mut conn: DbConn) -> EmptyResult {
#[put("/devices/identifier/<device_id>/token", data = "<data>")]
async fn put_device_token(
device_id: DeviceId,
data: Json<PushToken>,
headers: Headers,
mut conn: DbConn,
) -> EmptyResult {
let data = data.into_inner();
let token = data.push_token;
let Some(mut device) = Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await
else {
err!(format!("Error: device {uuid} should be present before a token can be assigned"))
err!(format!("Error: device {device_id} should be present before a token can be assigned"))
};
// if the device already has been registered
if device.is_registered() {
// check if the new token is the same as the registered token
if device.push_token.is_some() && device.push_token.unwrap() == token.clone() {
debug!("Device {} is already registered and token is the same", uuid);
debug!("Device {} is already registered and token is the same", device_id);
return Ok(());
} else {
// Try to unregister already registered device
@ -1111,8 +1117,8 @@ async fn put_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, m
Ok(())
}
#[put("/devices/identifier/<uuid>/clear-token")]
async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult {
#[put("/devices/identifier/<device_id>/clear-token")]
async fn put_clear_device_token(device_id: DeviceId, mut conn: DbConn) -> EmptyResult {
// This only clears push token
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
@ -1121,8 +1127,8 @@ async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult {
return Ok(());
}
if let Some(device) = Device::find_by_uuid(uuid, &mut conn).await {
Device::clear_push_token_by_uuid(uuid, &mut conn).await?;
if let Some(device) = Device::find_by_uuid(&device_id, &mut conn).await {
Device::clear_push_token_by_uuid(&device_id, &mut conn).await?;
unregister_push_device(device.push_uuid).await?;
}
@ -1130,16 +1136,16 @@ async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult {
}
// On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere
#[post("/devices/identifier/<uuid>/clear-token")]
async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult {
put_clear_device_token(uuid, conn).await
#[post("/devices/identifier/<device_id>/clear-token")]
async fn post_clear_device_token(device_id: DeviceId, conn: DbConn) -> EmptyResult {
put_clear_device_token(device_id, conn).await
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AuthRequestRequest {
access_code: String,
device_identifier: String,
device_identifier: DeviceId,
email: String,
public_key: String,
// Not used for now
@ -1193,16 +1199,17 @@ async fn post_auth_request(
})))
}
#[get("/auth-requests/<uuid>")]
async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
let Some(auth_request) = AuthRequest::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else {
#[get("/auth-requests/<auth_request_id>")]
async fn get_auth_request(auth_request_id: AuthRequestId, headers: Headers, mut conn: DbConn) -> JsonResult {
let Some(auth_request) = AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &mut conn).await
else {
err!("AuthRequest doesn't exist", "Record not found or user uuid does not match")
};
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
Ok(Json(json!({
"id": uuid,
"id": &auth_request_id,
"publicKey": auth_request.public_key,
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
"requestIpAddress": auth_request.request_ip,
@ -1219,15 +1226,15 @@ async fn get_auth_request(uuid: &str, headers: Headers, mut conn: DbConn) -> Jso
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AuthResponseRequest {
device_identifier: String,
device_identifier: DeviceId,
key: String,
master_password_hash: Option<String>,
request_approved: bool,
}
#[put("/auth-requests/<uuid>", data = "<data>")]
#[put("/auth-requests/<auth_request_id>", data = "<data>")]
async fn put_auth_request(
uuid: &str,
auth_request_id: AuthRequestId,
data: Json<AuthResponseRequest>,
headers: Headers,
mut conn: DbConn,
@ -1235,7 +1242,9 @@ async fn put_auth_request(
nt: Notify<'_>,
) -> JsonResult {
let data = data.into_inner();
let Some(mut auth_request) = AuthRequest::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else {
let Some(mut auth_request) =
AuthRequest::find_by_uuid_and_user(&auth_request_id, &headers.user.uuid, &mut conn).await
else {
err!("AuthRequest doesn't exist", "Record not found or user uuid does not match")
};
@ -1262,7 +1271,7 @@ async fn put_auth_request(
}
Ok(Json(json!({
"id": uuid,
"id": &auth_request_id,
"publicKey": auth_request.public_key,
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
"requestIpAddress": auth_request.request_ip,
@ -1276,14 +1285,14 @@ async fn put_auth_request(
})))
}
#[get("/auth-requests/<uuid>/response?<code>")]
#[get("/auth-requests/<auth_request_id>/response?<code>")]
async fn get_auth_request_response(
uuid: &str,
auth_request_id: AuthRequestId,
code: &str,
client_headers: ClientHeaders,
mut conn: DbConn,
) -> JsonResult {
let Some(auth_request) = AuthRequest::find_by_uuid(uuid, &mut conn).await else {
let Some(auth_request) = AuthRequest::find_by_uuid(&auth_request_id, &mut conn).await else {
err!("AuthRequest doesn't exist", "User not found")
};
@ -1297,7 +1306,7 @@ async fn get_auth_request_response(
let response_date_utc = auth_request.response_date.map(|response_date| format_date(&response_date));
Ok(Json(json!({
"id": uuid,
"id": &auth_request_id,
"publicKey": auth_request.public_key,
"requestDeviceType": DeviceType::from_i32(auth_request.device_type).to_string(),
"requestIpAddress": auth_request.request_ip,

File diff suppressed because it is too large Load Diff

View File

@ -93,10 +93,10 @@ async fn get_grantees(headers: Headers, mut conn: DbConn) -> Json<Value> {
}
#[get("/emergency-access/<emer_id>")]
async fn get_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn get_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
match EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await {
match EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await {
Some(emergency_access) => Ok(Json(
emergency_access.to_json_grantee_details(&mut conn).await.expect("Grantee user should exist but does not!"),
)),
@ -118,7 +118,7 @@ struct EmergencyAccessUpdateData {
#[put("/emergency-access/<emer_id>", data = "<data>")]
async fn put_emergency_access(
emer_id: &str,
emer_id: EmergencyAccessId,
data: Json<EmergencyAccessUpdateData>,
headers: Headers,
conn: DbConn,
@ -128,7 +128,7 @@ async fn put_emergency_access(
#[post("/emergency-access/<emer_id>", data = "<data>")]
async fn post_emergency_access(
emer_id: &str,
emer_id: EmergencyAccessId,
data: Json<EmergencyAccessUpdateData>,
headers: Headers,
mut conn: DbConn,
@ -138,7 +138,7 @@ async fn post_emergency_access(
let data: EmergencyAccessUpdateData = data.into_inner();
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -163,12 +163,12 @@ async fn post_emergency_access(
// region delete
#[delete("/emergency-access/<emer_id>")]
async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> EmptyResult {
check_emergency_access_enabled()?;
let emergency_access = match (
EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await,
EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &headers.user.uuid, &mut conn).await,
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await,
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await,
) {
(Some(grantor_emer), None) => {
info!("Grantor deleted emergency access {emer_id}");
@ -186,7 +186,7 @@ async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo
}
#[post("/emergency-access/<emer_id>/delete")]
async fn post_delete_emergency_access(emer_id: &str, headers: Headers, conn: DbConn) -> EmptyResult {
async fn post_delete_emergency_access(emer_id: EmergencyAccessId, headers: Headers, conn: DbConn) -> EmptyResult {
delete_emergency_access(emer_id, headers, conn).await
}
@ -266,8 +266,8 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
if CONFIG.mail_enabled() {
mail::send_emergency_access_invite(
&new_emergency_access.email.expect("Grantee email does not exists"),
&grantee_user.uuid,
&new_emergency_access.uuid,
grantee_user.uuid,
new_emergency_access.uuid,
&grantor_user.name,
&grantor_user.email,
)
@ -281,11 +281,11 @@ async fn send_invite(data: Json<EmergencyAccessInviteData>, headers: Headers, mu
}
#[post("/emergency-access/<emer_id>/reinvite")]
async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn resend_invite(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> EmptyResult {
check_emergency_access_enabled()?;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -307,8 +307,8 @@ async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> Emp
if CONFIG.mail_enabled() {
mail::send_emergency_access_invite(
&email,
&grantor_user.uuid,
&emergency_access.uuid,
grantor_user.uuid,
emergency_access.uuid,
&grantor_user.name,
&grantor_user.email,
)
@ -331,7 +331,12 @@ struct AcceptData {
}
#[post("/emergency-access/<emer_id>/accept", data = "<data>")]
async fn accept_invite(emer_id: &str, data: Json<AcceptData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn accept_invite(
emer_id: EmergencyAccessId,
data: Json<AcceptData>,
headers: Headers,
mut conn: DbConn,
) -> EmptyResult {
check_emergency_access_enabled()?;
let data: AcceptData = data.into_inner();
@ -355,7 +360,7 @@ async fn accept_invite(emer_id: &str, data: Json<AcceptData>, headers: Headers,
// We need to search for the uuid in combination with the email, since we do not yet store the uuid of the grantee in the database.
// The uuid of the grantee gets stored once accepted.
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_email(emer_id, &headers.user.email, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_email(&emer_id, &headers.user.email, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -389,7 +394,7 @@ struct ConfirmData {
#[post("/emergency-access/<emer_id>/confirm", data = "<data>")]
async fn confirm_emergency_access(
emer_id: &str,
emer_id: EmergencyAccessId,
data: Json<ConfirmData>,
headers: Headers,
mut conn: DbConn,
@ -401,7 +406,7 @@ async fn confirm_emergency_access(
let key = data.key;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &confirming_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &confirming_user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -441,12 +446,12 @@ async fn confirm_emergency_access(
// region access emergency access
#[post("/emergency-access/<emer_id>/initiate")]
async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn initiate_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let initiating_user = headers.user;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &initiating_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &initiating_user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -479,11 +484,11 @@ async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: Db
}
#[post("/emergency-access/<emer_id>/approve")]
async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn approve_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -514,11 +519,11 @@ async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbC
}
#[post("/emergency-access/<emer_id>/reject")]
async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn reject_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let Some(mut emergency_access) =
EmergencyAccess::find_by_uuid_and_grantor_uuid(emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantor_uuid(&emer_id, &headers.user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -551,11 +556,11 @@ async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo
// region action
#[post("/emergency-access/<emer_id>/view")]
async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn view_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &headers.user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &headers.user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -589,12 +594,12 @@ async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn
}
#[post("/emergency-access/<emer_id>/takeover")]
async fn takeover_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn takeover_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
check_emergency_access_enabled()?;
let requesting_user = headers.user;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -628,7 +633,7 @@ struct EmergencyAccessPasswordData {
#[post("/emergency-access/<emer_id>/password", data = "<data>")]
async fn password_emergency_access(
emer_id: &str,
emer_id: EmergencyAccessId,
data: Json<EmergencyAccessPasswordData>,
headers: Headers,
mut conn: DbConn,
@ -641,7 +646,7 @@ async fn password_emergency_access(
let requesting_user = headers.user;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -662,9 +667,9 @@ async fn password_emergency_access(
TwoFactor::delete_all_by_user(&grantor_user.uuid, &mut conn).await?;
// Remove grantor from all organisations unless Owner
for user_org in UserOrganization::find_any_state_by_user(&grantor_user.uuid, &mut conn).await {
if user_org.atype != UserOrgType::Owner as i32 {
user_org.delete(&mut conn).await?;
for member in Membership::find_any_state_by_user(&grantor_user.uuid, &mut conn).await {
if member.atype != MembershipType::Owner as i32 {
member.delete(&mut conn).await?;
}
}
Ok(())
@ -673,10 +678,10 @@ async fn password_emergency_access(
// endregion
#[get("/emergency-access/<emer_id>/policies")]
async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
async fn policies_emergency_access(emer_id: EmergencyAccessId, headers: Headers, mut conn: DbConn) -> JsonResult {
let requesting_user = headers.user;
let Some(emergency_access) =
EmergencyAccess::find_by_uuid_and_grantee_uuid(emer_id, &requesting_user.uuid, &mut conn).await
EmergencyAccess::find_by_uuid_and_grantee_uuid(&emer_id, &requesting_user.uuid, &mut conn).await
else {
err!("Emergency access not valid.")
};
@ -701,11 +706,11 @@ async fn policies_emergency_access(emer_id: &str, headers: Headers, mut conn: Db
fn is_valid_request(
emergency_access: &EmergencyAccess,
requesting_user_uuid: &str,
requesting_user_id: &UserId,
requested_access_type: EmergencyAccessType,
) -> bool {
emergency_access.grantee_uuid.is_some()
&& emergency_access.grantee_uuid.as_ref().unwrap() == requesting_user_uuid
&& emergency_access.grantee_uuid.as_ref().unwrap() == requesting_user_id
&& emergency_access.status == EmergencyAccessStatus::RecoveryApproved as i32
&& emergency_access.atype == requested_access_type as i32
}

View File

@ -8,7 +8,7 @@ use crate::{
api::{EmptyResult, JsonResult},
auth::{AdminHeaders, Headers},
db::{
models::{Cipher, Event, UserOrganization},
models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId},
DbConn, DbPool,
},
util::parse_date,
@ -31,69 +31,8 @@ struct EventRange {
// Upstream: https://github.com/bitwarden/server/blob/9ecf69d9cabce732cf2c57976dd9afa5728578fb/src/Api/Controllers/EventsController.cs#LL84C35-L84C41
#[get("/organizations/<org_id>/events?<data..>")]
async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
// Return an empty vec when we org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0)
} else {
let start_date = parse_date(&data.start);
let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date)
} else {
parse_date(&data.end)
};
Event::find_by_organization_uuid(org_id, &start_date, &end_date, &mut conn)
.await
.iter()
.map(|e| e.to_json())
.collect()
};
Ok(Json(json!({
"data": events_json,
"object": "list",
"continuationToken": get_continuation_token(&events_json),
})))
}
#[get("/ciphers/<cipher_id>/events?<data..>")]
async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult {
// Return an empty vec when we org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0)
} else {
let mut events_json = Vec::with_capacity(0);
if UserOrganization::user_has_ge_admin_access_to_cipher(&headers.user.uuid, cipher_id, &mut conn).await {
let start_date = parse_date(&data.start);
let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date)
} else {
parse_date(&data.end)
};
events_json = Event::find_by_cipher_uuid(cipher_id, &start_date, &end_date, &mut conn)
.await
.iter()
.map(|e| e.to_json())
.collect()
}
events_json
};
Ok(Json(json!({
"data": events_json,
"object": "list",
"continuationToken": get_continuation_token(&events_json),
})))
}
#[get("/organizations/<org_id>/users/<user_org_id>/events?<data..>")]
async fn get_user_events(
org_id: &str,
user_org_id: &str,
async fn get_org_events(
org_id: OrganizationId,
data: EventRange,
_headers: AdminHeaders,
mut conn: DbConn,
@ -110,7 +49,73 @@ async fn get_user_events(
parse_date(&data.end)
};
Event::find_by_org_and_user_org(org_id, user_org_id, &start_date, &end_date, &mut conn)
Event::find_by_organization_uuid(&org_id, &start_date, &end_date, &mut conn)
.await
.iter()
.map(|e| e.to_json())
.collect()
};
Ok(Json(json!({
"data": events_json,
"object": "list",
"continuationToken": get_continuation_token(&events_json),
})))
}
#[get("/ciphers/<cipher_id>/events?<data..>")]
async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult {
// Return an empty vec when we org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0)
} else {
let mut events_json = Vec::with_capacity(0);
if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &mut conn).await {
let start_date = parse_date(&data.start);
let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date)
} else {
parse_date(&data.end)
};
events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &mut conn)
.await
.iter()
.map(|e| e.to_json())
.collect()
}
events_json
};
Ok(Json(json!({
"data": events_json,
"object": "list",
"continuationToken": get_continuation_token(&events_json),
})))
}
#[get("/organizations/<org_id>/users/<member_id>/events?<data..>")]
async fn get_user_events(
org_id: OrganizationId,
member_id: MembershipId,
data: EventRange,
_headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
// Return an empty vec when we org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
Vec::with_capacity(0)
} else {
let start_date = parse_date(&data.start);
let end_date = if let Some(before_date) = &data.continuation_token {
parse_date(before_date)
} else {
parse_date(&data.end)
};
Event::find_by_org_and_member(&org_id, &member_id, &start_date, &end_date, &mut conn)
.await
.iter()
.map(|e| e.to_json())
@ -152,8 +157,8 @@ struct EventCollection {
date: String,
// Optional
cipher_id: Option<String>,
organization_id: Option<String>,
cipher_id: Option<CipherId>,
organization_id: Option<OrganizationId>,
}
// Upstream:
@ -180,11 +185,11 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
.await;
}
1600..=1699 => {
if let Some(org_uuid) = &event.organization_id {
if let Some(org_id) = &event.organization_id {
_log_event(
event.r#type,
org_uuid,
org_uuid,
org_id,
org_id,
&headers.user.uuid,
headers.device.atype,
Some(event_date),
@ -197,11 +202,11 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
_ => {
if let Some(cipher_uuid) = &event.cipher_id {
if let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await {
if let Some(org_uuid) = cipher.organization_uuid {
if let Some(org_id) = cipher.organization_uuid {
_log_event(
event.r#type,
cipher_uuid,
&org_uuid,
&org_id,
&headers.user.uuid,
headers.device.atype,
Some(event_date),
@ -218,38 +223,38 @@ async fn post_events_collect(data: Json<Vec<EventCollection>>, headers: Headers,
Ok(())
}
pub async fn log_user_event(event_type: i32, user_uuid: &str, device_type: i32, ip: &IpAddr, conn: &mut DbConn) {
pub async fn log_user_event(event_type: i32, user_id: &UserId, device_type: i32, ip: &IpAddr, conn: &mut DbConn) {
if !CONFIG.org_events_enabled() {
return;
}
_log_user_event(event_type, user_uuid, device_type, None, ip, conn).await;
_log_user_event(event_type, user_id, device_type, None, ip, conn).await;
}
async fn _log_user_event(
event_type: i32,
user_uuid: &str,
user_id: &UserId,
device_type: i32,
event_date: Option<NaiveDateTime>,
ip: &IpAddr,
conn: &mut DbConn,
) {
let orgs = UserOrganization::get_org_uuid_by_user(user_uuid, conn).await;
let orgs = Membership::get_orgs_by_user(user_id, conn).await;
let mut events: Vec<Event> = Vec::with_capacity(orgs.len() + 1); // We need an event per org and one without an org
// Upstream saves the event also without any org_uuid.
// Upstream saves the event also without any org_id.
let mut event = Event::new(event_type, event_date);
event.user_uuid = Some(String::from(user_uuid));
event.act_user_uuid = Some(String::from(user_uuid));
event.user_uuid = Some(user_id.clone());
event.act_user_uuid = Some(user_id.clone());
event.device_type = Some(device_type);
event.ip_address = Some(ip.to_string());
events.push(event);
// For each org a user is a member of store these events per org
for org_uuid in orgs {
for org_id in orgs {
let mut event = Event::new(event_type, event_date);
event.user_uuid = Some(String::from(user_uuid));
event.org_uuid = Some(org_uuid);
event.act_user_uuid = Some(String::from(user_uuid));
event.user_uuid = Some(user_id.clone());
event.org_uuid = Some(org_id);
event.act_user_uuid = Some(user_id.clone());
event.device_type = Some(device_type);
event.ip_address = Some(ip.to_string());
events.push(event);
@ -261,8 +266,8 @@ async fn _log_user_event(
pub async fn log_event(
event_type: i32,
source_uuid: &str,
org_uuid: &str,
act_user_uuid: &str,
org_id: &OrganizationId,
act_user_id: &UserId,
device_type: i32,
ip: &IpAddr,
conn: &mut DbConn,
@ -270,15 +275,15 @@ pub async fn log_event(
if !CONFIG.org_events_enabled() {
return;
}
_log_event(event_type, source_uuid, org_uuid, act_user_uuid, device_type, None, ip, conn).await;
_log_event(event_type, source_uuid, org_id, act_user_id, device_type, None, ip, conn).await;
}
#[allow(clippy::too_many_arguments)]
async fn _log_event(
event_type: i32,
source_uuid: &str,
org_uuid: &str,
act_user_uuid: &str,
org_id: &OrganizationId,
act_user_id: &UserId,
device_type: i32,
event_date: Option<NaiveDateTime>,
ip: &IpAddr,
@ -290,31 +295,31 @@ async fn _log_event(
// 1000..=1099 Are user events, they need to be logged via log_user_event()
// Cipher Events
1100..=1199 => {
event.cipher_uuid = Some(String::from(source_uuid));
event.cipher_uuid = Some(source_uuid.to_string().into());
}
// Collection Events
1300..=1399 => {
event.collection_uuid = Some(String::from(source_uuid));
event.collection_uuid = Some(source_uuid.to_string().into());
}
// Group Events
1400..=1499 => {
event.group_uuid = Some(String::from(source_uuid));
event.group_uuid = Some(source_uuid.to_string().into());
}
// Org User Events
1500..=1599 => {
event.org_user_uuid = Some(String::from(source_uuid));
event.org_user_uuid = Some(source_uuid.to_string().into());
}
// 1600..=1699 Are organizational events, and they do not need the source_uuid
// Policy Events
1700..=1799 => {
event.policy_uuid = Some(String::from(source_uuid));
event.policy_uuid = Some(source_uuid.to_string().into());
}
// Ignore others
_ => {}
}
event.org_uuid = Some(String::from(org_uuid));
event.act_user_uuid = Some(String::from(act_user_uuid));
event.org_uuid = Some(org_id.clone());
event.act_user_uuid = Some(act_user_id.clone());
event.device_type = Some(device_type);
event.ip_address = Some(ip.to_string());
event.save(conn).await.unwrap_or(());

View File

@ -23,9 +23,9 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json<Value> {
}))
}
#[get("/folders/<uuid>")]
async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
match Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await {
#[get("/folders/<folder_id>")]
async fn get_folder(folder_id: FolderId, headers: Headers, mut conn: DbConn) -> JsonResult {
match Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await {
Some(folder) => Ok(Json(folder.to_json())),
_ => err!("Invalid folder", "Folder does not exist or belongs to another user"),
}
@ -35,7 +35,7 @@ async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResul
#[serde(rename_all = "camelCase")]
pub struct FolderData {
pub name: String,
pub id: Option<String>,
pub id: Option<FolderId>,
}
#[post("/folders", data = "<data>")]
@ -50,14 +50,20 @@ async fn post_folders(data: Json<FolderData>, headers: Headers, mut conn: DbConn
Ok(Json(folder.to_json()))
}
#[post("/folders/<uuid>", data = "<data>")]
async fn post_folder(uuid: &str, data: Json<FolderData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
put_folder(uuid, data, headers, conn, nt).await
#[post("/folders/<folder_id>", data = "<data>")]
async fn post_folder(
folder_id: FolderId,
data: Json<FolderData>,
headers: Headers,
conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
put_folder(folder_id, data, headers, conn, nt).await
}
#[put("/folders/<uuid>", data = "<data>")]
#[put("/folders/<folder_id>", data = "<data>")]
async fn put_folder(
uuid: &str,
folder_id: FolderId,
data: Json<FolderData>,
headers: Headers,
mut conn: DbConn,
@ -65,7 +71,7 @@ async fn put_folder(
) -> JsonResult {
let data: FolderData = data.into_inner();
let Some(mut folder) = Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else {
let Some(mut folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await else {
err!("Invalid folder", "Folder does not exist or belongs to another user")
};
@ -77,14 +83,14 @@ async fn put_folder(
Ok(Json(folder.to_json()))
}
#[post("/folders/<uuid>/delete")]
async fn delete_folder_post(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
delete_folder(uuid, headers, conn, nt).await
#[post("/folders/<folder_id>/delete")]
async fn delete_folder_post(folder_id: FolderId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult {
delete_folder(folder_id, headers, conn, nt).await
}
#[delete("/folders/<uuid>")]
async fn delete_folder(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let Some(folder) = Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else {
#[delete("/folders/<folder_id>")]
async fn delete_folder(folder_id: FolderId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let Some(folder) = Folder::find_by_uuid_and_user(&folder_id, &headers.user.uuid, &mut conn).await else {
err!("Invalid folder", "Folder does not exist or belongs to another user")
};

File diff suppressed because it is too large Load Diff

View File

@ -55,38 +55,33 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
let mut user_created: bool = false;
if user_data.deleted {
// If user is marked for deletion and it exists, revoke it
if let Some(mut user_org) =
UserOrganization::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await
{
if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await {
// Only revoke a user if it is not the last confirmed owner
let revoked = if user_org.atype == UserOrgType::Owner
&& user_org.status == UserOrgStatus::Confirmed as i32
let revoked = if member.atype == MembershipType::Owner
&& member.status == MembershipStatus::Confirmed as i32
{
if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn).await
<= 1
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await <= 1
{
warn!("Can't revoke the last owner");
false
} else {
user_org.revoke()
member.revoke()
}
} else {
user_org.revoke()
member.revoke()
};
let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone()));
let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));
if revoked || ext_modified {
user_org.save(&mut conn).await?;
member.save(&mut conn).await?;
}
}
// If user is part of the organization, restore it
} else if let Some(mut user_org) =
UserOrganization::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await
{
let restored = user_org.restore();
let ext_modified = user_org.set_external_id(Some(user_data.external_id.clone()));
} else if let Some(mut member) = Membership::find_by_email_and_org(&user_data.email, &org_id, &mut conn).await {
let restored = member.restore();
let ext_modified = member.set_external_id(Some(user_data.external_id.clone()));
if restored || ext_modified {
user_org.save(&mut conn).await?;
member.save(&mut conn).await?;
}
} else {
// If user is not part of the organization
@ -104,17 +99,17 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
new_user
}
};
let user_org_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() {
UserOrgStatus::Invited as i32
let member_status = if CONFIG.mail_enabled() || user.password_hash.is_empty() {
MembershipStatus::Invited as i32
} else {
UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
MembershipStatus::Accepted as i32 // Automatically mark user as accepted if no email invites
};
let mut new_member = UserOrganization::new(user.uuid.clone(), org_id.clone());
let mut new_member = Membership::new(user.uuid.clone(), org_id.clone());
new_member.set_external_id(Some(user_data.external_id.clone()));
new_member.access_all = false;
new_member.atype = UserOrgType::User as i32;
new_member.status = user_org_status;
new_member.atype = MembershipType::User as i32;
new_member.status = member_status;
new_member.save(&mut conn).await?;
@ -166,9 +161,8 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?;
for ext_id in &group_data.member_external_ids {
if let Some(user_org) = UserOrganization::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await
{
let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone());
if let Some(member) = Membership::find_by_external_id_and_org(ext_id, &org_id, &mut conn).await {
let mut group_user = GroupUser::new(group_uuid.clone(), member.uuid.clone());
group_user.save(&mut conn).await?;
}
}
@ -181,20 +175,19 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
if data.overwrite_existing {
// Generate a HashSet to quickly verify if a member is listed or not.
let sync_members: HashSet<String> = data.members.into_iter().map(|m| m.external_id).collect();
for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await {
if let Some(ref user_external_id) = user_org.external_id {
for member in Membership::find_by_org(&org_id, &mut conn).await {
if let Some(ref user_external_id) = member.external_id {
if !sync_members.contains(user_external_id) {
if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 {
if member.atype == MembershipType::Owner && member.status == MembershipStatus::Confirmed as i32 {
// Removing owner, check that there is at least one other confirmed owner
if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn)
.await
if Membership::count_confirmed_by_org_and_type(&org_id, MembershipType::Owner, &mut conn).await
<= 1
{
warn!("Can't delete the last owner");
continue;
}
}
user_org.delete(&mut conn).await?;
member.delete(&mut conn).await?;
}
}
}
@ -203,7 +196,7 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
Ok(())
}
pub struct PublicToken(String);
pub struct PublicToken(OrganizationId);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for PublicToken {
@ -243,10 +236,11 @@ impl<'r> FromRequest<'r> for PublicToken {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB"),
};
let Some(org_uuid) = claims.client_id.strip_prefix("organization.") else {
let Some(org_id) = claims.client_id.strip_prefix("organization.") else {
err_handler!("Malformed client_id")
};
let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await else {
let org_id: OrganizationId = org_id.to_string().into();
let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, &conn).await else {
err_handler!("Invalid client_id")
};
if org_api_key.org_uuid != claims.client_sub {

View File

@ -12,7 +12,7 @@ use crate::{
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
auth::{ClientIp, Headers, Host},
db::{models::*, DbConn, DbPool},
util::{NumberOrString, SafeString},
util::NumberOrString,
CONFIG,
};
@ -67,7 +67,7 @@ pub struct SendData {
file_length: Option<NumberOrString>,
// Used for key rotations
pub id: Option<String>,
pub id: Option<SendId>,
}
/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to
@ -79,9 +79,9 @@ pub struct SendData {
/// There is also a Vaultwarden-specific `sends_allowed` config setting that
/// controls this policy globally.
async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> EmptyResult {
let user_uuid = &headers.user.uuid;
let user_id = &headers.user.uuid;
if !CONFIG.sends_allowed()
|| OrgPolicy::is_applicable_to_user(user_uuid, OrgPolicyType::DisableSend, None, conn).await
|| OrgPolicy::is_applicable_to_user(user_id, OrgPolicyType::DisableSend, None, conn).await
{
err!("Due to an Enterprise Policy, you are only able to delete an existing Send.")
}
@ -95,9 +95,9 @@ async fn enforce_disable_send_policy(headers: &Headers, conn: &mut DbConn) -> Em
///
/// Ref: https://bitwarden.com/help/article/policies/#send-options
async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &mut DbConn) -> EmptyResult {
let user_uuid = &headers.user.uuid;
let user_id = &headers.user.uuid;
let hide_email = data.hide_email.unwrap_or(false);
if hide_email && OrgPolicy::is_hide_email_disabled(user_uuid, conn).await {
if hide_email && OrgPolicy::is_hide_email_disabled(user_id, conn).await {
err!(
"Due to an Enterprise Policy, you are not allowed to hide your email address \
from recipients when creating or editing a Send."
@ -106,7 +106,7 @@ async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, c
Ok(())
}
fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> {
fn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> {
let data_val = if data.r#type == SendType::Text as i32 {
data.text
} else if data.r#type == SendType::File as i32 {
@ -129,7 +129,7 @@ fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> {
}
let mut send = Send::new(data.r#type, data.name, data_str, data.key, data.deletion_date.naive_utc());
send.user_uuid = Some(user_uuid);
send.user_uuid = Some(user_id);
send.notes = data.notes;
send.max_access_count = match data.max_access_count {
Some(m) => Some(m.into_i32()?),
@ -157,11 +157,11 @@ async fn get_sends(headers: Headers, mut conn: DbConn) -> Json<Value> {
}))
}
#[get("/sends/<uuid>")]
async fn get_send(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult {
match Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await {
#[get("/sends/<send_id>")]
async fn get_send(send_id: SendId, headers: Headers, mut conn: DbConn) -> JsonResult {
match Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await {
Some(send) => Ok(Json(send.to_json())),
None => err!("Send not found", "Invalid uuid or does not belong to user"),
None => err!("Send not found", "Invalid send uuid or does not belong to user"),
}
}
@ -249,7 +249,7 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
err!("Send content is not a file");
}
let file_id = crate::crypto::generate_send_id();
let file_id = crate::crypto::generate_send_file_id();
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(&send.uuid);
let file_path = folder_path.join(&file_id);
tokio::fs::create_dir_all(&folder_path).await?;
@ -324,7 +324,7 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
let mut send = create_send(data, headers.user.uuid)?;
let file_id = crate::crypto::generate_send_id();
let file_id = crate::crypto::generate_send_file_id();
let mut data_value: Value = serde_json::from_str(&send.data)?;
if let Some(o) = data_value.as_object_mut() {
@ -346,16 +346,16 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, mut conn: DbC
#[derive(Deserialize)]
#[allow(non_snake_case)]
pub struct SendFileData {
id: String,
id: SendFileId,
size: u64,
fileName: String,
}
// https://github.com/bitwarden/server/blob/66f95d1c443490b653e5a15d32977e2f5a3f9e32/src/Api/Tools/Controllers/SendsController.cs#L250
#[post("/sends/<send_uuid>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
#[post("/sends/<send_id>/file/<file_id>", format = "multipart/form-data", data = "<data>")]
async fn post_send_file_v2_data(
send_uuid: &str,
file_id: &str,
send_id: SendId,
file_id: SendFileId,
data: Form<UploadDataV2<'_>>,
headers: Headers,
mut conn: DbConn,
@ -365,8 +365,8 @@ async fn post_send_file_v2_data(
let mut data = data.into_inner();
let Some(send) = Send::find_by_uuid_and_user(send_uuid, &headers.user.uuid, &mut conn).await else {
err!("Send not found. Unable to save the file.", "Invalid uuid or does not belong to user.")
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
err!("Send not found. Unable to save the file.", "Invalid send uuid or does not belong to user.")
};
if send.atype != SendType::File as i32 {
@ -402,7 +402,7 @@ async fn post_send_file_v2_data(
err!("Send file size does not match.", format!("Expected a file size of {} got {size}", send_data.size));
}
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_uuid);
let folder_path = tokio::fs::canonicalize(&CONFIG.sends_folder()).await?.join(send_id);
let file_path = folder_path.join(file_id);
// Check if the file already exists, if that is the case do not overwrite it
@ -485,7 +485,7 @@ async fn post_access(
UpdateType::SyncSendUpdate,
&send,
&send.update_users_revision(&mut conn).await,
&String::from("00000000-0000-0000-0000-000000000000"),
&String::from("00000000-0000-0000-0000-000000000000").into(),
&mut conn,
)
.await;
@ -495,14 +495,14 @@ async fn post_access(
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
async fn post_access_file(
send_id: &str,
file_id: &str,
send_id: SendId,
file_id: SendFileId,
data: Json<SendAccessData>,
host: Host,
mut conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
let Some(mut send) = Send::find_by_uuid(send_id, &mut conn).await else {
let Some(mut send) = Send::find_by_uuid(&send_id, &mut conn).await else {
err_code!(SEND_INACCESSIBLE_MSG, 404)
};
@ -542,12 +542,12 @@ async fn post_access_file(
UpdateType::SyncSendUpdate,
&send,
&send.update_users_revision(&mut conn).await,
&String::from("00000000-0000-0000-0000-000000000000"),
&String::from("00000000-0000-0000-0000-000000000000").into(),
&mut conn,
)
.await;
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
let token_claims = crate::auth::generate_send_claims(&send_id, &file_id);
let token = crate::auth::encode_jwt(&token_claims);
Ok(Json(json!({
"object": "send-fileDownload",
@ -557,7 +557,7 @@ async fn post_access_file(
}
#[get("/sends/<send_id>/<file_id>?<t>")]
async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Option<NamedFile> {
async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option<NamedFile> {
if let Ok(claims) = crate::auth::decode_send(t) {
if claims.sub == format!("{send_id}/{file_id}") {
return NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).await.ok();
@ -566,15 +566,21 @@ async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Opt
None
}
#[put("/sends/<uuid>", data = "<data>")]
async fn put_send(uuid: &str, data: Json<SendData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
#[put("/sends/<send_id>", data = "<data>")]
async fn put_send(
send_id: SendId,
data: Json<SendData>,
headers: Headers,
mut conn: DbConn,
nt: Notify<'_>,
) -> JsonResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
let data: SendData = data.into_inner();
enforce_disable_hide_email_policy(&data, &headers, &mut conn).await?;
let Some(mut send) = Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else {
err!("Send not found", "Send uuid is invalid or does not belong to user")
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
err!("Send not found", "Send send_id is invalid or does not belong to user")
};
update_send_from_data(&mut send, data, &headers, &mut conn, &nt, UpdateType::SyncSendUpdate).await?;
@ -640,9 +646,9 @@ pub async fn update_send_from_data(
Ok(())
}
#[delete("/sends/<uuid>")]
async fn delete_send(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let Some(send) = Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else {
#[delete("/sends/<send_id>")]
async fn delete_send(send_id: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
err!("Send not found", "Invalid send uuid, or does not belong to user")
};
@ -659,11 +665,11 @@ async fn delete_send(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<
Ok(())
}
#[put("/sends/<uuid>/remove-password")]
async fn put_remove_password(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
#[put("/sends/<send_id>/remove-password")]
async fn put_remove_password(send_id: SendId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &mut conn).await?;
let Some(mut send) = Send::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else {
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &mut conn).await else {
err!("Send not found", "Invalid send uuid, or does not belong to user")
};

View File

@ -7,7 +7,7 @@ use crate::{
auth::{ClientIp, Headers},
crypto,
db::{
models::{EventType, TwoFactor, TwoFactorType},
models::{EventType, TwoFactor, TwoFactorType, UserId},
DbConn,
},
util::NumberOrString,
@ -95,7 +95,7 @@ async fn activate_authenticator_put(data: Json<EnableAuthenticatorData>, headers
}
pub async fn validate_totp_code_str(
user_uuid: &str,
user_id: &UserId,
totp_code: &str,
secret: &str,
ip: &ClientIp,
@ -105,11 +105,11 @@ pub async fn validate_totp_code_str(
err!("TOTP code is not a number");
}
validate_totp_code(user_uuid, totp_code, secret, ip, conn).await
validate_totp_code(user_id, totp_code, secret, ip, conn).await
}
pub async fn validate_totp_code(
user_uuid: &str,
user_id: &UserId,
totp_code: &str,
secret: &str,
ip: &ClientIp,
@ -121,11 +121,11 @@ pub async fn validate_totp_code(
err!("Invalid TOTP secret")
};
let mut twofactor =
match TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Authenticator as i32, conn).await {
Some(tf) => tf,
_ => TwoFactor::new(user_uuid.to_string(), TwoFactorType::Authenticator, secret.to_string()),
};
let mut twofactor = match TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Authenticator as i32, conn).await
{
Some(tf) => tf,
_ => TwoFactor::new(user_id.clone(), TwoFactorType::Authenticator, secret.to_string()),
};
// The amount of steps back and forward in time
// Also check if we need to disable time drifted TOTP codes.

View File

@ -11,7 +11,7 @@ use crate::{
auth::Headers,
crypto,
db::{
models::{EventType, TwoFactor, TwoFactorType, User},
models::{EventType, TwoFactor, TwoFactorType, User, UserId},
DbConn,
},
error::MapResult,
@ -228,11 +228,11 @@ const AUTH_PREFIX: &str = "AUTH";
const DUO_PREFIX: &str = "TX";
const APP_PREFIX: &str = "APP";
async fn get_user_duo_data(uuid: &str, conn: &mut DbConn) -> DuoStatus {
async fn get_user_duo_data(user_id: &UserId, conn: &mut DbConn) -> DuoStatus {
let type_ = TwoFactorType::Duo as i32;
// If the user doesn't have an entry, disabled
let Some(twofactor) = TwoFactor::find_by_user_and_type(uuid, type_, conn).await else {
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, type_, conn).await else {
return DuoStatus::Disabled(DuoData::global().is_some());
};

View File

@ -10,7 +10,7 @@ use crate::{
api::{core::two_factor::duo::get_duo_keys_email, EmptyResult},
crypto,
db::{
models::{EventType, TwoFactorDuoContext},
models::{DeviceId, EventType, TwoFactorDuoContext},
DbConn, DbPool,
},
error::Error,
@ -379,7 +379,7 @@ fn make_callback_url(client_name: &str) -> Result<String, Error> {
pub async fn get_duo_auth_url(
email: &str,
client_id: &str,
device_identifier: &String,
device_identifier: &DeviceId,
conn: &mut DbConn,
) -> Result<String, Error> {
let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;
@ -417,7 +417,7 @@ pub async fn validate_duo_login(
email: &str,
two_factor_token: &str,
client_id: &str,
device_identifier: &str,
device_identifier: &DeviceId,
conn: &mut DbConn,
) -> EmptyResult {
// Result supplied to us by clients in the form "<authz code>|<state>"

View File

@ -10,7 +10,7 @@ use crate::{
auth::Headers,
crypto,
db::{
models::{EventType, TwoFactor, TwoFactorType, User},
models::{EventType, TwoFactor, TwoFactorType, User, UserId},
DbConn,
},
error::{Error, MapResult},
@ -59,10 +59,9 @@ async fn send_email_login(data: Json<SendEmailLoginData>, mut conn: DbConn) -> E
}
/// Generate the token, save the data for later verification and send email to user
pub async fn send_token(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn send_token(user_id: &UserId, conn: &mut DbConn) -> EmptyResult {
let type_ = TwoFactorType::Email as i32;
let mut twofactor =
TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await.map_res("Two factor not found")?;
let mut twofactor = TwoFactor::find_by_user_and_type(user_id, type_, conn).await.map_res("Two factor not found")?;
let generated_token = crypto::generate_email_token(CONFIG.email_token_size());
@ -198,9 +197,9 @@ async fn email(data: Json<EmailData>, headers: Headers, mut conn: DbConn) -> Jso
}
/// Validate the email code when used as TwoFactor token mechanism
pub async fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn validate_email_code_str(user_id: &UserId, token: &str, data: &str, conn: &mut DbConn) -> EmptyResult {
let mut email_data = EmailTokenData::from_json(data)?;
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::Email as i32, conn)
let mut twofactor = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::Email as i32, conn)
.await
.map_res("Two factor not found")?;
let Some(issued_token) = &email_data.last_token else {
@ -327,8 +326,8 @@ pub fn obscure_email(email: &str) -> String {
format!("{}@{}", new_name, &domain)
}
pub async fn find_and_activate_email_2fa(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
if let Some(user) = User::find_by_uuid(user_uuid, conn).await {
pub async fn find_and_activate_email_2fa(user_id: &UserId, conn: &mut DbConn) -> EmptyResult {
if let Some(user) = User::find_by_uuid(user_id, conn).await {
activate_email_2fa(&user, conn).await
} else {
err!("User not found!");

View File

@ -173,17 +173,16 @@ async fn disable_twofactor_put(data: Json<DisableTwoFactorData>, headers: Header
pub async fn enforce_2fa_policy(
user: &User,
act_uuid: &str,
act_user_id: &UserId,
device_type: i32,
ip: &std::net::IpAddr,
conn: &mut DbConn,
) -> EmptyResult {
for member in UserOrganization::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn)
.await
.into_iter()
for member in
Membership::find_by_user_and_policy(&user.uuid, OrgPolicyType::TwoFactorAuthentication, conn).await.into_iter()
{
// Policy only applies to non-Owner/non-Admin members who have accepted joining the org
if member.atype < UserOrgType::Admin {
if member.atype < MembershipType::Admin {
if CONFIG.mail_enabled() {
let org = Organization::find_by_uuid(&member.org_uuid, conn).await.unwrap();
mail::send_2fa_removed_from_org(&user.email, &org.name).await?;
@ -196,7 +195,7 @@ pub async fn enforce_2fa_policy(
EventType::OrganizationUserRevoked as i32,
&member.uuid,
&member.org_uuid,
act_uuid,
act_user_id,
device_type,
ip,
conn,
@ -209,16 +208,16 @@ pub async fn enforce_2fa_policy(
}
pub async fn enforce_2fa_policy_for_org(
org_uuid: &str,
act_uuid: &str,
org_id: &OrganizationId,
act_user_id: &UserId,
device_type: i32,
ip: &std::net::IpAddr,
conn: &mut DbConn,
) -> EmptyResult {
let org = Organization::find_by_uuid(org_uuid, conn).await.unwrap();
for member in UserOrganization::find_confirmed_by_org(org_uuid, conn).await.into_iter() {
let org = Organization::find_by_uuid(org_id, conn).await.unwrap();
for member in Membership::find_confirmed_by_org(org_id, conn).await.into_iter() {
// Don't enforce the policy for Admins and Owners.
if member.atype < UserOrgType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() {
if member.atype < MembershipType::Admin && TwoFactor::find_by_user(&member.user_uuid, conn).await.is_empty() {
if CONFIG.mail_enabled() {
let user = User::find_by_uuid(&member.user_uuid, conn).await.unwrap();
mail::send_2fa_removed_from_org(&user.email, &org.name).await?;
@ -230,8 +229,8 @@ pub async fn enforce_2fa_policy_for_org(
log_event(
EventType::OrganizationUserRevoked as i32,
&member.uuid,
org_uuid,
act_uuid,
org_id,
act_user_id,
device_type,
ip,
conn,

View File

@ -6,7 +6,7 @@ use crate::{
auth::Headers,
crypto,
db::{
models::{TwoFactor, TwoFactorType},
models::{TwoFactor, TwoFactorType, UserId},
DbConn,
},
error::{Error, MapResult},
@ -104,11 +104,11 @@ async fn verify_otp(data: Json<ProtectedActionVerify>, headers: Headers, mut con
pub async fn validate_protected_action_otp(
otp: &str,
user_uuid: &str,
user_id: &UserId,
delete_if_valid: bool,
conn: &mut DbConn,
) -> EmptyResult {
let pa = TwoFactor::find_by_user_and_type(user_uuid, TwoFactorType::ProtectedActions as i32, conn)
let pa = TwoFactor::find_by_user_and_type(user_id, TwoFactorType::ProtectedActions as i32, conn)
.await
.map_res("Protected action token not found, try sending the code again or restart the process")?;
let mut pa_data = ProtectedActionData::from_json(&pa.data)?;

View File

@ -11,7 +11,7 @@ use crate::{
},
auth::Headers,
db::{
models::{EventType, TwoFactor, TwoFactorType},
models::{EventType, TwoFactor, TwoFactorType, UserId},
DbConn,
},
error::Error,
@ -148,7 +148,7 @@ async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Hea
)?;
let type_ = TwoFactorType::WebauthnRegisterChallenge;
TwoFactor::new(user.uuid, type_, serde_json::to_string(&state)?).save(&mut conn).await?;
TwoFactor::new(user.uuid.clone(), type_, serde_json::to_string(&state)?).save(&mut conn).await?;
let mut challenge_value = serde_json::to_value(challenge.public_key)?;
challenge_value["status"] = "ok".into();
@ -352,20 +352,20 @@ async fn delete_webauthn(data: Json<DeleteU2FData>, headers: Headers, mut conn:
}
pub async fn get_webauthn_registrations(
user_uuid: &str,
user_id: &UserId,
conn: &mut DbConn,
) -> Result<(bool, Vec<WebauthnRegistration>), Error> {
let type_ = TwoFactorType::Webauthn as i32;
match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await {
match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
Some(tf) => Ok((tf.enabled, serde_json::from_str(&tf.data)?)),
None => Ok((false, Vec::new())), // If no data, return empty list
}
}
pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> JsonResult {
pub async fn generate_webauthn_login(user_id: &UserId, conn: &mut DbConn) -> JsonResult {
// Load saved credentials
let creds: Vec<Credential> =
get_webauthn_registrations(user_uuid, conn).await?.1.into_iter().map(|r| r.credential).collect();
get_webauthn_registrations(user_id, conn).await?.1.into_iter().map(|r| r.credential).collect();
if creds.is_empty() {
err!("No Webauthn devices registered")
@ -376,7 +376,7 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> Json
let (response, state) = WebauthnConfig::load().generate_challenge_authenticate_options(creds, Some(ext))?;
// Save the challenge state for later validation
TwoFactor::new(user_uuid.into(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
TwoFactor::new(user_id.clone(), TwoFactorType::WebauthnLoginChallenge, serde_json::to_string(&state)?)
.save(conn)
.await?;
@ -384,9 +384,9 @@ pub async fn generate_webauthn_login(user_uuid: &str, conn: &mut DbConn) -> Json
Ok(Json(serde_json::to_value(response.public_key)?))
}
pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn validate_webauthn_login(user_id: &UserId, response: &str, conn: &mut DbConn) -> EmptyResult {
let type_ = TwoFactorType::WebauthnLoginChallenge as i32;
let state = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn).await {
let state = match TwoFactor::find_by_user_and_type(user_id, type_, conn).await {
Some(tf) => {
let state: AuthenticationState = serde_json::from_str(&tf.data)?;
tf.delete(conn).await?;
@ -403,7 +403,7 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut
let rsp: PublicKeyCredentialCopy = serde_json::from_str(response)?;
let rsp: PublicKeyCredential = rsp.into();
let mut registrations = get_webauthn_registrations(user_uuid, conn).await?.1;
let mut registrations = get_webauthn_registrations(user_id, conn).await?.1;
// If the credential we received is migrated from U2F, enable the U2F compatibility
//let use_u2f = registrations.iter().any(|r| r.migrated && r.credential.cred_id == rsp.raw_id.0);
@ -413,7 +413,7 @@ pub async fn validate_webauthn_login(user_uuid: &str, response: &str, conn: &mut
if &reg.credential.cred_id == cred_id {
reg.credential.counter = auth_data.counter;
TwoFactor::new(user_uuid.to_string(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?)
TwoFactor::new(user_id.clone(), TwoFactorType::Webauthn, serde_json::to_string(&registrations)?)
.save(conn)
.await?;
return Ok(());

View File

@ -92,10 +92,10 @@ async fn generate_yubikey(data: Json<PasswordOrOtpData>, headers: Headers, mut c
data.validate(&user, false, &mut conn).await?;
let user_uuid = &user.uuid;
let user_id = &user.uuid;
let yubikey_type = TwoFactorType::YubiKey as i32;
let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &mut conn).await;
let r = TwoFactor::find_by_user_and_type(user_id, yubikey_type, &mut conn).await;
if let Some(r) = r {
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;

View File

@ -31,7 +31,7 @@ pub fn routes() -> Vec<Route> {
async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn: DbConn) -> JsonResult {
let data: ConnectData = data.into_inner();
let mut user_uuid: Option<String> = None;
let mut user_id: Option<UserId> = None;
let login_result = match data.grant_type.as_ref() {
"refresh_token" => {
@ -48,7 +48,7 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
_check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?;
_password_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
_password_login(data, &mut user_id, &mut conn, &client_header.ip).await
}
"client_credentials" => {
_check_is_some(&data.client_id, "client_id cannot be blank")?;
@ -59,17 +59,17 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
_check_is_some(&data.device_name, "device_name cannot be blank")?;
_check_is_some(&data.device_type, "device_type cannot be blank")?;
_api_key_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
_api_key_login(data, &mut user_id, &mut conn, &client_header.ip).await
}
t => err!("Invalid type", t),
};
if let Some(user_uuid) = user_uuid {
if let Some(user_id) = user_id {
match &login_result {
Ok(_) => {
log_user_event(
EventType::UserLoggedIn as i32,
&user_uuid,
&user_id,
client_header.device_type,
&client_header.ip.ip,
&mut conn,
@ -80,7 +80,7 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
if let Some(ev) = e.get_event() {
log_user_event(
ev.event as i32,
&user_uuid,
&user_id,
client_header.device_type,
&client_header.ip.ip,
&mut conn,
@ -111,7 +111,7 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
// Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
device.save(conn).await?;
@ -141,7 +141,7 @@ struct MasterPasswordPolicy {
async fn _password_login(
data: ConnectData,
user_uuid: &mut Option<String>,
user_id: &mut Option<UserId>,
conn: &mut DbConn,
ip: &ClientIp,
) -> JsonResult {
@ -161,8 +161,8 @@ async fn _password_login(
err!("Username or password is incorrect. Try again", format!("IP: {}. Username: {}.", ip.ip, username))
};
// Set the user_uuid here to be passed back used for event logging.
*user_uuid = Some(user.uuid.clone());
// Set the user_id here to be passed back used for event logging.
*user_id = Some(user.uuid.clone());
// Check if the user is disabled
if !user.enabled {
@ -178,9 +178,8 @@ async fn _password_login(
let password = data.password.as_ref().unwrap();
// If we get an auth request, we don't check the user's password, but the access code of the auth request
if let Some(ref auth_request_uuid) = data.auth_request {
let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_uuid.as_str(), &user.uuid, conn).await
else {
if let Some(ref auth_request_id) = data.auth_request {
let Some(auth_request) = AuthRequest::find_by_uuid_and_user(auth_request_id, &user.uuid, conn).await else {
err!(
"Auth request not found. Try again.",
format!("IP: {}. Username: {}.", ip.ip, username),
@ -291,7 +290,7 @@ async fn _password_login(
// Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
device.save(conn).await?;
@ -359,7 +358,7 @@ async fn _password_login(
async fn _api_key_login(
data: ConnectData,
user_uuid: &mut Option<String>,
user_id: &mut Option<UserId>,
conn: &mut DbConn,
ip: &ClientIp,
) -> JsonResult {
@ -368,7 +367,7 @@ async fn _api_key_login(
// Validate scope
match data.scope.as_ref().unwrap().as_ref() {
"api" => _user_api_key_login(data, user_uuid, conn, ip).await,
"api" => _user_api_key_login(data, user_id, conn, ip).await,
"api.organization" => _organization_api_key_login(data, conn, ip).await,
_ => err!("Scope not supported"),
}
@ -376,21 +375,22 @@ async fn _api_key_login(
async fn _user_api_key_login(
data: ConnectData,
user_uuid: &mut Option<String>,
user_id: &mut Option<UserId>,
conn: &mut DbConn,
ip: &ClientIp,
) -> JsonResult {
// Get the user via the client_id
let client_id = data.client_id.as_ref().unwrap();
let Some(client_user_uuid) = client_id.strip_prefix("user.") else {
let Some(client_user_id) = client_id.strip_prefix("user.") else {
err!("Malformed client_id", format!("IP: {}.", ip.ip))
};
let Some(user) = User::find_by_uuid(client_user_uuid, conn).await else {
let client_user_id: UserId = client_user_id.into();
let Some(user) = User::find_by_uuid(&client_user_id, conn).await else {
err!("Invalid client_id", format!("IP: {}.", ip.ip))
};
// Set the user_uuid here to be passed back used for event logging.
*user_uuid = Some(user.uuid.clone());
// Set the user_id here to be passed back used for event logging.
*user_id = Some(user.uuid.clone());
// Check if the user is disabled
if !user.enabled {
@ -440,7 +440,7 @@ async fn _user_api_key_login(
// Because this might get used in the future, and is add by the Bitwarden Server, lets keep it, but then commented out
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
// let members = Membership::find_confirmed_by_user(&user.uuid, conn).await;
let (access_token, expires_in) = device.refresh_tokens(&user, scope_vec);
device.save(conn).await?;
@ -469,10 +469,11 @@ async fn _user_api_key_login(
async fn _organization_api_key_login(data: ConnectData, conn: &mut DbConn, ip: &ClientIp) -> JsonResult {
// Get the org via the client_id
let client_id = data.client_id.as_ref().unwrap();
let Some(org_uuid) = client_id.strip_prefix("organization.") else {
let Some(org_id) = client_id.strip_prefix("organization.") else {
err!("Malformed client_id", format!("IP: {}.", ip.ip))
};
let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(org_uuid, conn).await else {
let org_id: OrganizationId = org_id.to_string().into();
let Some(org_api_key) = OrganizationApiKey::find_by_org_uuid(&org_id, conn).await else {
err!("Invalid client_id", format!("IP: {}.", ip.ip))
};
@ -614,7 +615,7 @@ fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
async fn _json_err_twofactor(
providers: &[i32],
user_uuid: &str,
user_id: &UserId,
data: &ConnectData,
conn: &mut DbConn,
) -> ApiResult<Value> {
@ -635,12 +636,12 @@ async fn _json_err_twofactor(
Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ }
Some(TwoFactorType::Webauthn) if CONFIG.domain_set() => {
let request = webauthn::generate_webauthn_login(user_uuid, conn).await?;
let request = webauthn::generate_webauthn_login(user_id, conn).await?;
result["TwoFactorProviders2"][provider.to_string()] = request.0;
}
Some(TwoFactorType::Duo) => {
let email = match User::find_by_uuid(user_uuid, conn).await {
let email = match User::find_by_uuid(user_id, conn).await {
Some(u) => u.email,
None => err!("User does not exist"),
};
@ -672,7 +673,7 @@ async fn _json_err_twofactor(
}
Some(tf_type @ TwoFactorType::YubiKey) => {
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await else {
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else {
err!("No YubiKey devices registered")
};
@ -684,13 +685,13 @@ async fn _json_err_twofactor(
}
Some(tf_type @ TwoFactorType::Email) => {
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, conn).await else {
let Some(twofactor) = TwoFactor::find_by_user_and_type(user_id, tf_type as i32, conn).await else {
err!("No twofactor email registered")
};
// Send email immediately if email is the only 2FA option
if providers.len() == 1 {
email::send_token(user_uuid, conn).await?
email::send_token(user_id, conn).await?
}
let email_data = email::EmailTokenData::from_json(&twofactor.data)?;
@ -745,7 +746,7 @@ struct ConnectData {
#[field(name = uncased("device_identifier"))]
#[field(name = uncased("deviceidentifier"))]
device_identifier: Option<String>,
device_identifier: Option<DeviceId>,
#[field(name = uncased("device_name"))]
#[field(name = uncased("devicename"))]
device_name: Option<String>,
@ -768,7 +769,7 @@ struct ConnectData {
#[field(name = uncased("twofactorremember"))]
two_factor_remember: Option<i32>,
#[field(name = uncased("authrequest"))]
auth_request: Option<String>,
auth_request: Option<AuthRequestId>,
}
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {

View File

@ -10,7 +10,7 @@ use rocket_ws::{Message, WebSocket};
use crate::{
auth::{ClientIp, WsAccessTokenHeader},
db::{
models::{Cipher, Folder, Send as DbSend, User},
models::{Cipher, CollectionId, DeviceId, Folder, Send as DbSend, User, UserId},
DbConn,
},
Error, CONFIG,
@ -53,13 +53,13 @@ struct WsAccessToken {
struct WSEntryMapGuard {
users: Arc<WebSocketUsers>,
user_uuid: String,
user_uuid: UserId,
entry_uuid: uuid::Uuid,
addr: IpAddr,
}
impl WSEntryMapGuard {
fn new(users: Arc<WebSocketUsers>, user_uuid: String, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self {
fn new(users: Arc<WebSocketUsers>, user_uuid: UserId, entry_uuid: uuid::Uuid, addr: IpAddr) -> Self {
Self {
users,
user_uuid,
@ -72,7 +72,7 @@ impl WSEntryMapGuard {
impl Drop for WSEntryMapGuard {
fn drop(&mut self) {
info!("Closing WS connection from {}", self.addr);
if let Some(mut entry) = self.users.map.get_mut(&self.user_uuid) {
if let Some(mut entry) = self.users.map.get_mut(self.user_uuid.as_ref()) {
entry.retain(|(uuid, _)| uuid != &self.entry_uuid);
}
}
@ -130,7 +130,7 @@ fn websockets_hub<'r>(
// Add a channel to send messages to this client to the map
let entry_uuid = uuid::Uuid::new_v4();
let (tx, rx) = tokio::sync::mpsc::channel::<Message>(100);
users.map.entry(claims.sub.clone()).or_default().push((entry_uuid, tx));
users.map.entry(claims.sub.to_string()).or_default().push((entry_uuid, tx));
// Once the guard goes out of scope, the connection will have been closed and the entry will be deleted from the map
(rx, WSEntryMapGuard::new(users, claims.sub, entry_uuid, addr))
@ -330,8 +330,8 @@ pub struct WebSocketUsers {
}
impl WebSocketUsers {
async fn send_update(&self, user_uuid: &str, data: &[u8]) {
if let Some(user) = self.map.get(user_uuid).map(|v| v.clone()) {
async fn send_update(&self, user_id: &UserId, data: &[u8]) {
if let Some(user) = self.map.get(user_id.as_ref()).map(|v| v.clone()) {
for (_, sender) in user.iter() {
if let Err(e) = sender.send(Message::binary(data)).await {
error!("Error sending WS update {e}");
@ -347,7 +347,7 @@ impl WebSocketUsers {
return;
}
let data = create_update(
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))],
ut,
None,
);
@ -361,15 +361,15 @@ impl WebSocketUsers {
}
}
pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>) {
pub async fn send_logout(&self, user: &User, acting_device_id: Option<DeviceId>) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
return;
}
let data = create_update(
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
vec![("UserId".into(), user.uuid.to_string().into()), ("Date".into(), serialize_date(user.updated_at))],
UpdateType::LogOut,
acting_device_uuid.clone(),
acting_device_id.clone(),
);
if CONFIG.enable_websocket() {
@ -377,7 +377,7 @@ impl WebSocketUsers {
}
if CONFIG.push_enabled() {
push_logout(user, acting_device_uuid);
push_logout(user, acting_device_id.clone());
}
}
@ -385,7 +385,7 @@ impl WebSocketUsers {
&self,
ut: UpdateType,
folder: &Folder,
acting_device_uuid: &String,
acting_device_id: &DeviceId,
conn: &mut DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
@ -394,12 +394,12 @@ impl WebSocketUsers {
}
let data = create_update(
vec![
("Id".into(), folder.uuid.clone().into()),
("UserId".into(), folder.user_uuid.clone().into()),
("Id".into(), folder.uuid.to_string().into()),
("UserId".into(), folder.user_uuid.to_string().into()),
("RevisionDate".into(), serialize_date(folder.updated_at)),
],
ut,
Some(acting_device_uuid.into()),
Some(acting_device_id.clone()),
);
if CONFIG.enable_websocket() {
@ -407,7 +407,7 @@ impl WebSocketUsers {
}
if CONFIG.push_enabled() {
push_folder_update(ut, folder, acting_device_uuid, conn).await;
push_folder_update(ut, folder, acting_device_id, conn).await;
}
}
@ -415,48 +415,48 @@ impl WebSocketUsers {
&self,
ut: UpdateType,
cipher: &Cipher,
user_uuids: &[String],
acting_device_uuid: &String,
collection_uuids: Option<Vec<String>>,
user_ids: &[UserId],
acting_device_id: &DeviceId,
collection_uuids: Option<Vec<CollectionId>>,
conn: &mut DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
return;
}
let org_uuid = convert_option(cipher.organization_uuid.clone());
let org_id = convert_option(cipher.organization_uuid.as_deref());
// Depending if there are collections provided or not, we need to have different values for the following variables.
// The user_uuid should be `null`, and the revision date should be set to now, else the clients won't sync the collection change.
let (user_uuid, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids {
let (user_id, collection_uuids, revision_date) = if let Some(collection_uuids) = collection_uuids {
(
Value::Nil,
Value::Array(collection_uuids.into_iter().map(|v| v.into()).collect::<Vec<Value>>()),
Value::Array(collection_uuids.into_iter().map(|v| v.to_string().into()).collect::<Vec<Value>>()),
serialize_date(Utc::now().naive_utc()),
)
} else {
(convert_option(cipher.user_uuid.clone()), Value::Nil, serialize_date(cipher.updated_at))
(convert_option(cipher.user_uuid.as_deref()), Value::Nil, serialize_date(cipher.updated_at))
};
let data = create_update(
vec![
("Id".into(), cipher.uuid.clone().into()),
("UserId".into(), user_uuid),
("OrganizationId".into(), org_uuid),
("Id".into(), cipher.uuid.to_string().into()),
("UserId".into(), user_id),
("OrganizationId".into(), org_id),
("CollectionIds".into(), collection_uuids),
("RevisionDate".into(), revision_date),
],
ut,
Some(acting_device_uuid.into()),
Some(acting_device_id.clone()),
);
if CONFIG.enable_websocket() {
for uuid in user_uuids {
for uuid in user_ids {
self.send_update(uuid, &data).await;
}
}
if CONFIG.push_enabled() && user_uuids.len() == 1 {
push_cipher_update(ut, cipher, acting_device_uuid, conn).await;
if CONFIG.push_enabled() && user_ids.len() == 1 {
push_cipher_update(ut, cipher, acting_device_id, conn).await;
}
}
@ -464,20 +464,20 @@ impl WebSocketUsers {
&self,
ut: UpdateType,
send: &DbSend,
user_uuids: &[String],
acting_device_uuid: &String,
user_ids: &[UserId],
acting_device_id: &DeviceId,
conn: &mut DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
return;
}
let user_uuid = convert_option(send.user_uuid.clone());
let user_id = convert_option(send.user_uuid.as_deref());
let data = create_update(
vec![
("Id".into(), send.uuid.clone().into()),
("UserId".into(), user_uuid),
("Id".into(), send.uuid.to_string().into()),
("UserId".into(), user_id),
("RevisionDate".into(), serialize_date(send.revision_date)),
],
ut,
@ -485,20 +485,20 @@ impl WebSocketUsers {
);
if CONFIG.enable_websocket() {
for uuid in user_uuids {
for uuid in user_ids {
self.send_update(uuid, &data).await;
}
}
if CONFIG.push_enabled() && user_uuids.len() == 1 {
push_send_update(ut, send, acting_device_uuid, conn).await;
if CONFIG.push_enabled() && user_ids.len() == 1 {
push_send_update(ut, send, acting_device_id, conn).await;
}
}
pub async fn send_auth_request(
&self,
user_uuid: &String,
user_id: &UserId,
auth_request_uuid: &String,
acting_device_uuid: &String,
acting_device_id: &DeviceId,
conn: &mut DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
@ -506,24 +506,24 @@ impl WebSocketUsers {
return;
}
let data = create_update(
vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_uuid.clone().into())],
vec![("Id".into(), auth_request_uuid.clone().into()), ("UserId".into(), user_id.to_string().into())],
UpdateType::AuthRequest,
Some(acting_device_uuid.to_string()),
Some(acting_device_id.clone()),
);
if CONFIG.enable_websocket() {
self.send_update(user_uuid, &data).await;
self.send_update(user_id, &data).await;
}
if CONFIG.push_enabled() {
push_auth_request(user_uuid.to_string(), auth_request_uuid.to_string(), conn).await;
push_auth_request(user_id.clone(), auth_request_uuid.to_string(), conn).await;
}
}
pub async fn send_auth_response(
&self,
user_uuid: &String,
user_id: &UserId,
auth_response_uuid: &str,
approving_device_uuid: String,
approving_device_uuid: DeviceId,
conn: &mut DbConn,
) {
// Skip any processing if both WebSockets and Push are not active
@ -531,17 +531,16 @@ impl WebSocketUsers {
return;
}
let data = create_update(
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())],
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())],
UpdateType::AuthRequestResponse,
approving_device_uuid.clone().into(),
Some(approving_device_uuid.clone()),
);
if CONFIG.enable_websocket() {
self.send_update(auth_response_uuid, &data).await;
self.send_update(user_id, &data).await;
}
if CONFIG.push_enabled() {
push_auth_response(user_uuid.to_string(), auth_response_uuid.to_string(), approving_device_uuid, conn)
.await;
push_auth_response(user_id.clone(), auth_response_uuid.to_string(), approving_device_uuid, conn).await;
}
}
}
@ -560,16 +559,16 @@ impl AnonymousWebSocketSubscriptions {
}
}
pub async fn send_auth_response(&self, user_uuid: &String, auth_response_uuid: &str) {
pub async fn send_auth_response(&self, user_id: &UserId, auth_response_uuid: &str) {
if !CONFIG.enable_websocket() {
return;
}
let data = create_anonymous_update(
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_uuid.clone().into())],
vec![("Id".into(), auth_response_uuid.to_owned().into()), ("UserId".into(), user_id.to_string().into())],
UpdateType::AuthRequestResponse,
user_uuid.to_string(),
user_id.clone(),
);
self.send_update(auth_response_uuid, &data).await;
self.send_update(user_id, &data).await;
}
}
@ -581,14 +580,14 @@ impl AnonymousWebSocketSubscriptions {
"ReceiveMessage", // Target
[ // Arguments
{
"ContextId": acting_device_uuid || Nil,
"ContextId": acting_device_id || Nil,
"Type": ut as i32,
"Payload": {}
}
]
]
*/
fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uuid: Option<String>) -> Vec<u8> {
fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_id: Option<DeviceId>) -> Vec<u8> {
use rmpv::Value as V;
let value = V::Array(vec![
@ -597,7 +596,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uui
V::Nil,
"ReceiveMessage".into(),
V::Array(vec![V::Map(vec![
("ContextId".into(), acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| V::Nil)),
("ContextId".into(), acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| V::Nil)),
("Type".into(), (ut as i32).into()),
("Payload".into(), payload.into()),
])]),
@ -606,7 +605,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uui
serialize(value)
}
fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: String) -> Vec<u8> {
fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: UserId) -> Vec<u8> {
use rmpv::Value as V;
let value = V::Array(vec![
@ -617,7 +616,7 @@ fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id
V::Array(vec![V::Map(vec![
("Type".into(), (ut as i32).into()),
("Payload".into(), payload.into()),
("UserId".into(), user_id.into()),
("UserId".into(), user_id.to_string().into()),
])]),
]);

View File

@ -7,7 +7,7 @@ use tokio::sync::RwLock;
use crate::{
api::{ApiResult, EmptyResult, UpdateType},
db::models::{Cipher, Device, Folder, Send, User},
db::models::{Cipher, Device, DeviceId, Folder, Send, User, UserId},
http_client::make_http_request,
util::format_date,
CONFIG,
@ -126,15 +126,15 @@ pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbC
Ok(())
}
pub async fn unregister_push_device(push_uuid: Option<String>) -> EmptyResult {
if !CONFIG.push_enabled() || push_uuid.is_none() {
pub async fn unregister_push_device(push_id: Option<String>) -> EmptyResult {
if !CONFIG.push_enabled() || push_id.is_none() {
return Ok(());
}
let auth_push_token = get_auth_push_token().await?;
let auth_header = format!("Bearer {}", &auth_push_token);
match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_uuid.unwrap()))?
match make_http_request(Method::DELETE, &(CONFIG.push_relay_uri() + "/push/" + &push_id.unwrap()))?
.header(AUTHORIZATION, auth_header)
.send()
.await
@ -148,24 +148,24 @@ pub async fn unregister_push_device(push_uuid: Option<String>) -> EmptyResult {
pub async fn push_cipher_update(
ut: UpdateType,
cipher: &Cipher,
acting_device_uuid: &String,
acting_device_id: &DeviceId,
conn: &mut crate::db::DbConn,
) {
// We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.
if cipher.organization_uuid.is_some() {
return;
};
let Some(user_uuid) = &cipher.user_uuid else {
let Some(user_id) = &cipher.user_uuid else {
debug!("Cipher has no uuid");
return;
};
if Device::check_user_has_push_device(user_uuid, conn).await {
if Device::check_user_has_push_device(user_id, conn).await {
send_to_push_relay(json!({
"userId": user_uuid,
"userId": user_id,
"organizationId": (),
"deviceId": acting_device_uuid,
"identifier": acting_device_uuid,
"deviceId": acting_device_id,
"identifier": acting_device_id,
"type": ut as i32,
"payload": {
"Id": cipher.uuid,
@ -178,14 +178,14 @@ pub async fn push_cipher_update(
}
}
pub fn push_logout(user: &User, acting_device_uuid: Option<String>) {
let acting_device_uuid: Value = acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| Value::Null);
pub fn push_logout(user: &User, acting_device_id: Option<DeviceId>) {
let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null);
tokio::task::spawn(send_to_push_relay(json!({
"userId": user.uuid,
"organizationId": (),
"deviceId": acting_device_uuid,
"identifier": acting_device_uuid,
"deviceId": acting_device_id,
"identifier": acting_device_id,
"type": UpdateType::LogOut as i32,
"payload": {
"UserId": user.uuid,
@ -211,15 +211,15 @@ pub fn push_user_update(ut: UpdateType, user: &User) {
pub async fn push_folder_update(
ut: UpdateType,
folder: &Folder,
acting_device_uuid: &String,
acting_device_id: &DeviceId,
conn: &mut crate::db::DbConn,
) {
if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": folder.user_uuid,
"organizationId": (),
"deviceId": acting_device_uuid,
"identifier": acting_device_uuid,
"deviceId": acting_device_id,
"identifier": acting_device_id,
"type": ut as i32,
"payload": {
"Id": folder.uuid,
@ -230,14 +230,14 @@ pub async fn push_folder_update(
}
}
pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_uuid: &String, conn: &mut crate::db::DbConn) {
pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_id: &DeviceId, conn: &mut crate::db::DbConn) {
if let Some(s) = &send.user_uuid {
if Device::check_user_has_push_device(s, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": send.user_uuid,
"organizationId": (),
"deviceId": acting_device_uuid,
"identifier": acting_device_uuid,
"deviceId": acting_device_id,
"identifier": acting_device_id,
"type": ut as i32,
"payload": {
"Id": send.uuid,
@ -284,38 +284,38 @@ async fn send_to_push_relay(notification_data: Value) {
};
}
pub async fn push_auth_request(user_uuid: String, auth_request_uuid: String, conn: &mut crate::db::DbConn) {
if Device::check_user_has_push_device(user_uuid.as_str(), conn).await {
pub async fn push_auth_request(user_id: UserId, auth_request_id: String, conn: &mut crate::db::DbConn) {
if Device::check_user_has_push_device(&user_id, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user_uuid,
"userId": user_id,
"organizationId": (),
"deviceId": null,
"identifier": null,
"type": UpdateType::AuthRequest as i32,
"payload": {
"Id": auth_request_uuid,
"UserId": user_uuid,
"Id": auth_request_id,
"UserId": user_id,
}
})));
}
}
pub async fn push_auth_response(
user_uuid: String,
auth_request_uuid: String,
approving_device_uuid: String,
user_id: UserId,
auth_request_id: String,
approving_device_id: DeviceId,
conn: &mut crate::db::DbConn,
) {
if Device::check_user_has_push_device(user_uuid.as_str(), conn).await {
if Device::check_user_has_push_device(&user_id, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user_uuid,
"userId": user_id,
"organizationId": (),
"deviceId": approving_device_uuid,
"identifier": approving_device_uuid,
"deviceId": approving_device_id,
"identifier": approving_device_id,
"type": UpdateType::AuthRequestResponse as i32,
"payload": {
"Id": auth_request_uuid,
"UserId": user_uuid,
"Id": auth_request_id,
"UserId": user_id,
}
})));
}

View File

@ -12,8 +12,9 @@ use serde_json::Value;
use crate::{
api::{core::now, ApiResult, EmptyResult},
auth::decode_file_download,
db::models::{AttachmentId, CipherId},
error::Error,
util::{Cached, SafeString},
util::Cached,
CONFIG,
};
@ -158,16 +159,16 @@ async fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> {
Cached::long(NamedFile::open(Path::new(&CONFIG.web_vault_folder()).join(p)).await.ok(), true)
}
#[get("/attachments/<uuid>/<file_id>?<token>")]
async fn attachments(uuid: SafeString, file_id: SafeString, token: String) -> Option<NamedFile> {
#[get("/attachments/<cipher_id>/<file_id>?<token>")]
async fn attachments(cipher_id: CipherId, file_id: AttachmentId, token: String) -> Option<NamedFile> {
let Ok(claims) = decode_file_download(&token) else {
return None;
};
if claims.sub != *uuid || claims.file_id != *file_id {
if claims.sub != cipher_id || claims.file_id != file_id {
return None;
}
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).await.ok()
NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(cipher_id.as_ref()).join(file_id.as_ref())).await.ok()
}
// We use DbConn here to let the alive healthcheck also verify the database connection.

View File

@ -14,6 +14,10 @@ use std::{
net::IpAddr,
};
use crate::db::models::{
AttachmentId, CipherId, CollectionId, DeviceId, EmergencyAccessId, MembershipId, OrgApiKeyId, OrganizationId,
SendFileId, SendId, UserId,
};
use crate::{error::Error, CONFIG};
const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
@ -150,7 +154,7 @@ pub struct LoginJwtClaims {
// Issuer
pub iss: String,
// Subject
pub sub: String,
pub sub: UserId,
pub premium: bool,
pub name: String,
@ -171,7 +175,7 @@ pub struct LoginJwtClaims {
// user security_stamp
pub sstamp: String,
// device uuid
pub device: String,
pub device: DeviceId,
// [ "api", "offline_access" ]
pub scope: Vec<String>,
// [ "Application" ]
@ -187,19 +191,19 @@ pub struct InviteJwtClaims {
// Issuer
pub iss: String,
// Subject
pub sub: String,
pub sub: UserId,
pub email: String,
pub org_id: Option<String>,
pub user_org_id: Option<String>,
pub org_id: Option<OrganizationId>,
pub member_id: Option<MembershipId>,
pub invited_by_email: Option<String>,
}
pub fn generate_invite_claims(
uuid: String,
user_id: UserId,
email: String,
org_id: Option<String>,
user_org_id: Option<String>,
org_id: Option<OrganizationId>,
member_id: Option<MembershipId>,
invited_by_email: Option<String>,
) -> InviteJwtClaims {
let time_now = Utc::now();
@ -208,10 +212,10 @@ pub fn generate_invite_claims(
nbf: time_now.timestamp(),
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
iss: JWT_INVITE_ISSUER.to_string(),
sub: uuid,
sub: user_id,
email,
org_id,
user_org_id,
member_id,
invited_by_email,
}
}
@ -225,18 +229,18 @@ pub struct EmergencyAccessInviteJwtClaims {
// Issuer
pub iss: String,
// Subject
pub sub: String,
pub sub: UserId,
pub email: String,
pub emer_id: String,
pub emer_id: EmergencyAccessId,
pub grantor_name: String,
pub grantor_email: String,
}
pub fn generate_emergency_access_invite_claims(
uuid: String,
user_id: UserId,
email: String,
emer_id: String,
emer_id: EmergencyAccessId,
grantor_name: String,
grantor_email: String,
) -> EmergencyAccessInviteJwtClaims {
@ -246,7 +250,7 @@ pub fn generate_emergency_access_invite_claims(
nbf: time_now.timestamp(),
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
iss: JWT_EMERGENCY_ACCESS_INVITE_ISSUER.to_string(),
sub: uuid,
sub: user_id,
email,
emer_id,
grantor_name,
@ -263,21 +267,24 @@ pub struct OrgApiKeyLoginJwtClaims {
// Issuer
pub iss: String,
// Subject
pub sub: String,
pub sub: OrgApiKeyId,
pub client_id: String,
pub client_sub: String,
pub client_sub: OrganizationId,
pub scope: Vec<String>,
}
pub fn generate_organization_api_key_login_claims(uuid: String, org_id: String) -> OrgApiKeyLoginJwtClaims {
pub fn generate_organization_api_key_login_claims(
org_api_key_uuid: OrgApiKeyId,
org_id: OrganizationId,
) -> OrgApiKeyLoginJwtClaims {
let time_now = Utc::now();
OrgApiKeyLoginJwtClaims {
nbf: time_now.timestamp(),
exp: (time_now + TimeDelta::try_hours(1).unwrap()).timestamp(),
iss: JWT_ORG_API_KEY_ISSUER.to_string(),
sub: uuid,
client_id: format!("organization.{org_id}"),
sub: org_api_key_uuid,
client_id: format!("organization.{}", org_id),
client_sub: org_id,
scope: vec!["api.organization".into()],
}
@ -292,18 +299,18 @@ pub struct FileDownloadClaims {
// Issuer
pub iss: String,
// Subject
pub sub: String,
pub sub: CipherId,
pub file_id: String,
pub file_id: AttachmentId,
}
pub fn generate_file_download_claims(uuid: String, file_id: String) -> FileDownloadClaims {
pub fn generate_file_download_claims(cipher_id: CipherId, file_id: AttachmentId) -> FileDownloadClaims {
let time_now = Utc::now();
FileDownloadClaims {
nbf: time_now.timestamp(),
exp: (time_now + TimeDelta::try_minutes(5).unwrap()).timestamp(),
iss: JWT_FILE_DOWNLOAD_ISSUER.to_string(),
sub: uuid,
sub: cipher_id,
file_id,
}
}
@ -331,14 +338,14 @@ pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {
}
}
pub fn generate_verify_email_claims(uuid: String) -> BasicJwtClaims {
pub fn generate_verify_email_claims(user_id: UserId) -> BasicJwtClaims {
let time_now = Utc::now();
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
BasicJwtClaims {
nbf: time_now.timestamp(),
exp: (time_now + TimeDelta::try_hours(expire_hours).unwrap()).timestamp(),
iss: JWT_VERIFYEMAIL_ISSUER.to_string(),
sub: uuid,
sub: user_id.to_string(),
}
}
@ -352,7 +359,7 @@ pub fn generate_admin_claims() -> BasicJwtClaims {
}
}
pub fn generate_send_claims(send_id: &str, file_id: &str) -> BasicJwtClaims {
pub fn generate_send_claims(send_id: &SendId, file_id: &SendFileId) -> BasicJwtClaims {
let time_now = Utc::now();
BasicJwtClaims {
nbf: time_now.timestamp(),
@ -371,7 +378,7 @@ use rocket::{
};
use crate::db::{
models::{Collection, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException},
models::{Collection, Device, Membership, MembershipStatus, MembershipType, User, UserStampException},
DbConn,
};
@ -475,19 +482,19 @@ impl<'r> FromRequest<'r> for Headers {
err_handler!("Invalid claim")
};
let device_uuid = claims.device;
let user_uuid = claims.sub;
let device_id = claims.device;
let user_id = claims.sub;
let mut conn = match DbConn::from_request(request).await {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB"),
};
let Some(device) = Device::find_by_uuid_and_user(&device_uuid, &user_uuid, &mut conn).await else {
let Some(device) = Device::find_by_uuid_and_user(&device_id, &user_id, &mut conn).await else {
err_handler!("Invalid device id")
};
let Some(user) = User::find_by_uuid(&user_uuid, &mut conn).await else {
let Some(user) = User::find_by_uuid(&user_id, &mut conn).await else {
err_handler!("Device has no user associated")
};
@ -534,8 +541,8 @@ pub struct OrgHeaders {
pub host: String,
pub device: Device,
pub user: User,
pub org_user_type: UserOrgType,
pub org_user: UserOrganization,
pub membership_type: MembershipType,
pub membership: Membership,
pub ip: ClientIp,
}
@ -549,17 +556,17 @@ impl<'r> FromRequest<'r> for OrgHeaders {
// org_id is usually the second path param ("/organizations/<org_id>"),
// but there are cases where it is a query value.
// First check the path, if this is not a valid uuid, try the query values.
let url_org_id: Option<&str> = {
let url_org_id: Option<OrganizationId> = {
let mut url_org_id = None;
if let Some(Ok(org_id)) = request.param::<&str>(1) {
if uuid::Uuid::parse_str(org_id).is_ok() {
url_org_id = Some(org_id);
url_org_id = Some(org_id.to_string().into());
}
}
if let Some(Ok(org_id)) = request.query_value::<&str>("organizationId") {
if uuid::Uuid::parse_str(org_id).is_ok() {
url_org_id = Some(org_id);
url_org_id = Some(org_id.to_string().into());
}
}
@ -574,10 +581,10 @@ impl<'r> FromRequest<'r> for OrgHeaders {
};
let user = headers.user;
let org_user = match UserOrganization::find_by_user_and_org(&user.uuid, org_id, &mut conn).await {
Some(user) => {
if user.status == UserOrgStatus::Confirmed as i32 {
user
let membership = match Membership::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await {
Some(member) => {
if member.status == MembershipStatus::Confirmed as i32 {
member
} else {
err_handler!("The current user isn't confirmed member of the organization")
}
@ -589,15 +596,15 @@ impl<'r> FromRequest<'r> for OrgHeaders {
host: headers.host,
device: headers.device,
user,
org_user_type: {
if let Some(org_usr_type) = UserOrgType::from_i32(org_user.atype) {
membership_type: {
if let Some(org_usr_type) = MembershipType::from_i32(membership.atype) {
org_usr_type
} else {
// This should only happen if the DB is corrupted
err_handler!("Unknown user type in the database")
}
},
org_user,
membership,
ip: headers.ip,
})
}
@ -610,7 +617,7 @@ pub struct AdminHeaders {
pub host: String,
pub device: Device,
pub user: User,
pub org_user_type: UserOrgType,
pub membership_type: MembershipType,
pub ip: ClientIp,
}
@ -620,12 +627,12 @@ impl<'r> FromRequest<'r> for AdminHeaders {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = try_outcome!(OrgHeaders::from_request(request).await);
if headers.org_user_type >= UserOrgType::Admin {
if headers.membership_type >= MembershipType::Admin {
Outcome::Success(Self {
host: headers.host,
device: headers.device,
user: headers.user,
org_user_type: headers.org_user_type,
membership_type: headers.membership_type,
ip: headers.ip,
})
} else {
@ -648,16 +655,16 @@ impl From<AdminHeaders> for Headers {
// col_id is usually the fourth path param ("/organizations/<org_id>/collections/<col_id>"),
// but there could be cases where it is a query value.
// First check the path, if this is not a valid uuid, try the query values.
fn get_col_id(request: &Request<'_>) -> Option<String> {
fn get_col_id(request: &Request<'_>) -> Option<CollectionId> {
if let Some(Ok(col_id)) = request.param::<String>(3) {
if uuid::Uuid::parse_str(&col_id).is_ok() {
return Some(col_id);
return Some(col_id.into());
}
}
if let Some(Ok(col_id)) = request.query_value::<String>("collectionId") {
if uuid::Uuid::parse_str(&col_id).is_ok() {
return Some(col_id);
return Some(col_id.into());
}
}
@ -680,7 +687,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = try_outcome!(OrgHeaders::from_request(request).await);
if headers.org_user_type >= UserOrgType::Manager {
if headers.membership_type >= MembershipType::Manager {
match get_col_id(request) {
Some(col_id) => {
let mut conn = match DbConn::from_request(request).await {
@ -688,7 +695,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
_ => err_handler!("Error getting DB"),
};
if !Collection::can_access_collection(&headers.org_user, &col_id, &mut conn).await {
if !Collection::can_access_collection(&headers.membership, &col_id, &mut conn).await {
err_handler!("The current user isn't a manager for this collection")
}
}
@ -724,7 +731,7 @@ pub struct ManagerHeadersLoose {
pub host: String,
pub device: Device,
pub user: User,
pub org_user: UserOrganization,
pub membership: Membership,
pub ip: ClientIp,
}
@ -734,12 +741,12 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = try_outcome!(OrgHeaders::from_request(request).await);
if headers.org_user_type >= UserOrgType::Manager {
if headers.membership_type >= MembershipType::Manager {
Outcome::Success(Self {
host: headers.host,
device: headers.device,
user: headers.user,
org_user: headers.org_user,
membership: headers.membership,
ip: headers.ip,
})
} else {
@ -762,14 +769,14 @@ impl From<ManagerHeadersLoose> for Headers {
impl ManagerHeaders {
pub async fn from_loose(
h: ManagerHeadersLoose,
collections: &Vec<String>,
collections: &Vec<CollectionId>,
conn: &mut DbConn,
) -> Result<ManagerHeaders, Error> {
for col_id in collections {
if uuid::Uuid::parse_str(col_id).is_err() {
if uuid::Uuid::parse_str(col_id.as_ref()).is_err() {
err!("Collection Id is malformed!");
}
if !Collection::can_access_collection(&h.org_user, col_id, conn).await {
if !Collection::can_access_collection(&h.membership, col_id, conn).await {
err!("You don't have access to all collections!");
}
}
@ -795,7 +802,7 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = try_outcome!(OrgHeaders::from_request(request).await);
if headers.org_user_type == UserOrgType::Owner {
if headers.membership_type == MembershipType::Owner {
Outcome::Success(Self {
device: headers.device,
user: headers.user,

View File

@ -84,14 +84,15 @@ pub fn generate_id<const N: usize>() -> String {
encode_random_bytes::<N>(HEXLOWER)
}
pub fn generate_send_id() -> String {
// Send IDs are globally scoped, so make them longer to avoid collisions.
pub fn generate_send_file_id() -> String {
// Send File IDs are globally scoped, so make them longer to avoid collisions.
generate_id::<32>() // 256 bits
}
pub fn generate_attachment_id() -> String {
use crate::db::models::AttachmentId;
pub fn generate_attachment_id() -> AttachmentId {
// Attachment IDs are scoped to a cipher, so they can be smaller.
generate_id::<10>() // 80 bits
AttachmentId(generate_id::<10>()) // 80 bits
}
/// Generates a numeric token for email-based verifications.

View File

@ -1,9 +1,12 @@
use std::io::ErrorKind;
use bigdecimal::{BigDecimal, ToPrimitive};
use derive_more::{AsRef, Deref, Display};
use serde_json::Value;
use super::{CipherId, OrganizationId, UserId};
use crate::CONFIG;
use macros::IdFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -11,8 +14,8 @@ db_object! {
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(id))]
pub struct Attachment {
pub id: String,
pub cipher_uuid: String,
pub id: AttachmentId,
pub cipher_uuid: CipherId,
pub file_name: String, // encrypted
pub file_size: i64,
pub akey: Option<String>,
@ -21,7 +24,13 @@ db_object! {
/// Local methods
impl Attachment {
pub const fn new(id: String, cipher_uuid: String, file_name: String, file_size: i64, akey: Option<String>) -> Self {
pub const fn new(
id: AttachmentId,
cipher_uuid: CipherId,
file_name: String,
file_size: i64,
akey: Option<String>,
) -> Self {
Self {
id,
cipher_uuid,
@ -117,14 +126,14 @@ impl Attachment {
}}
}
pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
for attachment in Attachment::find_by_cipher(cipher_uuid, conn).await {
attachment.delete(conn).await?;
}
Ok(())
}
pub async fn find_by_id(id: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_id(id: &AttachmentId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
attachments::table
.filter(attachments::id.eq(id.to_lowercase()))
@ -134,7 +143,7 @@ impl Attachment {
}}
}
pub async fn find_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
attachments::table
.filter(attachments::cipher_uuid.eq(cipher_uuid))
@ -144,7 +153,7 @@ impl Attachment {
}}
}
pub async fn size_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn size_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
let result: Option<BigDecimal> = attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@ -161,7 +170,7 @@ impl Attachment {
}}
}
pub async fn count_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn count_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@ -172,7 +181,7 @@ impl Attachment {
}}
}
pub async fn size_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn size_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
let result: Option<BigDecimal> = attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@ -189,7 +198,7 @@ impl Attachment {
}}
}
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@ -203,7 +212,11 @@ impl Attachment {
// This will return all attachments linked to the user or org
// There is no filtering done here if the user actually has access!
// It is used to speed up the sync process, and the matching is done in a different part.
pub async fn find_all_by_user_and_orgs(user_uuid: &str, org_uuids: &Vec<String>, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_all_by_user_and_orgs(
user_uuid: &UserId,
org_uuids: &Vec<OrganizationId>,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
attachments::table
.left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid)))
@ -216,3 +229,20 @@ impl Attachment {
}}
}
}
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
IdFromParam,
)]
pub struct AttachmentId(pub String);

View File

@ -1,5 +1,8 @@
use super::{DeviceId, OrganizationId, UserId};
use crate::crypto::ct_eq;
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use macros::UuidFromParam;
db_object! {
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset, Deserialize, Serialize)]
@ -7,15 +10,15 @@ db_object! {
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct AuthRequest {
pub uuid: String,
pub user_uuid: String,
pub organization_uuid: Option<String>,
pub uuid: AuthRequestId,
pub user_uuid: UserId,
pub organization_uuid: Option<OrganizationId>,
pub request_device_identifier: String,
pub request_device_identifier: DeviceId,
pub device_type: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
pub request_ip: String,
pub response_device_id: Option<String>,
pub response_device_id: Option<DeviceId>,
pub access_code: String,
pub public_key: String,
@ -33,8 +36,8 @@ db_object! {
impl AuthRequest {
pub fn new(
user_uuid: String,
request_device_identifier: String,
user_uuid: UserId,
request_device_identifier: DeviceId,
device_type: i32,
request_ip: String,
access_code: String,
@ -43,7 +46,7 @@ impl AuthRequest {
let now = Utc::now().naive_utc();
Self {
uuid: crate::util::get_uuid(),
uuid: AuthRequestId(crate::util::get_uuid()),
user_uuid,
organization_uuid: None,
@ -101,7 +104,7 @@ impl AuthRequest {
}
}
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &AuthRequestId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
auth_requests::table
.filter(auth_requests::uuid.eq(uuid))
@ -111,7 +114,7 @@ impl AuthRequest {
}}
}
pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_user(uuid: &AuthRequestId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
auth_requests::table
.filter(auth_requests::uuid.eq(uuid))
@ -122,7 +125,7 @@ impl AuthRequest {
}}
}
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
auth_requests::table
.filter(auth_requests::user_uuid.eq(user_uuid))
@ -157,3 +160,21 @@ impl AuthRequest {
}
}
}
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct AuthRequestId(String);

View File

@ -1,13 +1,15 @@
use crate::util::LowerCase;
use crate::CONFIG;
use chrono::{NaiveDateTime, TimeDelta, Utc};
use derive_more::{AsRef, Deref, Display, From};
use serde_json::Value;
use super::{
Attachment, CollectionCipher, Favorite, FolderCipher, Group, User, UserOrgStatus, UserOrgType, UserOrganization,
Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus,
MembershipType, OrganizationId, User, UserId,
};
use crate::api::core::{CipherData, CipherSyncData, CipherSyncType};
use macros::UuidFromParam;
use std::borrow::Cow;
@ -17,12 +19,12 @@ db_object! {
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Cipher {
pub uuid: String,
pub uuid: CipherId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: Option<String>,
pub organization_uuid: Option<String>,
pub user_uuid: Option<UserId>,
pub organization_uuid: Option<OrganizationId>,
pub key: Option<String>,
@ -57,7 +59,7 @@ impl Cipher {
let now = Utc::now().naive_utc();
Self {
uuid: crate::util::get_uuid(),
uuid: CipherId(crate::util::get_uuid()),
created_at: now,
updated_at: now,
@ -135,7 +137,7 @@ impl Cipher {
pub async fn to_json(
&self,
host: &str,
user_uuid: &str,
user_uuid: &UserId,
cipher_sync_data: Option<&CipherSyncData>,
sync_type: CipherSyncType,
conn: &mut DbConn,
@ -302,7 +304,7 @@ impl Cipher {
Cow::from(Vec::with_capacity(0))
}
} else {
Cow::from(self.get_admin_collections(user_uuid.to_string(), conn).await)
Cow::from(self.get_admin_collections(user_uuid.clone(), conn).await)
};
// There are three types of cipher response models in upstream
@ -351,7 +353,7 @@ impl Cipher {
// Skip adding these fields in that case
if sync_type == CipherSyncType::User {
json_object["folderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string())
cipher_sync_data.cipher_folders.get(&self.uuid).cloned()
} else {
self.get_folder_uuid(user_uuid, conn).await
});
@ -380,7 +382,7 @@ impl Cipher {
json_object
}
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<String> {
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
let mut user_uuids = Vec::new();
match self.user_uuid {
Some(ref user_uuid) => {
@ -391,17 +393,16 @@ impl Cipher {
// Belongs to Organization, need to update affected users
if let Some(ref org_uuid) = self.organization_uuid {
// users having access to the collection
let mut collection_users =
UserOrganization::find_by_cipher_and_org(&self.uuid, org_uuid, conn).await;
let mut collection_users = Membership::find_by_cipher_and_org(&self.uuid, org_uuid, conn).await;
if CONFIG.org_groups_enabled() {
// members of a group having access to the collection
let group_users =
UserOrganization::find_by_cipher_and_org_with_group(&self.uuid, org_uuid, conn).await;
Membership::find_by_cipher_and_org_with_group(&self.uuid, org_uuid, conn).await;
collection_users.extend(group_users);
}
for user_org in collection_users {
User::update_uuid_revision(&user_org.user_uuid, conn).await;
user_uuids.push(user_org.user_uuid.clone())
for member in collection_users {
User::update_uuid_revision(&member.user_uuid, conn).await;
user_uuids.push(member.user_uuid.clone())
}
}
}
@ -459,7 +460,7 @@ impl Cipher {
}}
}
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
// TODO: Optimize this by executing a DELETE directly on the database, instead of first fetching.
for cipher in Self::find_by_org(org_uuid, conn).await {
cipher.delete(conn).await?;
@ -467,7 +468,7 @@ impl Cipher {
Ok(())
}
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
for cipher in Self::find_owned_by_user(user_uuid, conn).await {
cipher.delete(conn).await?;
}
@ -485,52 +486,59 @@ impl Cipher {
}
}
pub async fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn move_to_folder(
&self,
folder_uuid: Option<FolderId>,
user_uuid: &UserId,
conn: &mut DbConn,
) -> EmptyResult {
User::update_uuid_revision(user_uuid, conn).await;
match (self.get_folder_uuid(user_uuid, conn).await, folder_uuid) {
// No changes
(None, None) => Ok(()),
(Some(ref old), Some(ref new)) if old == new => Ok(()),
(Some(ref old_folder), Some(ref new_folder)) if old_folder == new_folder => Ok(()),
// Add to folder
(None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(conn).await,
(None, Some(new_folder)) => FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await,
// Remove from folder
(Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn).await {
Some(old) => old.delete(conn).await,
None => err!("Couldn't move from previous folder"),
},
(Some(old_folder), None) => {
match FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await {
Some(old_folder) => old_folder.delete(conn).await,
None => err!("Couldn't move from previous folder"),
}
}
// Move to another folder
(Some(old), Some(new)) => {
if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn).await {
old.delete(conn).await?;
(Some(old_folder), Some(new_folder)) => {
if let Some(old_folder) = FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await {
old_folder.delete(conn).await?;
}
FolderCipher::new(&new, &self.uuid).save(conn).await
FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await
}
}
}
/// Returns whether this cipher is directly owned by the user.
pub fn is_owned_by_user(&self, user_uuid: &str) -> bool {
pub fn is_owned_by_user(&self, user_uuid: &UserId) -> bool {
self.user_uuid.is_some() && self.user_uuid.as_ref().unwrap() == user_uuid
}
/// Returns whether this cipher is owned by an org in which the user has full access.
async fn is_in_full_access_org(
&self,
user_uuid: &str,
user_uuid: &UserId,
cipher_sync_data: Option<&CipherSyncData>,
conn: &mut DbConn,
) -> bool {
if let Some(ref org_uuid) = self.organization_uuid {
if let Some(cipher_sync_data) = cipher_sync_data {
if let Some(cached_user_org) = cipher_sync_data.user_organizations.get(org_uuid) {
return cached_user_org.has_full_access();
if let Some(cached_member) = cipher_sync_data.members.get(org_uuid) {
return cached_member.has_full_access();
}
} else if let Some(user_org) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn).await {
return user_org.has_full_access();
} else if let Some(member) = Membership::find_by_user_and_org(user_uuid, org_uuid, conn).await {
return member.has_full_access();
}
}
false
@ -539,7 +547,7 @@ impl Cipher {
/// Returns whether this cipher is owned by an group in which the user has full access.
async fn is_in_full_access_group(
&self,
user_uuid: &str,
user_uuid: &UserId,
cipher_sync_data: Option<&CipherSyncData>,
conn: &mut DbConn,
) -> bool {
@ -563,7 +571,7 @@ impl Cipher {
/// the access restrictions.
pub async fn get_access_restrictions(
&self,
user_uuid: &str,
user_uuid: &UserId,
cipher_sync_data: Option<&CipherSyncData>,
conn: &mut DbConn,
) -> Option<(bool, bool)> {
@ -623,7 +631,7 @@ impl Cipher {
Some((read_only, hide_passwords))
}
async fn get_user_collections_access_flags(&self, user_uuid: &str, conn: &mut DbConn) -> Vec<(bool, bool)> {
async fn get_user_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> {
db_run! {conn: {
// Check whether this cipher is in any collections accessible to the
// user. If so, retrieve the access flags for each collection.
@ -640,7 +648,7 @@ impl Cipher {
}}
}
async fn get_group_collections_access_flags(&self, user_uuid: &str, conn: &mut DbConn) -> Vec<(bool, bool)> {
async fn get_group_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> {
if !CONFIG.org_groups_enabled() {
return Vec::new();
}
@ -666,43 +674,43 @@ impl Cipher {
}}
}
pub async fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
match self.get_access_restrictions(user_uuid, None, conn).await {
Some((read_only, _hide_passwords)) => !read_only,
None => false,
}
}
pub async fn is_accessible_to_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn is_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
self.get_access_restrictions(user_uuid, None, conn).await.is_some()
}
// Returns whether this cipher is a favorite of the specified user.
pub async fn is_favorite(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn is_favorite(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
Favorite::is_favorite(&self.uuid, user_uuid, conn).await
}
// Sets whether this cipher is a favorite of the specified user.
pub async fn set_favorite(&self, favorite: Option<bool>, user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn set_favorite(&self, favorite: Option<bool>, user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
match favorite {
None => Ok(()), // No change requested.
Some(status) => Favorite::set_favorite(status, &self.uuid, user_uuid, conn).await,
}
}
pub async fn get_folder_uuid(&self, user_uuid: &str, conn: &mut DbConn) -> Option<String> {
pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &mut DbConn) -> Option<FolderId> {
db_run! {conn: {
folders_ciphers::table
.inner_join(folders::table)
.filter(folders::user_uuid.eq(&user_uuid))
.filter(folders_ciphers::cipher_uuid.eq(&self.uuid))
.select(folders_ciphers::folder_uuid)
.first::<String>(conn)
.first::<FolderId>(conn)
.ok()
}}
}
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &CipherId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
ciphers::table
.filter(ciphers::uuid.eq(uuid))
@ -712,7 +720,11 @@ impl Cipher {
}}
}
pub async fn find_by_uuid_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_org(
cipher_uuid: &CipherId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! {conn: {
ciphers::table
.filter(ciphers::uuid.eq(cipher_uuid))
@ -735,7 +747,7 @@ impl Cipher {
// true, then the non-interesting ciphers will not be returned. As a
// result, those ciphers will not appear in "My Vault" for the org
// owner/admin, but they can still be accessed via the org vault view.
pub async fn find_by_user(user_uuid: &str, visible_only: bool, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, visible_only: bool, conn: &mut DbConn) -> Vec<Self> {
if CONFIG.org_groups_enabled() {
db_run! {conn: {
let mut query = ciphers::table
@ -745,7 +757,7 @@ impl Cipher {
.left_join(users_organizations::table.on(
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
.and(users_organizations::user_uuid.eq(user_uuid))
.and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
))
.left_join(users_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
@ -772,7 +784,7 @@ impl Cipher {
if !visible_only {
query = query.or_filter(
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
);
}
@ -790,7 +802,7 @@ impl Cipher {
.left_join(users_organizations::table.on(
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
.and(users_organizations::user_uuid.eq(user_uuid))
.and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.and(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
))
.left_join(users_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
@ -804,7 +816,7 @@ impl Cipher {
if !visible_only {
query = query.or_filter(
users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin/owner
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin/owner
);
}
@ -817,12 +829,12 @@ impl Cipher {
}
// Find all ciphers visible to the specified user.
pub async fn find_by_user_visible(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user_visible(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
Self::find_by_user(user_uuid, true, conn).await
}
// Find all ciphers directly owned by the specified user.
pub async fn find_owned_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_owned_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
ciphers::table
.filter(
@ -833,7 +845,7 @@ impl Cipher {
}}
}
pub async fn count_owned_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn count_owned_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
db_run! {conn: {
ciphers::table
.filter(ciphers::user_uuid.eq(user_uuid))
@ -844,7 +856,7 @@ impl Cipher {
}}
}
pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
ciphers::table
.filter(ciphers::organization_uuid.eq(org_uuid))
@ -852,7 +864,7 @@ impl Cipher {
}}
}
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! {conn: {
ciphers::table
.filter(ciphers::organization_uuid.eq(org_uuid))
@ -863,7 +875,7 @@ impl Cipher {
}}
}
pub async fn find_by_folder(folder_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
folders_ciphers::table.inner_join(ciphers::table)
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
@ -881,7 +893,7 @@ impl Cipher {
}}
}
pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec<String> {
pub async fn get_collections(&self, user_uuid: UserId, conn: &mut DbConn) -> Vec<CollectionId> {
if CONFIG.org_groups_enabled() {
db_run! {conn: {
ciphers_collections::table
@ -891,11 +903,11 @@ impl Cipher {
))
.left_join(users_organizations::table.on(
users_organizations::org_uuid.eq(collections::org_uuid)
.and(users_organizations::user_uuid.eq(user_id.clone()))
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
))
.left_join(users_collections::table.on(
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_id.clone()))
.and(users_collections::user_uuid.eq(user_uuid.clone()))
))
.left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
@ -906,14 +918,14 @@ impl Cipher {
.and(collections_groups::groups_uuid.eq(groups::uuid))
))
.filter(users_organizations::access_all.eq(true) // User has access all
.or(users_collections::user_uuid.eq(user_id) // User has access to collection
.or(users_collections::user_uuid.eq(user_uuid) // User has access to collection
.and(users_collections::read_only.eq(false)))
.or(groups::access_all.eq(true)) // Access via groups
.or(collections_groups::collections_uuid.is_not_null() // Access via groups
.and(collections_groups::read_only.eq(false)))
)
.select(ciphers_collections::collection_uuid)
.load::<String>(conn).unwrap_or_default()
.load::<CollectionId>(conn).unwrap_or_default()
}}
} else {
db_run! {conn: {
@ -924,23 +936,23 @@ impl Cipher {
))
.inner_join(users_organizations::table.on(
users_organizations::org_uuid.eq(collections::org_uuid)
.and(users_organizations::user_uuid.eq(user_id.clone()))
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
))
.left_join(users_collections::table.on(
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_id.clone()))
.and(users_collections::user_uuid.eq(user_uuid.clone()))
))
.filter(users_organizations::access_all.eq(true) // User has access all
.or(users_collections::user_uuid.eq(user_id) // User has access to collection
.or(users_collections::user_uuid.eq(user_uuid) // User has access to collection
.and(users_collections::read_only.eq(false)))
)
.select(ciphers_collections::collection_uuid)
.load::<String>(conn).unwrap_or_default()
.load::<CollectionId>(conn).unwrap_or_default()
}}
}
}
pub async fn get_admin_collections(&self, user_id: String, conn: &mut DbConn) -> Vec<String> {
pub async fn get_admin_collections(&self, user_uuid: UserId, conn: &mut DbConn) -> Vec<CollectionId> {
if CONFIG.org_groups_enabled() {
db_run! {conn: {
ciphers_collections::table
@ -950,11 +962,11 @@ impl Cipher {
))
.left_join(users_organizations::table.on(
users_organizations::org_uuid.eq(collections::org_uuid)
.and(users_organizations::user_uuid.eq(user_id.clone()))
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
))
.left_join(users_collections::table.on(
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_id.clone()))
.and(users_collections::user_uuid.eq(user_uuid.clone()))
))
.left_join(groups_users::table.on(
groups_users::users_organizations_uuid.eq(users_organizations::uuid)
@ -965,15 +977,15 @@ impl Cipher {
.and(collections_groups::groups_uuid.eq(groups::uuid))
))
.filter(users_organizations::access_all.eq(true) // User has access all
.or(users_collections::user_uuid.eq(user_id) // User has access to collection
.or(users_collections::user_uuid.eq(user_uuid) // User has access to collection
.and(users_collections::read_only.eq(false)))
.or(groups::access_all.eq(true)) // Access via groups
.or(collections_groups::collections_uuid.is_not_null() // Access via groups
.and(collections_groups::read_only.eq(false)))
.or(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner
.or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
)
.select(ciphers_collections::collection_uuid)
.load::<String>(conn).unwrap_or_default()
.load::<CollectionId>(conn).unwrap_or_default()
}}
} else {
db_run! {conn: {
@ -984,26 +996,29 @@ impl Cipher {
))
.inner_join(users_organizations::table.on(
users_organizations::org_uuid.eq(collections::org_uuid)
.and(users_organizations::user_uuid.eq(user_id.clone()))
.and(users_organizations::user_uuid.eq(user_uuid.clone()))
))
.left_join(users_collections::table.on(
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_id.clone()))
.and(users_collections::user_uuid.eq(user_uuid.clone()))
))
.filter(users_organizations::access_all.eq(true) // User has access all
.or(users_collections::user_uuid.eq(user_id) // User has access to collection
.or(users_collections::user_uuid.eq(user_uuid) // User has access to collection
.and(users_collections::read_only.eq(false)))
.or(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner
.or(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
)
.select(ciphers_collections::collection_uuid)
.load::<String>(conn).unwrap_or_default()
.load::<CollectionId>(conn).unwrap_or_default()
}}
}
}
/// Return a Vec with (cipher_uuid, collection_uuid)
/// This is used during a full sync so we only need one query for all collections accessible.
pub async fn get_collections_with_cipher_by_user(user_id: String, conn: &mut DbConn) -> Vec<(String, String)> {
pub async fn get_collections_with_cipher_by_user(
user_uuid: UserId,
conn: &mut DbConn,
) -> Vec<(CipherId, CollectionId)> {
db_run! {conn: {
ciphers_collections::table
.inner_join(collections::table.on(
@ -1011,12 +1026,12 @@ impl Cipher {
))
.inner_join(users_organizations::table.on(
users_organizations::org_uuid.eq(collections::org_uuid).and(
users_organizations::user_uuid.eq(user_id.clone())
users_organizations::user_uuid.eq(user_uuid.clone())
)
))
.left_join(users_collections::table.on(
users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and(
users_collections::user_uuid.eq(user_id.clone())
users_collections::user_uuid.eq(user_uuid.clone())
)
))
.left_join(groups_users::table.on(
@ -1030,14 +1045,32 @@ impl Cipher {
collections_groups::groups_uuid.eq(groups::uuid)
)
))
.or_filter(users_collections::user_uuid.eq(user_id)) // User has access to collection
.or_filter(users_collections::user_uuid.eq(user_uuid)) // User has access to collection
.or_filter(users_organizations::access_all.eq(true)) // User has access all
.or_filter(users_organizations::atype.le(UserOrgType::Admin as i32)) // User is admin or owner
.or_filter(users_organizations::atype.le(MembershipType::Admin as i32)) // User is admin or owner
.or_filter(groups::access_all.eq(true)) //Access via group
.or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
.select(ciphers_collections::all_columns)
.distinct()
.load::<(String, String)>(conn).unwrap_or_default()
.load::<(CipherId, CollectionId)>(conn).unwrap_or_default()
}}
}
}
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct CipherId(String);

View File

@ -1,15 +1,20 @@
use derive_more::{AsRef, Deref, Display, From};
use serde_json::Value;
use super::{CollectionGroup, GroupUser, User, UserOrgStatus, UserOrgType, UserOrganization};
use super::{
CipherId, CollectionGroup, GroupUser, Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId,
User, UserId,
};
use crate::CONFIG;
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = collections)]
#[diesel(primary_key(uuid))]
pub struct Collection {
pub uuid: String,
pub org_uuid: String,
pub uuid: CollectionId,
pub org_uuid: OrganizationId,
pub name: String,
pub external_id: Option<String>,
}
@ -18,8 +23,8 @@ db_object! {
#[diesel(table_name = users_collections)]
#[diesel(primary_key(user_uuid, collection_uuid))]
pub struct CollectionUser {
pub user_uuid: String,
pub collection_uuid: String,
pub user_uuid: UserId,
pub collection_uuid: CollectionId,
pub read_only: bool,
pub hide_passwords: bool,
}
@ -28,16 +33,16 @@ db_object! {
#[diesel(table_name = ciphers_collections)]
#[diesel(primary_key(cipher_uuid, collection_uuid))]
pub struct CollectionCipher {
pub cipher_uuid: String,
pub collection_uuid: String,
pub cipher_uuid: CipherId,
pub collection_uuid: CollectionId,
}
}
/// Local methods
impl Collection {
pub fn new(org_uuid: String, name: String, external_id: Option<String>) -> Self {
pub fn new(org_uuid: OrganizationId, name: String, external_id: Option<String>) -> Self {
let mut new_model = Self {
uuid: crate::util::get_uuid(),
uuid: CollectionId(crate::util::get_uuid()),
org_uuid,
name,
external_id: None,
@ -74,18 +79,18 @@ impl Collection {
pub async fn to_json_details(
&self,
user_uuid: &str,
user_uuid: &UserId,
cipher_sync_data: Option<&crate::api::core::CipherSyncData>,
conn: &mut DbConn,
) -> Value {
let (read_only, hide_passwords, can_manage) = if let Some(cipher_sync_data) = cipher_sync_data {
match cipher_sync_data.user_organizations.get(&self.org_uuid) {
match cipher_sync_data.members.get(&self.org_uuid) {
// Only for Manager types Bitwarden returns true for the can_manage option
// Owners and Admins always have true
Some(uo) if uo.has_full_access() => (false, false, uo.atype >= UserOrgType::Manager),
Some(uo) => {
Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
Some(m) => {
// Only let a manager manage collections when the have full read/write access
let is_manager = uo.atype == UserOrgType::Manager;
let is_manager = m.atype == MembershipType::Manager;
if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) {
(uc.read_only, uc.hide_passwords, is_manager && !uc.read_only && !uc.hide_passwords)
} else if let Some(cg) = cipher_sync_data.user_collections_groups.get(&self.uuid) {
@ -97,10 +102,10 @@ impl Collection {
_ => (true, true, false),
}
} else {
match UserOrganization::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
Some(ou) if ou.has_full_access() => (false, false, ou.atype >= UserOrgType::Manager),
Some(ou) => {
let is_manager = ou.atype == UserOrgType::Manager;
match Membership::find_confirmed_by_user_and_org(user_uuid, &self.org_uuid, conn).await {
Some(m) if m.has_full_access() => (false, false, m.atype >= MembershipType::Manager),
Some(m) => {
let is_manager = m.atype == MembershipType::Manager;
let read_only = !self.is_writable_by_user(user_uuid, conn).await;
let hide_passwords = self.hide_passwords_for_user(user_uuid, conn).await;
(read_only, hide_passwords, is_manager && !read_only && !hide_passwords)
@ -121,13 +126,13 @@ impl Collection {
json_object
}
pub async fn can_access_collection(org_user: &UserOrganization, col_id: &str, conn: &mut DbConn) -> bool {
org_user.has_status(UserOrgStatus::Confirmed)
&& (org_user.has_full_access()
|| CollectionUser::has_access_to_collection_by_user(col_id, &org_user.user_uuid, conn).await
pub async fn can_access_collection(member: &Membership, col_id: &CollectionId, conn: &mut DbConn) -> bool {
member.has_status(MembershipStatus::Confirmed)
&& (member.has_full_access()
|| CollectionUser::has_access_to_collection_by_user(col_id, &member.user_uuid, conn).await
|| (CONFIG.org_groups_enabled()
&& (GroupUser::has_full_access_by_member(&org_user.org_uuid, &org_user.uuid, conn).await
|| GroupUser::has_access_to_collection_by_member(col_id, &org_user.uuid, conn).await)))
&& (GroupUser::has_full_access_by_member(&member.org_uuid, &member.uuid, conn).await
|| GroupUser::has_access_to_collection_by_member(col_id, &member.uuid, conn).await)))
}
}
@ -185,7 +190,7 @@ impl Collection {
}}
}
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
for collection in Self::find_by_organization(org_uuid, conn).await {
collection.delete(conn).await?;
}
@ -193,12 +198,12 @@ impl Collection {
}
pub async fn update_users_revision(&self, conn: &mut DbConn) {
for user_org in UserOrganization::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() {
User::update_uuid_revision(&user_org.user_uuid, conn).await;
for member in Membership::find_by_collection_and_org(&self.uuid, &self.org_uuid, conn).await.iter() {
User::update_uuid_revision(&member.user_uuid, conn).await;
}
}
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &CollectionId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
collections::table
.filter(collections::uuid.eq(uuid))
@ -208,7 +213,7 @@ impl Collection {
}}
}
pub async fn find_by_user_uuid(user_uuid: String, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user_uuid(user_uuid: UserId, conn: &mut DbConn) -> Vec<Self> {
if CONFIG.org_groups_enabled() {
db_run! { conn: {
collections::table
@ -234,7 +239,7 @@ impl Collection {
)
))
.filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.filter(
users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
@ -265,7 +270,7 @@ impl Collection {
)
))
.filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.filter(
users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection
@ -279,15 +284,19 @@ impl Collection {
}
}
pub async fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_organization_and_user_uuid(
org_uuid: &OrganizationId,
user_uuid: &UserId,
conn: &mut DbConn,
) -> Vec<Self> {
Self::find_by_user_uuid(user_uuid.to_owned(), conn)
.await
.into_iter()
.filter(|c| c.org_uuid == org_uuid)
.filter(|c| &c.org_uuid == org_uuid)
.collect()
}
pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
collections::table
.filter(collections::org_uuid.eq(org_uuid))
@ -297,7 +306,7 @@ impl Collection {
}}
}
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
collections::table
.filter(collections::org_uuid.eq(org_uuid))
@ -308,7 +317,11 @@ impl Collection {
}}
}
pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_org(
uuid: &CollectionId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
collections::table
.filter(collections::uuid.eq(uuid))
@ -320,7 +333,7 @@ impl Collection {
}}
}
pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: String, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_user(uuid: &CollectionId, user_uuid: UserId, conn: &mut DbConn) -> Option<Self> {
if CONFIG.org_groups_enabled() {
db_run! { conn: {
collections::table
@ -349,7 +362,7 @@ impl Collection {
.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
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
)).or(
groups::access_all.eq(true) // access_all in groups
).or( // access via groups
@ -378,7 +391,7 @@ impl Collection {
.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
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
))
).select(collections::all_columns)
.first::<CollectionDb>(conn).ok()
@ -387,7 +400,7 @@ impl Collection {
}
}
pub async fn is_writable_by_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn is_writable_by_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
let user_uuid = user_uuid.to_string();
if CONFIG.org_groups_enabled() {
db_run! { conn: {
@ -411,7 +424,7 @@ impl Collection {
collections_groups::groups_uuid.eq(groups_users::groups_uuid)
.and(collections_groups::collections_uuid.eq(collections::uuid))
))
.filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
.filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
.or(users_organizations::access_all.eq(true)) // access_all via membership
.or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection
.and(users_collections::read_only.eq(false)))
@ -436,7 +449,7 @@ impl Collection {
users_collections::collection_uuid.eq(collections::uuid)
.and(users_collections::user_uuid.eq(user_uuid))
))
.filter(users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner
.filter(users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
.or(users_organizations::access_all.eq(true)) // access_all via membership
.or(users_collections::collection_uuid.eq(&self.uuid) // write access given to collection
.and(users_collections::read_only.eq(false)))
@ -449,7 +462,7 @@ impl Collection {
}
}
pub async fn hide_passwords_for_user(&self, user_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn hide_passwords_for_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
let user_uuid = user_uuid.to_string();
db_run! { conn: {
collections::table
@ -478,7 +491,7 @@ impl Collection {
.filter(
users_collections::collection_uuid.eq(&self.uuid).and(users_collections::hide_passwords.eq(true)).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
users_organizations::atype.le(MembershipType::Admin as i32) // Org admin or owner
)).or(
groups::access_all.eq(true) // access_all in groups
).or( // access via groups
@ -498,7 +511,11 @@ impl Collection {
/// Database methods
impl CollectionUser {
pub async fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_organization_and_user_uuid(
org_uuid: &OrganizationId,
user_uuid: &UserId,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_collections::table
.filter(users_collections::user_uuid.eq(user_uuid))
@ -511,11 +528,11 @@ impl CollectionUser {
}}
}
pub async fn find_by_organization_swap_user_uuid_with_org_user_uuid(
org_uuid: &str,
pub async fn find_by_organization_swap_user_uuid_with_member_uuid(
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
) -> Vec<CollectionMembership> {
let col_users = db_run! { conn: {
users_collections::table
.inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid)))
.filter(collections::org_uuid.eq(org_uuid))
@ -524,12 +541,13 @@ impl CollectionUser {
.load::<CollectionUserDb>(conn)
.expect("Error loading users_collections")
.from_db()
}}
}};
col_users.into_iter().map(|c| c.into()).collect()
}
pub async fn save(
user_uuid: &str,
collection_uuid: &str,
user_uuid: &UserId,
collection_uuid: &CollectionId,
read_only: bool,
hide_passwords: bool,
conn: &mut DbConn,
@ -599,7 +617,7 @@ impl CollectionUser {
}}
}
pub async fn find_by_collection(collection_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_collections::table
.filter(users_collections::collection_uuid.eq(collection_uuid))
@ -610,11 +628,11 @@ impl CollectionUser {
}}
}
pub async fn find_by_collection_swap_user_uuid_with_org_user_uuid(
collection_uuid: &str,
pub async fn find_by_collection_swap_user_uuid_with_member_uuid(
collection_uuid: &CollectionId,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
) -> Vec<CollectionMembership> {
let col_users = db_run! { conn: {
users_collections::table
.filter(users_collections::collection_uuid.eq(collection_uuid))
.inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
@ -622,12 +640,13 @@ impl CollectionUser {
.load::<CollectionUserDb>(conn)
.expect("Error loading users_collections")
.from_db()
}}
}};
col_users.into_iter().map(|c| c.into()).collect()
}
pub async fn find_by_collection_and_user(
collection_uuid: &str,
user_uuid: &str,
collection_uuid: &CollectionId,
user_uuid: &UserId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
@ -641,7 +660,7 @@ impl CollectionUser {
}}
}
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_collections::table
.filter(users_collections::user_uuid.eq(user_uuid))
@ -652,7 +671,7 @@ impl CollectionUser {
}}
}
pub async fn delete_all_by_collection(collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
for collection in CollectionUser::find_by_collection(collection_uuid, conn).await.iter() {
User::update_uuid_revision(&collection.user_uuid, conn).await;
}
@ -664,7 +683,11 @@ impl CollectionUser {
}}
}
pub async fn delete_all_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user_and_org(
user_uuid: &UserId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> EmptyResult {
let collectionusers = Self::find_by_organization_and_user_uuid(org_uuid, user_uuid, conn).await;
db_run! { conn: {
@ -680,14 +703,18 @@ impl CollectionUser {
}}
}
pub async fn has_access_to_collection_by_user(col_id: &str, user_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn has_access_to_collection_by_user(
col_id: &CollectionId,
user_uuid: &UserId,
conn: &mut DbConn,
) -> bool {
Self::find_by_collection_and_user(col_id, user_uuid, conn).await.is_some()
}
}
/// Database methods
impl CollectionCipher {
pub async fn save(cipher_uuid: &str, collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn save(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
Self::update_users_revision(collection_uuid, conn).await;
db_run! { conn:
@ -717,7 +744,7 @@ impl CollectionCipher {
}
}
pub async fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
Self::update_users_revision(collection_uuid, conn).await;
db_run! { conn: {
@ -731,7 +758,7 @@ impl CollectionCipher {
}}
}
pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)))
.execute(conn)
@ -739,7 +766,7 @@ impl CollectionCipher {
}}
}
pub async fn delete_all_by_collection(collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid)))
.execute(conn)
@ -747,9 +774,60 @@ impl CollectionCipher {
}}
}
pub async fn update_users_revision(collection_uuid: &str, conn: &mut DbConn) {
pub async fn update_users_revision(collection_uuid: &CollectionId, conn: &mut DbConn) {
if let Some(collection) = Collection::find_by_uuid(collection_uuid, conn).await {
collection.update_users_revision(conn).await;
}
}
}
// Added in case we need the membership_uuid instead of the user_uuid
pub struct CollectionMembership {
pub membership_uuid: MembershipId,
pub collection_uuid: CollectionId,
pub read_only: bool,
pub hide_passwords: bool,
}
impl CollectionMembership {
pub fn to_json_details_for_user(&self, membership_type: i32) -> Value {
json!({
"id": self.membership_uuid,
"readOnly": self.read_only,
"hidePasswords": self.hide_passwords,
"manage": membership_type >= MembershipType::Admin
|| (membership_type == MembershipType::Manager
&& !self.read_only
&& !self.hide_passwords),
})
}
}
impl From<CollectionUser> for CollectionMembership {
fn from(c: CollectionUser) -> Self {
Self {
membership_uuid: c.user_uuid.to_string().into(),
collection_uuid: c.collection_uuid,
read_only: c.read_only,
hide_passwords: c.hide_passwords,
}
}
}
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct CollectionId(String);

View File

@ -1,7 +1,9 @@
use chrono::{NaiveDateTime, Utc};
use derive_more::{Display, From};
use super::UserId;
use crate::{crypto, CONFIG};
use core::fmt;
use macros::IdFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -9,11 +11,11 @@ db_object! {
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid, user_uuid))]
pub struct Device {
pub uuid: String,
pub uuid: DeviceId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: String,
pub user_uuid: UserId,
pub name: String,
pub atype: i32, // https://github.com/bitwarden/server/blob/dcc199bcce4aa2d5621f6fab80f1b49d8b143418/src/Core/Enums/DeviceType.cs
@ -28,7 +30,7 @@ db_object! {
/// Local methods
impl Device {
pub fn new(uuid: String, user_uuid: String, name: String, atype: i32) -> Self {
pub fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32) -> Self {
let now = Utc::now().naive_utc();
Self {
@ -75,12 +77,12 @@ impl Device {
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
// Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
// ---
// fn arg: orgs: Vec<super::UserOrganization>,
// fn arg: members: Vec<super::Membership>,
// ---
// let orgowner: Vec<_> = orgs.iter().filter(|o| o.atype == 0).map(|o| o.org_uuid.clone()).collect();
// let orgadmin: Vec<_> = orgs.iter().filter(|o| o.atype == 1).map(|o| o.org_uuid.clone()).collect();
// let orguser: Vec<_> = orgs.iter().filter(|o| o.atype == 2).map(|o| o.org_uuid.clone()).collect();
// let orgmanager: Vec<_> = orgs.iter().filter(|o| o.atype == 3).map(|o| o.org_uuid.clone()).collect();
// let orgowner: Vec<_> = members.iter().filter(|m| m.atype == 0).map(|o| o.org_uuid.clone()).collect();
// let orgadmin: Vec<_> = members.iter().filter(|m| m.atype == 1).map(|o| o.org_uuid.clone()).collect();
// let orguser: Vec<_> = members.iter().filter(|m| m.atype == 2).map(|o| o.org_uuid.clone()).collect();
// let orgmanager: Vec<_> = members.iter().filter(|m| m.atype == 3).map(|o| o.org_uuid.clone()).collect();
// Create the JWT claims struct, to send to the client
use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
@ -150,7 +152,7 @@ impl Device {
}
}
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid)))
.execute(conn)
@ -158,7 +160,7 @@ impl Device {
}}
}
pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_user(uuid: &DeviceId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::uuid.eq(uuid))
@ -169,7 +171,7 @@ impl Device {
}}
}
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
@ -179,7 +181,7 @@ impl Device {
}}
}
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &DeviceId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::uuid.eq(uuid))
@ -189,7 +191,7 @@ impl Device {
}}
}
pub async fn clear_push_token_by_uuid(uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn clear_push_token_by_uuid(uuid: &DeviceId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::update(devices::table)
.filter(devices::uuid.eq(uuid))
@ -208,7 +210,7 @@ impl Device {
}}
}
pub async fn find_latest_active_by_user(user_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_latest_active_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
@ -219,7 +221,7 @@ impl Device {
}}
}
pub async fn find_push_devices_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_push_devices_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
@ -230,7 +232,7 @@ impl Device {
}}
}
pub async fn check_user_has_push_device(user_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn check_user_has_push_device(user_uuid: &UserId, conn: &mut DbConn) -> bool {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
@ -243,68 +245,62 @@ impl Device {
}
}
#[derive(Display)]
pub enum DeviceType {
#[display("Android")]
Android = 0,
#[display("iOS")]
Ios = 1,
#[display("Chrome Extension")]
ChromeExtension = 2,
#[display("Firefox Extension")]
FirefoxExtension = 3,
#[display("Opera Extension")]
OperaExtension = 4,
#[display("Edge Extension")]
EdgeExtension = 5,
#[display("Windows")]
WindowsDesktop = 6,
#[display("macOS")]
MacOsDesktop = 7,
#[display("Linux")]
LinuxDesktop = 8,
#[display("Chrome")]
ChromeBrowser = 9,
#[display("Firefox")]
FirefoxBrowser = 10,
#[display("Opera")]
OperaBrowser = 11,
#[display("Edge")]
EdgeBrowser = 12,
#[display("Internet Explorer")]
IEBrowser = 13,
#[display("Unknown Browser")]
UnknownBrowser = 14,
#[display("Android")]
AndroidAmazon = 15,
#[display("UWP")]
Uwp = 16,
#[display("Safari")]
SafariBrowser = 17,
#[display("Vivaldi")]
VivaldiBrowser = 18,
#[display("Vivaldi Extension")]
VivaldiExtension = 19,
#[display("Safari Extension")]
SafariExtension = 20,
#[display("SDK")]
Sdk = 21,
#[display("Server")]
Server = 22,
#[display("Windows CLI")]
WindowsCLI = 23,
#[display("macOS CLI")]
MacOsCLI = 24,
#[display("Linux CLI")]
LinuxCLI = 25,
}
impl fmt::Display for DeviceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DeviceType::Android => write!(f, "Android"),
DeviceType::Ios => write!(f, "iOS"),
DeviceType::ChromeExtension => write!(f, "Chrome Extension"),
DeviceType::FirefoxExtension => write!(f, "Firefox Extension"),
DeviceType::OperaExtension => write!(f, "Opera Extension"),
DeviceType::EdgeExtension => write!(f, "Edge Extension"),
DeviceType::WindowsDesktop => write!(f, "Windows"),
DeviceType::MacOsDesktop => write!(f, "macOS"),
DeviceType::LinuxDesktop => write!(f, "Linux"),
DeviceType::ChromeBrowser => write!(f, "Chrome"),
DeviceType::FirefoxBrowser => write!(f, "Firefox"),
DeviceType::OperaBrowser => write!(f, "Opera"),
DeviceType::EdgeBrowser => write!(f, "Edge"),
DeviceType::IEBrowser => write!(f, "Internet Explorer"),
DeviceType::UnknownBrowser => write!(f, "Unknown Browser"),
DeviceType::AndroidAmazon => write!(f, "Android"),
DeviceType::Uwp => write!(f, "UWP"),
DeviceType::SafariBrowser => write!(f, "Safari"),
DeviceType::VivaldiBrowser => write!(f, "Vivaldi"),
DeviceType::VivaldiExtension => write!(f, "Vivaldi Extension"),
DeviceType::SafariExtension => write!(f, "Safari Extension"),
DeviceType::Sdk => write!(f, "SDK"),
DeviceType::Server => write!(f, "Server"),
DeviceType::WindowsCLI => write!(f, "Windows CLI"),
DeviceType::MacOsCLI => write!(f, "macOS CLI"),
DeviceType::LinuxCLI => write!(f, "Linux CLI"),
}
}
}
impl DeviceType {
pub fn from_i32(value: i32) -> DeviceType {
match value {
@ -338,3 +334,8 @@ impl DeviceType {
}
}
}
#[derive(
Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
)]
pub struct DeviceId(String);

View File

@ -1,9 +1,10 @@
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use serde_json::Value;
use super::{User, UserId};
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
use super::User;
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -11,9 +12,9 @@ db_object! {
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct EmergencyAccess {
pub uuid: String,
pub grantor_uuid: String,
pub grantee_uuid: Option<String>,
pub uuid: EmergencyAccessId,
pub grantor_uuid: UserId,
pub grantee_uuid: Option<UserId>,
pub email: Option<String>,
pub key_encrypted: Option<String>,
pub atype: i32, //EmergencyAccessType
@ -29,11 +30,11 @@ db_object! {
// Local methods
impl EmergencyAccess {
pub fn new(grantor_uuid: String, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self {
pub fn new(grantor_uuid: UserId, email: String, status: i32, atype: i32, wait_time_days: i32) -> Self {
let now = Utc::now().naive_utc();
Self {
uuid: crate::util::get_uuid(),
uuid: EmergencyAccessId(crate::util::get_uuid()),
grantor_uuid,
grantee_uuid: None,
email: Some(email),
@ -82,7 +83,7 @@ impl EmergencyAccess {
}
pub async fn to_json_grantee_details(&self, conn: &mut DbConn) -> Option<Value> {
let grantee_user = if let Some(grantee_uuid) = self.grantee_uuid.as_deref() {
let grantee_user = if let Some(grantee_uuid) = &self.grantee_uuid {
User::find_by_uuid(grantee_uuid, conn).await.expect("Grantee user not found.")
} else if let Some(email) = self.email.as_deref() {
match User::find_by_mail(email, conn).await {
@ -211,7 +212,7 @@ impl EmergencyAccess {
}}
}
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
for ea in Self::find_all_by_grantor_uuid(user_uuid, conn).await {
ea.delete(conn).await?;
}
@ -239,8 +240,8 @@ impl EmergencyAccess {
}
pub async fn find_by_grantor_uuid_and_grantee_uuid_or_email(
grantor_uuid: &str,
grantee_uuid: &str,
grantor_uuid: &UserId,
grantee_uuid: &UserId,
email: &str,
conn: &mut DbConn,
) -> Option<Self> {
@ -262,7 +263,11 @@ impl EmergencyAccess {
}}
}
pub async fn find_by_uuid_and_grantor_uuid(uuid: &str, grantor_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_grantor_uuid(
uuid: &EmergencyAccessId,
grantor_uuid: &UserId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::uuid.eq(uuid))
@ -272,7 +277,11 @@ impl EmergencyAccess {
}}
}
pub async fn find_by_uuid_and_grantee_uuid(uuid: &str, grantee_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_grantee_uuid(
uuid: &EmergencyAccessId,
grantee_uuid: &UserId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::uuid.eq(uuid))
@ -282,7 +291,11 @@ impl EmergencyAccess {
}}
}
pub async fn find_by_uuid_and_grantee_email(uuid: &str, grantee_email: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_grantee_email(
uuid: &EmergencyAccessId,
grantee_email: &str,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::uuid.eq(uuid))
@ -292,7 +305,7 @@ impl EmergencyAccess {
}}
}
pub async fn find_all_by_grantee_uuid(grantee_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_all_by_grantee_uuid(grantee_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::grantee_uuid.eq(grantee_uuid))
@ -319,7 +332,7 @@ impl EmergencyAccess {
}}
}
pub async fn find_all_by_grantor_uuid(grantor_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_all_by_grantor_uuid(grantor_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
emergency_access::table
.filter(emergency_access::grantor_uuid.eq(grantor_uuid))
@ -327,7 +340,12 @@ impl EmergencyAccess {
}}
}
pub async fn accept_invite(&mut self, grantee_uuid: &str, grantee_email: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn accept_invite(
&mut self,
grantee_uuid: &UserId,
grantee_email: &str,
conn: &mut DbConn,
) -> EmptyResult {
if self.email.is_none() || self.email.as_ref().unwrap() != grantee_email {
err!("User email does not match invite.");
}
@ -337,10 +355,28 @@ impl EmergencyAccess {
}
self.status = EmergencyAccessStatus::Accepted as i32;
self.grantee_uuid = Some(String::from(grantee_uuid));
self.grantee_uuid = Some(grantee_uuid.clone());
self.email = None;
self.save(conn).await
}
}
// endregion
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct EmergencyAccessId(String);

View File

@ -1,9 +1,9 @@
use crate::db::DbConn;
use chrono::{NaiveDateTime, TimeDelta, Utc};
//use derive_more::{AsRef, Deref, Display, From};
use serde_json::Value;
use crate::{api::EmptyResult, error::MapResult, CONFIG};
use chrono::{NaiveDateTime, TimeDelta, Utc};
use super::{CipherId, CollectionId, GroupId, MembershipId, OrgPolicyId, OrganizationId, UserId};
use crate::{api::EmptyResult, db::DbConn, error::MapResult, CONFIG};
// https://bitwarden.com/help/event-logs/
@ -15,20 +15,20 @@ db_object! {
#[diesel(table_name = event)]
#[diesel(primary_key(uuid))]
pub struct Event {
pub uuid: String,
pub uuid: EventId,
pub event_type: i32, // EventType
pub user_uuid: Option<String>,
pub org_uuid: Option<String>,
pub cipher_uuid: Option<String>,
pub collection_uuid: Option<String>,
pub group_uuid: Option<String>,
pub org_user_uuid: Option<String>,
pub act_user_uuid: Option<String>,
pub user_uuid: Option<UserId>,
pub org_uuid: Option<OrganizationId>,
pub cipher_uuid: Option<CipherId>,
pub collection_uuid: Option<CollectionId>,
pub group_uuid: Option<GroupId>,
pub org_user_uuid: Option<MembershipId>,
pub act_user_uuid: Option<UserId>,
// Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs
pub device_type: Option<i32>,
pub ip_address: Option<String>,
pub event_date: NaiveDateTime,
pub policy_uuid: Option<String>,
pub policy_uuid: Option<OrgPolicyId>,
pub provider_uuid: Option<String>,
pub provider_user_uuid: Option<String>,
pub provider_org_uuid: Option<String>,
@ -128,7 +128,7 @@ impl Event {
};
Self {
uuid: crate::util::get_uuid(),
uuid: EventId(crate::util::get_uuid()),
event_type,
user_uuid: None,
org_uuid: None,
@ -246,7 +246,7 @@ impl Event {
/// ##############
/// Custom Queries
pub async fn find_by_organization_uuid(
org_uuid: &str,
org_uuid: &OrganizationId,
start: &NaiveDateTime,
end: &NaiveDateTime,
conn: &mut DbConn,
@ -263,7 +263,7 @@ impl Event {
}}
}
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
event::table
.filter(event::org_uuid.eq(org_uuid))
@ -274,16 +274,16 @@ impl Event {
}}
}
pub async fn find_by_org_and_user_org(
org_uuid: &str,
user_org_uuid: &str,
pub async fn find_by_org_and_member(
org_uuid: &OrganizationId,
member_uuid: &MembershipId,
start: &NaiveDateTime,
end: &NaiveDateTime,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
event::table
.inner_join(users_organizations::table.on(users_organizations::uuid.eq(user_org_uuid)))
.inner_join(users_organizations::table.on(users_organizations::uuid.eq(member_uuid)))
.filter(event::org_uuid.eq(org_uuid))
.filter(event::event_date.between(start, end))
.filter(event::user_uuid.eq(users_organizations::user_uuid.nullable()).or(event::act_user_uuid.eq(users_organizations::user_uuid.nullable())))
@ -297,7 +297,7 @@ impl Event {
}
pub async fn find_by_cipher_uuid(
cipher_uuid: &str,
cipher_uuid: &CipherId,
start: &NaiveDateTime,
end: &NaiveDateTime,
conn: &mut DbConn,
@ -327,3 +327,6 @@ impl Event {
}
}
}
#[derive(Clone, Debug, DieselNewType, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventId(String);

View File

@ -1,12 +1,12 @@
use super::User;
use super::{CipherId, User, UserId};
db_object! {
#[derive(Identifiable, Queryable, Insertable)]
#[diesel(table_name = favorites)]
#[diesel(primary_key(user_uuid, cipher_uuid))]
pub struct Favorite {
pub user_uuid: String,
pub cipher_uuid: String,
pub user_uuid: UserId,
pub cipher_uuid: CipherId,
}
}
@ -17,7 +17,7 @@ use crate::error::MapResult;
impl Favorite {
// Returns whether the specified cipher is a favorite of the specified user.
pub async fn is_favorite(cipher_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn is_favorite(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &mut DbConn) -> bool {
db_run! { conn: {
let query = favorites::table
.filter(favorites::cipher_uuid.eq(cipher_uuid))
@ -29,7 +29,12 @@ impl Favorite {
}
// Sets whether the specified cipher is a favorite of the specified user.
pub async fn set_favorite(favorite: bool, cipher_uuid: &str, user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn set_favorite(
favorite: bool,
cipher_uuid: &CipherId,
user_uuid: &UserId,
conn: &mut DbConn,
) -> EmptyResult {
let (old, new) = (Self::is_favorite(cipher_uuid, user_uuid, conn).await, favorite);
match (old, new) {
(false, true) => {
@ -62,7 +67,7 @@ impl Favorite {
}
// Delete all favorite entries associated with the specified cipher.
pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(favorites::table.filter(favorites::cipher_uuid.eq(cipher_uuid)))
.execute(conn)
@ -71,7 +76,7 @@ impl Favorite {
}
// Delete all favorite entries associated with the specified user.
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(favorites::table.filter(favorites::user_uuid.eq(user_uuid)))
.execute(conn)
@ -81,12 +86,12 @@ impl Favorite {
/// Return a vec with (cipher_uuid) this will only contain favorite flagged ciphers
/// This is used during a full sync so we only need one query for all favorite cipher matches.
pub async fn get_all_cipher_uuid_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<CipherId> {
db_run! { conn: {
favorites::table
.filter(favorites::user_uuid.eq(user_uuid))
.select(favorites::cipher_uuid)
.load::<String>(conn)
.load::<CipherId>(conn)
.unwrap_or_default()
}}
}

View File

@ -1,17 +1,19 @@
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use serde_json::Value;
use super::User;
use super::{CipherId, User, UserId};
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = folders)]
#[diesel(primary_key(uuid))]
pub struct Folder {
pub uuid: String,
pub uuid: FolderId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: String,
pub user_uuid: UserId,
pub name: String,
}
@ -19,18 +21,18 @@ db_object! {
#[diesel(table_name = folders_ciphers)]
#[diesel(primary_key(cipher_uuid, folder_uuid))]
pub struct FolderCipher {
pub cipher_uuid: String,
pub folder_uuid: String,
pub cipher_uuid: CipherId,
pub folder_uuid: FolderId,
}
}
/// Local methods
impl Folder {
pub fn new(user_uuid: String, name: String) -> Self {
pub fn new(user_uuid: UserId, name: String) -> Self {
let now = Utc::now().naive_utc();
Self {
uuid: crate::util::get_uuid(),
uuid: FolderId(crate::util::get_uuid()),
created_at: now,
updated_at: now,
@ -52,10 +54,10 @@ impl Folder {
}
impl FolderCipher {
pub fn new(folder_uuid: &str, cipher_uuid: &str) -> Self {
pub fn new(folder_uuid: FolderId, cipher_uuid: CipherId) -> Self {
Self {
folder_uuid: folder_uuid.to_string(),
cipher_uuid: cipher_uuid.to_string(),
folder_uuid,
cipher_uuid,
}
}
}
@ -113,14 +115,14 @@ impl Folder {
}}
}
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
for folder in Self::find_by_user(user_uuid, conn).await {
folder.delete(conn).await?;
}
Ok(())
}
pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_user(uuid: &FolderId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
folders::table
.filter(folders::uuid.eq(uuid))
@ -131,7 +133,7 @@ impl Folder {
}}
}
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
folders::table
.filter(folders::user_uuid.eq(user_uuid))
@ -177,7 +179,7 @@ impl FolderCipher {
}}
}
pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)))
.execute(conn)
@ -185,7 +187,7 @@ impl FolderCipher {
}}
}
pub async fn delete_all_by_folder(folder_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid)))
.execute(conn)
@ -193,7 +195,11 @@ impl FolderCipher {
}}
}
pub async fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_folder_and_cipher(
folder_uuid: &FolderId,
cipher_uuid: &CipherId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
folders_ciphers::table
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
@ -204,7 +210,7 @@ impl FolderCipher {
}}
}
pub async fn find_by_folder(folder_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
folders_ciphers::table
.filter(folders_ciphers::folder_uuid.eq(folder_uuid))
@ -216,14 +222,32 @@ impl FolderCipher {
/// Return a vec with (cipher_uuid, folder_uuid)
/// This is used during a full sync so we only need one query for all folder matches.
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<(String, String)> {
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(CipherId, FolderId)> {
db_run! { conn: {
folders_ciphers::table
.inner_join(folders::table)
.filter(folders::user_uuid.eq(user_uuid))
.select(folders_ciphers::all_columns)
.load::<(String, String)>(conn)
.load::<(CipherId, FolderId)>(conn)
.unwrap_or_default()
}}
}
}
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct FolderId(String);

View File

@ -1,8 +1,10 @@
use super::{User, UserOrganization};
use super::{CollectionId, Membership, MembershipId, OrganizationId, User, UserId};
use crate::api::EmptyResult;
use crate::db::DbConn;
use crate::error::MapResult;
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use macros::UuidFromParam;
use serde_json::Value;
db_object! {
@ -10,8 +12,8 @@ db_object! {
#[diesel(table_name = groups)]
#[diesel(primary_key(uuid))]
pub struct Group {
pub uuid: String,
pub organizations_uuid: String,
pub uuid: GroupId,
pub organizations_uuid: OrganizationId,
pub name: String,
pub access_all: bool,
pub external_id: Option<String>,
@ -23,8 +25,8 @@ db_object! {
#[diesel(table_name = collections_groups)]
#[diesel(primary_key(collections_uuid, groups_uuid))]
pub struct CollectionGroup {
pub collections_uuid: String,
pub groups_uuid: String,
pub collections_uuid: CollectionId,
pub groups_uuid: GroupId,
pub read_only: bool,
pub hide_passwords: bool,
}
@ -33,18 +35,23 @@ db_object! {
#[diesel(table_name = groups_users)]
#[diesel(primary_key(groups_uuid, users_organizations_uuid))]
pub struct GroupUser {
pub groups_uuid: String,
pub users_organizations_uuid: String
pub groups_uuid: GroupId,
pub users_organizations_uuid: MembershipId
}
}
/// Local methods
impl Group {
pub fn new(organizations_uuid: String, name: String, access_all: bool, external_id: Option<String>) -> Self {
pub fn new(
organizations_uuid: OrganizationId,
name: String,
access_all: bool,
external_id: Option<String>,
) -> Self {
let now = Utc::now().naive_utc();
let mut new_model = Self {
uuid: crate::util::get_uuid(),
uuid: GroupId(crate::util::get_uuid()),
organizations_uuid,
name,
access_all,
@ -111,7 +118,7 @@ impl Group {
}
impl CollectionGroup {
pub fn new(collections_uuid: String, groups_uuid: String, read_only: bool, hide_passwords: bool) -> Self {
pub fn new(collections_uuid: CollectionId, groups_uuid: GroupId, read_only: bool, hide_passwords: bool) -> Self {
Self {
collections_uuid,
groups_uuid,
@ -119,10 +126,22 @@ impl CollectionGroup {
hide_passwords,
}
}
pub fn to_json_details_for_group(&self) -> Value {
// If both read_only and hide_passwords are false, then manage should be true
// You can't have an entry with read_only and manage, or hide_passwords and manage
// Or an entry with everything to false
json!({
"id": self.groups_uuid,
"readOnly": self.read_only,
"hidePasswords": self.hide_passwords,
"manage": !self.read_only && !self.hide_passwords,
})
}
}
impl GroupUser {
pub fn new(groups_uuid: String, users_organizations_uuid: String) -> Self {
pub fn new(groups_uuid: GroupId, users_organizations_uuid: MembershipId) -> Self {
Self {
groups_uuid,
users_organizations_uuid,
@ -166,27 +185,27 @@ impl Group {
}
}
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
for group in Self::find_by_organization(org_uuid, conn).await {
group.delete(conn).await?;
}
Ok(())
}
pub async fn find_by_organization(organizations_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
groups::table
.filter(groups::organizations_uuid.eq(organizations_uuid))
.filter(groups::organizations_uuid.eq(org_uuid))
.load::<GroupDb>(conn)
.expect("Error loading groups")
.from_db()
}}
}
pub async fn count_by_org(organizations_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
groups::table
.filter(groups::organizations_uuid.eq(organizations_uuid))
.filter(groups::organizations_uuid.eq(org_uuid))
.count()
.first::<i64>(conn)
.ok()
@ -194,7 +213,7 @@ impl Group {
}}
}
pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_org(uuid: &GroupId, org_uuid: &OrganizationId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
groups::table
.filter(groups::uuid.eq(uuid))
@ -205,7 +224,11 @@ impl Group {
}}
}
pub async fn find_by_external_id_and_org(external_id: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_external_id_and_org(
external_id: &str,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
groups::table
.filter(groups::external_id.eq(external_id))
@ -216,7 +239,7 @@ impl Group {
}}
}
//Returns all organizations the user has full access to
pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
pub async fn get_orgs_by_user_with_full_access(user_uuid: &UserId, conn: &mut DbConn) -> Vec<OrganizationId> {
db_run! { conn: {
groups_users::table
.inner_join(users_organizations::table.on(
@ -229,12 +252,12 @@ impl Group {
.filter(groups::access_all.eq(true))
.select(groups::organizations_uuid)
.distinct()
.load::<String>(conn)
.load::<OrganizationId>(conn)
.expect("Error loading organization group full access information for user")
}}
}
pub async fn is_in_full_access_group(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn is_in_full_access_group(user_uuid: &UserId, org_uuid: &OrganizationId, conn: &mut DbConn) -> bool {
db_run! { conn: {
groups::table
.inner_join(groups_users::table.on(
@ -263,13 +286,13 @@ impl Group {
}}
}
pub async fn update_revision(uuid: &str, conn: &mut DbConn) {
pub async fn update_revision(uuid: &GroupId, conn: &mut DbConn) {
if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
warn!("Failed to update revision for {}: {:#?}", uuid, e);
}
}
async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
async fn _update_revision(uuid: &GroupId, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
db_run! {conn: {
crate::util::retry(|| {
diesel::update(groups::table.filter(groups::uuid.eq(uuid)))
@ -337,7 +360,7 @@ impl CollectionGroup {
}
}
pub async fn find_by_group(group_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
collections_groups::table
.filter(collections_groups::groups_uuid.eq(group_uuid))
@ -347,7 +370,7 @@ impl CollectionGroup {
}}
}
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
collections_groups::table
.inner_join(groups_users::table.on(
@ -364,7 +387,7 @@ impl CollectionGroup {
}}
}
pub async fn find_by_collection(collection_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
collections_groups::table
.filter(collections_groups::collections_uuid.eq(collection_uuid))
@ -390,7 +413,7 @@ impl CollectionGroup {
}}
}
pub async fn delete_all_by_group(group_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(group_uuid, conn).await;
for group_user in group_users {
group_user.update_user_revision(conn).await;
@ -404,7 +427,7 @@ impl CollectionGroup {
}}
}
pub async fn delete_all_by_collection(collection_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_collection(collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult {
let collection_assigned_to_groups = CollectionGroup::find_by_collection(collection_uuid, conn).await;
for collection_assigned_to_group in collection_assigned_to_groups {
let group_users = GroupUser::find_by_group(&collection_assigned_to_group.groups_uuid, conn).await;
@ -469,7 +492,7 @@ impl GroupUser {
}
}
pub async fn find_by_group(group_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
groups_users::table
.filter(groups_users::groups_uuid.eq(group_uuid))
@ -479,10 +502,10 @@ impl GroupUser {
}}
}
pub async fn find_by_user(users_organizations_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_member(member_uuid: &MembershipId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
groups_users::table
.filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
.load::<GroupUserDb>(conn)
.expect("Error loading groups for user")
.from_db()
@ -490,8 +513,8 @@ impl GroupUser {
}
pub async fn has_access_to_collection_by_member(
collection_uuid: &str,
member_uuid: &str,
collection_uuid: &CollectionId,
member_uuid: &MembershipId,
conn: &mut DbConn,
) -> bool {
db_run! { conn: {
@ -507,7 +530,11 @@ impl GroupUser {
}}
}
pub async fn has_full_access_by_member(org_uuid: &str, member_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn has_full_access_by_member(
org_uuid: &OrganizationId,
member_uuid: &MembershipId,
conn: &mut DbConn,
) -> bool {
db_run! { conn: {
groups_users::table
.inner_join(groups::table.on(
@ -523,32 +550,32 @@ impl GroupUser {
}
pub async fn update_user_revision(&self, conn: &mut DbConn) {
match UserOrganization::find_by_uuid(&self.users_organizations_uuid, conn).await {
Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
None => warn!("User could not be found!"),
match Membership::find_by_uuid(&self.users_organizations_uuid, conn).await {
Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
None => warn!("Member could not be found!"),
}
}
pub async fn delete_by_group_id_and_user_id(
group_uuid: &str,
users_organizations_uuid: &str,
pub async fn delete_by_group_and_member(
group_uuid: &GroupId,
member_uuid: &MembershipId,
conn: &mut DbConn,
) -> EmptyResult {
match UserOrganization::find_by_uuid(users_organizations_uuid, conn).await {
Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
None => warn!("User could not be found!"),
match Membership::find_by_uuid(member_uuid, conn).await {
Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
None => warn!("Member could not be found!"),
};
db_run! { conn: {
diesel::delete(groups_users::table)
.filter(groups_users::groups_uuid.eq(group_uuid))
.filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
.execute(conn)
.map_res("Error deleting group users")
}}
}
pub async fn delete_all_by_group(group_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_group(group_uuid: &GroupId, conn: &mut DbConn) -> EmptyResult {
let group_users = GroupUser::find_by_group(group_uuid, conn).await;
for group_user in group_users {
group_user.update_user_revision(conn).await;
@ -562,17 +589,35 @@ impl GroupUser {
}}
}
pub async fn delete_all_by_user(users_organizations_uuid: &str, conn: &mut DbConn) -> EmptyResult {
match UserOrganization::find_by_uuid(users_organizations_uuid, conn).await {
Some(user) => User::update_uuid_revision(&user.user_uuid, conn).await,
None => warn!("User could not be found!"),
pub async fn delete_all_by_member(member_uuid: &MembershipId, conn: &mut DbConn) -> EmptyResult {
match Membership::find_by_uuid(member_uuid, conn).await {
Some(member) => User::update_uuid_revision(&member.user_uuid, conn).await,
None => warn!("Member could not be found!"),
}
db_run! { conn: {
diesel::delete(groups_users::table)
.filter(groups_users::users_organizations_uuid.eq(users_organizations_uuid))
.filter(groups_users::users_organizations_uuid.eq(member_uuid))
.execute(conn)
.map_res("Error deleting user groups")
}}
}
}
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct GroupId(String);

View File

@ -16,20 +16,26 @@ mod two_factor_duo_context;
mod two_factor_incomplete;
mod user;
pub use self::attachment::Attachment;
pub use self::auth_request::AuthRequest;
pub use self::cipher::{Cipher, RepromptType};
pub use self::collection::{Collection, CollectionCipher, CollectionUser};
pub use self::device::{Device, DeviceType};
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType};
pub use self::attachment::{Attachment, AttachmentId};
pub use self::auth_request::{AuthRequest, AuthRequestId};
pub use self::cipher::{Cipher, CipherId, RepromptType};
pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};
pub use self::device::{Device, DeviceId, DeviceType};
pub use self::emergency_access::{EmergencyAccess, EmergencyAccessId, EmergencyAccessStatus, EmergencyAccessType};
pub use self::event::{Event, EventType};
pub use self::favorite::Favorite;
pub use self::folder::{Folder, FolderCipher};
pub use self::group::{CollectionGroup, Group, GroupUser};
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
pub use self::send::{Send, SendType};
pub use self::folder::{Folder, FolderCipher, FolderId};
pub use self::group::{CollectionGroup, Group, GroupId, GroupUser};
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyId, OrgPolicyType};
pub use self::organization::{
Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey,
OrganizationId,
};
pub use self::send::{
id::{SendFileId, SendId},
Send, SendType,
};
pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::two_factor_duo_context::TwoFactorDuoContext;
pub use self::two_factor_incomplete::TwoFactorIncomplete;
pub use self::user::{Invitation, User, UserKdfType, UserStampException};
pub use self::user::{Invitation, User, UserId, UserKdfType, UserStampException};

View File

@ -1,3 +1,4 @@
use derive_more::{AsRef, From};
use serde::Deserialize;
use serde_json::Value;
@ -5,15 +6,15 @@ use crate::api::EmptyResult;
use crate::db::DbConn;
use crate::error::MapResult;
use super::{TwoFactor, UserOrgStatus, UserOrgType, UserOrganization};
use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId};
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = org_policies)]
#[diesel(primary_key(uuid))]
pub struct OrgPolicy {
pub uuid: String,
pub org_uuid: String,
pub uuid: OrgPolicyId,
pub org_uuid: OrganizationId,
pub atype: i32,
pub enabled: bool,
pub data: String,
@ -62,9 +63,9 @@ pub enum OrgPolicyErr {
/// Local methods
impl OrgPolicy {
pub fn new(org_uuid: String, atype: OrgPolicyType, data: String) -> Self {
pub fn new(org_uuid: OrganizationId, atype: OrgPolicyType, data: String) -> Self {
Self {
uuid: crate::util::get_uuid(),
uuid: OrgPolicyId(crate::util::get_uuid()),
org_uuid,
atype: atype as i32,
enabled: false,
@ -142,7 +143,7 @@ impl OrgPolicy {
}}
}
pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
org_policies::table
.filter(org_policies::org_uuid.eq(org_uuid))
@ -152,7 +153,7 @@ impl OrgPolicy {
}}
}
pub async fn find_confirmed_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
org_policies::table
.inner_join(
@ -161,7 +162,7 @@ impl OrgPolicy {
.and(users_organizations::user_uuid.eq(user_uuid)))
)
.filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.select(org_policies::all_columns)
.load::<OrgPolicyDb>(conn)
@ -170,7 +171,11 @@ impl OrgPolicy {
}}
}
pub async fn find_by_org_and_type(org_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_org_and_type(
org_uuid: &OrganizationId,
policy_type: OrgPolicyType,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
org_policies::table
.filter(org_policies::org_uuid.eq(org_uuid))
@ -181,7 +186,7 @@ impl OrgPolicy {
}}
}
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid)))
.execute(conn)
@ -190,7 +195,7 @@ impl OrgPolicy {
}
pub async fn find_accepted_and_confirmed_by_user_and_active_policy(
user_uuid: &str,
user_uuid: &UserId,
policy_type: OrgPolicyType,
conn: &mut DbConn,
) -> Vec<Self> {
@ -202,10 +207,10 @@ impl OrgPolicy {
.and(users_organizations::user_uuid.eq(user_uuid)))
)
.filter(
users_organizations::status.eq(UserOrgStatus::Accepted as i32)
users_organizations::status.eq(MembershipStatus::Accepted as i32)
)
.or_filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.filter(org_policies::atype.eq(policy_type as i32))
.filter(org_policies::enabled.eq(true))
@ -217,7 +222,7 @@ impl OrgPolicy {
}
pub async fn find_confirmed_by_user_and_active_policy(
user_uuid: &str,
user_uuid: &UserId,
policy_type: OrgPolicyType,
conn: &mut DbConn,
) -> Vec<Self> {
@ -229,7 +234,7 @@ impl OrgPolicy {
.and(users_organizations::user_uuid.eq(user_uuid)))
)
.filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.filter(org_policies::atype.eq(policy_type as i32))
.filter(org_policies::enabled.eq(true))
@ -244,21 +249,21 @@ impl OrgPolicy {
/// and the user is not an owner or admin of that org. This is only useful for checking
/// applicability of policy types that have these particular semantics.
pub async fn is_applicable_to_user(
user_uuid: &str,
user_uuid: &UserId,
policy_type: OrgPolicyType,
exclude_org_uuid: Option<&str>,
exclude_org_uuid: Option<&OrganizationId>,
conn: &mut DbConn,
) -> bool {
for policy in
OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(user_uuid, policy_type, conn).await
{
// Check if we need to skip this organization.
if exclude_org_uuid.is_some() && exclude_org_uuid.unwrap() == policy.org_uuid {
if exclude_org_uuid.is_some() && *exclude_org_uuid.unwrap() == policy.org_uuid {
continue;
}
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
if user.atype < UserOrgType::Admin {
if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
if user.atype < MembershipType::Admin {
return true;
}
}
@ -267,8 +272,8 @@ impl OrgPolicy {
}
pub async fn is_user_allowed(
user_uuid: &str,
org_uuid: &str,
user_uuid: &UserId,
org_uuid: &OrganizationId,
exclude_current_org: bool,
conn: &mut DbConn,
) -> OrgPolicyResult {
@ -296,7 +301,7 @@ impl OrgPolicy {
Ok(())
}
pub async fn org_is_reset_password_auto_enroll(org_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &mut DbConn) -> bool {
match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await {
Some(policy) => match serde_json::from_str::<ResetPasswordDataModel>(&policy.data) {
Ok(opts) => {
@ -312,12 +317,12 @@ impl OrgPolicy {
/// Returns true if the user belongs to an org that has enabled the `DisableHideEmail`
/// option of the `Send Options` policy, and the user is not an owner or admin of that org.
pub async fn is_hide_email_disabled(user_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn is_hide_email_disabled(user_uuid: &UserId, conn: &mut DbConn) -> bool {
for policy in
OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await
{
if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
if user.atype < UserOrgType::Admin {
if let Some(user) = Membership::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await {
if user.atype < MembershipType::Admin {
match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {
Ok(opts) => {
if opts.disable_hide_email {
@ -332,12 +337,19 @@ impl OrgPolicy {
false
}
pub async fn is_enabled_for_member(org_user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> bool {
if let Some(membership) = UserOrganization::find_by_uuid(org_user_uuid, conn).await {
if let Some(policy) = OrgPolicy::find_by_org_and_type(&membership.org_uuid, policy_type, conn).await {
pub async fn is_enabled_for_member(
member_uuid: &MembershipId,
policy_type: OrgPolicyType,
conn: &mut DbConn,
) -> bool {
if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await {
if let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await {
return policy.enabled;
}
}
false
}
}
#[derive(Clone, Debug, AsRef, DieselNewType, From, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct OrgPolicyId(String);

View File

@ -1,4 +1,5 @@
use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, Deref, Display, From};
use num_traits::FromPrimitive;
use serde_json::Value;
use std::{
@ -6,16 +7,19 @@ use std::{
collections::{HashMap, HashSet},
};
use super::{CollectionUser, Group, GroupUser, OrgPolicy, OrgPolicyType, TwoFactor, User};
use crate::db::models::{Collection, CollectionGroup};
use super::{
CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy,
OrgPolicyType, TwoFactor, User, UserId,
};
use crate::CONFIG;
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = organizations)]
#[diesel(primary_key(uuid))]
pub struct Organization {
pub uuid: String,
pub uuid: OrganizationId,
pub name: String,
pub billing_email: String,
pub private_key: Option<String>,
@ -25,10 +29,10 @@ db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = users_organizations)]
#[diesel(primary_key(uuid))]
pub struct UserOrganization {
pub uuid: String,
pub user_uuid: String,
pub org_uuid: String,
pub struct Membership {
pub uuid: MembershipId,
pub user_uuid: UserId,
pub org_uuid: OrganizationId,
pub access_all: bool,
pub akey: String,
@ -42,8 +46,8 @@ db_object! {
#[diesel(table_name = organization_api_key)]
#[diesel(primary_key(uuid, org_uuid))]
pub struct OrganizationApiKey {
pub uuid: String,
pub org_uuid: String,
pub uuid: OrgApiKeyId,
pub org_uuid: OrganizationId,
pub atype: i32,
pub api_key: String,
pub revision_date: NaiveDateTime,
@ -51,7 +55,7 @@ db_object! {
}
// https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
pub enum UserOrgStatus {
pub enum MembershipStatus {
Revoked = -1,
Invited = 0,
Accepted = 1,
@ -59,29 +63,29 @@ pub enum UserOrgStatus {
}
#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)]
pub enum UserOrgType {
pub enum MembershipType {
Owner = 0,
Admin = 1,
User = 2,
Manager = 3,
}
impl UserOrgType {
impl MembershipType {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"0" | "Owner" => Some(UserOrgType::Owner),
"1" | "Admin" => Some(UserOrgType::Admin),
"2" | "User" => Some(UserOrgType::User),
"3" | "Manager" => Some(UserOrgType::Manager),
"0" | "Owner" => Some(MembershipType::Owner),
"1" | "Admin" => Some(MembershipType::Admin),
"2" | "User" => Some(MembershipType::User),
"3" | "Manager" => Some(MembershipType::Manager),
// HACK: We convert the custom role to a manager role
"4" | "Custom" => Some(UserOrgType::Manager),
"4" | "Custom" => Some(MembershipType::Manager),
_ => None,
}
}
}
impl Ord for UserOrgType {
fn cmp(&self, other: &UserOrgType) -> Ordering {
impl Ord for MembershipType {
fn cmp(&self, other: &MembershipType) -> Ordering {
// For easy comparison, map each variant to an access level (where 0 is lowest).
static ACCESS_LEVEL: [i32; 4] = [
3, // Owner
@ -93,19 +97,19 @@ impl Ord for UserOrgType {
}
}
impl PartialOrd for UserOrgType {
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
impl PartialOrd for MembershipType {
fn partial_cmp(&self, other: &MembershipType) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq<i32> for UserOrgType {
impl PartialEq<i32> for MembershipType {
fn eq(&self, other: &i32) -> bool {
*other == *self as i32
}
}
impl PartialOrd<i32> for UserOrgType {
impl PartialOrd<i32> for MembershipType {
fn partial_cmp(&self, other: &i32) -> Option<Ordering> {
if let Some(other) = Self::from_i32(*other) {
return Some(self.cmp(&other));
@ -122,25 +126,25 @@ impl PartialOrd<i32> for UserOrgType {
}
}
impl PartialEq<UserOrgType> for i32 {
fn eq(&self, other: &UserOrgType) -> bool {
impl PartialEq<MembershipType> for i32 {
fn eq(&self, other: &MembershipType) -> bool {
*self == *other as i32
}
}
impl PartialOrd<UserOrgType> for i32 {
fn partial_cmp(&self, other: &UserOrgType) -> Option<Ordering> {
if let Some(self_type) = UserOrgType::from_i32(*self) {
impl PartialOrd<MembershipType> for i32 {
fn partial_cmp(&self, other: &MembershipType) -> Option<Ordering> {
if let Some(self_type) = MembershipType::from_i32(*self) {
return Some(self_type.cmp(other));
}
None
}
fn lt(&self, other: &UserOrgType) -> bool {
fn lt(&self, other: &MembershipType) -> bool {
matches!(self.partial_cmp(other), Some(Ordering::Less) | None)
}
fn le(&self, other: &UserOrgType) -> bool {
fn le(&self, other: &MembershipType) -> bool {
matches!(self.partial_cmp(other), Some(Ordering::Less | Ordering::Equal) | None)
}
}
@ -149,7 +153,7 @@ impl PartialOrd<UserOrgType> for i32 {
impl Organization {
pub fn new(name: String, billing_email: String, private_key: Option<String>, public_key: Option<String>) -> Self {
Self {
uuid: crate::util::get_uuid(),
uuid: OrganizationId(crate::util::get_uuid()),
name,
billing_email,
private_key,
@ -214,25 +218,25 @@ impl Organization {
// It should also provide enough room for 100+ types, which i doubt will ever happen.
static ACTIVATE_REVOKE_DIFF: i32 = 128;
impl UserOrganization {
pub fn new(user_uuid: String, org_uuid: String) -> Self {
impl Membership {
pub fn new(user_uuid: UserId, org_uuid: OrganizationId) -> Self {
Self {
uuid: crate::util::get_uuid(),
uuid: MembershipId(crate::util::get_uuid()),
user_uuid,
org_uuid,
access_all: false,
akey: String::new(),
status: UserOrgStatus::Accepted as i32,
atype: UserOrgType::User as i32,
status: MembershipStatus::Accepted as i32,
atype: MembershipType::User as i32,
reset_password_key: None,
external_id: None,
}
}
pub fn restore(&mut self) -> bool {
if self.status < UserOrgStatus::Invited as i32 {
if self.status < MembershipStatus::Invited as i32 {
self.status += ACTIVATE_REVOKE_DIFF;
return true;
}
@ -240,7 +244,7 @@ impl UserOrganization {
}
pub fn revoke(&mut self) -> bool {
if self.status > UserOrgStatus::Revoked as i32 {
if self.status > MembershipStatus::Revoked as i32 {
self.status -= ACTIVATE_REVOKE_DIFF;
return true;
}
@ -249,7 +253,7 @@ impl UserOrganization {
/// Return the status of the user in an unrevoked state
pub fn get_unrevoked_status(&self) -> i32 {
if self.status <= UserOrgStatus::Revoked as i32 {
if self.status <= MembershipStatus::Revoked as i32 {
return self.status + ACTIVATE_REVOKE_DIFF;
}
self.status
@ -279,9 +283,9 @@ impl UserOrganization {
}
impl OrganizationApiKey {
pub fn new(org_uuid: String, api_key: String) -> Self {
pub fn new(org_uuid: OrganizationId, api_key: String) -> Self {
Self {
uuid: crate::util::get_uuid(),
uuid: OrgApiKeyId(crate::util::get_uuid()),
org_uuid,
atype: 0, // Type 0 is the default and only type we support currently
@ -307,8 +311,8 @@ impl Organization {
err!(format!("BillingEmail {} is not a valid email address", self.billing_email.trim()))
}
for user_org in UserOrganization::find_by_org(&self.uuid, conn).await.iter() {
User::update_uuid_revision(&user_org.user_uuid, conn).await;
for member in Membership::find_by_org(&self.uuid, conn).await.iter() {
User::update_uuid_revision(&member.user_uuid, conn).await;
}
db_run! { conn:
@ -348,7 +352,7 @@ impl Organization {
Cipher::delete_all_by_organization(&self.uuid, conn).await?;
Collection::delete_all_by_organization(&self.uuid, conn).await?;
UserOrganization::delete_all_by_organization(&self.uuid, conn).await?;
Membership::delete_all_by_organization(&self.uuid, conn).await?;
OrgPolicy::delete_all_by_organization(&self.uuid, conn).await?;
Group::delete_all_by_organization(&self.uuid, conn).await?;
OrganizationApiKey::delete_all_by_organization(&self.uuid, conn).await?;
@ -360,7 +364,7 @@ impl Organization {
}}
}
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &OrganizationId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
organizations::table
.filter(organizations::uuid.eq(uuid))
@ -376,13 +380,13 @@ impl Organization {
}
}
impl UserOrganization {
impl Membership {
pub async fn to_json(&self, conn: &mut DbConn) -> Value {
let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap();
// HACK: Convert the manager type to a custom type
// It will be converted back on other locations
let user_org_type = self.type_manager_as_custom();
let membership_type = self.type_manager_as_custom();
let permissions = json!({
// TODO: Add full support for Custom User Roles
@ -392,9 +396,9 @@ impl UserOrganization {
"accessImportExport": false,
"accessReports": false,
// If the following 3 Collection roles are set to true a custom user has access all permission
"createNewCollections": user_org_type == 4 && self.access_all,
"editAnyCollection": user_org_type == 4 && self.access_all,
"deleteAnyCollection": user_org_type == 4 && self.access_all,
"createNewCollections": membership_type == 4 && self.access_all,
"editAnyCollection": membership_type == 4 && self.access_all,
"deleteAnyCollection": membership_type == 4 && self.access_all,
"manageGroups": false,
"managePolicies": false,
"manageSso": false, // Not supported
@ -459,7 +463,7 @@ impl UserOrganization {
"userId": self.user_uuid,
"key": self.akey,
"status": self.status,
"type": user_org_type,
"type": membership_type,
"enabled": true,
"object": "profileOrganization",
@ -476,16 +480,16 @@ impl UserOrganization {
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
// We subtract/add a number so we can restore/activate the user to it's previous state again.
let status = if self.status < UserOrgStatus::Revoked as i32 {
UserOrgStatus::Revoked as i32
let status = if self.status < MembershipStatus::Revoked as i32 {
MembershipStatus::Revoked as i32
} else {
self.status
};
let twofactor_enabled = !TwoFactor::find_by_user(&user.uuid, conn).await.is_empty();
let groups: Vec<String> = if include_groups && CONFIG.org_groups_enabled() {
GroupUser::find_by_user(&self.uuid, conn).await.iter().map(|gu| gu.groups_uuid.clone()).collect()
let groups: Vec<GroupId> = if include_groups && CONFIG.org_groups_enabled() {
GroupUser::find_by_member(&self.uuid, conn).await.iter().map(|gu| gu.groups_uuid.clone()).collect()
} else {
// The Bitwarden clients seem to call this API regardless of whether groups are enabled,
// so just act as if there are no groups.
@ -500,7 +504,7 @@ impl UserOrganization {
// If collections are to be included, only include them if the user does not have full access via a group or defined to the user it self
let collections: Vec<Value> = if include_collections && !(full_access_group || self.has_full_access()) {
// Get all collections for the user here already to prevent more queries
let cu: HashMap<String, CollectionUser> =
let cu: HashMap<CollectionId, CollectionUser> =
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
.await
.into_iter()
@ -508,7 +512,7 @@ impl UserOrganization {
.collect();
// Get all collection groups for this user to prevent there inclusion
let cg: HashSet<String> = CollectionGroup::find_by_user(&self.user_uuid, conn)
let cg: HashSet<CollectionId> = CollectionGroup::find_by_user(&self.user_uuid, conn)
.await
.into_iter()
.map(|cg| cg.collections_uuid)
@ -519,12 +523,12 @@ impl UserOrganization {
.into_iter()
.filter_map(|c| {
let (read_only, hide_passwords, can_manage) = if self.has_full_access() {
(false, false, self.atype >= UserOrgType::Manager)
(false, false, self.atype >= MembershipType::Manager)
} else if let Some(cu) = cu.get(&c.uuid) {
(
cu.read_only,
cu.hide_passwords,
self.atype == UserOrgType::Manager && !cu.read_only && !cu.hide_passwords,
self.atype == MembershipType::Manager && !cu.read_only && !cu.hide_passwords,
)
// If previous checks failed it might be that this user has access via a group, but we should not return those elements here
// Those are returned via a special group endpoint
@ -548,11 +552,11 @@ impl UserOrganization {
// HACK: Convert the manager type to a custom type
// It will be converted back on other locations
let user_org_type = self.type_manager_as_custom();
let membership_type = self.type_manager_as_custom();
// HACK: Only return permissions if the user is of type custom and has access_all
// Else Bitwarden will assume the defaults of all false
let permissions = if user_org_type == 4 && self.access_all {
let permissions = if membership_type == 4 && self.access_all {
json!({
// TODO: Add full support for Custom User Roles
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
@ -578,7 +582,7 @@ impl UserOrganization {
json!({
"id": self.uuid,
"userId": self.user_uuid,
"name": if self.get_unrevoked_status() >= UserOrgStatus::Accepted as i32 { Some(user.name) } else { None },
"name": if self.get_unrevoked_status() >= MembershipStatus::Accepted as i32 { Some(user.name) } else { None },
"email": user.email,
"externalId": self.external_id,
"avatarColor": user.avatar_color,
@ -586,7 +590,7 @@ impl UserOrganization {
"collections": collections,
"status": status,
"type": user_org_type,
"type": membership_type,
"accessAll": self.access_all,
"twoFactorEnabled": twofactor_enabled,
"resetPasswordEnrolled": self.reset_password_key.is_some(),
@ -630,8 +634,8 @@ impl UserOrganization {
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
// We subtract/add a number so we can restore/activate the user to it's previous state again.
let status = if self.status < UserOrgStatus::Revoked as i32 {
UserOrgStatus::Revoked as i32
let status = if self.status < MembershipStatus::Revoked as i32 {
MembershipStatus::Revoked as i32
} else {
self.status
};
@ -654,8 +658,8 @@ impl UserOrganization {
// Because Bitwarden wants the status to be -1 for revoked users we need to catch that here.
// We subtract/add a number so we can restore/activate the user to it's previous state again.
let status = if self.status < UserOrgStatus::Revoked as i32 {
UserOrgStatus::Revoked as i32
let status = if self.status < MembershipStatus::Revoked as i32 {
MembershipStatus::Revoked as i32
} else {
self.status
};
@ -677,7 +681,7 @@ impl UserOrganization {
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(users_organizations::table)
.values(UserOrganizationDb::to_db(self))
.values(MembershipDb::to_db(self))
.execute(conn)
{
Ok(_) => Ok(()),
@ -685,7 +689,7 @@ impl UserOrganization {
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(users_organizations::table)
.filter(users_organizations::uuid.eq(&self.uuid))
.set(UserOrganizationDb::to_db(self))
.set(MembershipDb::to_db(self))
.execute(conn)
.map_res("Error adding user to organization")
},
@ -693,7 +697,7 @@ impl UserOrganization {
}.map_res("Error adding user to organization")
}
postgresql {
let value = UserOrganizationDb::to_db(self);
let value = MembershipDb::to_db(self);
diesel::insert_into(users_organizations::table)
.values(&value)
.on_conflict(users_organizations::uuid)
@ -709,7 +713,7 @@ impl UserOrganization {
User::update_uuid_revision(&self.user_uuid, conn).await;
CollectionUser::delete_all_by_user_and_org(&self.user_uuid, &self.org_uuid, conn).await?;
GroupUser::delete_all_by_user(&self.uuid, conn).await?;
GroupUser::delete_all_by_member(&self.uuid, conn).await?;
db_run! { conn: {
diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid)))
@ -718,121 +722,129 @@ impl UserOrganization {
}}
}
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
for user_org in Self::find_by_org(org_uuid, conn).await {
user_org.delete(conn).await?;
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
for member in Self::find_by_org(org_uuid, conn).await {
member.delete(conn).await?;
}
Ok(())
}
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
for user_org in Self::find_any_state_by_user(user_uuid, conn).await {
user_org.delete(conn).await?;
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
for member in Self::find_any_state_by_user(user_uuid, conn).await {
member.delete(conn).await?;
}
Ok(())
}
pub async fn find_by_email_and_org(email: &str, org_id: &str, conn: &mut DbConn) -> Option<UserOrganization> {
pub async fn find_by_email_and_org(
email: &str,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Membership> {
if let Some(user) = User::find_by_mail(email, conn).await {
if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, org_id, conn).await {
return Some(user_org);
if let Some(member) = Membership::find_by_user_and_org(&user.uuid, org_uuid, conn).await {
return Some(member);
}
}
None
}
pub fn has_status(&self, status: UserOrgStatus) -> bool {
pub fn has_status(&self, status: MembershipStatus) -> bool {
self.status == status as i32
}
pub fn has_type(&self, user_type: UserOrgType) -> bool {
pub fn has_type(&self, user_type: MembershipType) -> bool {
self.atype == user_type as i32
}
pub fn has_full_access(&self) -> bool {
(self.access_all || self.atype >= UserOrgType::Admin) && self.has_status(UserOrgStatus::Confirmed)
(self.access_all || self.atype >= MembershipType::Admin) && self.has_status(MembershipStatus::Confirmed)
}
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &MembershipId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::uuid.eq(uuid))
.first::<UserOrganizationDb>(conn)
.first::<MembershipDb>(conn)
.ok().from_db()
}}
}
pub async fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_org(
uuid: &MembershipId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::uuid.eq(uuid))
.filter(users_organizations::org_uuid.eq(org_uuid))
.first::<UserOrganizationDb>(conn)
.first::<MembershipDb>(conn)
.ok().from_db()
}}
}
pub async fn find_confirmed_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.load::<UserOrganizationDb>(conn)
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
}}
}
pub async fn find_invited_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_invited_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Invited as i32))
.load::<UserOrganizationDb>(conn)
.filter(users_organizations::status.eq(MembershipStatus::Invited as i32))
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
}}
}
pub async fn find_any_state_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_any_state_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.load::<UserOrganizationDb>(conn)
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
}}
}
pub async fn count_accepted_and_confirmed_by_user(user_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn count_accepted_and_confirmed_by_user(user_uuid: &UserId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Accepted as i32).or(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)))
.filter(users_organizations::status.eq(MembershipStatus::Accepted as i32).or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)))
.count()
.first::<i64>(conn)
.unwrap_or(0)
}}
}
pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.load::<UserOrganizationDb>(conn)
.load::<MembershipDb>(conn)
.expect("Error loading user organizations").from_db()
}}
}
pub async fn find_confirmed_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_confirmed_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.load::<UserOrganizationDb>(conn)
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
}}
}
pub async fn count_by_org(org_uuid: &str, conn: &mut DbConn) -> i64 {
pub async fn count_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> i64 {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
@ -843,71 +855,91 @@ impl UserOrganization {
}}
}
pub async fn find_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_org_and_type(
org_uuid: &OrganizationId,
atype: MembershipType,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::atype.eq(atype as i32))
.load::<UserOrganizationDb>(conn)
.load::<MembershipDb>(conn)
.expect("Error loading user organizations").from_db()
}}
}
pub async fn count_confirmed_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &mut DbConn) -> i64 {
pub async fn count_confirmed_by_org_and_type(
org_uuid: &OrganizationId,
atype: MembershipType,
conn: &mut DbConn,
) -> i64 {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(users_organizations::atype.eq(atype as i32))
.filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.count()
.first::<i64>(conn)
.unwrap_or(0)
}}
}
pub async fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_user_and_org(
user_uuid: &UserId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::org_uuid.eq(org_uuid))
.first::<UserOrganizationDb>(conn)
.first::<MembershipDb>(conn)
.ok().from_db()
}}
}
pub async fn find_confirmed_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_confirmed_by_user_and_org(
user_uuid: &UserId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::org_uuid.eq(org_uuid))
.filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.first::<UserOrganizationDb>(conn)
.first::<MembershipDb>(conn)
.ok().from_db()
}}
}
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.load::<UserOrganizationDb>(conn)
.load::<MembershipDb>(conn)
.expect("Error loading user organizations").from_db()
}}
}
pub async fn get_org_uuid_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<String> {
pub async fn get_orgs_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<OrganizationId> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::user_uuid.eq(user_uuid))
.select(users_organizations::org_uuid)
.load::<String>(conn)
.load::<OrganizationId>(conn)
.unwrap_or_default()
}}
}
pub async fn find_by_user_and_policy(user_uuid: &str, policy_type: OrgPolicyType, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user_and_policy(
user_uuid: &UserId,
policy_type: OrgPolicyType,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.inner_join(
@ -918,15 +950,19 @@ impl UserOrganization {
.and(org_policies::enabled.eq(true)))
)
.filter(
users_organizations::status.eq(UserOrgStatus::Confirmed as i32)
users_organizations::status.eq(MembershipStatus::Confirmed as i32)
)
.select(users_organizations::all_columns)
.load::<UserOrganizationDb>(conn)
.load::<MembershipDb>(conn)
.unwrap_or_default().from_db()
}}
}
pub async fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_cipher_and_org(
cipher_uuid: &CipherId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
@ -945,11 +981,15 @@ impl UserOrganization {
)
.select(users_organizations::all_columns)
.distinct()
.load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
.load::<MembershipDb>(conn).expect("Error loading user organizations").from_db()
}}
}
pub async fn find_by_cipher_and_org_with_group(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_cipher_and_org_with_group(
cipher_uuid: &CipherId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
@ -971,23 +1011,31 @@ impl UserOrganization {
)
.select(users_organizations::all_columns)
.distinct()
.load::<UserOrganizationDb>(conn).expect("Error loading user organizations with groups").from_db()
.load::<MembershipDb>(conn).expect("Error loading user organizations with groups").from_db()
}}
}
pub async fn user_has_ge_admin_access_to_cipher(user_uuid: &str, cipher_uuid: &str, conn: &mut DbConn) -> bool {
pub async fn user_has_ge_admin_access_to_cipher(
user_uuid: &UserId,
cipher_uuid: &CipherId,
conn: &mut DbConn,
) -> bool {
db_run! { conn: {
users_organizations::table
.inner_join(ciphers::table.on(ciphers::uuid.eq(cipher_uuid).and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))))
.filter(users_organizations::user_uuid.eq(user_uuid))
.filter(users_organizations::atype.eq_any(vec![UserOrgType::Owner as i32, UserOrgType::Admin as i32]))
.filter(users_organizations::atype.eq_any(vec![MembershipType::Owner as i32, MembershipType::Admin as i32]))
.count()
.first::<i64>(conn)
.ok().unwrap_or(0) != 0
}}
}
pub async fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_collection_and_org(
collection_uuid: &CollectionId,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Vec<Self> {
db_run! { conn: {
users_organizations::table
.filter(users_organizations::org_uuid.eq(org_uuid))
@ -1000,18 +1048,22 @@ impl UserOrganization {
)
)
.select(users_organizations::all_columns)
.load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db()
.load::<MembershipDb>(conn).expect("Error loading user organizations").from_db()
}}
}
pub async fn find_by_external_id_and_org(ext_id: &str, org_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_external_id_and_org(
ext_id: &str,
org_uuid: &OrganizationId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! {conn: {
users_organizations::table
.filter(
users_organizations::external_id.eq(ext_id)
.and(users_organizations::org_uuid.eq(org_uuid))
)
.first::<UserOrganizationDb>(conn).ok().from_db()
.first::<MembershipDb>(conn).ok().from_db()
}}
}
}
@ -1050,7 +1102,7 @@ impl OrganizationApiKey {
}
}
pub async fn find_by_org_uuid(org_uuid: &str, conn: &DbConn) -> Option<Self> {
pub async fn find_by_org_uuid(org_uuid: &OrganizationId, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
organization_api_key::table
.filter(organization_api_key::org_uuid.eq(org_uuid))
@ -1059,7 +1111,7 @@ impl OrganizationApiKey {
}}
}
pub async fn delete_all_by_organization(org_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(organization_api_key::table.filter(organization_api_key::org_uuid.eq(org_uuid)))
.execute(conn)
@ -1068,16 +1120,56 @@ impl OrganizationApiKey {
}
}
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
#[deref(forward)]
#[from(forward)]
pub struct OrganizationId(String);
#[derive(
Clone,
Debug,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct MembershipId(String);
#[derive(Clone, Debug, DieselNewType, Display, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrgApiKeyId(String);
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(non_snake_case)]
fn partial_cmp_UserOrgType() {
assert!(UserOrgType::Owner > UserOrgType::Admin);
assert!(UserOrgType::Admin > UserOrgType::Manager);
assert!(UserOrgType::Manager > UserOrgType::User);
assert!(UserOrgType::Manager == UserOrgType::from_str("4").unwrap());
fn partial_cmp_MembershipType() {
assert!(MembershipType::Owner > MembershipType::Admin);
assert!(MembershipType::Admin > MembershipType::Manager);
assert!(MembershipType::Manager > MembershipType::User);
assert!(MembershipType::Manager == MembershipType::from_str("4").unwrap());
}
}

View File

@ -3,7 +3,8 @@ use serde_json::Value;
use crate::util::LowerCase;
use super::User;
use super::{OrganizationId, User, UserId};
use id::SendId;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -11,11 +12,10 @@ db_object! {
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct Send {
pub uuid: String,
pub user_uuid: Option<String>,
pub organization_uuid: Option<String>,
pub uuid: SendId,
pub user_uuid: Option<UserId>,
pub organization_uuid: Option<OrganizationId>,
pub name: String,
pub notes: Option<String>,
@ -51,7 +51,7 @@ impl Send {
let now = Utc::now().naive_utc();
Self {
uuid: crate::util::get_uuid(),
uuid: SendId::from(crate::util::get_uuid()),
user_uuid: None,
organization_uuid: None,
@ -243,7 +243,7 @@ impl Send {
}
}
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<String> {
pub async fn update_users_revision(&self, conn: &mut DbConn) -> Vec<UserId> {
let mut user_uuids = Vec::new();
match &self.user_uuid {
Some(user_uuid) => {
@ -257,7 +257,7 @@ impl Send {
user_uuids
}
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
for send in Self::find_by_user(user_uuid, conn).await {
send.delete(conn).await?;
}
@ -273,14 +273,14 @@ impl Send {
};
let uuid = match Uuid::from_slice(&uuid_vec) {
Ok(u) => u.to_string(),
Ok(u) => SendId::from(u.to_string()),
Err(_) => return None,
};
Self::find_by_uuid(&uuid, conn).await
}
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &SendId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
sends::table
.filter(sends::uuid.eq(uuid))
@ -290,7 +290,7 @@ impl Send {
}}
}
pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid_and_user(uuid: &SendId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
sends::table
.filter(sends::uuid.eq(uuid))
@ -301,7 +301,7 @@ impl Send {
}}
}
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
sends::table
.filter(sends::user_uuid.eq(user_uuid))
@ -309,7 +309,7 @@ impl Send {
}}
}
pub async fn size_by_user(user_uuid: &str, conn: &mut DbConn) -> Option<i64> {
pub async fn size_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option<i64> {
let sends = Self::find_by_user(user_uuid, conn).await;
#[derive(serde::Deserialize)]
@ -332,7 +332,7 @@ impl Send {
Some(total)
}
pub async fn find_by_org(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec<Self> {
db_run! {conn: {
sends::table
.filter(sends::organization_uuid.eq(org_uuid))
@ -349,3 +349,48 @@ impl Send {
}}
}
}
// separate namespace to avoid name collision with std::marker::Send
pub mod id {
use derive_more::{AsRef, Deref, Display, From};
use macros::{IdFromParam, UuidFromParam};
use std::marker::Send;
use std::path::Path;
#[derive(
Clone,
Debug,
AsRef,
Deref,
DieselNewType,
Display,
From,
FromForm,
Hash,
PartialEq,
Eq,
Serialize,
Deserialize,
UuidFromParam,
)]
pub struct SendId(String);
impl AsRef<Path> for SendId {
#[inline]
fn as_ref(&self) -> &Path {
Path::new(&self.0)
}
}
#[derive(
Clone, Debug, AsRef, Deref, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
)]
pub struct SendFileId(String);
impl AsRef<Path> for SendFileId {
#[inline]
fn as_ref(&self) -> &Path {
Path::new(&self.0)
}
}
}

View File

@ -1,5 +1,6 @@
use serde_json::Value;
use super::UserId;
use crate::{api::EmptyResult, db::DbConn, error::MapResult};
db_object! {
@ -7,8 +8,8 @@ db_object! {
#[diesel(table_name = twofactor)]
#[diesel(primary_key(uuid))]
pub struct TwoFactor {
pub uuid: String,
pub user_uuid: String,
pub uuid: TwoFactorId,
pub user_uuid: UserId,
pub atype: i32,
pub enabled: bool,
pub data: String,
@ -41,9 +42,9 @@ pub enum TwoFactorType {
/// Local methods
impl TwoFactor {
pub fn new(user_uuid: String, atype: TwoFactorType, data: String) -> Self {
pub fn new(user_uuid: UserId, atype: TwoFactorType, data: String) -> Self {
Self {
uuid: crate::util::get_uuid(),
uuid: TwoFactorId(crate::util::get_uuid()),
user_uuid,
atype: atype as i32,
enabled: true,
@ -118,7 +119,7 @@ impl TwoFactor {
}}
}
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<Self> {
db_run! { conn: {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
@ -129,7 +130,7 @@ impl TwoFactor {
}}
}
pub async fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_user_and_type(user_uuid: &UserId, atype: i32, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
twofactor::table
.filter(twofactor::user_uuid.eq(user_uuid))
@ -140,7 +141,7 @@ impl TwoFactor {
}}
}
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid)))
.execute(conn)
@ -217,3 +218,6 @@ impl TwoFactor {
Ok(())
}
}
#[derive(Clone, Debug, DieselNewType, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TwoFactorId(String);

View File

@ -1,17 +1,26 @@
use chrono::{NaiveDateTime, Utc};
use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG};
use crate::{
api::EmptyResult,
auth::ClientIp,
db::{
models::{DeviceId, UserId},
DbConn,
},
error::MapResult,
CONFIG,
};
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = twofactor_incomplete)]
#[diesel(primary_key(user_uuid, device_uuid))]
pub struct TwoFactorIncomplete {
pub user_uuid: String,
pub user_uuid: UserId,
// This device UUID is simply what's claimed by the device. It doesn't
// necessarily correspond to any UUID in the devices table, since a device
// must complete 2FA login before being added into the devices table.
pub device_uuid: String,
pub device_uuid: DeviceId,
pub device_name: String,
pub device_type: i32,
pub login_time: NaiveDateTime,
@ -21,8 +30,8 @@ db_object! {
impl TwoFactorIncomplete {
pub async fn mark_incomplete(
user_uuid: &str,
device_uuid: &str,
user_uuid: &UserId,
device_uuid: &DeviceId,
device_name: &str,
device_type: i32,
ip: &ClientIp,
@ -55,7 +64,7 @@ impl TwoFactorIncomplete {
}}
}
pub async fn mark_complete(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn mark_complete(user_uuid: &UserId, device_uuid: &DeviceId, conn: &mut DbConn) -> EmptyResult {
if CONFIG.incomplete_2fa_time_limit() <= 0 || !CONFIG.mail_enabled() {
return Ok(());
}
@ -63,7 +72,11 @@ impl TwoFactorIncomplete {
Self::delete_by_user_and_device(user_uuid, device_uuid, conn).await
}
pub async fn find_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_user_and_device(
user_uuid: &UserId,
device_uuid: &DeviceId,
conn: &mut DbConn,
) -> Option<Self> {
db_run! { conn: {
twofactor_incomplete::table
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
@ -88,7 +101,11 @@ impl TwoFactorIncomplete {
Self::delete_by_user_and_device(&self.user_uuid, &self.device_uuid, conn).await
}
pub async fn delete_by_user_and_device(user_uuid: &str, device_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_by_user_and_device(
user_uuid: &UserId,
device_uuid: &DeviceId,
conn: &mut DbConn,
) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor_incomplete::table
.filter(twofactor_incomplete::user_uuid.eq(user_uuid))
@ -98,7 +115,7 @@ impl TwoFactorIncomplete {
}}
}
pub async fn delete_all_by_user(user_uuid: &str, conn: &mut DbConn) -> EmptyResult {
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(twofactor_incomplete::table.filter(twofactor_incomplete::user_uuid.eq(user_uuid)))
.execute(conn)

View File

@ -1,9 +1,19 @@
use crate::util::{format_date, get_uuid, retry};
use chrono::{NaiveDateTime, TimeDelta, Utc};
use derive_more::{AsRef, Deref, Display, From};
use serde_json::Value;
use crate::crypto;
use crate::CONFIG;
use super::{
Cipher, Device, EmergencyAccess, Favorite, Folder, Membership, MembershipType, TwoFactor, TwoFactorIncomplete,
};
use crate::{
api::EmptyResult,
crypto,
db::DbConn,
error::MapResult,
util::{format_date, get_uuid, retry},
CONFIG,
};
use macros::UuidFromParam;
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
@ -11,7 +21,7 @@ db_object! {
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid))]
pub struct User {
pub uuid: String,
pub uuid: UserId,
pub enabled: bool,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
@ -91,7 +101,7 @@ impl User {
let email = email.to_lowercase();
Self {
uuid: get_uuid(),
uuid: UserId(get_uuid()),
enabled: true,
created_at: now,
updated_at: now,
@ -214,20 +224,11 @@ impl User {
}
}
use super::{
Cipher, Device, EmergencyAccess, Favorite, Folder, Send, TwoFactor, TwoFactorIncomplete, UserOrgType,
UserOrganization,
};
use crate::db::DbConn;
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl User {
pub async fn to_json(&self, conn: &mut DbConn) -> Value {
let mut orgs_json = Vec::new();
for c in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
for c in Membership::find_confirmed_by_user(&self.uuid, conn).await {
orgs_json.push(c.to_json(conn).await);
}
@ -304,19 +305,18 @@ impl User {
}
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await {
if user_org.atype == UserOrgType::Owner
&& UserOrganization::count_confirmed_by_org_and_type(&user_org.org_uuid, UserOrgType::Owner, conn).await
<= 1
for member in Membership::find_confirmed_by_user(&self.uuid, conn).await {
if member.atype == MembershipType::Owner
&& Membership::count_confirmed_by_org_and_type(&member.org_uuid, MembershipType::Owner, conn).await <= 1
{
err!("Can't delete last owner")
}
}
Send::delete_all_by_user(&self.uuid, conn).await?;
super::Send::delete_all_by_user(&self.uuid, conn).await?;
EmergencyAccess::delete_all_by_user(&self.uuid, conn).await?;
EmergencyAccess::delete_all_by_grantee_email(&self.email, conn).await?;
UserOrganization::delete_all_by_user(&self.uuid, conn).await?;
Membership::delete_all_by_user(&self.uuid, conn).await?;
Cipher::delete_all_by_user(&self.uuid, conn).await?;
Favorite::delete_all_by_user(&self.uuid, conn).await?;
Folder::delete_all_by_user(&self.uuid, conn).await?;
@ -332,7 +332,7 @@ impl User {
}}
}
pub async fn update_uuid_revision(uuid: &str, conn: &mut DbConn) {
pub async fn update_uuid_revision(uuid: &UserId, conn: &mut DbConn) {
if let Err(e) = Self::_update_revision(uuid, &Utc::now().naive_utc(), conn).await {
warn!("Failed to update revision for {}: {:#?}", uuid, e);
}
@ -357,7 +357,7 @@ impl User {
Self::_update_revision(&self.uuid, &self.updated_at, conn).await
}
async fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
async fn _update_revision(uuid: &UserId, date: &NaiveDateTime, conn: &mut DbConn) -> EmptyResult {
db_run! {conn: {
retry(|| {
diesel::update(users::table.filter(users::uuid.eq(uuid)))
@ -379,7 +379,7 @@ impl User {
}}
}
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
pub async fn find_by_uuid(uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! {conn: {
users::table.filter(users::uuid.eq(uuid)).first::<UserDb>(conn).ok().from_db()
}}
@ -458,3 +458,23 @@ impl Invitation {
}
}
}
#[derive(
Clone,
Debug,
DieselNewType,
FromForm,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
AsRef,
Deref,
Display,
From,
UuidFromParam,
)]
#[deref(forward)]
#[from(forward)]
pub struct UserId(String);

View File

@ -17,7 +17,7 @@ use crate::{
encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, generate_invite_claims,
generate_verify_email_claims,
},
db::models::{Device, DeviceType, User},
db::models::{Device, DeviceType, EmergencyAccessId, MembershipId, OrganizationId, User, UserId},
error::Error,
CONFIG,
};
@ -166,8 +166,8 @@ pub async fn send_password_hint(address: &str, hint: Option<String>) -> EmptyRes
send_email(address, &subject, body_html, body_text).await
}
pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
let claims = generate_delete_claims(uuid.to_string());
pub async fn send_delete_account(address: &str, user_id: &UserId) -> EmptyResult {
let claims = generate_delete_claims(user_id.to_string());
let delete_token = encode_jwt(&claims);
let (subject, body_html, body_text) = get_text(
@ -175,7 +175,7 @@ pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"user_id": uuid,
"user_id": user_id,
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"token": delete_token,
}),
@ -184,8 +184,8 @@ pub async fn send_delete_account(address: &str, uuid: &str) -> EmptyResult {
send_email(address, &subject, body_html, body_text).await
}
pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
let claims = generate_verify_email_claims(uuid.to_string());
pub async fn send_verify_email(address: &str, user_id: &UserId) -> EmptyResult {
let claims = generate_verify_email_claims(user_id.clone());
let verify_email_token = encode_jwt(&claims);
let (subject, body_html, body_text) = get_text(
@ -193,7 +193,7 @@ pub async fn send_verify_email(address: &str, uuid: &str) -> EmptyResult {
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"user_id": uuid,
"user_id": user_id,
"email": percent_encode(address.as_bytes(), NON_ALPHANUMERIC).to_string(),
"token": verify_email_token,
}),
@ -214,8 +214,8 @@ pub async fn send_welcome(address: &str) -> EmptyResult {
send_email(address, &subject, body_html, body_text).await
}
pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult {
let claims = generate_verify_email_claims(uuid.to_string());
pub async fn send_welcome_must_verify(address: &str, user_id: &UserId) -> EmptyResult {
let claims = generate_verify_email_claims(user_id.clone());
let verify_email_token = encode_jwt(&claims);
let (subject, body_html, body_text) = get_text(
@ -223,7 +223,7 @@ pub async fn send_welcome_must_verify(address: &str, uuid: &str) -> EmptyResult
json!({
"url": CONFIG.domain(),
"img_src": CONFIG._smtp_img_src(),
"user_id": uuid,
"user_id": user_id,
"token": verify_email_token,
}),
)?;
@ -259,8 +259,8 @@ pub async fn send_single_org_removed_from_org(address: &str, org_name: &str) ->
pub async fn send_invite(
user: &User,
org_id: Option<String>,
org_user_id: Option<String>,
org_id: Option<OrganizationId>,
member_id: Option<MembershipId>,
org_name: &str,
invited_by_email: Option<String>,
) -> EmptyResult {
@ -268,18 +268,26 @@ pub async fn send_invite(
user.uuid.clone(),
user.email.clone(),
org_id.clone(),
org_user_id.clone(),
member_id.clone(),
invited_by_email,
);
let invite_token = encode_jwt(&claims);
let org_id = match org_id {
Some(ref org_id) => org_id.as_ref(),
None => "_",
};
let member_id = match member_id {
Some(ref member_id) => member_id.as_ref(),
None => "_",
};
let mut query = url::Url::parse("https://query.builder").unwrap();
{
let mut query_params = query.query_pairs_mut();
query_params
.append_pair("email", &user.email)
.append_pair("organizationName", org_name)
.append_pair("organizationId", org_id.as_deref().unwrap_or("_"))
.append_pair("organizationUserId", org_user_id.as_deref().unwrap_or("_"))
.append_pair("organizationId", org_id)
.append_pair("organizationUserId", member_id)
.append_pair("token", &invite_token);
if user.private_key.is_some() {
query_params.append_pair("orgUserHasExistingUser", "true");
@ -305,15 +313,15 @@ pub async fn send_invite(
pub async fn send_emergency_access_invite(
address: &str,
uuid: &str,
emer_id: &str,
user_id: UserId,
emer_id: EmergencyAccessId,
grantor_name: &str,
grantor_email: &str,
) -> EmptyResult {
let claims = generate_emergency_access_invite_claims(
String::from(uuid),
user_id,
String::from(address),
String::from(emer_id),
emer_id.clone(),
String::from(grantor_name),
String::from(grantor_email),
);
@ -323,7 +331,7 @@ pub async fn send_emergency_access_invite(
{
let mut query_params = query.query_pairs_mut();
query_params
.append_pair("id", emer_id)
.append_pair("id", &emer_id.to_string())
.append_pair("name", grantor_name)
.append_pair("email", address)
.append_pair("token", &encode_jwt(&claims));

View File

@ -24,6 +24,8 @@ extern crate log;
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
#[macro_use]
extern crate diesel_derive_newtype;
use std::{
collections::HashMap,

View File

@ -1,13 +1,12 @@
//
// Web Headers and caching
//
use std::{collections::HashMap, io::Cursor, ops::Deref, path::Path};
use std::{collections::HashMap, io::Cursor, path::Path};
use num_traits::ToPrimitive;
use rocket::{
fairing::{Fairing, Info, Kind},
http::{ContentType, Header, HeaderMap, Method, Status},
request::FromParam,
response::{self, Responder},
Data, Orbit, Request, Response, Rocket,
};
@ -223,42 +222,6 @@ impl<'r, R: 'r + Responder<'r, 'static> + Send> Responder<'r, 'static> for Cache
}
}
pub struct SafeString(String);
impl fmt::Display for SafeString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl Deref for SafeString {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<Path> for SafeString {
#[inline]
fn as_ref(&self) -> &Path {
Path::new(&self.0)
}
}
impl<'r> FromParam<'r> for SafeString {
type Error = ();
#[inline(always)]
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) {
Ok(SafeString(param.to_string()))
} else {
Err(())
}
}
}
// Log all the routes from the main paths list, and the attachments endpoint
// Effectively ignores, any static file route, and the alive endpoint
const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];