From 8e34495e73f280e1c62f0f1b63e0219225f001d7 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Fri, 2 Jun 2023 22:28:30 +0200 Subject: [PATCH] Merge and modify PR from @Kurnihil Merging a PR from @Kurnihil into the already rebased branch. Made some small changes to make it work with newer changes. Some finetuning is probably still needed. Co-authored-by: Daniele Andrei Co-authored-by: Kurnihil --- .../down.sql | 0 .../up.sql | 2 + .../down.sql | 0 .../up.sql | 2 + .../down.sql | 0 .../up.sql | 2 + src/api/core/mod.rs | 2 + src/api/core/organizations.rs | 2 +- src/api/core/public.rs | 231 ++++++++++++++++++ src/auth.rs | 4 + src/db/models/group.rs | 15 +- src/db/models/organization.rs | 2 +- src/db/models/user.rs | 24 ++ src/db/schemas/mysql/schema.rs | 1 + src/db/schemas/postgresql/schema.rs | 1 + src/db/schemas/sqlite/schema.rs | 1 + 16 files changed, 282 insertions(+), 7 deletions(-) rename migrations/mysql/{2022-07-21-200424_create_organization_api_key => 2023-06-02-200424_create_organization_api_key}/down.sql (100%) rename migrations/mysql/{2022-07-21-200424_create_organization_api_key => 2023-06-02-200424_create_organization_api_key}/up.sql (83%) rename migrations/postgresql/{2022-07-21-200424_create_organization_api_key => 2023-06-02-200424_create_organization_api_key}/down.sql (100%) rename migrations/postgresql/{2022-07-21-200424_create_organization_api_key => 2023-06-02-200424_create_organization_api_key}/up.sql (83%) rename migrations/sqlite/{2022-07-21-200424_create_organization_api_key => 2023-06-02-200424_create_organization_api_key}/down.sql (100%) rename migrations/sqlite/{2022-07-21-200424_create_organization_api_key => 2023-06-02-200424_create_organization_api_key}/up.sql (86%) create mode 100644 src/api/core/public.rs diff --git a/migrations/mysql/2022-07-21-200424_create_organization_api_key/down.sql b/migrations/mysql/2023-06-02-200424_create_organization_api_key/down.sql similarity index 100% rename from migrations/mysql/2022-07-21-200424_create_organization_api_key/down.sql rename to migrations/mysql/2023-06-02-200424_create_organization_api_key/down.sql diff --git a/migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql b/migrations/mysql/2023-06-02-200424_create_organization_api_key/up.sql similarity index 83% rename from migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql rename to migrations/mysql/2023-06-02-200424_create_organization_api_key/up.sql index e9e0b739..6c4f5cb6 100644 --- a/migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql +++ b/migrations/mysql/2023-06-02-200424_create_organization_api_key/up.sql @@ -6,3 +6,5 @@ CREATE TABLE organization_api_key ( revision_date DATETIME NOT NULL, PRIMARY KEY(uuid, org_uuid) ); + +ALTER TABLE users ADD COLUMN external_id TEXT; diff --git a/migrations/postgresql/2022-07-21-200424_create_organization_api_key/down.sql b/migrations/postgresql/2023-06-02-200424_create_organization_api_key/down.sql similarity index 100% rename from migrations/postgresql/2022-07-21-200424_create_organization_api_key/down.sql rename to migrations/postgresql/2023-06-02-200424_create_organization_api_key/down.sql diff --git a/migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql b/migrations/postgresql/2023-06-02-200424_create_organization_api_key/up.sql similarity index 83% rename from migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql rename to migrations/postgresql/2023-06-02-200424_create_organization_api_key/up.sql index 3c37bb5c..9c3ba41c 100644 --- a/migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql +++ b/migrations/postgresql/2023-06-02-200424_create_organization_api_key/up.sql @@ -6,3 +6,5 @@ CREATE TABLE organization_api_key ( revision_date TIMESTAMP NOT NULL, PRIMARY KEY(uuid, org_uuid) ); + +ALTER TABLE users ADD COLUMN external_id TEXT; diff --git a/migrations/sqlite/2022-07-21-200424_create_organization_api_key/down.sql b/migrations/sqlite/2023-06-02-200424_create_organization_api_key/down.sql similarity index 100% rename from migrations/sqlite/2022-07-21-200424_create_organization_api_key/down.sql rename to migrations/sqlite/2023-06-02-200424_create_organization_api_key/down.sql diff --git a/migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql b/migrations/sqlite/2023-06-02-200424_create_organization_api_key/up.sql similarity index 86% rename from migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql rename to migrations/sqlite/2023-06-02-200424_create_organization_api_key/up.sql index 986b00d9..2880bb22 100644 --- a/migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql +++ b/migrations/sqlite/2023-06-02-200424_create_organization_api_key/up.sql @@ -7,3 +7,5 @@ CREATE TABLE organization_api_key ( PRIMARY KEY(uuid, org_uuid), FOREIGN KEY(org_uuid) REFERENCES organizations(uuid) ); + +ALTER TABLE users ADD COLUMN external_id TEXT; diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 00cfcf85..f7e912cf 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -4,6 +4,7 @@ mod emergency_access; mod events; mod folders; mod organizations; +mod public; mod sends; pub mod two_factor; @@ -27,6 +28,7 @@ pub fn routes() -> Vec { routes.append(&mut organizations::routes()); routes.append(&mut two_factor::routes()); routes.append(&mut sends::routes()); + routes.append(&mut public::routes()); routes.append(&mut eq_domains_routes); routes.append(&mut hibp_routes); routes.append(&mut meta_routes); diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index a3aee5be..a71eb641 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -2382,7 +2382,7 @@ async fn add_update_group( "OrganizationId": group.organizations_uuid, "Name": group.name, "AccessAll": group.access_all, - "ExternalId": group.get_external_id() + "ExternalId": group.external_id }))) } diff --git a/src/api/core/public.rs b/src/api/core/public.rs new file mode 100644 index 00000000..c8689222 --- /dev/null +++ b/src/api/core/public.rs @@ -0,0 +1,231 @@ +use chrono::Utc; +use rocket::{ + request::{self, FromRequest, Outcome}, + Request, Route, +}; + +use crate::{ + api::{EmptyResult, JsonUpcase}, + auth, + db::{models::*, DbConn}, + mail, CONFIG, +}; + +pub fn routes() -> Vec { + routes![ldap_import] +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct OrgImportGroupData { + Name: String, + ExternalId: String, + MemberExternalIds: Vec, +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct OrgImportUserData { + Email: String, + ExternalId: String, + Deleted: bool, +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct OrgImportData { + Groups: Vec, + Members: Vec, + OverwriteExisting: bool, + #[allow(dead_code)] + LargeImport: bool, +} + +#[post("/public/organization/import", data = "")] +async fn ldap_import(data: JsonUpcase, token: PublicToken, mut conn: DbConn) -> EmptyResult { + let _ = &conn; + let org_id = token.0; + let data = data.into_inner().data; + + for user_data in &data.Members { + if user_data.Deleted { + // If user is marked for deletion and it exists, revoke it + if let Some(mut user_org) = + UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await + { + user_org.revoke(); + user_org.save(&mut conn).await?; + } + + // If user is part of the organization, restore it + } else if let Some(mut user_org) = + UserOrganization::find_by_email_and_org(&user_data.Email, &org_id, &mut conn).await + { + if user_org.status < UserOrgStatus::Revoked as i32 { + user_org.restore(); + user_org.save(&mut conn).await?; + } + } else { + // If user is not part of the organization + let user = match User::find_by_mail(&user_data.Email, &mut conn).await { + Some(user) => user, // exists in vaultwarden + None => { + // doesn't exist in vaultwarden + let mut new_user = User::new(user_data.Email.clone()); + new_user.set_external_id(Some(user_data.ExternalId.clone())); + new_user.save(&mut conn).await?; + + if !CONFIG.mail_enabled() { + let invitation = Invitation::new(&new_user.email); + invitation.save(&mut conn).await?; + } + new_user + } + }; + let user_org_status = if CONFIG.mail_enabled() { + UserOrgStatus::Invited as i32 + } else { + UserOrgStatus::Accepted as i32 // Automatically mark user as accepted if no email invites + }; + + let mut new_org_user = UserOrganization::new(user.uuid.clone(), org_id.clone()); + new_org_user.access_all = false; + new_org_user.atype = UserOrgType::User as i32; + new_org_user.status = user_org_status; + + new_org_user.save(&mut conn).await?; + + if CONFIG.mail_enabled() { + let (org_name, org_email) = match Organization::find_by_uuid(&org_id, &mut conn).await { + Some(org) => (org.name, org.billing_email), + None => err!("Error looking up organization"), + }; + + mail::send_invite( + &user_data.Email, + &user.uuid, + Some(org_id.clone()), + Some(new_org_user.uuid), + &org_name, + Some(org_email), + ) + .await?; + } + } + } + + for group_data in &data.Groups { + let group_uuid = match Group::find_by_external_id(&group_data.ExternalId, &mut conn).await { + Some(group) => group.uuid, + None => { + let mut group = + Group::new(org_id.clone(), group_data.Name.clone(), false, Some(group_data.ExternalId.clone())); + group.save(&mut conn).await?; + group.uuid + } + }; + + GroupUser::delete_all_by_group(&group_uuid, &mut conn).await?; + + for ext_id in &group_data.MemberExternalIds { + if let Some(user) = User::find_by_external_id(ext_id, &mut conn).await { + if let Some(user_org) = UserOrganization::find_by_user_and_org(&user.uuid, &org_id, &mut conn).await { + let mut group_user = GroupUser::new(group_uuid.clone(), user_org.uuid.clone()); + group_user.save(&mut conn).await?; + } + } + } + } + + // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true) + if data.OverwriteExisting { + for user_org in UserOrganization::find_by_org(&org_id, &mut conn).await { + if let Some(user_external_id) = + User::find_by_uuid(&user_org.user_uuid, &mut conn).await.map(|u| u.external_id) + { + if user_external_id.is_some() + && !data.Members.iter().any(|u| u.ExternalId == *user_external_id.as_ref().unwrap()) + { + if user_org.atype == UserOrgType::Owner && user_org.status == UserOrgStatus::Confirmed as i32 { + // Removing owner, check that there is at least one other confirmed owner + if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &mut conn) + .await + <= 1 + { + warn!("Can't delete the last owner"); + continue; + } + } + user_org.delete(&mut conn).await?; + } + } + } + } + + Ok(()) +} + +#[derive(Debug)] +pub struct PublicToken(String); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for PublicToken { + type Error = &'static str; + + async fn from_request(request: &'r Request<'_>) -> request::Outcome { + let headers = request.headers(); + // Get access_token + let access_token: &str = match headers.get_one("Authorization") { + Some(a) => match a.rsplit("Bearer ").next() { + Some(split) => split, + None => err_handler!("No access token provided"), + }, + None => err_handler!("No access token provided"), + }; + // Check JWT token is valid and get device and user from it + let claims = match auth::decode_api_org(access_token) { + Ok(claims) => claims, + Err(_) => err_handler!("Invalid claim"), + }; + // Check if time is between claims.nbf and claims.exp + let time_now = Utc::now().naive_utc().timestamp(); + if time_now < claims.nbf { + err_handler!("Token issued in the future"); + } + if time_now > claims.exp { + err_handler!("Token expired"); + } + // Check if claims.iss is host|claims.scope[0] + let host = match auth::Host::from_request(request).await { + Outcome::Success(host) => host, + _ => err_handler!("Error getting Host"), + }; + let complete_host = format!("{}|{}", host.host, claims.scope[0]); + if complete_host != claims.iss { + err_handler!("Token not issued by this server"); + } + + // Check if claims.sub is org_api_key.uuid + // Check if claims.client_sub is org_api_key.org_uuid + let conn = match DbConn::from_request(request).await { + Outcome::Success(conn) => conn, + _ => err_handler!("Error getting DB"), + }; + let org_uuid = match claims.client_id.strip_prefix("organization.") { + Some(uuid) => uuid, + None => err_handler!("Malformed client_id"), + }; + let org_api_key = match OrganizationApiKey::find_by_org_uuid(org_uuid, &conn).await { + Some(org_api_key) => org_api_key, + None => err_handler!("Invalid client_id"), + }; + if org_api_key.org_uuid != claims.client_sub { + err_handler!("Token not issued for this org"); + } + if org_api_key.uuid != claims.sub { + err_handler!("Token not issued for this client"); + } + + Outcome::Success(PublicToken(claims.client_sub)) + } +} diff --git a/src/auth.rs b/src/auth.rs index d96e98e1..6b01a4d4 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -94,6 +94,10 @@ pub fn decode_send(token: &str) -> Result { decode_jwt(token, JWT_SEND_ISSUER.to_string()) } +pub fn decode_api_org(token: &str) -> Result { + decode_jwt(token, JWT_ORG_API_KEY_ISSUER.to_string()) +} + #[derive(Debug, Serialize, Deserialize)] pub struct LoginJwtClaims { // Not before diff --git a/src/db/models/group.rs b/src/db/models/group.rs index 258b9e42..5bae798d 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -10,7 +10,7 @@ db_object! { pub organizations_uuid: String, pub name: String, pub access_all: bool, - external_id: Option, + pub external_id: Option, pub creation_date: NaiveDateTime, pub revision_date: NaiveDateTime, } @@ -107,10 +107,6 @@ impl Group { None => self.external_id = None, } } - - pub fn get_external_id(&self) -> Option { - self.external_id.clone() - } } impl CollectionGroup { @@ -214,6 +210,15 @@ impl Group { }} } + pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option { + db_run! { conn: { + groups::table + .filter(groups::external_id.eq(id)) + .first::(conn) + .ok() + .from_db() + }} + } //Returns all organizations the user has full access to pub async fn gather_user_organizations_full_access(user_uuid: &str, conn: &mut DbConn) -> Vec { db_run! { conn: { diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 47151a96..5d1f0af2 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -510,7 +510,7 @@ impl UserOrganization { .set(UserOrganizationDb::to_db(self)) .execute(conn) .map_res("Error adding user to organization") - } + }, Err(e) => Err(e.into()), }.map_res("Error adding user to organization") } diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 83a59524..a4764ada 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -50,6 +50,8 @@ db_object! { pub api_key: Option, pub avatar_color: Option, + + pub external_id: Option, } #[derive(Identifiable, Queryable, Insertable)] @@ -126,6 +128,8 @@ impl User { api_key: None, avatar_color: None, + + external_id: None, } } @@ -150,6 +154,21 @@ impl User { matches!(self.api_key, Some(ref api_key) if crate::crypto::ct_eq(api_key, key)) } + pub fn set_external_id(&mut self, external_id: Option) { + //Check if external id is empty. We don't want to have + //empty strings in the database + match external_id { + Some(external_id) => { + if external_id.is_empty() { + self.external_id = None; + } else { + self.external_id = Some(external_id) + } + } + None => self.external_id = None, + } + } + /// Set the password hash generated /// And resets the security_stamp. Based upon the allow_next_route the security_stamp will be different. /// @@ -376,6 +395,11 @@ impl User { }} } + pub async fn find_by_external_id(id: &str, conn: &mut DbConn) -> Option { + db_run! {conn: { + users::table.filter(users::external_id.eq(id)).first::(conn).ok().from_db() + }} + } pub async fn get_all(conn: &mut DbConn) -> Vec { db_run! {conn: { users::table.load::(conn).expect("Error loading users").from_db() diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index 4d58f63f..37803275 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -204,6 +204,7 @@ table! { client_kdf_parallelism -> Nullable, api_key -> Nullable, avatar_color -> Nullable, + external_id -> Nullable, } } diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 941f51cb..0b69bbc4 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -204,6 +204,7 @@ table! { client_kdf_parallelism -> Nullable, api_key -> Nullable, avatar_color -> Nullable, + external_id -> Nullable, } } diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index ce9c7e33..10dd3fe8 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -204,6 +204,7 @@ table! { client_kdf_parallelism -> Nullable, api_key -> Nullable, avatar_color -> Nullable, + external_id -> Nullable, } }