From 3fa78e7bb141979d6f6fdfa20aecc70493b80842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Sat, 14 Mar 2020 13:22:30 +0100 Subject: [PATCH] Initial version of policies --- .../down.sql | 1 + .../2020-03-13-205045_add_policy_table/up.sql | 9 ++ .../down.sql | 1 + .../2020-03-13-205045_add_policy_table/up.sql | 9 ++ .../down.sql | 1 + .../2020-03-13-205045_add_policy_table/up.sql | 9 ++ src/api/core/ciphers.rs | 6 +- src/api/core/organizations.rs | 73 +++++++-- src/auth.rs | 11 ++ src/db/models/mod.rs | 2 + src/db/models/org_policy.rs | 142 ++++++++++++++++++ src/db/models/organization.rs | 19 +-- src/db/schemas/mysql/schema.rs | 12 ++ src/db/schemas/postgresql/schema.rs | 12 ++ src/db/schemas/sqlite/schema.rs | 12 ++ 15 files changed, 296 insertions(+), 23 deletions(-) create mode 100644 migrations/mysql/2020-03-13-205045_add_policy_table/down.sql create mode 100644 migrations/mysql/2020-03-13-205045_add_policy_table/up.sql create mode 100644 migrations/postgresql/2020-03-13-205045_add_policy_table/down.sql create mode 100644 migrations/postgresql/2020-03-13-205045_add_policy_table/up.sql create mode 100644 migrations/sqlite/2020-03-13-205045_add_policy_table/down.sql create mode 100644 migrations/sqlite/2020-03-13-205045_add_policy_table/up.sql create mode 100644 src/db/models/org_policy.rs diff --git a/migrations/mysql/2020-03-13-205045_add_policy_table/down.sql b/migrations/mysql/2020-03-13-205045_add_policy_table/down.sql new file mode 100644 index 00000000..8dc1826a --- /dev/null +++ b/migrations/mysql/2020-03-13-205045_add_policy_table/down.sql @@ -0,0 +1 @@ +DROP TABLE org_policies; diff --git a/migrations/mysql/2020-03-13-205045_add_policy_table/up.sql b/migrations/mysql/2020-03-13-205045_add_policy_table/up.sql new file mode 100644 index 00000000..642df162 --- /dev/null +++ b/migrations/mysql/2020-03-13-205045_add_policy_table/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE org_policies ( + uuid CHAR(36) NOT NULL PRIMARY KEY, + org_uuid CHAR(36) NOT NULL REFERENCES organizations (uuid), + atype INTEGER NOT NULL, + enabled BOOLEAN NOT NULL, + data TEXT NOT NULL, + + UNIQUE (org_uuid, atype) +); diff --git a/migrations/postgresql/2020-03-13-205045_add_policy_table/down.sql b/migrations/postgresql/2020-03-13-205045_add_policy_table/down.sql new file mode 100644 index 00000000..8dc1826a --- /dev/null +++ b/migrations/postgresql/2020-03-13-205045_add_policy_table/down.sql @@ -0,0 +1 @@ +DROP TABLE org_policies; diff --git a/migrations/postgresql/2020-03-13-205045_add_policy_table/up.sql b/migrations/postgresql/2020-03-13-205045_add_policy_table/up.sql new file mode 100644 index 00000000..a6b234ee --- /dev/null +++ b/migrations/postgresql/2020-03-13-205045_add_policy_table/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE org_policies ( + uuid CHAR(36) NOT NULL PRIMARY KEY, + org_uuid CHAR(36) NOT NULL REFERENCES organizations (uuid), + atype INTEGER NOT NULL, + enabled BOOLEAN NOT NULL, + data TEXT NOT NULL, + + UNIQUE (org_uuid, atype) +); diff --git a/migrations/sqlite/2020-03-13-205045_add_policy_table/down.sql b/migrations/sqlite/2020-03-13-205045_add_policy_table/down.sql new file mode 100644 index 00000000..8dc1826a --- /dev/null +++ b/migrations/sqlite/2020-03-13-205045_add_policy_table/down.sql @@ -0,0 +1 @@ +DROP TABLE org_policies; diff --git a/migrations/sqlite/2020-03-13-205045_add_policy_table/up.sql b/migrations/sqlite/2020-03-13-205045_add_policy_table/up.sql new file mode 100644 index 00000000..0fadc67b --- /dev/null +++ b/migrations/sqlite/2020-03-13-205045_add_policy_table/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE org_policies ( + uuid TEXT NOT NULL PRIMARY KEY, + org_uuid TEXT NOT NULL REFERENCES organizations (uuid), + atype INTEGER NOT NULL, + enabled BOOLEAN NOT NULL, + data TEXT NOT NULL, + + UNIQUE (org_uuid, atype) +); diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 7793c623..b625f849 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -79,6 +79,9 @@ fn sync(data: Form, headers: Headers, conn: DbConn) -> JsonResult { let collections = Collection::find_by_user_uuid(&headers.user.uuid, &conn); let collections_json: Vec = collections.iter().map(Collection::to_json).collect(); + let policies = OrgPolicy::find_by_user(&headers.user.uuid, &conn); + let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); + let ciphers = Cipher::find_by_user(&headers.user.uuid, &conn); let ciphers_json: Vec = ciphers .iter() @@ -95,6 +98,7 @@ fn sync(data: Form, headers: Headers, conn: DbConn) -> JsonResult { "Profile": user_json, "Folders": folders_json, "Collections": collections_json, + "Policies": policies_json, "Ciphers": ciphers_json, "Domains": domains_json, "Object": "sync" @@ -648,7 +652,7 @@ fn post_attachment( if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) { err_discard!("Cipher is not write accessible", data) } - + let mut params = content_type.params(); let boundary_pair = params.next().expect("No boundary provided"); let boundary = boundary_pair.1; diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 1b092d1a..f54d947c 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2,6 +2,7 @@ use rocket::request::Form; use rocket::Route; use rocket_contrib::json::Json; use serde_json::Value; +use num_traits::FromPrimitive; use crate::api::{ EmptyResult, JsonResult, JsonUpcase, JsonUpcaseVec, Notify, NumberOrString, PasswordData, UpdateType, @@ -45,6 +46,9 @@ pub fn routes() -> Vec { delete_user, post_delete_user, post_org_import, + list_policies, + get_policy, + put_policy, ] } @@ -830,22 +834,13 @@ struct RelationsData { fn post_org_import( query: Form, data: JsonUpcase, - headers: Headers, + headers: AdminHeaders, conn: DbConn, nt: Notify, ) -> EmptyResult { let data: ImportData = data.into_inner().data; let org_id = query.into_inner().organization_id; - let org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn) { - Some(user) => user, - None => err!("User is not part of the organization"), - }; - - if org_user.atype < UserOrgType::Admin { - err!("Only admins or owners can import into an organization") - } - // Read and create the collections let collections: Vec<_> = data .Collections @@ -866,6 +861,8 @@ fn post_org_import( relations.push((relation.Key, relation.Value)); } + let headers: Headers = headers.into(); + // Read and create the ciphers let ciphers: Vec<_> = data .Ciphers @@ -901,3 +898,59 @@ fn post_org_import( let mut user = headers.user; user.update_revision(&conn) } + +#[get("/organizations//policies")] +fn list_policies(org_id: String, _headers: AdminHeaders, conn: DbConn) -> JsonResult { + let policies = OrgPolicy::find_by_org(&org_id, &conn); + let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); + + Ok(Json(json!({ + "Data": policies_json, + "Object": "list", + "ContinuationToken": null + }))) +} + +#[get("/organizations//policies/")] +fn get_policy(org_id: String, pol_type: i32, _headers: AdminHeaders, conn: DbConn) -> JsonResult { + let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { + Some(pt) => pt, + None => err!("Invalid policy type"), + }; + + let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn) { + Some(p) => p, + None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), + }; + + Ok(Json(policy.to_json())) +} + +#[derive(Deserialize)] +struct PolicyData { + enabled: bool, + #[serde(rename = "type")] + _type: i32, + data: Value, +} + +#[put("/organizations//policies/", data = "")] +fn put_policy(org_id: String, pol_type: i32, data: Json, _headers: AdminHeaders, conn: DbConn) -> JsonResult { + let data: PolicyData = data.into_inner(); + + let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { + Some(pt) => pt, + None => err!("Invalid policy type"), + }; + + let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn) { + Some(p) => p, + None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), + }; + + policy.enabled = data.enabled; + policy.data = serde_json::to_string(&data.data)?; + policy.save(&conn)?; + + Ok(Json(policy.to_json())) +} \ No newline at end of file diff --git a/src/auth.rs b/src/auth.rs index d007d727..425524fd 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -4,6 +4,7 @@ use crate::util::read_file; use chrono::{Duration, Utc}; use once_cell::sync::Lazy; +use num_traits::FromPrimitive; use jsonwebtoken::{self, Algorithm, Header}; use serde::de::DeserializeOwned; @@ -385,6 +386,16 @@ impl<'a, 'r> FromRequest<'a, 'r> for AdminHeaders { } } +impl Into for AdminHeaders { + fn into(self) -> Headers { + Headers { + host: self.host, + device: self.device, + user: self.user + } + } +} + pub struct OwnerHeaders { pub host: String, pub device: Device, diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 1ae6197f..7d8951cc 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -7,6 +7,7 @@ mod user; mod collection; mod organization; mod two_factor; +mod org_policy; pub use self::attachment::Attachment; pub use self::cipher::Cipher; @@ -17,3 +18,4 @@ pub use self::organization::Organization; pub use self::organization::{UserOrgStatus, UserOrgType, UserOrganization}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::user::{Invitation, User}; +pub use self::org_policy::{OrgPolicy, OrgPolicyType}; \ No newline at end of file diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs new file mode 100644 index 00000000..63eabe1b --- /dev/null +++ b/src/db/models/org_policy.rs @@ -0,0 +1,142 @@ +use diesel; +use diesel::prelude::*; +use serde_json::Value; + +use crate::api::EmptyResult; +use crate::db::schema::org_policies; +use crate::db::DbConn; +use crate::error::MapResult; + +use super::Organization; + +#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] +#[table_name = "org_policies"] +#[belongs_to(Organization, foreign_key = "org_uuid")] +#[primary_key(uuid)] +pub struct OrgPolicy { + pub uuid: String, + pub org_uuid: String, + pub atype: i32, + pub enabled: bool, + pub data: String, +} + +#[allow(dead_code)] +#[derive(FromPrimitive)] +pub enum OrgPolicyType { + TwoFactorAuthentication = 0, + MasterPassword = 1, + PasswordGenerator = 2, +} + +/// Local methods +impl OrgPolicy { + pub fn new(org_uuid: String, atype: OrgPolicyType, data: String) -> Self { + Self { + uuid: crate::util::get_uuid(), + org_uuid, + atype: atype as i32, + enabled: false, + data, + } + } + + pub fn to_json(&self) -> Value { + let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null); + json!({ + "Id": self.uuid, + "OrganizationId": self.org_uuid, + "Type": self.atype, + "Data": data_json, + "Enabled": self.enabled, + "Object": "policy", + }) + } +} + +/// Database methods +impl OrgPolicy { + #[cfg(feature = "postgresql")] + pub fn save(&mut self, conn: &DbConn) -> EmptyResult { + // We need to make sure we're not going to violate the unique constraint on org_uuid and atype. + // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does + // not support multiple constraints on ON CONFLICT clauses. + diesel::delete( + org_policies::table + .filter(org_policies::org_uuid.eq(&self.org_uuid)) + .filter(org_policies::atype.eq(&self.atype)), + ) + .execute(&**conn) + .map_res("Error deleting org_policy for insert")?; + + diesel::insert_into(org_policies::table) + .values(self) + .on_conflict(org_policies::uuid) + .do_update() + .set(self) + .execute(&**conn) + .map_res("Error saving org_policy") + } + + #[cfg(not(feature = "postgresql"))] + pub fn save(&mut self, conn: &DbConn) -> EmptyResult { + diesel::replace_into(org_policies::table) + .values(&*self) + .execute(&**conn) + .map_res("Error saving org_policy") + } + + pub fn delete(self, conn: &DbConn) -> EmptyResult { + diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid))) + .execute(&**conn) + .map_res("Error deleting org_policy") + } + + pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option { + org_policies::table + .filter(org_policies::uuid.eq(uuid)) + .first::(&**conn) + .ok() + } + + pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec { + org_policies::table + .filter(org_policies::org_uuid.eq(org_uuid)) + .load::(&**conn) + .expect("Error loading org_policy") + } + + pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec { + use crate::db::schema::users_organizations; + + org_policies::table + .left_join( + users_organizations::table.on( + users_organizations::org_uuid.eq(org_policies::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))) + ) + .select(org_policies::all_columns) + .load::(&**conn) + .expect("Error loading org_policy") + } + + pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Option { + org_policies::table + .filter(org_policies::org_uuid.eq(org_uuid)) + .filter(org_policies::atype.eq(atype)) + .first::(&**conn) + .ok() + } + + pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult { + diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid))) + .execute(&**conn) + .map_res("Error deleting org_policy") + } + + /*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { + diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) + .execute(&**conn) + .map_res("Error deleting twofactors") + }*/ +} diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index e092164e..a35e9cc5 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -1,7 +1,8 @@ use serde_json::Value; use std::cmp::Ordering; +use num_traits::FromPrimitive; -use super::{CollectionUser, User}; +use super::{CollectionUser, User, OrgPolicy}; #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "organizations"] @@ -33,6 +34,7 @@ pub enum UserOrgStatus { } #[derive(Copy, Clone, PartialEq, Eq)] +#[derive(FromPrimitive)] pub enum UserOrgType { Owner = 0, Admin = 1, @@ -135,16 +137,6 @@ impl UserOrgType { _ => None, } } - - pub fn from_i32(i: i32) -> Option { - match i { - 0 => Some(UserOrgType::Owner), - 1 => Some(UserOrgType::Admin), - 2 => Some(UserOrgType::User), - 3 => Some(UserOrgType::Manager), - _ => None, - } - } } /// Local methods @@ -170,6 +162,7 @@ impl Organization { "UseEvents": false, "UseGroups": false, "UseTotp": true, + "UsePolicies": true, "BusinessName": null, "BusinessAddress1": null, @@ -250,6 +243,7 @@ impl Organization { Cipher::delete_all_by_organization(&self.uuid, &conn)?; Collection::delete_all_by_organization(&self.uuid, &conn)?; UserOrganization::delete_all_by_organization(&self.uuid, &conn)?; + OrgPolicy::delete_all_by_organization(&self.uuid, &conn)?; diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid))) .execute(&**conn) @@ -267,7 +261,7 @@ impl Organization { impl UserOrganization { pub fn to_json(&self, conn: &DbConn) -> Value { let org = Organization::find_by_uuid(&self.org_uuid, conn).unwrap(); - + json!({ "Id": self.org_uuid, "Name": org.name, @@ -280,6 +274,7 @@ impl UserOrganization { "UseEvents": false, "UseGroups": false, "UseTotp": true, + "UsePolicies": true, "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index 36165b6b..c03f6a5d 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -77,6 +77,16 @@ table! { } } +table! { + org_policies (uuid) { + uuid -> Varchar, + org_uuid -> Varchar, + atype -> Integer, + enabled -> Bool, + data -> Text, + } +} + table! { organizations (uuid) { uuid -> Varchar, @@ -155,6 +165,7 @@ joinable!(devices -> users (user_uuid)); joinable!(folders -> users (user_uuid)); joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); +joinable!(org_policies -> organizations (org_uuid)); joinable!(twofactor -> users (user_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); @@ -170,6 +181,7 @@ allow_tables_to_appear_in_same_query!( folders, folders_ciphers, invitations, + org_policies, organizations, twofactor, users, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index a683212c..b5d0a6ec 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -77,6 +77,16 @@ table! { } } +table! { + org_policies (uuid) { + uuid -> Text, + org_uuid -> Text, + atype -> Integer, + enabled -> Bool, + data -> Text, + } +} + table! { organizations (uuid) { uuid -> Text, @@ -155,6 +165,7 @@ joinable!(devices -> users (user_uuid)); joinable!(folders -> users (user_uuid)); joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); +joinable!(org_policies -> organizations (org_uuid)); joinable!(twofactor -> users (user_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); @@ -170,6 +181,7 @@ allow_tables_to_appear_in_same_query!( folders, folders_ciphers, invitations, + org_policies, organizations, twofactor, users, diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index a683212c..b5d0a6ec 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -77,6 +77,16 @@ table! { } } +table! { + org_policies (uuid) { + uuid -> Text, + org_uuid -> Text, + atype -> Integer, + enabled -> Bool, + data -> Text, + } +} + table! { organizations (uuid) { uuid -> Text, @@ -155,6 +165,7 @@ joinable!(devices -> users (user_uuid)); joinable!(folders -> users (user_uuid)); joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); +joinable!(org_policies -> organizations (org_uuid)); joinable!(twofactor -> users (user_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); @@ -170,6 +181,7 @@ allow_tables_to_appear_in_same_query!( folders, folders_ciphers, invitations, + org_policies, organizations, twofactor, users,