Add support for hiding passwords in a collection
Ref: https://github.com/bitwarden/server/pull/743
This commit is contained in:
parent
b34d548246
commit
979d010dc2
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT FALSE;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT FALSE;
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE users_collections
|
||||||
|
ADD COLUMN hide_passwords BOOLEAN NOT NULL DEFAULT 0; -- FALSE
|
|
@ -374,7 +374,7 @@ fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders,
|
||||||
.map(|col_user| {
|
.map(|col_user| {
|
||||||
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
|
UserOrganization::find_by_user_and_org(&col_user.user_uuid, &org_id, &conn)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_json_read_only(col_user.read_only)
|
.to_json_user_access_restrictions(&col_user)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -408,7 +408,9 @@ fn put_collection_users(
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionUser::save(&user.user_uuid, &coll_id, d.ReadOnly, &conn)?;
|
CollectionUser::save(&user.user_uuid, &coll_id,
|
||||||
|
d.ReadOnly, d.HidePasswords,
|
||||||
|
&conn)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -452,6 +454,7 @@ fn get_org_users(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonRe
|
||||||
struct CollectionData {
|
struct CollectionData {
|
||||||
Id: String,
|
Id: String,
|
||||||
ReadOnly: bool,
|
ReadOnly: bool,
|
||||||
|
HidePasswords: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -523,7 +526,9 @@ fn send_invite(org_id: String, data: JsonUpcase<InviteData>, headers: AdminHeade
|
||||||
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
||||||
None => err!("Collection not found in Organization"),
|
None => err!("Collection not found in Organization"),
|
||||||
Some(collection) => {
|
Some(collection) => {
|
||||||
CollectionUser::save(&user.uuid, &collection.uuid, col.ReadOnly, &conn)?;
|
CollectionUser::save(&user.uuid, &collection.uuid,
|
||||||
|
col.ReadOnly, col.HidePasswords,
|
||||||
|
&conn)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -778,7 +783,9 @@ fn edit_user(
|
||||||
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
match Collection::find_by_uuid_and_org(&col.Id, &org_id, &conn) {
|
||||||
None => err!("Collection not found in Organization"),
|
None => err!("Collection not found in Organization"),
|
||||||
Some(collection) => {
|
Some(collection) => {
|
||||||
CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid, col.ReadOnly, &conn)?;
|
CollectionUser::save(&user_to_edit.user_uuid, &collection.uuid,
|
||||||
|
col.ReadOnly, col.HidePasswords,
|
||||||
|
&conn)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,15 @@ impl Cipher {
|
||||||
let fields_json = self.fields.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
let fields_json = self.fields.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
||||||
let password_history_json = self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null);
|
let password_history_json = 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, &conn) {
|
||||||
|
Some((ro, hp)) => (ro, hp),
|
||||||
|
None => {
|
||||||
|
error!("Cipher ownership assertion failure");
|
||||||
|
(true, true)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Get the data or a default empty value to avoid issues with the mobile apps
|
// Get the data or a default empty value to avoid issues with the mobile apps
|
||||||
let mut data_json: Value = serde_json::from_str(&self.data).unwrap_or_else(|_| json!({
|
let mut data_json: Value = serde_json::from_str(&self.data).unwrap_or_else(|_| json!({
|
||||||
"Fields":null,
|
"Fields":null,
|
||||||
|
@ -105,7 +114,15 @@ impl Cipher {
|
||||||
}
|
}
|
||||||
// TODO: ******* Backwards compat end **********
|
// TODO: ******* Backwards compat end **********
|
||||||
|
|
||||||
|
// There are three types of cipher response models in upstream
|
||||||
|
// Bitwarden: "cipherMini", "cipher", and "cipherDetails" (in order
|
||||||
|
// of increasing level of detail). bitwarden_rs currently only
|
||||||
|
// supports the "cipherDetails" type, though it seems like the
|
||||||
|
// Bitwarden clients will ignore extra fields.
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/bitwarden/server/blob/master/src/Core/Models/Api/Response/CipherResponseModel.cs
|
||||||
let mut json_object = json!({
|
let mut json_object = json!({
|
||||||
|
"Object": "cipherDetails",
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"Type": self.atype,
|
"Type": self.atype,
|
||||||
"RevisionDate": format_date(&self.updated_at),
|
"RevisionDate": format_date(&self.updated_at),
|
||||||
|
@ -115,6 +132,8 @@ impl Cipher {
|
||||||
"OrganizationId": self.organization_uuid,
|
"OrganizationId": self.organization_uuid,
|
||||||
"Attachments": attachments_json,
|
"Attachments": attachments_json,
|
||||||
"OrganizationUseTotp": true,
|
"OrganizationUseTotp": true,
|
||||||
|
|
||||||
|
// This field is specific to the cipherDetails type.
|
||||||
"CollectionIds": self.get_collections(user_uuid, &conn),
|
"CollectionIds": self.get_collections(user_uuid, &conn),
|
||||||
|
|
||||||
"Name": self.name,
|
"Name": self.name,
|
||||||
|
@ -123,8 +142,11 @@ impl Cipher {
|
||||||
|
|
||||||
"Data": data_json,
|
"Data": data_json,
|
||||||
|
|
||||||
"Object": "cipher",
|
// These values are true by default, but can be false if the
|
||||||
"Edit": true,
|
// 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,
|
||||||
});
|
});
|
||||||
|
@ -241,66 +263,97 @@ impl Cipher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
/// Returns whether this cipher is directly owned by the user.
|
||||||
|
pub fn is_owned_by_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
ciphers::table
|
ciphers::table
|
||||||
.filter(ciphers::uuid.eq(&self.uuid))
|
.filter(ciphers::uuid.eq(&self.uuid))
|
||||||
.left_join(
|
.filter(ciphers::user_uuid.eq(&user_uuid))
|
||||||
users_organizations::table.on(ciphers::organization_uuid
|
|
||||||
.eq(users_organizations::org_uuid.nullable())
|
|
||||||
.and(users_organizations::user_uuid.eq(user_uuid))),
|
|
||||||
)
|
|
||||||
.left_join(ciphers_collections::table)
|
|
||||||
.left_join(
|
|
||||||
users_collections::table
|
|
||||||
.on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)),
|
|
||||||
)
|
|
||||||
.filter(ciphers::user_uuid.eq(user_uuid).or(
|
|
||||||
// Cipher owner
|
|
||||||
users_organizations::access_all.eq(true).or(
|
|
||||||
// access_all in Organization
|
|
||||||
users_organizations::atype.le(UserOrgType::Admin as i32).or(
|
|
||||||
// Org admin or owner
|
|
||||||
users_collections::user_uuid.eq(user_uuid).and(
|
|
||||||
users_collections::read_only.eq(false), //R/W access to collection
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.select(ciphers::all_columns)
|
|
||||||
.first::<Self>(&**conn)
|
.first::<Self>(&**conn)
|
||||||
.ok()
|
.ok()
|
||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
/// Returns whether this cipher is owned by an org in which the user has full access.
|
||||||
|
pub fn is_in_full_access_org(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
ciphers::table
|
ciphers::table
|
||||||
.filter(ciphers::uuid.eq(&self.uuid))
|
.filter(ciphers::uuid.eq(&self.uuid))
|
||||||
.left_join(
|
.inner_join(ciphers_collections::table.on(
|
||||||
users_organizations::table.on(ciphers::organization_uuid
|
ciphers::uuid.eq(ciphers_collections::cipher_uuid)))
|
||||||
.eq(users_organizations::org_uuid.nullable())
|
.inner_join(users_organizations::table.on(
|
||||||
.and(users_organizations::user_uuid.eq(user_uuid))),
|
ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable())
|
||||||
)
|
.and(users_organizations::user_uuid.eq(user_uuid))
|
||||||
.left_join(ciphers_collections::table)
|
.and(users_organizations::status.eq(UserOrgStatus::Confirmed as i32))))
|
||||||
.left_join(
|
// The user is an org admin or higher.
|
||||||
users_collections::table
|
.filter(users_organizations::atype.le(UserOrgType::Admin as i32))
|
||||||
.on(ciphers_collections::collection_uuid.eq(users_collections::collection_uuid)),
|
// The user was granted full access to the org by an org owner/admin.
|
||||||
)
|
.or_filter(users_organizations::access_all.eq(true))
|
||||||
.filter(ciphers::user_uuid.eq(user_uuid).or(
|
.select(ciphers::uuid)
|
||||||
// Cipher owner
|
.first::<String>(&**conn)
|
||||||
users_organizations::access_all.eq(true).or(
|
|
||||||
// access_all in Organization
|
|
||||||
users_organizations::atype.le(UserOrgType::Admin as i32).or(
|
|
||||||
// Org admin or owner
|
|
||||||
users_collections::user_uuid.eq(user_uuid), // Access to Collection
|
|
||||||
),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.select(ciphers::all_columns)
|
|
||||||
.first::<Self>(&**conn)
|
|
||||||
.ok()
|
.ok()
|
||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// the access restrictions.
|
||||||
|
pub fn get_access_restrictions(&self, user_uuid: &str, conn: &DbConn) -> Option<(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, &conn) || self.is_in_full_access_org(&user_uuid, &conn) {
|
||||||
|
return Some((false, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether this cipher is in any collections accessible to the
|
||||||
|
// user. If so, retrieve the access flags for each collection.
|
||||||
|
let query = ciphers::table
|
||||||
|
.filter(ciphers::uuid.eq(&self.uuid))
|
||||||
|
.inner_join(ciphers_collections::table.on(
|
||||||
|
ciphers::uuid.eq(ciphers_collections::cipher_uuid)))
|
||||||
|
.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));
|
||||||
|
|
||||||
|
// There's an edge case where a cipher can be in multiple collections
|
||||||
|
// with inconsistent access flags. For example, a cipher could be in
|
||||||
|
// one collection where the user has read-only access, but also in
|
||||||
|
// another collection where the user has read/write access. To handle
|
||||||
|
// this, we do a boolean OR of all values in each of the `read_only`
|
||||||
|
// and `hide_passwords` columns. This could ideally be done as part
|
||||||
|
// of the query, but Diesel doesn't support a max() or bool_or()
|
||||||
|
// function on booleans and this behavior isn't portable anyway.
|
||||||
|
match query.load::<(bool, bool)>(&**conn).ok() {
|
||||||
|
Some(vec) => {
|
||||||
|
let mut read_only = false;
|
||||||
|
let mut hide_passwords = false;
|
||||||
|
for (ro, hp) in vec.iter() {
|
||||||
|
read_only |= ro;
|
||||||
|
hide_passwords |= hp;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((read_only, hide_passwords))
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
// This cipher isn't in any collections accessible to the user.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
|
match self.get_access_restrictions(&user_uuid, &conn) {
|
||||||
|
Some((read_only, _hide_passwords)) => !read_only,
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool {
|
||||||
|
self.get_access_restrictions(&user_uuid, &conn).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
|
pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> {
|
||||||
folders_ciphers::table
|
folders_ciphers::table
|
||||||
.inner_join(folders::table)
|
.inner_join(folders::table)
|
||||||
|
|
|
@ -199,6 +199,7 @@ pub struct CollectionUser {
|
||||||
pub user_uuid: String,
|
pub user_uuid: String,
|
||||||
pub collection_uuid: String,
|
pub collection_uuid: String,
|
||||||
pub read_only: bool,
|
pub read_only: bool,
|
||||||
|
pub hide_passwords: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Database methods
|
/// Database methods
|
||||||
|
@ -214,7 +215,7 @@ impl CollectionUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "postgresql")]
|
#[cfg(feature = "postgresql")]
|
||||||
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult {
|
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult {
|
||||||
User::update_uuid_revision(&user_uuid, conn);
|
User::update_uuid_revision(&user_uuid, conn);
|
||||||
|
|
||||||
diesel::insert_into(users_collections::table)
|
diesel::insert_into(users_collections::table)
|
||||||
|
@ -222,16 +223,18 @@ impl CollectionUser {
|
||||||
users_collections::user_uuid.eq(user_uuid),
|
users_collections::user_uuid.eq(user_uuid),
|
||||||
users_collections::collection_uuid.eq(collection_uuid),
|
users_collections::collection_uuid.eq(collection_uuid),
|
||||||
users_collections::read_only.eq(read_only),
|
users_collections::read_only.eq(read_only),
|
||||||
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
))
|
))
|
||||||
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
|
.on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
|
||||||
.do_update()
|
.do_update()
|
||||||
.set(users_collections::read_only.eq(read_only))
|
.set(users_collections::read_only.eq(read_only))
|
||||||
|
.set(users_collections::hide_passwords.eq(hide_passwords))
|
||||||
.execute(&**conn)
|
.execute(&**conn)
|
||||||
.map_res("Error adding user to collection")
|
.map_res("Error adding user to collection")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "postgresql"))]
|
#[cfg(not(feature = "postgresql"))]
|
||||||
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult {
|
pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult {
|
||||||
User::update_uuid_revision(&user_uuid, conn);
|
User::update_uuid_revision(&user_uuid, conn);
|
||||||
|
|
||||||
diesel::replace_into(users_collections::table)
|
diesel::replace_into(users_collections::table)
|
||||||
|
@ -239,6 +242,7 @@ impl CollectionUser {
|
||||||
users_collections::user_uuid.eq(user_uuid),
|
users_collections::user_uuid.eq(user_uuid),
|
||||||
users_collections::collection_uuid.eq(collection_uuid),
|
users_collections::collection_uuid.eq(collection_uuid),
|
||||||
users_collections::read_only.eq(read_only),
|
users_collections::read_only.eq(read_only),
|
||||||
|
users_collections::hide_passwords.eq(hide_passwords),
|
||||||
))
|
))
|
||||||
.execute(&**conn)
|
.execute(&**conn)
|
||||||
.map_res("Error adding user to collection")
|
.map_res("Error adding user to collection")
|
||||||
|
|
|
@ -310,10 +310,11 @@ impl UserOrganization {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_json_read_only(&self, read_only: bool) -> Value {
|
pub fn to_json_user_access_restrictions(&self, col_user: &CollectionUser) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"Id": self.uuid,
|
"Id": self.uuid,
|
||||||
"ReadOnly": read_only
|
"ReadOnly": col_user.read_only,
|
||||||
|
"HidePasswords": col_user.hide_passwords,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,7 +325,11 @@ impl UserOrganization {
|
||||||
let collections = CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn);
|
let collections = CollectionUser::find_by_organization_and_user_uuid(&self.org_uuid, &self.user_uuid, conn);
|
||||||
collections
|
collections
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| json!({"Id": c.collection_uuid, "ReadOnly": c.read_only}))
|
.map(|c| json!({
|
||||||
|
"Id": c.collection_uuid,
|
||||||
|
"ReadOnly": c.read_only,
|
||||||
|
"HidePasswords": c.hide_passwords,
|
||||||
|
}))
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,7 @@ table! {
|
||||||
user_uuid -> Varchar,
|
user_uuid -> Varchar,
|
||||||
collection_uuid -> Varchar,
|
collection_uuid -> Varchar,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
|
hide_passwords -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,7 @@ table! {
|
||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
collection_uuid -> Text,
|
collection_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
|
hide_passwords -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,7 @@ table! {
|
||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
collection_uuid -> Text,
|
collection_uuid -> Text,
|
||||||
read_only -> Bool,
|
read_only -> Bool,
|
||||||
|
hide_passwords -> Bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue