Skip to content

Commit

Permalink
Add manage role for collections and groups (#5386)
Browse files Browse the repository at this point in the history
* Add manage role for collections and groups

This commit will add the manage role/column to collections and groups.
We need this to allow users part of a collection either directly or via groups to be able to delete ciphers.
Without this, they are only able to either edit or view them when using new clients, since these check the manage role.

Still trying to keep it compatible with previous versions and able to revert to an older Vaultwarden version and the `access_all` feature of the older installations.
In a future version we should really check and fix these rights and create some kind of migration step to also remove the `access_all` feature and convert that to a `manage` option.
But this commit at least creates the base for this already.

This should resolve #5367

Signed-off-by: BlackDex <[email protected]>

* Fix an issue with access_all

If owners or admins do not have the `access_all` flag set, in case they do not want to see all collection on the password manager view, they didn't see any collections at all anymore.

This should fix that they are still able to view all the collections and have access to it.

Signed-off-by: BlackDex <[email protected]>

---------

Signed-off-by: BlackDex <[email protected]>
  • Loading branch information
BlackDex authored Jan 21, 2025
1 parent ef2695d commit d1dee04
Show file tree
Hide file tree
Showing 16 changed files with 184 additions and 70 deletions.
Empty file.
5 changes: 5 additions & 0 deletions migrations/mysql/2025-01-09-172300_add_manage/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE users_collections
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;

ALTER TABLE collections_groups
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
Empty file.
5 changes: 5 additions & 0 deletions migrations/postgresql/2025-01-09-172300_add_manage/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE users_collections
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;

ALTER TABLE collections_groups
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT FALSE;
Empty file.
5 changes: 5 additions & 0 deletions migrations/sqlite/2025-01-09-172300_add_manage/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TABLE users_collections
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE

ALTER TABLE collections_groups
ADD COLUMN manage BOOLEAN NOT NULL DEFAULT 0; -- FALSE
52 changes: 32 additions & 20 deletions src/api/core/organizations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ struct NewCollectionGroupData {
hide_passwords: bool,
id: GroupId,
read_only: bool,
manage: bool,
}

#[derive(Deserialize)]
Expand All @@ -148,6 +149,7 @@ struct NewCollectionMemberData {
hide_passwords: bool,
id: MembershipId,
read_only: bool,
manage: bool,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -362,18 +364,13 @@ async fn get_org_collections_details(
|| (CONFIG.org_groups_enabled()
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await);

// Not assigned collections should not be returned
if !assigned {
continue;
}

// get the users assigned directly to the given collection
let users: Vec<Value> = col_users
.iter()
.filter(|collection_user| collection_user.collection_uuid == col.uuid)
.map(|collection_user| {
collection_user.to_json_details_for_user(
*membership_type.get(&collection_user.membership_uuid).unwrap_or(&(MembershipType::User as i32)),
.filter(|collection_member| collection_member.collection_uuid == col.uuid)
.map(|collection_member| {
collection_member.to_json_details_for_member(
*membership_type.get(&collection_member.membership_uuid).unwrap_or(&(MembershipType::User as i32)),
)
})
.collect();
Expand Down Expand Up @@ -437,7 +434,7 @@ async fn post_organization_collections(
.await;

for group in data.groups {
CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords)
CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
.save(&mut conn)
.await?;
}
Expand All @@ -451,12 +448,19 @@ async fn post_organization_collections(
continue;
}

CollectionUser::save(&member.user_uuid, &collection.uuid, user.read_only, user.hide_passwords, &mut conn)
.await?;
CollectionUser::save(
&member.user_uuid,
&collection.uuid,
user.read_only,
user.hide_passwords,
user.manage,
&mut conn,
)
.await?;
}

if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all {
CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, &mut conn).await?;
CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, false, &mut conn).await?;
}

Ok(Json(collection.to_json()))
Expand Down Expand Up @@ -513,7 +517,9 @@ async fn post_organization_collection_update(
CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?;

for group in data.groups {
CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords).save(&mut conn).await?;
CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
.save(&mut conn)
.await?;
}

CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?;
Expand All @@ -527,7 +533,8 @@ async fn post_organization_collection_update(
continue;
}

CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, &mut conn).await?;
CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, user.manage, &mut conn)
.await?;
}

Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await))
Expand Down Expand Up @@ -685,10 +692,10 @@ async fn get_org_collection_detail(
CollectionUser::find_by_collection_swap_user_uuid_with_member_uuid(&collection.uuid, &mut conn)
.await
.iter()
.map(|collection_user| {
collection_user.to_json_details_for_user(
.map(|collection_member| {
collection_member.to_json_details_for_member(
*membership_type
.get(&collection_user.membership_uuid)
.get(&collection_member.membership_uuid)
.unwrap_or(&(MembershipType::User as i32)),
)
})
Expand Down Expand Up @@ -758,7 +765,7 @@ async fn put_collection_users(
continue;
}

CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, &mut conn).await?;
CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, d.manage, &mut conn).await?;
}

Ok(())
Expand Down Expand Up @@ -866,6 +873,7 @@ struct CollectionData {
id: CollectionId,
read_only: bool,
hide_passwords: bool,
manage: bool,
}

#[derive(Deserialize)]
Expand All @@ -874,6 +882,7 @@ struct MembershipData {
id: MembershipId,
read_only: bool,
hide_passwords: bool,
manage: bool,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -1012,6 +1021,7 @@ async fn send_invite(
&collection.uuid,
col.read_only,
col.hide_passwords,
col.manage,
&mut conn,
)
.await?;
Expand Down Expand Up @@ -1504,6 +1514,7 @@ async fn edit_member(
&collection.uuid,
col.read_only,
col.hide_passwords,
col.manage,
&mut conn,
)
.await?;
Expand Down Expand Up @@ -2475,11 +2486,12 @@ struct SelectedCollection {
id: CollectionId,
read_only: bool,
hide_passwords: bool,
manage: bool,
}

impl SelectedCollection {
pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup {
CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords)
CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage)
}
}

Expand Down
4 changes: 1 addition & 3 deletions src/api/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,7 @@ fn get_favicons_node(dom: Tokenizer<StringReader<'_>, FaviconEmitter>, icons: &m
TAG_HEAD if token.closing => {
break;
}
_ => {
continue;
}
_ => {}
}
}

Expand Down
2 changes: 0 additions & 2 deletions src/api/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,6 @@ fn websockets_hub<'r>(

if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
yield Message::binary(INITIAL_RESPONSE);
continue;
}
}

Expand Down Expand Up @@ -225,7 +224,6 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R

if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
yield Message::binary(INITIAL_RESPONSE);
continue;
}
}

Expand Down
50 changes: 30 additions & 20 deletions src/db/models/cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,16 @@ impl Cipher {

// We don't need these values at all for Organizational syncs
// Skip any other database calls if this is the case and just return false.
let (read_only, hide_passwords) = if sync_type == CipherSyncType::User {
let (read_only, hide_passwords, _) = if sync_type == CipherSyncType::User {
match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await {
Some((ro, hp)) => (ro, hp),
Some((ro, hp, mn)) => (ro, hp, mn),
None => {
error!("Cipher ownership assertion failure");
(true, true)
(true, true, false)
}
}
} else {
(false, false)
(false, false, false)
};

let fields_json: Vec<_> = self
Expand Down Expand Up @@ -567,36 +567,36 @@ impl Cipher {
/// Returns the user's access restrictions to this cipher. A return value
/// of None means that this cipher does not belong to the user, and is
/// not in any collection the user has access to. Otherwise, the user has
/// access to this cipher, and Some(read_only, hide_passwords) represents
/// access to this cipher, and Some(read_only, hide_passwords, manage) represents
/// the access restrictions.
pub async fn get_access_restrictions(
&self,
user_uuid: &UserId,
cipher_sync_data: Option<&CipherSyncData>,
conn: &mut DbConn,
) -> Option<(bool, bool)> {
) -> Option<(bool, bool, bool)> {
// Check whether this cipher is directly owned by the user, or is in
// a collection that the user has full access to. If so, there are no
// access restrictions.
if self.is_owned_by_user(user_uuid)
|| self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await
|| self.is_in_full_access_group(user_uuid, cipher_sync_data, conn).await
{
return Some((false, false));
return Some((false, false, true));
}

let rows = if let Some(cipher_sync_data) = cipher_sync_data {
let mut rows: Vec<(bool, bool)> = Vec::new();
let mut rows: Vec<(bool, bool, bool)> = Vec::new();
if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) {
for collection in collections {
//User permissions
if let Some(uc) = cipher_sync_data.user_collections.get(collection) {
rows.push((uc.read_only, uc.hide_passwords));
if let Some(cu) = cipher_sync_data.user_collections.get(collection) {
rows.push((cu.read_only, cu.hide_passwords, cu.manage));
}

//Group permissions
if let Some(cg) = cipher_sync_data.user_collections_groups.get(collection) {
rows.push((cg.read_only, cg.hide_passwords));
rows.push((cg.read_only, cg.hide_passwords, cg.manage));
}
}
}
Expand All @@ -623,15 +623,21 @@ impl Cipher {
// booleans and this behavior isn't portable anyway.
let mut read_only = true;
let mut hide_passwords = true;
for (ro, hp) in rows.iter() {
let mut manage = false;
for (ro, hp, mn) in rows.iter() {
read_only &= ro;
hide_passwords &= hp;
manage &= mn;
}

Some((read_only, hide_passwords))
Some((read_only, hide_passwords, manage))
}

async fn get_user_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> {
async fn get_user_collections_access_flags(
&self,
user_uuid: &UserId,
conn: &mut DbConn,
) -> Vec<(bool, bool, bool)> {
db_run! {conn: {
// Check whether this cipher is in any collections accessible to the
// user. If so, retrieve the access flags for each collection.
Expand All @@ -642,13 +648,17 @@ impl Cipher {
.inner_join(users_collections::table.on(
ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)
.and(users_collections::user_uuid.eq(user_uuid))))
.select((users_collections::read_only, users_collections::hide_passwords))
.load::<(bool, bool)>(conn)
.select((users_collections::read_only, users_collections::hide_passwords, users_collections::manage))
.load::<(bool, bool, bool)>(conn)
.expect("Error getting user access restrictions")
}}
}

async fn get_group_collections_access_flags(&self, user_uuid: &UserId, conn: &mut DbConn) -> Vec<(bool, bool)> {
async fn get_group_collections_access_flags(
&self,
user_uuid: &UserId,
conn: &mut DbConn,
) -> Vec<(bool, bool, bool)> {
if !CONFIG.org_groups_enabled() {
return Vec::new();
}
Expand All @@ -668,15 +678,15 @@ impl Cipher {
users_organizations::uuid.eq(groups_users::users_organizations_uuid)
))
.filter(users_organizations::user_uuid.eq(user_uuid))
.select((collections_groups::read_only, collections_groups::hide_passwords))
.load::<(bool, bool)>(conn)
.select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage))
.load::<(bool, bool, bool)>(conn)
.expect("Error getting group access restrictions")
}}
}

pub async fn is_write_accessible_to_user(&self, user_uuid: &UserId, conn: &mut DbConn) -> bool {
match self.get_access_restrictions(user_uuid, None, conn).await {
Some((read_only, _hide_passwords)) => !read_only,
Some((read_only, _hide_passwords, manage)) => !read_only || manage,
None => false,
}
}
Expand Down
Loading

0 comments on commit d1dee04

Please sign in to comment.