commit 8e34495e73f280e1c62f0f1b63e0219225f001d7 parent 4219249e11845bb8869c26e1182fa1d38b1a162a Author: BlackDex <black.dex@gmail.com> Date: Fri, 2 Jun 2023 22:28:30 +0200 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 <daniele.andrei@geo-satis.com> Co-authored-by: Kurnihil Diffstat:
19 files changed, 307 insertions(+), 32 deletions(-)
diff --git a/migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql b/migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql @@ -1,8 +0,0 @@ -CREATE TABLE organization_api_key ( - uuid CHAR(36) NOT NULL, - org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid), - atype INTEGER NOT NULL, - api_key VARCHAR(255) NOT NULL, - revision_date DATETIME NOT NULL, - PRIMARY KEY(uuid, org_uuid) -); 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 diff --git a/migrations/mysql/2023-06-02-200424_create_organization_api_key/up.sql b/migrations/mysql/2023-06-02-200424_create_organization_api_key/up.sql @@ -0,0 +1,10 @@ +CREATE TABLE organization_api_key ( + uuid CHAR(36) NOT NULL, + org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid), + atype INTEGER NOT NULL, + api_key VARCHAR(255) NOT NULL, + 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/up.sql b/migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql @@ -1,8 +0,0 @@ -CREATE TABLE organization_api_key ( - uuid CHAR(36) NOT NULL, - org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid), - atype INTEGER NOT NULL, - api_key VARCHAR(255), - revision_date TIMESTAMP NOT NULL, - PRIMARY KEY(uuid, org_uuid) -); 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 diff --git a/migrations/postgresql/2023-06-02-200424_create_organization_api_key/up.sql b/migrations/postgresql/2023-06-02-200424_create_organization_api_key/up.sql @@ -0,0 +1,10 @@ +CREATE TABLE organization_api_key ( + uuid CHAR(36) NOT NULL, + org_uuid CHAR(36) NOT NULL REFERENCES organizations(uuid), + atype INTEGER NOT NULL, + api_key VARCHAR(255), + 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/up.sql b/migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql @@ -1,9 +0,0 @@ -CREATE TABLE organization_api_key ( - uuid TEXT NOT NULL, - org_uuid TEXT NOT NULL, - atype INTEGER NOT NULL, - api_key TEXT NOT NULL, - revision_date DATETIME NOT NULL, - PRIMARY KEY(uuid, org_uuid), - FOREIGN KEY(org_uuid) REFERENCES organizations(uuid) -); 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 diff --git a/migrations/sqlite/2023-06-02-200424_create_organization_api_key/up.sql b/migrations/sqlite/2023-06-02-200424_create_organization_api_key/up.sql @@ -0,0 +1,11 @@ +CREATE TABLE organization_api_key ( + uuid TEXT NOT NULL, + org_uuid TEXT NOT NULL, + atype INTEGER NOT NULL, + api_key TEXT NOT NULL, + revision_date DATETIME NOT NULL, + 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 @@ -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<Route> { 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 @@ -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 @@ -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<Route> { + routes![ldap_import] +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct OrgImportGroupData { + Name: String, + ExternalId: String, + MemberExternalIds: Vec<String>, +} + +#[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<OrgImportGroupData>, + Members: Vec<OrgImportUserData>, + OverwriteExisting: bool, + #[allow(dead_code)] + LargeImport: bool, +} + +#[post("/public/organization/import", data = "<data>")] +async fn ldap_import(data: JsonUpcase<OrgImportData>, 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<Self, Self::Error> { + 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 @@ -94,6 +94,10 @@ pub fn decode_send(token: &str) -> Result<BasicJwtClaims, Error> { decode_jwt(token, JWT_SEND_ISSUER.to_string()) } +pub fn decode_api_org(token: &str) -> Result<OrgApiKeyLoginJwtClaims, Error> { + 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 @@ -10,7 +10,7 @@ db_object! { pub organizations_uuid: String, pub name: String, pub access_all: bool, - external_id: Option<String>, + pub external_id: Option<String>, 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<String> { - 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<Self> { + db_run! { conn: { + groups::table + .filter(groups::external_id.eq(id)) + .first::<GroupDb>(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<String> { db_run! { conn: { diff --git 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 @@ -50,6 +50,8 @@ db_object! { pub api_key: Option<String>, pub avatar_color: Option<String>, + + pub external_id: Option<String>, } #[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<String>) { + //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<Self> { + db_run! {conn: { + users::table.filter(users::external_id.eq(id)).first::<UserDb>(conn).ok().from_db() + }} + } pub async fn get_all(conn: &mut DbConn) -> Vec<Self> { db_run! {conn: { users::table.load::<UserDb>(conn).expect("Error loading users").from_db() diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs @@ -204,6 +204,7 @@ table! { client_kdf_parallelism -> Nullable<Integer>, api_key -> Nullable<Text>, avatar_color -> Nullable<Text>, + external_id -> Nullable<Text>, } } diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs @@ -204,6 +204,7 @@ table! { client_kdf_parallelism -> Nullable<Integer>, api_key -> Nullable<Text>, avatar_color -> Nullable<Text>, + external_id -> Nullable<Text>, } } diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs @@ -204,6 +204,7 @@ table! { client_kdf_parallelism -> Nullable<Integer>, api_key -> Nullable<Text>, avatar_color -> Nullable<Text>, + external_id -> Nullable<Text>, } }