mirror of
https://github.com/dani-garcia/vaultwarden.wiki.git
synced 2026-11-14 00:36:04 +02:00
[PR 3] Add collection permissions for Custom Roles (#7951)
* Add Custom Roles collection permissions * Return NotFound for denied collection access * Grant Manage on imported collections to every Custom member
This commit is contained in:
10 files changed
+795
-268
No files matched your search
+138
-53
@@ -32,9 +32,9 @@ use crate::{
|
||||
db::{
|
||||
DbConn,
|
||||
models::{
|
||||
AttachmentId, CipherId, Collection, CollectionId, Device, DeviceId, DeviceType, EmergencyAccessId,
|
||||
EventType, Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, OrganizationId,
|
||||
SendFileId, SendId, User, UserId,
|
||||
AttachmentId, CipherId, Collection, CollectionId, CustomPermission, Device, DeviceId, DeviceType,
|
||||
EmergencyAccessId, EventType, Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId,
|
||||
OrganizationId, SendFileId, SendId, User, UserId, custom_roles_enabled,
|
||||
},
|
||||
},
|
||||
error::Error,
|
||||
@@ -757,6 +757,12 @@ impl OrgHeaders {
|
||||
fn is_confirmed_and_owner(&self) -> bool {
|
||||
self.membership_status == MembershipStatus::Confirmed && self.membership_type == MembershipType::Owner
|
||||
}
|
||||
// A Custom member, while Custom roles are enabled. What they may do depends on their permissions.
|
||||
fn is_confirmed_and_custom(&self) -> bool {
|
||||
self.membership_status == MembershipStatus::Confirmed
|
||||
&& self.membership_type == MembershipType::Custom
|
||||
&& custom_roles_enabled()
|
||||
}
|
||||
}
|
||||
|
||||
// org_id is usually the second path param ("/organizations/<org_id>"),
|
||||
@@ -895,62 +901,134 @@ fn get_col_id(request: &Request<'_>) -> Option<CollectionId> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The ManagerHeaders are used to check if you are at least a Manager
|
||||
/// and have access to the specific collection provided via the <col_id>/collections/collectionId.
|
||||
/// This does strict checking on the collection_id, ManagerHeadersLoose does not.
|
||||
pub struct ManagerHeaders {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub ip: ClientIp,
|
||||
pub org_id: OrganizationId,
|
||||
/// What a collection route does with its collection, which decides what a Custom member needs for it.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CollectionAction {
|
||||
Edit,
|
||||
Read,
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for ManagerHeaders {
|
||||
type Error = &'static str;
|
||||
/// Whether `member`, a confirmed Custom member, may `action` the collection `col_id` of their organization.
|
||||
///
|
||||
/// Deleting needs Delete any collection: the clients are told collection deletion is limited, so the manage option
|
||||
/// on a collection is no permission to delete it. Editing is open with Edit any collection, reading also with Delete
|
||||
/// any collection, and otherwise both need the manage option on the collection itself. Neither the role nor any
|
||||
/// `access_all` counts as that.
|
||||
pub async fn custom_member_may(
|
||||
action: CollectionAction,
|
||||
member: &Membership,
|
||||
col_id: &CollectionId,
|
||||
conn: &DbConn,
|
||||
) -> bool {
|
||||
if !(member.has_type(MembershipType::Custom)
|
||||
&& member.has_status(MembershipStatus::Confirmed)
|
||||
&& custom_roles_enabled())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
match action {
|
||||
CollectionAction::Delete => member.has_custom_permission(CustomPermission::DeleteAnyCollection),
|
||||
CollectionAction::Edit if member.can_edit_any_collection() => true,
|
||||
CollectionAction::Read if member.can_view_all_collections() => true,
|
||||
CollectionAction::Edit | CollectionAction::Read => {
|
||||
Collection::find_explicitly_manageable_by_user(&member.org_uuid, &member.user_uuid, conn)
|
||||
.await
|
||||
.contains(col_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||
if headers.is_confirmed_and_manager() {
|
||||
if let Some(col_id) = get_col_id(request) {
|
||||
let Outcome::Success(conn) = DbConn::from_request(request).await else {
|
||||
err_handler!("Error getting DB")
|
||||
};
|
||||
/// Checks a collection route for the collection provided via the <col_id>/collections/collectionId: a Manager, Admin
|
||||
/// or Owner needs to be able to manage the collection, a Custom member is checked for the `action`.
|
||||
async fn collection_headers(request: &Request<'_>, action: CollectionAction) -> Outcome<OrgHeaders, &'static str> {
|
||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||
let is_custom = headers.is_confirmed_and_custom();
|
||||
if !headers.is_confirmed_and_manager() && !is_custom {
|
||||
err_handler!("You need to be a Manager, Admin or Owner to call this endpoint")
|
||||
}
|
||||
|
||||
if !Collection::is_coll_manageable_by_user(&col_id, &headers.membership.user_uuid, &conn).await {
|
||||
err_handler!("The current user isn't a manager for this collection")
|
||||
}
|
||||
} else {
|
||||
err_handler!("Error getting the collection id")
|
||||
let Some(col_id) = get_col_id(request) else {
|
||||
err_handler!("Error getting the collection id")
|
||||
};
|
||||
let Outcome::Success(conn) = DbConn::from_request(request).await else {
|
||||
err_handler!("Error getting DB")
|
||||
};
|
||||
|
||||
let allowed = if is_custom {
|
||||
custom_member_may(action, &headers.membership, &col_id, &conn).await
|
||||
} else {
|
||||
Collection::is_coll_manageable_by_user(&col_id, &headers.membership.user_uuid, &conn).await
|
||||
};
|
||||
if !allowed {
|
||||
// NotFound as in Bitwarden, the web vault logs the user out on a 401
|
||||
error!(target: "auth", "Collection not found. The current user isn't a manager for this collection");
|
||||
return Outcome::Error((rocket::http::Status::NotFound, "Collection not found"));
|
||||
}
|
||||
Outcome::Success(headers)
|
||||
}
|
||||
|
||||
macro_rules! collection_headers {
|
||||
($(#[$doc:meta])* $name:ident, $action:expr) => {
|
||||
$(#[$doc])*
|
||||
pub struct $name {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
pub user: User,
|
||||
pub ip: ClientIp,
|
||||
pub org_id: OrganizationId,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for $name {
|
||||
type Error = &'static str;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(collection_headers(request, $action).await);
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
ip: headers.ip,
|
||||
org_id: headers.membership.org_uuid,
|
||||
})
|
||||
}
|
||||
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
user: headers.user,
|
||||
ip: headers.ip,
|
||||
org_id: headers.membership.org_uuid,
|
||||
})
|
||||
} else {
|
||||
err_handler!("You need to be a Manager, Admin or Owner to call this endpoint")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$name> for Headers {
|
||||
fn from(h: $name) -> Headers {
|
||||
Headers {
|
||||
host: h.host,
|
||||
device: h.device,
|
||||
user: h.user,
|
||||
ip: h.ip,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl From<ManagerHeaders> for Headers {
|
||||
fn from(h: ManagerHeaders) -> Headers {
|
||||
Headers {
|
||||
host: h.host,
|
||||
device: h.device,
|
||||
user: h.user,
|
||||
ip: h.ip,
|
||||
}
|
||||
}
|
||||
}
|
||||
collection_headers!(
|
||||
/// The ManagerHeaders are used to check if you are at least a Manager
|
||||
/// and have access to the specific collection provided via the <col_id>/collections/collectionId.
|
||||
/// This does strict checking on the collection_id, ManagerHeadersLoose does not.
|
||||
ManagerHeaders,
|
||||
CollectionAction::Edit
|
||||
);
|
||||
collection_headers!(
|
||||
/// The same as ManagerHeaders, for the routes which only read the collection and its access.
|
||||
CollectionReadHeaders,
|
||||
CollectionAction::Read
|
||||
);
|
||||
collection_headers!(
|
||||
/// The same as ManagerHeaders, for the routes which delete the collection.
|
||||
CollectionDeleteHeaders,
|
||||
CollectionAction::Delete
|
||||
);
|
||||
|
||||
/// The ManagerHeadersLoose is used when you at least need to be a Manager,
|
||||
/// but there is no collection_id sent with the request (either in the path or as form data).
|
||||
/// A Custom member is let through as well, the routes check what their permissions allow.
|
||||
pub struct ManagerHeadersLoose {
|
||||
pub host: String,
|
||||
pub device: Device,
|
||||
@@ -965,7 +1043,7 @@ impl<'r> FromRequest<'r> for ManagerHeadersLoose {
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = try_outcome!(OrgHeaders::from_request(request).await);
|
||||
if headers.is_confirmed_and_manager() {
|
||||
if headers.is_confirmed_and_manager() || headers.is_confirmed_and_custom() {
|
||||
Outcome::Success(Self {
|
||||
host: headers.host,
|
||||
device: headers.device,
|
||||
@@ -990,22 +1068,29 @@ impl From<ManagerHeadersLoose> for Headers {
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagerHeaders {
|
||||
impl CollectionDeleteHeaders {
|
||||
pub async fn from_loose(
|
||||
h: ManagerHeadersLoose,
|
||||
collections: &Vec<CollectionId>,
|
||||
conn: &DbConn,
|
||||
) -> Result<ManagerHeaders, Error> {
|
||||
) -> Result<CollectionDeleteHeaders, Error> {
|
||||
let is_custom = h.membership.has_type(MembershipType::Custom) && custom_roles_enabled();
|
||||
for col_id in collections {
|
||||
if uuid::Uuid::parse_str(col_id.as_ref()).is_err() {
|
||||
err!("Collection Id is malformed!");
|
||||
}
|
||||
if !Collection::is_coll_manageable_by_user(col_id, &h.membership.user_uuid, conn).await {
|
||||
let allowed = if is_custom {
|
||||
Collection::find_by_uuid_and_org(col_id, &h.membership.org_uuid, conn).await.is_some()
|
||||
&& custom_member_may(CollectionAction::Delete, &h.membership, col_id, conn).await
|
||||
} else {
|
||||
Collection::is_coll_manageable_by_user(col_id, &h.membership.user_uuid, conn).await
|
||||
};
|
||||
if !allowed {
|
||||
err!("Collection not found", "The current user isn't a manager for this collection")
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ManagerHeaders {
|
||||
Ok(CollectionDeleteHeaders {
|
||||
host: h.host,
|
||||
device: h.device,
|
||||
user: h.user,
|
||||
|
||||
Reference in new issue
Block a user