mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-06-15 07:10:07 +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:
parent
e330f0fe95
commit
b0bff90b9e
5 changed files with 124 additions and 33 deletions
23
Cargo.lock
generated
23
Cargo.lock
generated
|
@ -490,9 +490,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.23"
|
||||
version = "1.2.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
|
||||
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
@ -1640,11 +1640,10 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.5"
|
||||
version = "0.27.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
|
||||
checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
|
@ -1673,9 +1672,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.11"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
|
||||
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
|
@ -3282,9 +3281,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
|
@ -4145,11 +4144,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.16.0"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -95,7 +95,7 @@ ring = "0.17.14"
|
|||
subtle = "2.6.1"
|
||||
|
||||
# UUID generation
|
||||
uuid = { version = "1.16.0", features = ["v4"] }
|
||||
uuid = { version = "1.17.0", features = ["v4"] }
|
||||
|
||||
# Date and time libraries
|
||||
chrono = { version = "0.4.41", features = ["clock", "serde"], default-features = false }
|
||||
|
|
|
@ -697,6 +697,9 @@ async fn _delete_organization_collection(
|
|||
headers: &ManagerHeaders,
|
||||
conn: &mut DbConn,
|
||||
) -> 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 {
|
||||
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..>")]
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -1196,6 +1199,9 @@ async fn reinvite_member(
|
|||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> 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
|
||||
}
|
||||
|
||||
|
@ -1413,6 +1419,9 @@ async fn _confirm_invite(
|
|||
conn: &mut DbConn,
|
||||
nt: &Notify<'_>,
|
||||
) -> 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() {
|
||||
err!("Key or UserId is not set, unable to process request");
|
||||
}
|
||||
|
@ -1735,6 +1744,9 @@ async fn _delete_member(
|
|||
conn: &mut DbConn,
|
||||
nt: &Notify<'_>,
|
||||
) -> 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 {
|
||||
err!("User to delete isn't member of the organization")
|
||||
};
|
||||
|
@ -1829,16 +1841,20 @@ struct RelationsData {
|
|||
value: usize,
|
||||
}
|
||||
|
||||
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/ImportCiphersController.cs#L62
|
||||
#[post("/ciphers/import-organization?<query..>", data = "<data>")]
|
||||
async fn post_org_import(
|
||||
query: OrgIdData,
|
||||
data: Json<ImportData>,
|
||||
headers: AdminHeaders,
|
||||
headers: OrgMemberHeaders,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
let data: ImportData = data.into_inner();
|
||||
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
|
||||
// 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());
|
||||
for col in data.collections {
|
||||
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 {
|
||||
// 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);
|
||||
new_collection.save(&mut conn).await?;
|
||||
new_collection.uuid
|
||||
|
@ -1875,7 +1903,17 @@ async fn post_org_import(
|
|||
// Always clear folder_id's via an organization import
|
||||
cipher_data.folder_id = None;
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -2420,6 +2458,9 @@ async fn _revoke_member(
|
|||
headers: &AdminHeaders,
|
||||
conn: &mut DbConn,
|
||||
) -> 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 {
|
||||
Some(mut member) if member.status > MembershipStatus::Revoked as i32 => {
|
||||
if member.user_uuid == headers.user.uuid {
|
||||
|
@ -2527,6 +2568,9 @@ async fn _restore_member(
|
|||
headers: &AdminHeaders,
|
||||
conn: &mut DbConn,
|
||||
) -> 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 {
|
||||
Some(mut member) if member.status < MembershipStatus::Accepted as i32 => {
|
||||
if member.user_uuid == headers.user.uuid {
|
||||
|
@ -2679,6 +2723,9 @@ async fn post_groups(
|
|||
data: Json<GroupRequest>,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
|
@ -2708,6 +2755,9 @@ async fn put_group(
|
|||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
|
@ -2824,6 +2874,9 @@ async fn _delete_group(
|
|||
headers: &AdminHeaders,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != &headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
|
@ -2853,6 +2906,9 @@ async fn bulk_delete_groups(
|
|||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
|
@ -2916,6 +2972,9 @@ async fn put_group_members(
|
|||
data: Json<Vec<MembershipId>>,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
|
@ -3100,7 +3159,7 @@ async fn get_organization_public_key(
|
|||
headers: OrgMemberHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
if org_id != headers.membership.org_uuid {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
|
||||
|
@ -3324,6 +3383,9 @@ async fn _api_key(
|
|||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != &headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: PasswordOrOtpData = data.into_inner();
|
||||
let user = headers.user;
|
||||
|
||||
|
|
30
src/auth.rs
30
src/auth.rs
|
@ -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>"),
|
||||
// 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.
|
||||
|
@ -874,8 +863,10 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
|
|||
|
||||
pub struct OrgMemberHeaders {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub org_id: OrganizationId,
|
||||
pub membership: Membership,
|
||||
pub ip: ClientIp,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
|
@ -887,8 +878,10 @@ impl<'r> FromRequest<'r> for OrgMemberHeaders {
|
|||
if headers.is_member() {
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
org_id: headers.membership.org_uuid,
|
||||
membership: headers.membership,
|
||||
ip: headers.ip,
|
||||
})
|
||||
} else {
|
||||
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
|
||||
//
|
||||
|
|
28
src/error.rs
28
src/error.rs
|
@ -59,6 +59,8 @@ use yubico::yubicoerror::YubicoError as YubiErr;
|
|||
#[derive(Serialize)]
|
||||
pub struct Empty {}
|
||||
|
||||
pub struct Small {}
|
||||
|
||||
// Error struct
|
||||
// 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,
|
||||
// Used to represent err! calls
|
||||
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
|
||||
CustomHttpClient(CustomHttpClientError): _has_source, _api_error,
|
||||
|
@ -132,6 +135,12 @@ impl Error {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_kind(mut self, kind: ErrorKind) -> Self {
|
||||
self.error = kind;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn with_code(mut self, code: u16) -> Self {
|
||||
self.error_code = code;
|
||||
|
@ -200,6 +209,18 @@ fn _api_error(_: &impl std::any::Any, msg: &str) -> String {
|
|||
_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
|
||||
//
|
||||
|
@ -212,8 +233,7 @@ use rocket::response::{self, Responder, Response};
|
|||
impl Responder<'_, 'static> for Error {
|
||||
fn respond_to(self, _: &Request<'_>) -> response::Result<'static> {
|
||||
match self.error {
|
||||
ErrorKind::Empty(_) => {} // Don't print the error in this situation
|
||||
ErrorKind::Simple(_) => {} // Don't print the error in this situation
|
||||
ErrorKind::Empty(_) | ErrorKind::Simple(_) | ErrorKind::Small(_) => {} // Don't print the error in this situation
|
||||
_ => error!(target: "error", "{self:#?}"),
|
||||
};
|
||||
|
||||
|
@ -228,6 +248,10 @@ impl Responder<'_, 'static> for Error {
|
|||
//
|
||||
#[macro_export]
|
||||
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) => {{
|
||||
error!("{}", $msg);
|
||||
return Err($crate::error::Error::new($msg, $msg));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue