vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 5210f9b95191389216c3944fde6f1719b5180616
parent e6132809d27164077c2bca7f4cebdcea5e03eff0
Author: Daniel GarcĂ­a <dani1861994@hotmail.com>
Date:   Wed, 25 Apr 2018 00:34:40 +0200

Added org user editing

Diffstat:
Msrc/api/core/mod.rs | 5++++-
Msrc/api/core/organizations.rs | 251+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/db/models/organization.rs | 42++++++++++++++++++++++++++++++++++++++++--
3 files changed, 225 insertions(+), 73 deletions(-)

diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs @@ -55,13 +55,16 @@ pub fn routes() -> Vec<Route> { disable_authenticator, create_organization, + delete_organization, get_user_collections, get_org_collections, + get_collection_users, get_org_details, get_org_users, - get_collection_users, send_invite, confirm_invite, + get_user, + edit_user, delete_user, clear_device_token, diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -5,7 +5,7 @@ use rocket_contrib::{Json, Value}; use db::DbConn; use db::models::*; -use api::{JsonResult, EmptyResult}; +use api::{PasswordData, JsonResult, EmptyResult}; use auth::Headers; @@ -38,6 +38,14 @@ fn create_organization(headers: Headers, data: Json<OrgData>, conn: DbConn) -> J Ok(Json(org.to_json())) } +#[post("/organizations/<org_id>/delete", data = "<data>")] +fn delete_organization(org_id: String, data: Json<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult { + let data: PasswordData = data.into_inner(); + let password_hash = data.masterPasswordHash; + + unimplemented!() +} + // GET /api/collections?writeOnly=false #[get("/collections")] @@ -62,6 +70,33 @@ fn get_org_collections(org_id: String, headers: Headers, conn: DbConn) -> JsonRe }))) } +#[get("/organizations/<org_id>/collections/<coll_id>/users")] +fn get_collection_users(org_id: String, coll_id: String, headers: Headers, conn: DbConn) -> JsonResult { + // Get org and collection, check that collection is from org + + // Get the users from collection + + /* + The elements from the data array to return have the following structure + + { + OrganizationUserId: <id> + AccessAll: true + Name: <user_name> + Email: <user_email> + Type: 0 + Status: 2 + ReadOnly: false + Object: collectionUser + } + */ + + Ok(Json(json!({ + "Data": [], + "Object": "list" + }))) +} + #[derive(FromForm)] #[allow(non_snake_case)] struct OrgIdData { @@ -81,13 +116,13 @@ fn get_org_details(data: OrgIdData, headers: Headers, conn: DbConn) -> JsonResul #[get("/organizations/<org_id>/users")] fn get_org_users(org_id: String, headers: Headers, conn: DbConn) -> JsonResult { - let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { - Some(user_org) => user_org, + match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { + Some(_) => (), None => err!("User isn't member of organization") } let users = UserOrganization::find_by_org(&org_id, &conn); - let users_json: Vec<Value> = users.iter().map(|c| c.to_json_details(&conn)).collect(); + let users_json: Vec<Value> = users.iter().map(|c| c.to_json_user_details(&conn)).collect(); Ok(Json(json!({ "Data": users_json, @@ -95,36 +130,9 @@ fn get_org_users(org_id: String, headers: Headers, conn: DbConn) -> JsonResult { }))) } -#[get("/organizations/<org_id>/collections/<coll_id>/users")] -fn get_collection_users(org_id: String, coll_id: String, headers: Headers, conn: DbConn) -> JsonResult { - // Get org and collection, check that collection is from org - - // Get the users from collection - - /* - The elements from the data array to return have the following structure - - { - OrganizationUserId: <id> - AccessAll: true - Name: <user_name> - Email: <user_email> - Type: 0 - Status: 2 - ReadOnly: false - Object: collectionUser - } - */ - - Ok(Json(json!({ - "Data": [], - "Object": "list" - }))) -} - #[derive(Deserialize)] #[allow(non_snake_case)] -struct InviteCollectionData { +struct CollectionData { id: String, readOnly: bool, } @@ -135,25 +143,30 @@ struct InviteData { emails: Vec<String>, #[serde(rename = "type")] type_: String, - collections: Vec<InviteCollectionData>, + collections: Vec<CollectionData>, accessAll: bool, - } #[post("/organizations/<org_id>/users/invite", data = "<data>")] fn send_invite(org_id: String, data: Json<InviteData>, headers: Headers, conn: DbConn) -> EmptyResult { let data: InviteData = data.into_inner(); - let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { - Some(user_org) => user_org, - None => err!("User isn't member of organization") + let current_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { + Some(user) => user, + None => err!("The current user isn't member of the organization") }; - if user_org.type_ == UserOrgType::User { + if current_user.type_ == UserOrgType::User as i32 { err!("Users can't invite other people. Ask an Admin or Owner") } - if type_ != UserOrgType::User && user_org.type_ != UserOrgType::Owner { + let new_type = match UserOrgType::from_str(data.type_.as_ref()) { + Some(new_type) => new_type as i32, + None => err!("Invalid type") + }; + + if new_type != UserOrgType::User as i32 && + current_user.type_ != UserOrgType::Owner as i32 { err!("Only Owners can invite Admins or Owners") } @@ -166,24 +179,19 @@ fn send_invite(org_id: String, data: Json<InviteData>, headers: Headers, conn: D None => () } - let mut new_user_org = UserOrganization::new( + let mut new_user = UserOrganization::new( user.uuid, org_id.clone()); if data.accessAll { - new_user_org.access_all = data.accessAll; + new_user.access_all = data.accessAll; } else { err!("Select collections unimplemented") // TODO create Users_collections } - new_user_org.type_ = match data.type_.as_ref() { - "Owner" => UserOrgType::Owner, - "Admin" => UserOrgType::Admin, - "User" => UserOrgType::User, - _ => err!("Invalid type") - } as i32; + new_user.type_ = new_type; - new_user_org.save(&conn); + new_user.save(&conn); } } } @@ -193,58 +201,161 @@ fn send_invite(org_id: String, data: Json<InviteData>, headers: Headers, conn: D #[post("/organizations/<org_id>/users/<user_id>/confirm", data = "<data>")] fn confirm_invite(org_id: String, user_id: String, data: Json<Value>, headers: Headers, conn: DbConn) -> EmptyResult { - let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { - Some(user_org) => user_org, - None => err!("User isn't member of organization") + let current_user = match UserOrganization::find_by_user_and_org( + &headers.user.uuid, &org_id, &conn) { + Some(user) => user, + None => err!("The current user isn't member of the organization") }; - if user_org.type_ == UserOrgType::User { + if current_user.type_ == UserOrgType::User as i32 { err!("Users can't confirm other people. Ask an Admin or Owner") } - if type_ != UserOrgType::User && user_org.type_ != UserOrgType::Owner { + let mut user_to_confirm = match UserOrganization::find_by_uuid(&user_id, &conn) { + Some(user) => user, + None => err!("User to confirm isn't member of the organization") + }; + + if user_to_confirm.type_ != UserOrgType::User as i32 && + current_user.type_ != UserOrgType::Owner as i32 { err!("Only Owners can confirm Admins or Owners") } - let mut user_org = match UserOrganization::find_by_user_and_org( - &user_id, &org_id, &conn) { - Some(user_org) => user_org, - None => err!("Can't find user") - }; - - if user_org.status != UserOrgStatus::Accepted as i32 { + if user_to_confirm.status != UserOrgStatus::Accepted as i32 { err!("User in invalid state") } - user_org.status = UserOrgStatus::Confirmed as i32; - user_org.key = match data["key"].as_str() { + user_to_confirm.status = UserOrgStatus::Confirmed as i32; + user_to_confirm.key = match data["key"].as_str() { Some(key) => key.to_string(), None => err!("Invalid key provided") }; - user_org.save(&conn); + user_to_confirm.save(&conn); + + Ok(()) +} + +#[get("/organizations/<org_id>/users/<user_id>")] +fn get_user(org_id: String, user_id: String, headers: Headers, conn: DbConn) -> JsonResult { + let current_user = match UserOrganization::find_by_user_and_org( + &headers.user.uuid, &org_id, &conn) { + Some(user) => user, + None => err!("The current user isn't member of the organization") + }; + + let user = match UserOrganization::find_by_uuid(&user_id, &conn) { + Some(user) => user, + None => err!("The specified user isn't member of the organization") + }; + + Ok(Json(user.to_json_details())) +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct EditUserData { + #[serde(rename = "type")] + type_: String, + collections: Vec<CollectionData>, + accessAll: bool, +} + +#[post("/organizations/<org_id>/users/<user_id>", data = "<data>", rank = 1)] +fn edit_user(org_id: String, user_id: String, data: Json<EditUserData>, headers: Headers, conn: DbConn) -> EmptyResult { + let data: EditUserData = data.into_inner(); + + let current_user = match UserOrganization::find_by_user_and_org( + &headers.user.uuid, &org_id, &conn) { + Some(user) => user, + None => err!("The current user isn't member of the organization") + }; + + let new_type = match UserOrgType::from_str(data.type_.as_ref()) { + Some(new_type) => new_type as i32, + None => err!("Invalid type") + }; + + let mut user_to_edit = match UserOrganization::find_by_uuid(&user_id, &conn) { + Some(user) => user, + None => err!("The specified user isn't member of the organization") + }; + + if current_user.type_ == UserOrgType::User as i32 { + err!("Users can't edit users. Ask an Admin or Owner") + } + + if new_type != UserOrgType::User as i32 && + current_user.type_ != UserOrgType::Owner as i32 { + err!("Only Owners can grant Admin or Owner type") + } + + if user_to_edit.type_ != UserOrgType::User as i32 && + current_user.type_ != UserOrgType::Owner as i32 { + err!("Only Owners can edit Admin or Owner") + } + + if user_to_edit.type_ == UserOrgType::Owner as i32 && + new_type != UserOrgType::Owner as i32 { + + // Removing owner permmission, check that there are at least another owner + let num_owners = UserOrganization::find_by_org_and_type( + &org_id, UserOrgType::Owner as i32, &conn) + .len(); + + if num_owners <= 1 { + err!("Can't delete the last owner") + } + } + + user_to_edit.access_all = data.accessAll; + user_to_edit.type_ = new_type; + + if data.accessAll { + // Remove users_collections if there is any + } else { + // TODO create users_collections + } + + user_to_edit.save(&conn); Ok(()) } #[post("/organizations/<org_id>/users/<user_id>/delete")] fn delete_user(org_id: String, user_id: String, headers: Headers, conn: DbConn) -> EmptyResult { - let user_org = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { - Some(user_org) => user_org, - None => err!("User isn't member of organization") + let current_user = match UserOrganization::find_by_user_and_org( + &headers.user.uuid, &org_id, &conn) { + Some(user) => user, + None => err!("The current user isn't member of the organization") }; - if user_org.type_ == UserOrgType::User { + if current_user.type_ == UserOrgType::User as i32 { err!("Users can't delete other people. Ask an Admin or Owner") } - if type_ != UserOrgType::User && user_org.type_ != UserOrgType::Owner { + let user_to_delete = match UserOrganization::find_by_uuid(&user_id, &conn) { + Some(user) => user, + None => err!("User to delete isn't member of the organization") + }; + + if user_to_delete.type_ != UserOrgType::User as i32 && + current_user.type_ != UserOrgType::Owner as i32 { err!("Only Owners can delete Admins or Owners") } - // TODO Don't delete the last owner + if user_to_delete.type_ == UserOrgType::Owner as i32 { + // Removing owner, check that there are at least another owner + let num_owners = UserOrganization::find_by_org_and_type( + &org_id, UserOrgType::Owner as i32, &conn) + .len(); + + if num_owners <= 1 { + err!("Can't delete the last owner") + } + } - user_org.delete(&conn); + user_to_delete.delete(&conn); // TODO Delete users_collections from this org diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs @@ -37,6 +37,17 @@ pub enum UserOrgType { User = 2, } +impl UserOrgType { + pub fn from_str(s: &str) -> Option<Self> { + match s { + "0" | "Owner" => Some(UserOrgType::Owner), + "1" | "Admin" => Some(UserOrgType::Admin), + "2" | "User" => Some(UserOrgType::User), + _ => None, + } + } +} + /// Local methods impl Organization { pub fn new(name: String, billing_email: String) -> Self { @@ -155,13 +166,13 @@ impl UserOrganization { }) } - pub fn to_json_details(&self, conn: &DbConn) -> JsonValue { + pub fn to_json_user_details(&self, conn: &DbConn) -> JsonValue { use super::User; let user = User::find_by_uuid(&self.user_uuid, conn).unwrap(); json!({ "Id": self.uuid, - "UserId": user.uuid, + "UserId": self.user_uuid, "Name": user.name, "Email": user.email, @@ -173,6 +184,20 @@ impl UserOrganization { }) } + pub fn to_json_details(&self) -> JsonValue { + json!({ + "Id": self.uuid, + "UserId": self.user_uuid, + + "Status": self.status, + "Type": self.type_, + "AccessAll": true, + "Collections": [], + + "Object": "organizationUserDetails", + }) + } + pub fn save(&mut self, conn: &DbConn) -> bool { match diesel::replace_into(users_organizations::table) .values(&*self) @@ -191,6 +216,12 @@ impl UserOrganization { } } + pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { + users_organizations::table + .filter(users_organizations::uuid.eq(uuid)) + .first::<Self>(&**conn).ok() + } + pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) @@ -203,6 +234,13 @@ impl UserOrganization { .load::<Self>(&**conn).expect("Error loading user organizations") } + pub fn find_by_org_and_type(org_uuid: &str, type_: i32, conn: &DbConn) -> Vec<Self> { + users_organizations::table + .filter(users_organizations::org_uuid.eq(org_uuid)) + .filter(users_organizations::type_.eq(type_)) + .load::<Self>(&**conn).expect("Error loading user organizations") + } + pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid))