mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-11 11:15:58 +03:00
group support
This commit is contained in:
@@ -1499,6 +1499,8 @@ pub struct CipherSyncData {
|
||||
pub cipher_collections: HashMap<String, Vec<String>>,
|
||||
pub user_organizations: HashMap<String, UserOrganization>,
|
||||
pub user_collections: HashMap<String, CollectionUser>,
|
||||
pub user_collections_groups: HashMap<String, CollectionGroup>,
|
||||
pub user_group_full_access_for_organizations: HashSet<String>,
|
||||
}
|
||||
|
||||
pub enum CipherSyncType {
|
||||
@@ -1554,6 +1556,16 @@ impl CipherSyncData {
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
// Generate a HashMap with the collections_uuid as key and the CollectionGroup record
|
||||
let user_collections_groups = stream::iter(CollectionGroup::find_by_user(user_uuid, conn).await)
|
||||
.map(|collection_group| (collection_group.collections_uuid.clone(), collection_group))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
// Get all organizations that the user has full access to via group assignement
|
||||
let user_group_full_access_for_organizations =
|
||||
stream::iter(Group::gather_user_organizations_full_access(user_uuid, conn).await).collect().await;
|
||||
|
||||
Self {
|
||||
cipher_attachments,
|
||||
cipher_folders,
|
||||
@@ -1561,6 +1573,8 @@ impl CipherSyncData {
|
||||
cipher_collections,
|
||||
user_organizations,
|
||||
user_collections,
|
||||
user_collections_groups,
|
||||
user_group_full_access_for_organizations,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,8 @@ use serde_json::Value;
|
||||
use crate::{
|
||||
api::{
|
||||
core::{CipherSyncData, CipherSyncType},
|
||||
EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType,
|
||||
ApiResult, EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, JsonVec, Notify, NumberOrString, PasswordData,
|
||||
UpdateType,
|
||||
},
|
||||
auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
|
||||
db::{models::*, DbConn},
|
||||
@@ -71,6 +72,21 @@ pub fn routes() -> Vec<Route> {
|
||||
bulk_activate_organization_user,
|
||||
restore_organization_user,
|
||||
bulk_restore_organization_user,
|
||||
get_groups,
|
||||
post_groups,
|
||||
get_group,
|
||||
put_group,
|
||||
post_group,
|
||||
get_group_details,
|
||||
delete_group,
|
||||
post_delete_group,
|
||||
get_group_users,
|
||||
put_group_users,
|
||||
get_user_groups,
|
||||
post_user_groups,
|
||||
put_user_groups,
|
||||
delete_group_user,
|
||||
post_delete_group_user,
|
||||
get_org_export
|
||||
]
|
||||
}
|
||||
@@ -94,10 +110,19 @@ struct OrganizationUpdateData {
|
||||
Name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct NewCollectionData {
|
||||
Name: String,
|
||||
Groups: Vec<NewCollectionGroupData>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct NewCollectionGroupData {
|
||||
HidePasswords: bool,
|
||||
Id: String,
|
||||
ReadOnly: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -287,6 +312,12 @@ async fn post_organization_collections(
|
||||
let collection = Collection::new(org.uuid, data.Name);
|
||||
collection.save(&conn).await?;
|
||||
|
||||
for group in data.Groups {
|
||||
CollectionGroup::new(collection.uuid.clone(), group.Id, group.ReadOnly, group.HidePasswords)
|
||||
.save(&conn)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// If the user doesn't have access to all collections, only in case of a Manger,
|
||||
// then we need to save the creating user uuid (Manager) to the users_collection table.
|
||||
// Else the user will not have access to his own created collection.
|
||||
@@ -335,6 +366,12 @@ async fn post_organization_collection_update(
|
||||
collection.name = data.Name;
|
||||
collection.save(&conn).await?;
|
||||
|
||||
CollectionGroup::delete_all_by_collection(&col_id, &conn).await?;
|
||||
|
||||
for group in data.Groups {
|
||||
CollectionGroup::new(col_id.clone(), group.Id, group.ReadOnly, group.HidePasswords).save(&conn).await?;
|
||||
}
|
||||
|
||||
Ok(Json(collection.to_json()))
|
||||
}
|
||||
|
||||
@@ -430,7 +467,19 @@ async fn get_org_collection_detail(
|
||||
err!("Collection is not owned by organization")
|
||||
}
|
||||
|
||||
Ok(Json(collection.to_json()))
|
||||
let groups: Vec<Value> = CollectionGroup::find_by_collection(&collection.uuid, &conn)
|
||||
.await
|
||||
.iter()
|
||||
.map(|collection_group| {
|
||||
SelectionReadOnly::to_collection_group_details_read_only(collection_group).to_json()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut json_object = collection.to_json();
|
||||
json_object["Groups"] = json!(groups);
|
||||
json_object["Object"] = json!("collectionGroupDetails");
|
||||
|
||||
Ok(Json(json_object))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1704,6 +1753,324 @@ async fn _restore_organization_user(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/groups")]
|
||||
async fn get_groups(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||
let groups = Group::find_by_organization(&org_id, &conn).await.iter().map(Group::to_json).collect::<Value>();
|
||||
|
||||
Ok(Json(json!({
|
||||
"Data": groups,
|
||||
"Object": "list",
|
||||
"ContinuationToken": null,
|
||||
})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct GroupRequest {
|
||||
Name: String,
|
||||
AccessAll: Option<bool>,
|
||||
ExternalId: Option<String>,
|
||||
Collections: Vec<SelectionReadOnly>,
|
||||
}
|
||||
|
||||
impl GroupRequest {
|
||||
pub fn to_group(&self, organizations_uuid: &str) -> ApiResult<Group> {
|
||||
match self.AccessAll {
|
||||
Some(access_all_value) => Ok(Group::new(
|
||||
organizations_uuid.to_owned(),
|
||||
self.Name.clone(),
|
||||
access_all_value,
|
||||
self.ExternalId.clone(),
|
||||
)),
|
||||
_ => err!("Could not convert GroupRequest to Group, because AccessAll has no value!"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_group(&self, mut group: Group) -> ApiResult<Group> {
|
||||
match self.AccessAll {
|
||||
Some(access_all_value) => {
|
||||
group.name = self.Name.clone();
|
||||
group.access_all = access_all_value;
|
||||
group.set_external_id(self.ExternalId.clone());
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
_ => err!("Could not update group, because AccessAll has no value!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct SelectionReadOnly {
|
||||
Id: String,
|
||||
ReadOnly: bool,
|
||||
HidePasswords: bool,
|
||||
}
|
||||
|
||||
impl SelectionReadOnly {
|
||||
pub fn to_collection_group(&self, groups_uuid: String) -> CollectionGroup {
|
||||
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 {
|
||||
SelectionReadOnly {
|
||||
Id: collection_group.groups_uuid.clone(),
|
||||
ReadOnly: collection_group.read_only,
|
||||
HidePasswords: collection_group.hide_passwords,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
json!(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/organizations/<_org_id>/groups/<group_id>", data = "<data>")]
|
||||
async fn post_group(
|
||||
_org_id: String,
|
||||
group_id: String,
|
||||
data: JsonUpcase<GroupRequest>,
|
||||
_headers: AdminHeaders,
|
||||
conn: DbConn,
|
||||
) -> JsonResult {
|
||||
put_group(_org_id, group_id, data, _headers, conn).await
|
||||
}
|
||||
|
||||
#[post("/organizations/<org_id>/groups", data = "<data>")]
|
||||
async fn post_groups(
|
||||
org_id: String,
|
||||
_headers: AdminHeaders,
|
||||
data: JsonUpcase<GroupRequest>,
|
||||
conn: DbConn,
|
||||
) -> JsonResult {
|
||||
let group_request = data.into_inner().data;
|
||||
let group = group_request.to_group(&org_id)?;
|
||||
|
||||
add_update_group(group, group_request.Collections, &conn).await
|
||||
}
|
||||
|
||||
#[put("/organizations/<_org_id>/groups/<group_id>", data = "<data>")]
|
||||
async fn put_group(
|
||||
_org_id: String,
|
||||
group_id: String,
|
||||
data: JsonUpcase<GroupRequest>,
|
||||
_headers: AdminHeaders,
|
||||
conn: DbConn,
|
||||
) -> JsonResult {
|
||||
let group = match Group::find_by_uuid(&group_id, &conn).await {
|
||||
Some(group) => group,
|
||||
None => err!("Group not found"),
|
||||
};
|
||||
|
||||
let group_request = data.into_inner().data;
|
||||
let updated_group = group_request.update_group(group)?;
|
||||
|
||||
CollectionGroup::delete_all_by_group(&group_id, &conn).await?;
|
||||
|
||||
add_update_group(updated_group, group_request.Collections, &conn).await
|
||||
}
|
||||
|
||||
async fn add_update_group(mut group: Group, collections: Vec<SelectionReadOnly>, conn: &DbConn) -> JsonResult {
|
||||
group.save(conn).await?;
|
||||
|
||||
for selection_read_only_request in collections {
|
||||
let mut collection_group = selection_read_only_request.to_collection_group(group.uuid.clone());
|
||||
|
||||
collection_group.save(conn).await?;
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
"Id": group.uuid,
|
||||
"OrganizationId": group.organizations_uuid,
|
||||
"Name": group.name,
|
||||
"AccessAll": group.access_all,
|
||||
"ExternalId": group.get_external_id()
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/organizations/<_org_id>/groups/<group_id>/details")]
|
||||
async fn get_group_details(_org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||
let group = match Group::find_by_uuid(&group_id, &conn).await {
|
||||
Some(group) => group,
|
||||
_ => err!("Group could not be found!"),
|
||||
};
|
||||
|
||||
let collections_groups = CollectionGroup::find_by_group(&group_id, &conn)
|
||||
.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")]
|
||||
async fn post_delete_group(org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
|
||||
delete_group(org_id, group_id, _headers, conn).await
|
||||
}
|
||||
|
||||
#[delete("/organizations/<_org_id>/groups/<group_id>")]
|
||||
async fn delete_group(_org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult {
|
||||
let group = match Group::find_by_uuid(&group_id, &conn).await {
|
||||
Some(group) => group,
|
||||
_ => err!("Group not found"),
|
||||
};
|
||||
|
||||
group.delete(&conn).await
|
||||
}
|
||||
|
||||
#[get("/organizations/<_org_id>/groups/<group_id>")]
|
||||
async fn get_group(_org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||
let group = match Group::find_by_uuid(&group_id, &conn).await {
|
||||
Some(group) => group,
|
||||
_ => err!("Group not found"),
|
||||
};
|
||||
|
||||
Ok(Json(group.to_json()))
|
||||
}
|
||||
|
||||
#[get("/organizations/<_org_id>/groups/<group_id>/users")]
|
||||
async fn get_group_users(_org_id: String, group_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||
match Group::find_by_uuid(&group_id, &conn).await {
|
||||
Some(_) => { /* Do nothing */ }
|
||||
_ => err!("Group could not be found!"),
|
||||
};
|
||||
|
||||
let group_users: Vec<String> = GroupUser::find_by_group(&group_id, &conn)
|
||||
.await
|
||||
.iter()
|
||||
.map(|entry| entry.users_organizations_uuid.clone())
|
||||
.collect();
|
||||
|
||||
Ok(Json(json!(group_users)))
|
||||
}
|
||||
|
||||
#[put("/organizations/<_org_id>/groups/<group_id>/users", data = "<data>")]
|
||||
async fn put_group_users(
|
||||
_org_id: String,
|
||||
group_id: String,
|
||||
_headers: AdminHeaders,
|
||||
data: JsonVec<String>,
|
||||
conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
match Group::find_by_uuid(&group_id, &conn).await {
|
||||
Some(_) => { /* Do nothing */ }
|
||||
_ => err!("Group could not be found!"),
|
||||
};
|
||||
|
||||
GroupUser::delete_all_by_group(&group_id, &conn).await?;
|
||||
|
||||
let assigned_user_ids = data.into_inner();
|
||||
for assigned_user_id in assigned_user_ids {
|
||||
let mut user_entry = GroupUser::new(group_id.clone(), assigned_user_id);
|
||||
user_entry.save(&conn).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[get("/organizations/<_org_id>/users/<user_id>/groups")]
|
||||
async fn get_user_groups(_org_id: String, user_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult {
|
||||
match UserOrganization::find_by_uuid(&user_id, &conn).await {
|
||||
Some(_) => { /* Do nothing */ }
|
||||
_ => err!("User could not be found!"),
|
||||
};
|
||||
|
||||
let user_groups: Vec<String> =
|
||||
GroupUser::find_by_user(&user_id, &conn).await.iter().map(|entry| entry.groups_uuid.clone()).collect();
|
||||
|
||||
Ok(Json(json!(user_groups)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(non_snake_case)]
|
||||
struct OrganizationUserUpdateGroupsRequest {
|
||||
GroupIds: Vec<String>,
|
||||
}
|
||||
|
||||
#[post("/organizations/<_org_id>/users/<user_id>/groups", data = "<data>")]
|
||||
async fn post_user_groups(
|
||||
_org_id: String,
|
||||
user_id: String,
|
||||
data: JsonUpcase<OrganizationUserUpdateGroupsRequest>,
|
||||
_headers: AdminHeaders,
|
||||
conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
put_user_groups(_org_id, user_id, data, _headers, conn).await
|
||||
}
|
||||
|
||||
#[put("/organizations/<_org_id>/users/<user_id>/groups", data = "<data>")]
|
||||
async fn put_user_groups(
|
||||
_org_id: String,
|
||||
user_id: String,
|
||||
data: JsonUpcase<OrganizationUserUpdateGroupsRequest>,
|
||||
_headers: AdminHeaders,
|
||||
conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
match UserOrganization::find_by_uuid(&user_id, &conn).await {
|
||||
Some(_) => { /* Do nothing */ }
|
||||
_ => err!("User could not be found!"),
|
||||
};
|
||||
|
||||
GroupUser::delete_all_by_user(&user_id, &conn).await?;
|
||||
|
||||
let assigned_group_ids = data.into_inner().data;
|
||||
for assigned_group_id in assigned_group_ids.GroupIds {
|
||||
let mut group_user = GroupUser::new(assigned_group_id.clone(), user_id.clone());
|
||||
group_user.save(&conn).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/organizations/<org_id>/groups/<group_id>/delete-user/<user_id>")]
|
||||
async fn post_delete_group_user(
|
||||
org_id: String,
|
||||
group_id: String,
|
||||
user_id: String,
|
||||
headers: AdminHeaders,
|
||||
conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
delete_group_user(org_id, group_id, user_id, headers, conn).await
|
||||
}
|
||||
|
||||
#[delete("/organizations/<_org_id>/groups/<group_id>/users/<user_id>")]
|
||||
async fn delete_group_user(
|
||||
_org_id: String,
|
||||
group_id: String,
|
||||
user_id: String,
|
||||
_headers: AdminHeaders,
|
||||
conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
match UserOrganization::find_by_uuid(&user_id, &conn).await {
|
||||
Some(_) => { /* Do nothing */ }
|
||||
_ => err!("User could not be found!"),
|
||||
};
|
||||
|
||||
match Group::find_by_uuid(&group_id, &conn).await {
|
||||
Some(_) => { /* Do nothing */ }
|
||||
_ => err!("Group could not be found!"),
|
||||
};
|
||||
|
||||
GroupUser::delete_by_group_id_and_user_id(&group_id, &user_id, &conn).await
|
||||
}
|
||||
|
||||
// This is a new function active since the v2022.9.x clients.
|
||||
// It combines the previous two calls done before.
|
||||
// We call those two functions here and combine them our selfs.
|
||||
|
@@ -33,6 +33,7 @@ pub type EmptyResult = ApiResult<()>;
|
||||
|
||||
type JsonUpcase<T> = Json<util::UpCase<T>>;
|
||||
type JsonUpcaseVec<T> = Json<Vec<util::UpCase<T>>>;
|
||||
type JsonVec<T> = Json<Vec<T>>;
|
||||
|
||||
// Common structs representing JSON data received
|
||||
#[derive(Deserialize)]
|
||||
|
Reference in New Issue
Block a user