mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2024-11-10 04:52:40 +01:00
Fix the web-vault v2023.2.0 API calls
- Supports the new Collection/Group/User editing UI's - Support `/partial` endpoint for cipher updating to allow folder and favorite update for read-only ciphers. - Prevent `Favorite`, `Folder`, `read-only` and `hide-passwords` from being added to the organizational sync. - Added and corrected some `Object` key's to the output json. Fixes #3279
This commit is contained in:
parent
af6d17b701
commit
7ec00d3850
@ -627,7 +627,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
|||||||
"latest_release": latest_release,
|
"latest_release": latest_release,
|
||||||
"latest_commit": latest_commit,
|
"latest_commit": latest_commit,
|
||||||
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
"web_vault_enabled": &CONFIG.web_vault_enabled(),
|
||||||
"web_vault_version": web_vault_version.version,
|
"web_vault_version": web_vault_version.version.trim_start_matches('v'),
|
||||||
"latest_web_build": latest_web_build,
|
"latest_web_build": latest_web_build,
|
||||||
"running_within_docker": running_within_docker,
|
"running_within_docker": running_within_docker,
|
||||||
"docker_base_image": if running_within_docker { docker_base_image() } else { "Not applicable" },
|
"docker_base_image": if running_within_docker { docker_base_image() } else { "Not applicable" },
|
||||||
|
@ -841,6 +841,8 @@ async fn _api_key(
|
|||||||
headers: Headers,
|
headers: Headers,
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
|
use crate::util::format_date;
|
||||||
|
|
||||||
let data: SecretVerificationRequest = data.into_inner().data;
|
let data: SecretVerificationRequest = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
@ -855,6 +857,7 @@ async fn _api_key(
|
|||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"ApiKey": user.api_key,
|
"ApiKey": user.api_key,
|
||||||
|
"RevisionDate": format_date(&user.updated_at),
|
||||||
"Object": "apiKey",
|
"Object": "apiKey",
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,9 @@ pub fn routes() -> Vec<Route> {
|
|||||||
put_cipher_share,
|
put_cipher_share,
|
||||||
put_cipher_share_selected,
|
put_cipher_share_selected,
|
||||||
post_cipher,
|
post_cipher,
|
||||||
|
post_cipher_partial,
|
||||||
put_cipher,
|
put_cipher,
|
||||||
|
put_cipher_partial,
|
||||||
delete_cipher_post,
|
delete_cipher_post,
|
||||||
delete_cipher_post_admin,
|
delete_cipher_post_admin,
|
||||||
delete_cipher_put,
|
delete_cipher_put,
|
||||||
@ -109,7 +111,10 @@ async fn sync(data: SyncData, headers: Headers, mut conn: DbConn) -> Json<Value>
|
|||||||
// Lets generate the ciphers_json using all the gathered info
|
// Lets generate the ciphers_json using all the gathered info
|
||||||
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
||||||
for c in ciphers {
|
for c in ciphers {
|
||||||
ciphers_json.push(c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), &mut conn).await);
|
ciphers_json.push(
|
||||||
|
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let collections = Collection::find_by_user_uuid(headers.user.uuid.clone(), &mut conn).await;
|
let collections = Collection::find_by_user_uuid(headers.user.uuid.clone(), &mut conn).await;
|
||||||
@ -153,7 +158,10 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
|||||||
|
|
||||||
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
||||||
for c in ciphers {
|
for c in ciphers {
|
||||||
ciphers_json.push(c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), &mut conn).await);
|
ciphers_json.push(
|
||||||
|
c.to_json(&headers.host, &headers.user.uuid, Some(&cipher_sync_data), CipherSyncType::User, &mut conn)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@ -174,7 +182,7 @@ async fn get_cipher(uuid: String, headers: Headers, mut conn: DbConn) -> JsonRes
|
|||||||
err!("Cipher is not owned by user")
|
err!("Cipher is not owned by user")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/ciphers/<uuid>/admin")]
|
#[get("/ciphers/<uuid>/admin")]
|
||||||
@ -235,6 +243,13 @@ pub struct CipherData {
|
|||||||
LastKnownRevisionDate: Option<String>,
|
LastKnownRevisionDate: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct PartialCipherData {
|
||||||
|
FolderId: Option<String>,
|
||||||
|
Favorite: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub struct Attachments2Data {
|
pub struct Attachments2Data {
|
||||||
@ -314,7 +329,7 @@ async fn post_ciphers(
|
|||||||
update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherCreate)
|
update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherCreate)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enforces the personal ownership policy on user-owned ciphers, if applicable.
|
/// Enforces the personal ownership policy on user-owned ciphers, if applicable.
|
||||||
@ -646,7 +661,51 @@ async fn put_cipher(
|
|||||||
update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherUpdate)
|
update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherUpdate)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/ciphers/<uuid>/partial", data = "<data>")]
|
||||||
|
async fn post_cipher_partial(
|
||||||
|
uuid: String,
|
||||||
|
data: JsonUpcase<PartialCipherData>,
|
||||||
|
headers: Headers,
|
||||||
|
conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
put_cipher_partial(uuid, data, headers, conn).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only update the folder and favorite for the user, since this cipher is read-only
|
||||||
|
#[put("/ciphers/<uuid>/partial", data = "<data>")]
|
||||||
|
async fn put_cipher_partial(
|
||||||
|
uuid: String,
|
||||||
|
data: JsonUpcase<PartialCipherData>,
|
||||||
|
headers: Headers,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
|
let data: PartialCipherData = data.into_inner().data;
|
||||||
|
|
||||||
|
let cipher = match Cipher::find_by_uuid(&uuid, &mut conn).await {
|
||||||
|
Some(cipher) => cipher,
|
||||||
|
None => err!("Cipher doesn't exist"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(ref folder_id) = data.FolderId {
|
||||||
|
match Folder::find_by_uuid(folder_id, &mut conn).await {
|
||||||
|
Some(folder) => {
|
||||||
|
if folder.user_uuid != headers.user.uuid {
|
||||||
|
err!("Folder is not owned by user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => err!("Folder doesn't exist"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move cipher
|
||||||
|
cipher.move_to_folder(data.FolderId.clone(), &headers.user.uuid, &mut conn).await?;
|
||||||
|
// Update favorite
|
||||||
|
cipher.set_favorite(Some(data.Favorite), &headers.user.uuid, &mut conn).await?;
|
||||||
|
|
||||||
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -873,7 +932,7 @@ async fn share_cipher_by_uuid(
|
|||||||
|
|
||||||
update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, ip, nt, ut).await?;
|
update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, ip, nt, ut).await?;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// v2 API for downloading an attachment. This just redirects the client to
|
/// v2 API for downloading an attachment. This just redirects the client to
|
||||||
@ -942,7 +1001,7 @@ async fn post_attachment_v2(
|
|||||||
"AttachmentId": attachment_id,
|
"AttachmentId": attachment_id,
|
||||||
"Url": url,
|
"Url": url,
|
||||||
"FileUploadType": FileUploadType::Direct as i32,
|
"FileUploadType": FileUploadType::Direct as i32,
|
||||||
response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await,
|
response_key: cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1135,7 +1194,7 @@ async fn post_attachment(
|
|||||||
|
|
||||||
let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, ip, nt).await?;
|
let (cipher, mut conn) = save_attachment(attachment, uuid, data, &headers, conn, ip, nt).await?;
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, &mut conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/ciphers/<uuid>/attachment-admin", format = "multipart/form-data", data = "<data>")]
|
#[post("/ciphers/<uuid>/attachment-admin", format = "multipart/form-data", data = "<data>")]
|
||||||
@ -1616,7 +1675,7 @@ async fn _restore_cipher_by_uuid(
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, conn).await))
|
Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, CipherSyncType::User, conn).await))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _restore_multiple_ciphers(
|
async fn _restore_multiple_ciphers(
|
||||||
@ -1716,6 +1775,7 @@ pub struct CipherSyncData {
|
|||||||
pub user_group_full_access_for_organizations: HashSet<String>,
|
pub user_group_full_access_for_organizations: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq)]
|
||||||
pub enum CipherSyncType {
|
pub enum CipherSyncType {
|
||||||
User,
|
User,
|
||||||
Organization,
|
Organization,
|
||||||
|
@ -590,8 +590,16 @@ async fn view_emergency_access(emer_id: String, headers: Headers, mut conn: DbCo
|
|||||||
|
|
||||||
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
||||||
for c in ciphers {
|
for c in ciphers {
|
||||||
ciphers_json
|
ciphers_json.push(
|
||||||
.push(c.to_json(&headers.host, &emergency_access.grantor_uuid, Some(&cipher_sync_data), &mut conn).await);
|
c.to_json(
|
||||||
|
&headers.host,
|
||||||
|
&emergency_access.grantor_uuid,
|
||||||
|
Some(&cipher_sync_data),
|
||||||
|
CipherSyncType::User,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
|
@ -237,6 +237,7 @@ fn config() -> Json<Value> {
|
|||||||
"notifications": format!("{domain}/notifications"),
|
"notifications": format!("{domain}/notifications"),
|
||||||
"sso": "",
|
"sso": "",
|
||||||
},
|
},
|
||||||
|
"object": "config",
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,12 +118,13 @@ struct OrganizationUpdateData {
|
|||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct NewCollectionData {
|
struct NewCollectionData {
|
||||||
Name: String,
|
Name: String,
|
||||||
Groups: Vec<NewCollectionGroupData>,
|
Groups: Vec<NewCollectionObjectData>,
|
||||||
|
Users: Vec<NewCollectionObjectData>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct NewCollectionGroupData {
|
struct NewCollectionObjectData {
|
||||||
HidePasswords: bool,
|
HidePasswords: bool,
|
||||||
Id: String,
|
Id: String,
|
||||||
ReadOnly: bool,
|
ReadOnly: bool,
|
||||||
@ -311,29 +312,62 @@ async fn get_org_collections(org_id: String, _headers: ManagerHeadersLoose, mut
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/collections/details")]
|
#[get("/organizations/<org_id>/collections/details")]
|
||||||
async fn get_org_collections_details(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
|
async fn get_org_collections_details(org_id: String, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
||||||
let mut data = Vec::new();
|
let mut data = Vec::new();
|
||||||
|
|
||||||
|
let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await {
|
||||||
|
Some(u) => u,
|
||||||
|
None => err!("User is not part of organization"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let coll_users = CollectionUser::find_by_organization(&org_id, &mut conn).await;
|
||||||
|
|
||||||
for col in Collection::find_by_organization(&org_id, &mut conn).await {
|
for col in Collection::find_by_organization(&org_id, &mut conn).await {
|
||||||
let groups: Vec<Value> = CollectionGroup::find_by_collection(&col.uuid, &mut conn)
|
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
||||||
.await
|
CollectionGroup::find_by_collection(&col.uuid, &mut conn)
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|collection_group| {
|
||||||
|
SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
|
||||||
|
})
|
||||||
|
.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.
|
||||||
|
Vec::with_capacity(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut assigned = false;
|
||||||
|
let users: Vec<Value> = coll_users
|
||||||
.iter()
|
.iter()
|
||||||
.map(|collection_group| {
|
.filter(|collection_user| collection_user.collection_uuid == col.uuid)
|
||||||
SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
|
.map(|collection_user| {
|
||||||
|
// Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `CollectionUser::find_by_organization` call.
|
||||||
|
// We check here if the current user is assigned to this collection or not.
|
||||||
|
if collection_user.user_uuid == user_org.uuid {
|
||||||
|
assigned = true;
|
||||||
|
}
|
||||||
|
SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
if user_org.access_all {
|
||||||
|
assigned = true;
|
||||||
|
}
|
||||||
|
|
||||||
let mut json_object = col.to_json();
|
let mut json_object = col.to_json();
|
||||||
|
json_object["Assigned"] = json!(assigned);
|
||||||
|
json_object["Users"] = json!(users);
|
||||||
json_object["Groups"] = json!(groups);
|
json_object["Groups"] = json!(groups);
|
||||||
json_object["Object"] = json!("collectionGroupDetails");
|
json_object["Object"] = json!("collectionAccessDetails");
|
||||||
data.push(json_object)
|
data.push(json_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
Json(json!({
|
Ok(Json(json!({
|
||||||
"Data": data,
|
"Data": data,
|
||||||
"Object": "list",
|
"Object": "list",
|
||||||
"ContinuationToken": null,
|
"ContinuationToken": null,
|
||||||
}))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _get_org_collections(org_id: &str, conn: &mut DbConn) -> Value {
|
async fn _get_org_collections(org_id: &str, conn: &mut DbConn) -> Value {
|
||||||
@ -355,12 +389,6 @@ async fn post_organization_collections(
|
|||||||
None => err!("Can't find organization details"),
|
None => err!("Can't find organization details"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the user_organization record so that we can check if the user has access to all collections.
|
|
||||||
let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await {
|
|
||||||
Some(u) => u,
|
|
||||||
None => err!("User is not part of organization"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let collection = Collection::new(org.uuid, data.Name);
|
let collection = Collection::new(org.uuid, data.Name);
|
||||||
collection.save(&mut conn).await?;
|
collection.save(&mut conn).await?;
|
||||||
|
|
||||||
@ -381,11 +409,18 @@ async fn post_organization_collections(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user doesn't have access to all collections, only in case of a Manger,
|
for user in data.Users {
|
||||||
// then we need to save the creating user uuid (Manager) to the users_collection table.
|
let org_user = match UserOrganization::find_by_uuid(&user.Id, &mut conn).await {
|
||||||
// Else the user will not have access to his own created collection.
|
Some(u) => u,
|
||||||
if !user_org.access_all {
|
None => err!("User is not part of organization"),
|
||||||
CollectionUser::save(&headers.user.uuid, &collection.uuid, false, false, &mut conn).await?;
|
};
|
||||||
|
|
||||||
|
if org_user.access_all {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionUser::save(&org_user.user_uuid, &collection.uuid, user.ReadOnly, user.HidePasswords, &mut conn)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(collection.to_json()))
|
Ok(Json(collection.to_json()))
|
||||||
@ -448,6 +483,21 @@ async fn post_organization_collection_update(
|
|||||||
CollectionGroup::new(col_id.clone(), group.Id, group.ReadOnly, group.HidePasswords).save(&mut conn).await?;
|
CollectionGroup::new(col_id.clone(), group.Id, group.ReadOnly, group.HidePasswords).save(&mut conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?;
|
||||||
|
|
||||||
|
for user in data.Users {
|
||||||
|
let org_user = match UserOrganization::find_by_uuid(&user.Id, &mut conn).await {
|
||||||
|
Some(u) => u,
|
||||||
|
None => err!("User is not part of organization"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if org_user.access_all {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionUser::save(&org_user.user_uuid, &col_id, user.ReadOnly, user.HidePasswords, &mut conn).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(collection.to_json()))
|
Ok(Json(collection.to_json()))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -555,17 +605,49 @@ async fn get_org_collection_detail(
|
|||||||
err!("Collection is not owned by organization")
|
err!("Collection is not owned by organization")
|
||||||
}
|
}
|
||||||
|
|
||||||
let groups: Vec<Value> = CollectionGroup::find_by_collection(&collection.uuid, &mut conn)
|
let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await {
|
||||||
.await
|
Some(u) => u,
|
||||||
.iter()
|
None => err!("User is not part of organization"),
|
||||||
.map(|collection_group| {
|
};
|
||||||
SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
|
|
||||||
})
|
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
||||||
.collect();
|
CollectionGroup::find_by_collection(&collection.uuid, &mut conn)
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|collection_group| {
|
||||||
|
SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
|
||||||
|
})
|
||||||
|
.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.
|
||||||
|
Vec::with_capacity(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut assigned = false;
|
||||||
|
let users: Vec<Value> =
|
||||||
|
CollectionUser::find_by_collection_swap_user_uuid_with_org_user_uuid(&collection.uuid, &mut conn)
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|collection_user| {
|
||||||
|
// Remember `user_uuid` is swapped here with the `user_org.uuid` with a join during the `find_by_collection_swap_user_uuid_with_org_user_uuid` call.
|
||||||
|
// We check here if the current user is assigned to this collection or not.
|
||||||
|
if collection_user.user_uuid == user_org.uuid {
|
||||||
|
assigned = true;
|
||||||
|
}
|
||||||
|
SelectionReadOnly::to_collection_user_details_read_only(collection_user).to_json()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if user_org.access_all {
|
||||||
|
assigned = true;
|
||||||
|
}
|
||||||
|
|
||||||
let mut json_object = collection.to_json();
|
let mut json_object = collection.to_json();
|
||||||
|
json_object["Assigned"] = json!(assigned);
|
||||||
|
json_object["Users"] = json!(users);
|
||||||
json_object["Groups"] = json!(groups);
|
json_object["Groups"] = json!(groups);
|
||||||
json_object["Object"] = json!("collectionGroupDetails");
|
json_object["Object"] = json!("collectionAccessDetails");
|
||||||
|
|
||||||
Ok(Json(json_object))
|
Ok(Json(json_object))
|
||||||
}
|
}
|
||||||
@ -652,16 +734,39 @@ async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut
|
|||||||
|
|
||||||
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
let mut ciphers_json = Vec::with_capacity(ciphers.len());
|
||||||
for c in ciphers {
|
for c in ciphers {
|
||||||
ciphers_json.push(c.to_json(host, user_uuid, Some(&cipher_sync_data), conn).await);
|
ciphers_json
|
||||||
|
.push(c.to_json(host, user_uuid, Some(&cipher_sync_data), CipherSyncType::Organization, conn).await);
|
||||||
}
|
}
|
||||||
json!(ciphers_json)
|
json!(ciphers_json)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/users")]
|
#[derive(FromForm)]
|
||||||
async fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
|
struct GetOrgUserData {
|
||||||
|
#[field(name = "includeCollections")]
|
||||||
|
include_collections: Option<bool>,
|
||||||
|
#[field(name = "includeGroups")]
|
||||||
|
include_groups: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// includeCollections
|
||||||
|
// includeGroups
|
||||||
|
#[get("/organizations/<org_id>/users?<data..>")]
|
||||||
|
async fn get_org_users(
|
||||||
|
data: GetOrgUserData,
|
||||||
|
org_id: String,
|
||||||
|
_headers: ManagerHeadersLoose,
|
||||||
|
mut conn: DbConn,
|
||||||
|
) -> Json<Value> {
|
||||||
let mut users_json = Vec::new();
|
let mut users_json = Vec::new();
|
||||||
for u in UserOrganization::find_by_org(&org_id, &mut conn).await {
|
for u in UserOrganization::find_by_org(&org_id, &mut conn).await {
|
||||||
users_json.push(u.to_json_user_details(&mut conn).await);
|
users_json.push(
|
||||||
|
u.to_json_user_details(
|
||||||
|
data.include_collections.unwrap_or(false),
|
||||||
|
data.include_groups.unwrap_or(false),
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Json(json!({
|
Json(json!({
|
||||||
@ -2056,12 +2161,18 @@ async fn _restore_organization_user(
|
|||||||
|
|
||||||
#[get("/organizations/<org_id>/groups")]
|
#[get("/organizations/<org_id>/groups")]
|
||||||
async fn get_groups(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
async fn get_groups(org_id: String, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
||||||
let groups = if CONFIG.org_groups_enabled() {
|
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
||||||
Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
|
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
|
||||||
|
let groups = Group::find_by_organization(&org_id, &mut conn).await;
|
||||||
|
let mut groups_json = Vec::with_capacity(groups.len());
|
||||||
|
for g in groups {
|
||||||
|
groups_json.push(g.to_json_details(&mut conn).await)
|
||||||
|
}
|
||||||
|
groups_json
|
||||||
} else {
|
} else {
|
||||||
// The Bitwarden clients seem to call this API regardless of whether groups are enabled,
|
// The Bitwarden clients seem to call this API regardless of whether groups are enabled,
|
||||||
// so just act as if there are no groups.
|
// so just act as if there are no groups.
|
||||||
Value::Array(Vec::new())
|
Vec::with_capacity(0)
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
@ -2078,6 +2189,7 @@ struct GroupRequest {
|
|||||||
AccessAll: Option<bool>,
|
AccessAll: Option<bool>,
|
||||||
ExternalId: Option<String>,
|
ExternalId: Option<String>,
|
||||||
Collections: Vec<SelectionReadOnly>,
|
Collections: Vec<SelectionReadOnly>,
|
||||||
|
Users: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GroupRequest {
|
impl GroupRequest {
|
||||||
@ -2120,14 +2232,6 @@ impl SelectionReadOnly {
|
|||||||
CollectionGroup::new(self.Id.clone(), groups_uuid, self.ReadOnly, self.HidePasswords)
|
CollectionGroup::new(self.Id.clone(), groups_uuid, self.ReadOnly, self.HidePasswords)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly {
|
|
||||||
SelectionReadOnly {
|
|
||||||
Id: collection_group.collections_uuid.clone(),
|
|
||||||
ReadOnly: collection_group.read_only,
|
|
||||||
HidePasswords: collection_group.hide_passwords,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly {
|
pub fn to_collection_group_details_read_only(collection_group: &CollectionGroup) -> SelectionReadOnly {
|
||||||
SelectionReadOnly {
|
SelectionReadOnly {
|
||||||
Id: collection_group.groups_uuid.clone(),
|
Id: collection_group.groups_uuid.clone(),
|
||||||
@ -2136,6 +2240,14 @@ impl SelectionReadOnly {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_collection_user_details_read_only(collection_user: &CollectionUser) -> SelectionReadOnly {
|
||||||
|
SelectionReadOnly {
|
||||||
|
Id: collection_user.user_uuid.clone(),
|
||||||
|
ReadOnly: collection_user.read_only,
|
||||||
|
HidePasswords: collection_user.hide_passwords,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
json!(self)
|
json!(self)
|
||||||
}
|
}
|
||||||
@ -2171,7 +2283,7 @@ async fn post_groups(
|
|||||||
log_event(
|
log_event(
|
||||||
EventType::GroupCreated as i32,
|
EventType::GroupCreated as i32,
|
||||||
&group.uuid,
|
&group.uuid,
|
||||||
org_id,
|
org_id.clone(),
|
||||||
headers.user.uuid.clone(),
|
headers.user.uuid.clone(),
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&ip.ip,
|
&ip.ip,
|
||||||
@ -2179,7 +2291,7 @@ async fn post_groups(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
add_update_group(group, group_request.Collections, &mut conn).await
|
add_update_group(group, group_request.Collections, group_request.Users, &org_id, &headers, &ip, &mut conn).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[put("/organizations/<org_id>/groups/<group_id>", data = "<data>")]
|
#[put("/organizations/<org_id>/groups/<group_id>", data = "<data>")]
|
||||||
@ -2204,11 +2316,12 @@ async fn put_group(
|
|||||||
let updated_group = group_request.update_group(group)?;
|
let updated_group = group_request.update_group(group)?;
|
||||||
|
|
||||||
CollectionGroup::delete_all_by_group(&group_id, &mut conn).await?;
|
CollectionGroup::delete_all_by_group(&group_id, &mut conn).await?;
|
||||||
|
GroupUser::delete_all_by_group(&group_id, &mut conn).await?;
|
||||||
|
|
||||||
log_event(
|
log_event(
|
||||||
EventType::GroupUpdated as i32,
|
EventType::GroupUpdated as i32,
|
||||||
&updated_group.uuid,
|
&updated_group.uuid,
|
||||||
org_id,
|
org_id.clone(),
|
||||||
headers.user.uuid.clone(),
|
headers.user.uuid.clone(),
|
||||||
headers.device.atype,
|
headers.device.atype,
|
||||||
&ip.ip,
|
&ip.ip,
|
||||||
@ -2216,18 +2329,42 @@ async fn put_group(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
add_update_group(updated_group, group_request.Collections, &mut conn).await
|
add_update_group(updated_group, group_request.Collections, group_request.Users, &org_id, &headers, &ip, &mut conn)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_update_group(mut group: Group, collections: Vec<SelectionReadOnly>, conn: &mut DbConn) -> JsonResult {
|
async fn add_update_group(
|
||||||
|
mut group: Group,
|
||||||
|
collections: Vec<SelectionReadOnly>,
|
||||||
|
users: Vec<String>,
|
||||||
|
org_id: &str,
|
||||||
|
headers: &AdminHeaders,
|
||||||
|
ip: &ClientIp,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> JsonResult {
|
||||||
group.save(conn).await?;
|
group.save(conn).await?;
|
||||||
|
|
||||||
for selection_read_only_request in collections {
|
for selection_read_only_request in collections {
|
||||||
let mut collection_group = selection_read_only_request.to_collection_group(group.uuid.clone());
|
let mut collection_group = selection_read_only_request.to_collection_group(group.uuid.clone());
|
||||||
|
|
||||||
collection_group.save(conn).await?;
|
collection_group.save(conn).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for assigned_user_id in users {
|
||||||
|
let mut user_entry = GroupUser::new(group.uuid.clone(), assigned_user_id.clone());
|
||||||
|
user_entry.save(conn).await?;
|
||||||
|
|
||||||
|
log_event(
|
||||||
|
EventType::OrganizationUserUpdatedGroups as i32,
|
||||||
|
&assigned_user_id,
|
||||||
|
String::from(org_id),
|
||||||
|
headers.user.uuid.clone(),
|
||||||
|
headers.device.atype,
|
||||||
|
&ip.ip,
|
||||||
|
conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Id": group.uuid,
|
"Id": group.uuid,
|
||||||
"OrganizationId": group.organizations_uuid,
|
"OrganizationId": group.organizations_uuid,
|
||||||
@ -2248,20 +2385,7 @@ async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHea
|
|||||||
_ => err!("Group could not be found!"),
|
_ => err!("Group could not be found!"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let collections_groups = CollectionGroup::find_by_group(&group_id, &mut conn)
|
Ok(Json(group.to_json_details(&mut conn).await))
|
||||||
.await
|
|
||||||
.iter()
|
|
||||||
.map(|entry| SelectionReadOnly::to_group_details_read_only(entry).to_json())
|
|
||||||
.collect::<Value>();
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
|
||||||
"Id": group.uuid,
|
|
||||||
"OrganizationId": group.organizations_uuid,
|
|
||||||
"Name": group.name,
|
|
||||||
"AccessAll": group.access_all,
|
|
||||||
"ExternalId": group.get_external_id(),
|
|
||||||
"Collections": collections_groups
|
|
||||||
})))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/organizations/<org_id>/groups/<group_id>/delete")]
|
#[post("/organizations/<org_id>/groups/<group_id>/delete")]
|
||||||
|
@ -6,7 +6,7 @@ use super::{
|
|||||||
Attachment, CollectionCipher, Favorite, FolderCipher, Group, User, UserOrgStatus, UserOrgType, UserOrganization,
|
Attachment, CollectionCipher, Favorite, FolderCipher, Group, User, UserOrgStatus, UserOrgType, UserOrganization,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::api::core::{CipherData, CipherSyncData};
|
use crate::api::core::{CipherData, CipherSyncData, CipherSyncType};
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
@ -114,6 +114,7 @@ impl Cipher {
|
|||||||
host: &str,
|
host: &str,
|
||||||
user_uuid: &str,
|
user_uuid: &str,
|
||||||
cipher_sync_data: Option<&CipherSyncData>,
|
cipher_sync_data: Option<&CipherSyncData>,
|
||||||
|
sync_type: CipherSyncType,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> Value {
|
) -> Value {
|
||||||
use crate::util::format_date;
|
use crate::util::format_date;
|
||||||
@ -134,12 +135,18 @@ impl Cipher {
|
|||||||
let password_history_json =
|
let password_history_json =
|
||||||
self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
||||||
|
|
||||||
let (read_only, hide_passwords) = match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await {
|
// We don't need these values at all for Organizational syncs
|
||||||
Some((ro, hp)) => (ro, hp),
|
// Skip any other database calls if this is the case and just return false.
|
||||||
None => {
|
let (read_only, hide_passwords) = if sync_type == CipherSyncType::User {
|
||||||
error!("Cipher ownership assertion failure");
|
match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await {
|
||||||
(true, true)
|
Some((ro, hp)) => (ro, hp),
|
||||||
|
None => {
|
||||||
|
error!("Cipher ownership assertion failure");
|
||||||
|
(true, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
(false, false)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the type_data or a default to an empty json object '{}'.
|
// Get the type_data or a default to an empty json object '{}'.
|
||||||
@ -192,8 +199,6 @@ impl Cipher {
|
|||||||
"CreationDate": format_date(&self.created_at),
|
"CreationDate": format_date(&self.created_at),
|
||||||
"RevisionDate": format_date(&self.updated_at),
|
"RevisionDate": format_date(&self.updated_at),
|
||||||
"DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
|
"DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))),
|
||||||
"FolderId": if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string() ) } else { self.get_folder_uuid(user_uuid, conn).await },
|
|
||||||
"Favorite": if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data.cipher_favorites.contains(&self.uuid) } else { self.is_favorite(user_uuid, conn).await },
|
|
||||||
"Reprompt": self.reprompt.unwrap_or(RepromptType::None as i32),
|
"Reprompt": self.reprompt.unwrap_or(RepromptType::None as i32),
|
||||||
"OrganizationId": self.organization_uuid,
|
"OrganizationId": self.organization_uuid,
|
||||||
"Attachments": attachments_json,
|
"Attachments": attachments_json,
|
||||||
@ -210,12 +215,6 @@ impl Cipher {
|
|||||||
|
|
||||||
"Data": data_json,
|
"Data": data_json,
|
||||||
|
|
||||||
// These values are true by default, but can be false if the
|
|
||||||
// cipher belongs to a collection where the org owner has enabled
|
|
||||||
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
|
||||||
"Edit": !read_only,
|
|
||||||
"ViewPassword": !hide_passwords,
|
|
||||||
|
|
||||||
"PasswordHistory": password_history_json,
|
"PasswordHistory": password_history_json,
|
||||||
|
|
||||||
// All Cipher types are included by default as null, but only the matching one will be populated
|
// All Cipher types are included by default as null, but only the matching one will be populated
|
||||||
@ -225,6 +224,27 @@ impl Cipher {
|
|||||||
"Identity": null,
|
"Identity": null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// These values are only needed for user/default syncs
|
||||||
|
// Not during an organizational sync like `get_org_details`
|
||||||
|
// 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())
|
||||||
|
} else {
|
||||||
|
self.get_folder_uuid(user_uuid, conn).await
|
||||||
|
});
|
||||||
|
json_object["Favorite"] = json!(if let Some(cipher_sync_data) = cipher_sync_data {
|
||||||
|
cipher_sync_data.cipher_favorites.contains(&self.uuid)
|
||||||
|
} else {
|
||||||
|
self.is_favorite(user_uuid, conn).await
|
||||||
|
});
|
||||||
|
// These values are true by default, but can be false if the
|
||||||
|
// cipher belongs to a collection or group where the org owner has enabled
|
||||||
|
// the "Read Only" or "Hide Passwords" restrictions for the user.
|
||||||
|
json_object["Edit"] = json!(!read_only);
|
||||||
|
json_object["ViewPassword"] = json!(!hide_passwords);
|
||||||
|
}
|
||||||
|
|
||||||
let key = match self.atype {
|
let key = match self.atype {
|
||||||
1 => "Login",
|
1 => "Login",
|
||||||
2 => "SecureNote",
|
2 => "SecureNote",
|
||||||
@ -740,6 +760,7 @@ impl Cipher {
|
|||||||
.or_filter(groups::access_all.eq(true)) //Access via group
|
.or_filter(groups::access_all.eq(true)) //Access via group
|
||||||
.or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
|
.or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group
|
||||||
.select(ciphers_collections::all_columns)
|
.select(ciphers_collections::all_columns)
|
||||||
|
.distinct()
|
||||||
.load::<(String, String)>(conn).unwrap_or_default()
|
.load::<(String, String)>(conn).unwrap_or_default()
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
@ -396,6 +396,19 @@ impl CollectionUser {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_organization(org_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||||
|
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))
|
||||||
|
.inner_join(users_organizations::table.on(users_organizations::user_uuid.eq(users_collections::user_uuid)))
|
||||||
|
.select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords))
|
||||||
|
.load::<CollectionUserDb>(conn)
|
||||||
|
.expect("Error loading users_collections")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn save(
|
pub async fn save(
|
||||||
user_uuid: &str,
|
user_uuid: &str,
|
||||||
collection_uuid: &str,
|
collection_uuid: &str,
|
||||||
@ -479,6 +492,21 @@ impl CollectionUser {
|
|||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_collection_swap_user_uuid_with_org_user_uuid(
|
||||||
|
collection_uuid: &str,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Vec<Self> {
|
||||||
|
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)))
|
||||||
|
.select((users_organizations::uuid, users_collections::collection_uuid, users_collections::read_only, users_collections::hide_passwords))
|
||||||
|
.load::<CollectionUserDb>(conn)
|
||||||
|
.expect("Error loading users_collections")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_by_collection_and_user(
|
pub async fn find_by_collection_and_user(
|
||||||
collection_uuid: &str,
|
collection_uuid: &str,
|
||||||
user_uuid: &str,
|
user_uuid: &str,
|
||||||
|
@ -64,7 +64,32 @@ impl Group {
|
|||||||
"AccessAll": self.access_all,
|
"AccessAll": self.access_all,
|
||||||
"ExternalId": self.external_id,
|
"ExternalId": self.external_id,
|
||||||
"CreationDate": format_date(&self.creation_date),
|
"CreationDate": format_date(&self.creation_date),
|
||||||
"RevisionDate": format_date(&self.revision_date)
|
"RevisionDate": format_date(&self.revision_date),
|
||||||
|
"Object": "group"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn to_json_details(&self, conn: &mut DbConn) -> Value {
|
||||||
|
let collections_groups: Vec<Value> = CollectionGroup::find_by_group(&self.uuid, conn)
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|entry| {
|
||||||
|
json!({
|
||||||
|
"Id": entry.collections_uuid,
|
||||||
|
"ReadOnly": entry.read_only,
|
||||||
|
"HidePasswords": entry.hide_passwords
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"Id": self.uuid,
|
||||||
|
"OrganizationId": self.organizations_uuid,
|
||||||
|
"Name": self.name,
|
||||||
|
"AccessAll": self.access_all,
|
||||||
|
"ExternalId": self.external_id,
|
||||||
|
"Collections": collections_groups,
|
||||||
|
"Object": "groupDetails"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,7 +326,7 @@ impl UserOrganization {
|
|||||||
// TODO: Add support for Custom User Roles
|
// TODO: Add support for Custom User Roles
|
||||||
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
|
// See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
|
||||||
// "Permissions": {
|
// "Permissions": {
|
||||||
// "AccessEventLogs": false, // Not supported
|
// "AccessEventLogs": false,
|
||||||
// "AccessImportExport": false,
|
// "AccessImportExport": false,
|
||||||
// "AccessReports": false,
|
// "AccessReports": false,
|
||||||
// "ManageAllCollections": false,
|
// "ManageAllCollections": false,
|
||||||
@ -337,9 +337,9 @@ impl UserOrganization {
|
|||||||
// "editAssignedCollections": false,
|
// "editAssignedCollections": false,
|
||||||
// "deleteAssignedCollections": false,
|
// "deleteAssignedCollections": false,
|
||||||
// "ManageCiphers": false,
|
// "ManageCiphers": false,
|
||||||
// "ManageGroups": false, // Not supported
|
// "ManageGroups": false,
|
||||||
// "ManagePolicies": false,
|
// "ManagePolicies": false,
|
||||||
// "ManageResetPassword": false, // Not supported
|
// "ManageResetPassword": false,
|
||||||
// "ManageSso": false, // Not supported
|
// "ManageSso": false, // Not supported
|
||||||
// "ManageUsers": false,
|
// "ManageUsers": false,
|
||||||
// "ManageScim": false, // Not supported (Not AGPLv3 Licensed)
|
// "ManageScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||||
@ -358,7 +358,12 @@ impl UserOrganization {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn to_json_user_details(&self, conn: &mut DbConn) -> Value {
|
pub async fn to_json_user_details(
|
||||||
|
&self,
|
||||||
|
include_collections: bool,
|
||||||
|
include_groups: bool,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) -> Value {
|
||||||
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
|
let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap();
|
||||||
|
|
||||||
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
|
// Because BitWarden want the status to be -1 for revoked users we need to catch that here.
|
||||||
@ -371,11 +376,37 @@ impl UserOrganization {
|
|||||||
|
|
||||||
let twofactor_enabled = !TwoFactor::find_by_user(&user.uuid, conn).await.is_empty();
|
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()
|
||||||
|
} else {
|
||||||
|
// The Bitwarden clients seem to call this API regardless of whether groups are enabled,
|
||||||
|
// so just act as if there are no groups.
|
||||||
|
Vec::with_capacity(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let collections: Vec<Value> = if include_collections {
|
||||||
|
CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn)
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.map(|cu| {
|
||||||
|
json!({
|
||||||
|
"Id": cu.collection_uuid,
|
||||||
|
"ReadOnly": cu.read_only,
|
||||||
|
"HidePasswords": cu.hide_passwords,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
Vec::with_capacity(0)
|
||||||
|
};
|
||||||
|
|
||||||
json!({
|
json!({
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"UserId": self.user_uuid,
|
"UserId": self.user_uuid,
|
||||||
"Name": user.name,
|
"Name": user.name,
|
||||||
"Email": user.email,
|
"Email": user.email,
|
||||||
|
"Groups": groups,
|
||||||
|
"Collections": collections,
|
||||||
|
|
||||||
"Status": status,
|
"Status": status,
|
||||||
"Type": self.atype,
|
"Type": self.atype,
|
||||||
|
Loading…
Reference in New Issue
Block a user