Browse Source

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
BlackDex 5 years ago
parent
commit
7cf8809d77
2 changed files with 153 additions and 11 deletions
  1. 23 10
      src/api/core/organizations.rs
  2. 130 1
      src/auth.rs

+ 23 - 10
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/<org_id>/collections", data = "<data>")]
 fn post_organization_collections(
     org_id: String,
-    _headers: AdminHeaders,
+    headers: ManagerHeadersLoose,
     data: JsonUpcase<NewCollectionData>,
     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<NewCollectionData>,
     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<NewCollectionData>,
     conn: DbConn,
 ) -> JsonResult {
@@ -317,7 +330,7 @@ fn post_organization_collection_delete_user(
 }
 
 #[delete("/organizations/<org_id>/collections/<col_id>")]
-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<DeleteCollectionData>,
     conn: DbConn,
 ) -> EmptyResult {
@@ -349,7 +362,7 @@ fn post_organization_collection_delete(
 }
 
 #[get("/organizations/<org_id>/collections/<coll_id>/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/<org_id>/collections/<coll_id>/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<CollectionData>,
-    _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<OrgIdData>, headers: Headers, conn: DbConn) -> Jso
 }
 
 #[get("/organizations/<org_id>/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<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect();
 

+ 130 - 1
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/<org_id>")
@@ -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<Headers> for AdminHeaders {
     }
 }
 
+
+
+
+
+// col_id is usually the forth param ("/organizations/<org_id>/collections/<col_id>")
+// 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<String> {
+    if let Some(Ok(col_id)) = request.get_param::<String>(3) {
+        if uuid::Uuid::parse_str(&col_id).is_ok() {
+            return Some(col_id);
+        }
+    }
+
+    if let Some(Ok(col_id)) = request.get_query_value::<String>("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 <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 org_user_type: UserOrgType,
+}
+
+impl<'a, 'r> FromRequest<'a, 'r> for ManagerHeaders {
+    type Error = &'static str;
+
+    fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
+        match request.guard::<OrgHeaders>() {
+            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::<DbConn>() {
+                                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<Headers> 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<Self, Self::Error> {
+        match request.guard::<OrgHeaders>() {
+            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<Headers> 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,