From 7cf8809d777cd88ad5aa932324e51561724e3c32 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Wed, 2 Dec 2020 22:50:51 +0100 Subject: [PATCH] Adding Manager Role support This has been requested a few times (#1136 & #246 & forum), and there already were two (1:1 duplicate) PR's (#1222 & #1223) which needed some changes and no followups or further comments unfortunally. This PR adds two auth headers. - ManagerHeaders Checks if the user-type is Manager or higher and if the manager is part of that collection or not. - ManagerHeadersLoose Check if the user-type is Manager or higher, but does not check if the user is part of the collection, needed for a few features like retreiving all the users of an org. I think this is the safest way to implement this instead of having to check this within every function which needs this manually. Also some extra checks if a manager has access to all collections or just a selection. fixes #1136 --- src/api/core/organizations.rs | 33 ++++++--- src/auth.rs | 131 +++++++++++++++++++++++++++++++++- 2 files changed, 153 insertions(+), 11 deletions(-) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 1d7b99b6..9d3fa7c6 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -5,7 +5,7 @@ use serde_json::Value; use crate::{ api::{EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType}, - auth::{decode_invite, AdminHeaders, Headers, OwnerHeaders}, + auth::{decode_invite, AdminHeaders, Headers, OwnerHeaders, ManagerHeaders, ManagerHeadersLoose}, db::{models::*, DbConn}, mail, CONFIG, }; @@ -217,7 +217,7 @@ fn get_org_collections(org_id: String, _headers: AdminHeaders, conn: DbConn) -> #[post("/organizations//collections", data = "")] fn post_organization_collections( org_id: String, - _headers: AdminHeaders, + headers: ManagerHeadersLoose, data: JsonUpcase, conn: DbConn, ) -> JsonResult { @@ -228,9 +228,22 @@ fn post_organization_collections( 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, &conn) { + Some(u) => u, + None => err!("User is not part of organization"), + }; + let collection = Collection::new(org.uuid, data.Name); collection.save(&conn)?; + // 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. + if !user_org.access_all { + CollectionUser::save(&headers.user.uuid, &collection.uuid, false, false, &conn)?; + } + Ok(Json(collection.to_json())) } @@ -238,7 +251,7 @@ fn post_organization_collections( fn put_organization_collection_update( org_id: String, col_id: String, - headers: AdminHeaders, + headers: ManagerHeaders, data: JsonUpcase, conn: DbConn, ) -> JsonResult { @@ -249,7 +262,7 @@ fn put_organization_collection_update( fn post_organization_collection_update( org_id: String, col_id: String, - _headers: AdminHeaders, + _headers: ManagerHeaders, data: JsonUpcase, conn: DbConn, ) -> JsonResult { @@ -317,7 +330,7 @@ fn post_organization_collection_delete_user( } #[delete("/organizations//collections/")] -fn delete_organization_collection(org_id: String, col_id: String, _headers: AdminHeaders, conn: DbConn) -> EmptyResult { +fn delete_organization_collection(org_id: String, col_id: String, _headers: ManagerHeaders, conn: DbConn) -> EmptyResult { match Collection::find_by_uuid(&col_id, &conn) { None => err!("Collection not found"), Some(collection) => { @@ -341,7 +354,7 @@ struct DeleteCollectionData { fn post_organization_collection_delete( org_id: String, col_id: String, - headers: AdminHeaders, + headers: ManagerHeaders, _data: JsonUpcase, conn: DbConn, ) -> EmptyResult { @@ -349,7 +362,7 @@ fn post_organization_collection_delete( } #[get("/organizations//collections//details")] -fn get_org_collection_detail(org_id: String, coll_id: String, headers: AdminHeaders, conn: DbConn) -> JsonResult { +fn get_org_collection_detail(org_id: String, coll_id: String, headers: ManagerHeaders, conn: DbConn) -> JsonResult { match Collection::find_by_uuid_and_user(&coll_id, &headers.user.uuid, &conn) { None => err!("Collection not found"), Some(collection) => { @@ -363,7 +376,7 @@ fn get_org_collection_detail(org_id: String, coll_id: String, headers: AdminHead } #[get("/organizations//collections//users")] -fn get_collection_users(org_id: String, coll_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult { +fn get_collection_users(org_id: String, coll_id: String, _headers: ManagerHeaders, conn: DbConn) -> JsonResult { // Get org and collection, check that collection is from org let collection = match Collection::find_by_uuid_and_org(&coll_id, &org_id, &conn) { None => err!("Collection not found in Organization"), @@ -388,7 +401,7 @@ fn put_collection_users( org_id: String, coll_id: String, data: JsonUpcaseVec, - _headers: AdminHeaders, + _headers: ManagerHeaders, conn: DbConn, ) -> EmptyResult { // Get org and collection, check that collection is from org @@ -440,7 +453,7 @@ fn get_org_details(data: Form, headers: Headers, conn: DbConn) -> Jso } #[get("/organizations//users")] -fn get_org_users(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult { +fn get_org_users(org_id: String, _headers: ManagerHeadersLoose, conn: DbConn) -> JsonResult { let users = UserOrganization::find_by_org(&org_id, &conn); let users_json: Vec = users.iter().map(|c| c.to_json_user_details(&conn)).collect(); diff --git a/src/auth.rs b/src/auth.rs index da6f8fa4..53a25357 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -220,7 +220,7 @@ use rocket::{ }; use crate::db::{ - models::{Device, User, UserOrgStatus, UserOrgType, UserOrganization}, + models::{Device, User, UserOrgStatus, UserOrgType, UserOrganization, CollectionUser}, DbConn, }; @@ -310,6 +310,8 @@ pub struct OrgHeaders { pub device: Device, pub user: User, pub org_user_type: UserOrgType, + pub org_user: UserOrganization, + pub org_id: String, } // org_id is usually the second param ("/organizations/") @@ -370,6 +372,8 @@ impl<'a, 'r> FromRequest<'a, 'r> for OrgHeaders { err_handler!("Unknown user type in the database") } }, + org_user, + org_id, }) } _ => err_handler!("Error getting the organization id"), @@ -419,6 +423,131 @@ impl Into for AdminHeaders { } } + + + + +// col_id is usually the forth param ("/organizations//collections/") +// But there cloud be cases where it is located in a query value. +// First check the param, if this is not a valid uuid, we will try the query value. +fn get_col_id(request: &Request) -> Option { + if let Some(Ok(col_id)) = request.get_param::(3) { + if uuid::Uuid::parse_str(&col_id).is_ok() { + return Some(col_id); + } + } + + if let Some(Ok(col_id)) = request.get_query_value::("collectionId") { + if uuid::Uuid::parse_str(&col_id).is_ok() { + return Some(col_id); + } + } + + None +} + +/// The ManagerHeaders are used to check if you are at least a Manager +/// and have access to the specific collection provided via the /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 org_user_type: UserOrgType, +} + +impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeaders { + type Error = &'static str; + + fn from_request(request: &'a Request<'r>) -> Outcome { + match request.guard::() { + Outcome::Forward(_) => Outcome::Forward(()), + Outcome::Failure(f) => Outcome::Failure(f), + Outcome::Success(headers) => { + if headers.org_user_type >= UserOrgType::Manager { + match get_col_id(request) { + Some(col_id) => { + let conn = match request.guard::() { + Outcome::Success(conn) => conn, + _ => err_handler!("Error getting DB"), + }; + + if !headers.org_user.access_all { + match CollectionUser::find_by_collection_and_user(&col_id, &headers.org_user.user_uuid, &conn) { + Some(_) => (), + None => err_handler!("The current user isn't a manager for this collection"), + } + } + }, + _ => err_handler!("Error getting the collection id"), + } + + Outcome::Success(Self { + host: headers.host, + device: headers.device, + user: headers.user, + org_user_type: headers.org_user_type, + }) + } else { + err_handler!("You need to be a Manager, Admin or Owner to call this endpoint") + } + } + } + } +} + +impl Into for ManagerHeaders { + fn into(self) -> Headers { + Headers { + host: self.host, + device: self.device, + user: self.user, + } + } +} + +/// 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). +pub struct ManagerHeadersLoose { + pub host: String, + pub device: Device, + pub user: User, + pub org_user_type: UserOrgType, +} + +impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeadersLoose { + type Error = &'static str; + + fn from_request(request: &'a Request<'r>) -> Outcome { + match request.guard::() { + Outcome::Forward(_) => Outcome::Forward(()), + Outcome::Failure(f) => Outcome::Failure(f), + Outcome::Success(headers) => { + if headers.org_user_type >= UserOrgType::Manager { + Outcome::Success(Self { + host: headers.host, + device: headers.device, + user: headers.user, + org_user_type: headers.org_user_type, + }) + } else { + err_handler!("You need to be a Manager, Admin or Owner to call this endpoint") + } + } + } + } +} + +impl Into for ManagerHeadersLoose { + fn into(self) -> Headers { + Headers { + host: self.host, + device: self.device, + user: self.user, + } + } +} + pub struct OwnerHeaders { pub host: String, pub device: Device,