1
0
Fork 0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2025-06-16 15:50:08 +00:00

Allow col managers to import

This commit adds functionality to allow users with manage access to a collection, or managers with all access to import into an organization.

Fixes #5592

Signed-off-by: BlackDex <black.dex@gmail.com>
This commit is contained in:
BlackDex 2025-05-21 15:19:03 +02:00
parent e330f0fe95
commit b0bff90b9e
No known key found for this signature in database
GPG key ID: 58C80A2AA6C765E1
5 changed files with 124 additions and 33 deletions

23
Cargo.lock generated
View file

@ -490,9 +490,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.23" version = "1.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
dependencies = [ dependencies = [
"shlex", "shlex",
] ]
@ -1640,11 +1640,10 @@ dependencies = [
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.27.5" version = "0.27.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
dependencies = [ dependencies = [
"futures-util",
"http 1.3.1", "http 1.3.1",
"hyper 1.6.0", "hyper 1.6.0",
"hyper-util", "hyper-util",
@ -1673,9 +1672,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.11" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -3282,9 +3281,9 @@ dependencies = [
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.20" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]] [[package]]
name = "ryu" name = "ryu"
@ -4145,11 +4144,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.16.0" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"js-sys",
"wasm-bindgen",
] ]
[[package]] [[package]]

View file

@ -95,7 +95,7 @@ ring = "0.17.14"
subtle = "2.6.1" subtle = "2.6.1"
# UUID generation # UUID generation
uuid = { version = "1.16.0", features = ["v4"] } uuid = { version = "1.17.0", features = ["v4"] }
# Date and time libraries # Date and time libraries
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false } chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }

View file

@ -697,6 +697,9 @@ async fn _delete_organization_collection(
headers: &ManagerHeaders, headers: &ManagerHeaders,
conn: &mut DbConn, conn: &mut DbConn,
) -> EmptyResult { ) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, conn).await else { let Some(collection) = Collection::find_by_uuid_and_org(col_id, org_id, conn).await else {
err!("Collection not found", "Collection does not exist or does not belong to this organization") err!("Collection not found", "Collection does not exist or does not belong to this organization")
}; };
@ -909,7 +912,7 @@ struct OrgIdData {
#[get("/ciphers/organization-details?<data..>")] #[get("/ciphers/organization-details?<data..>")]
async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: DbConn) -> JsonResult { async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: DbConn) -> JsonResult {
if data.organization_id != headers.org_id { if data.organization_id != headers.membership.org_uuid {
err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code); err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code);
} }
@ -1196,6 +1199,9 @@ async fn reinvite_member(
headers: AdminHeaders, headers: AdminHeaders,
mut conn: DbConn, mut conn: DbConn,
) -> EmptyResult { ) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
_reinvite_member(&org_id, &member_id, &headers.user.email, &mut conn).await _reinvite_member(&org_id, &member_id, &headers.user.email, &mut conn).await
} }
@ -1413,6 +1419,9 @@ async fn _confirm_invite(
conn: &mut DbConn, conn: &mut DbConn,
nt: &Notify<'_>, nt: &Notify<'_>,
) -> EmptyResult { ) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if key.is_empty() || member_id.is_empty() { if key.is_empty() || member_id.is_empty() {
err!("Key or UserId is not set, unable to process request"); err!("Key or UserId is not set, unable to process request");
} }
@ -1735,6 +1744,9 @@ async fn _delete_member(
conn: &mut DbConn, conn: &mut DbConn,
nt: &Notify<'_>, nt: &Notify<'_>,
) -> EmptyResult { ) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(member_to_delete) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else { let Some(member_to_delete) = Membership::find_by_uuid_and_org(member_id, org_id, conn).await else {
err!("User to delete isn't member of the organization") err!("User to delete isn't member of the organization")
}; };
@ -1829,16 +1841,20 @@ struct RelationsData {
value: usize, value: usize,
} }
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/ImportCiphersController.cs#L62
#[post("/ciphers/import-organization?<query..>", data = "<data>")] #[post("/ciphers/import-organization?<query..>", data = "<data>")]
async fn post_org_import( async fn post_org_import(
query: OrgIdData, query: OrgIdData,
data: Json<ImportData>, data: Json<ImportData>,
headers: AdminHeaders, headers: OrgMemberHeaders,
mut conn: DbConn, mut conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> EmptyResult { ) -> EmptyResult {
let data: ImportData = data.into_inner();
let org_id = query.organization_id; let org_id = query.organization_id;
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let data: ImportData = data.into_inner();
// Validate the import before continuing // Validate the import before continuing
// Bitwarden does not process the import if there is one item invalid. // Bitwarden does not process the import if there is one item invalid.
@ -1851,8 +1867,20 @@ async fn post_org_import(
let mut collections: Vec<CollectionId> = Vec::with_capacity(data.collections.len()); let mut collections: Vec<CollectionId> = Vec::with_capacity(data.collections.len());
for col in data.collections { for col in data.collections {
let collection_uuid = if existing_collections.contains(&col.id) { let collection_uuid = if existing_collections.contains(&col.id) {
col.id.unwrap() let col_id = col.id.unwrap();
// When not an Owner or Admin, check if the member is allowed to access the collection.
if headers.membership.atype < MembershipType::Admin
&& !Collection::can_access_collection(&headers.membership, &col_id, &mut conn).await
{
err!(Small, "The current user isn't allowed to manage this collection")
}
col_id
} else { } else {
// We do not allow users or managers which can not manage all collections to create new collections
// If there is any collection other than an existing import collection, abort the import.
if headers.membership.atype <= MembershipType::Manager && !headers.membership.has_full_access() {
err!(Small, "The current user isn't allowed to create new collections")
}
let new_collection = Collection::new(org_id.clone(), col.name, col.external_id); let new_collection = Collection::new(org_id.clone(), col.name, col.external_id);
new_collection.save(&mut conn).await?; new_collection.save(&mut conn).await?;
new_collection.uuid new_collection.uuid
@ -1875,7 +1903,17 @@ async fn post_org_import(
// Always clear folder_id's via an organization import // Always clear folder_id's via an organization import
cipher_data.folder_id = None; cipher_data.folder_id = None;
let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone()); let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &mut conn, &nt, UpdateType::None).await.ok(); update_cipher_from_data(
&mut cipher,
cipher_data,
&headers,
Some(collections.clone()),
&mut conn,
&nt,
UpdateType::None,
)
.await
.ok();
ciphers.push(cipher.uuid); ciphers.push(cipher.uuid);
} }
@ -2420,6 +2458,9 @@ async fn _revoke_member(
headers: &AdminHeaders, headers: &AdminHeaders,
conn: &mut DbConn, conn: &mut DbConn,
) -> EmptyResult { ) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
match Membership::find_by_uuid_and_org(member_id, org_id, conn).await { match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
Some(mut member) if member.status > MembershipStatus::Revoked as i32 => { Some(mut member) if member.status > MembershipStatus::Revoked as i32 => {
if member.user_uuid == headers.user.uuid { if member.user_uuid == headers.user.uuid {
@ -2527,6 +2568,9 @@ async fn _restore_member(
headers: &AdminHeaders, headers: &AdminHeaders,
conn: &mut DbConn, conn: &mut DbConn,
) -> EmptyResult { ) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
match Membership::find_by_uuid_and_org(member_id, org_id, conn).await { match Membership::find_by_uuid_and_org(member_id, org_id, conn).await {
Some(mut member) if member.status < MembershipStatus::Accepted as i32 => { Some(mut member) if member.status < MembershipStatus::Accepted as i32 => {
if member.user_uuid == headers.user.uuid { if member.user_uuid == headers.user.uuid {
@ -2679,6 +2723,9 @@ async fn post_groups(
data: Json<GroupRequest>, data: Json<GroupRequest>,
mut conn: DbConn, mut conn: DbConn,
) -> JsonResult { ) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() { if !CONFIG.org_groups_enabled() {
err!("Group support is disabled"); err!("Group support is disabled");
} }
@ -2708,6 +2755,9 @@ async fn put_group(
headers: AdminHeaders, headers: AdminHeaders,
mut conn: DbConn, mut conn: DbConn,
) -> JsonResult { ) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() { if !CONFIG.org_groups_enabled() {
err!("Group support is disabled"); err!("Group support is disabled");
} }
@ -2824,6 +2874,9 @@ async fn _delete_group(
headers: &AdminHeaders, headers: &AdminHeaders,
conn: &mut DbConn, conn: &mut DbConn,
) -> EmptyResult { ) -> EmptyResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() { if !CONFIG.org_groups_enabled() {
err!("Group support is disabled"); err!("Group support is disabled");
} }
@ -2853,6 +2906,9 @@ async fn bulk_delete_groups(
headers: AdminHeaders, headers: AdminHeaders,
mut conn: DbConn, mut conn: DbConn,
) -> EmptyResult { ) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() { if !CONFIG.org_groups_enabled() {
err!("Group support is disabled"); err!("Group support is disabled");
} }
@ -2916,6 +2972,9 @@ async fn put_group_members(
data: Json<Vec<MembershipId>>, data: Json<Vec<MembershipId>>,
mut conn: DbConn, mut conn: DbConn,
) -> EmptyResult { ) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() { if !CONFIG.org_groups_enabled() {
err!("Group support is disabled"); err!("Group support is disabled");
} }
@ -3100,7 +3159,7 @@ async fn get_organization_public_key(
headers: OrgMemberHeaders, headers: OrgMemberHeaders,
mut conn: DbConn, mut conn: DbConn,
) -> JsonResult { ) -> JsonResult {
if org_id != headers.org_id { if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match"); err!("Organization not found", "Organization id's do not match");
} }
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else { let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
@ -3324,6 +3383,9 @@ async fn _api_key(
headers: AdminHeaders, headers: AdminHeaders,
mut conn: DbConn, mut conn: DbConn,
) -> JsonResult { ) -> JsonResult {
if org_id != &headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: PasswordOrOtpData = data.into_inner(); let data: PasswordOrOtpData = data.into_inner();
let user = headers.user; let user = headers.user;

View file

@ -694,17 +694,6 @@ impl<'r> FromRequest<'r> for AdminHeaders {
} }
} }
impl From<AdminHeaders> for Headers {
fn from(h: AdminHeaders) -> Headers {
Headers {
host: h.host,
device: h.device,
user: h.user,
ip: h.ip,
}
}
}
// col_id is usually the fourth path param ("/organizations/<org_id>/collections/<col_id>"), // 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. // 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. // First check the path, if this is not a valid uuid, try the query values.
@ -874,8 +863,10 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
pub struct OrgMemberHeaders { pub struct OrgMemberHeaders {
pub host: String, pub host: String,
pub device: Device,
pub user: User, pub user: User,
pub org_id: OrganizationId, pub membership: Membership,
pub ip: ClientIp,
} }
#[rocket::async_trait] #[rocket::async_trait]
@ -887,8 +878,10 @@ impl<'r> FromRequest<'r> for OrgMemberHeaders {
if headers.is_member() { if headers.is_member() {
Outcome::Success(Self { Outcome::Success(Self {
host: headers.host, host: headers.host,
device: headers.device,
user: headers.user, user: headers.user,
org_id: headers.membership.org_uuid, membership: headers.membership,
ip: headers.ip,
}) })
} else { } else {
err_handler!("You need to be a Member of the Organization to call this endpoint") err_handler!("You need to be a Member of the Organization to call this endpoint")
@ -896,6 +889,17 @@ impl<'r> FromRequest<'r> for OrgMemberHeaders {
} }
} }
impl From<OrgMemberHeaders> for Headers {
fn from(h: OrgMemberHeaders) -> Headers {
Headers {
host: h.host,
device: h.device,
user: h.user,
ip: h.ip,
}
}
}
// //
// Client IP address detection // Client IP address detection
// //

View file

@ -59,6 +59,8 @@ use yubico::yubicoerror::YubicoError as YubiErr;
#[derive(Serialize)] #[derive(Serialize)]
pub struct Empty {} pub struct Empty {}
pub struct Small {}
// Error struct // Error struct
// Contains a String error message, meant for the user and an enum variant, with an error of different types. // Contains a String error message, meant for the user and an enum variant, with an error of different types.
// //
@ -69,6 +71,7 @@ make_error! {
Empty(Empty): _no_source, _serialize, Empty(Empty): _no_source, _serialize,
// Used to represent err! calls // Used to represent err! calls
Simple(String): _no_source, _api_error, Simple(String): _no_source, _api_error,
Small(Small): _no_source, _api_error_small,
// Used in our custom http client to handle non-global IPs and blocked domains // Used in our custom http client to handle non-global IPs and blocked domains
CustomHttpClient(CustomHttpClientError): _has_source, _api_error, CustomHttpClient(CustomHttpClientError): _has_source, _api_error,
@ -132,6 +135,12 @@ impl Error {
self self
} }
#[must_use]
pub fn with_kind(mut self, kind: ErrorKind) -> Self {
self.error = kind;
self
}
#[must_use] #[must_use]
pub const fn with_code(mut self, code: u16) -> Self { pub const fn with_code(mut self, code: u16) -> Self {
self.error_code = code; self.error_code = code;
@ -200,6 +209,18 @@ fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
_serialize(&json, "") _serialize(&json, "")
} }
fn _api_error_small(_: &impl std::any::Any, msg: &str) -> String {
let json = json!({
"message": msg,
"validationErrors": null,
"exceptionMessage": null,
"exceptionStackTrace": null,
"innerExceptionMessage": null,
"object": "error"
});
_serialize(&json, "")
}
// //
// Rocket responder impl // Rocket responder impl
// //
@ -212,8 +233,7 @@ use rocket::response::{self, Responder, Response};
impl Responder<'_, 'static> for Error { impl Responder<'_, 'static> for Error {
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> { fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
match self.error { match self.error {
ErrorKind::Empty(_) => {} // Don't print the error in this situation ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Small(_) => {} // Don't print the error in this situation
ErrorKind::Simple(_) => {} // Don't print the error in this situation
_ => error!(target: "error", "{self:#?}"), _ => error!(target: "error", "{self:#?}"),
}; };
@ -228,6 +248,10 @@ impl Responder<'_, 'static> for Error {
// //
#[macro_export] #[macro_export]
macro_rules! err { macro_rules! err {
($kind:ident, $msg:expr) => {{
error!("{}", $msg);
return Err($crate::error::Error::new($msg, $msg).with_kind($crate::error::ErrorKind::$kind($crate::error::$kind {})));
}};
($msg:expr) => {{ ($msg:expr) => {{
error!("{}", $msg); error!("{}", $msg);
return Err($crate::error::Error::new($msg, $msg)); return Err($crate::error::Error::new($msg, $msg));