瀏覽代碼

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 <[email protected]>
Co-authored-by: Kurnihil
BlackDex 2 年之前
父節點
當前提交
8e34495e73

+ 0 - 0
migrations/mysql/2022-07-21-200424_create_organization_api_key/down.sql → migrations/mysql/2023-06-02-200424_create_organization_api_key/down.sql


+ 2 - 0
migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql → 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;

+ 0 - 0
migrations/postgresql/2022-07-21-200424_create_organization_api_key/down.sql → migrations/postgresql/2023-06-02-200424_create_organization_api_key/down.sql


+ 2 - 0
migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql → 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;

+ 0 - 0
migrations/sqlite/2022-07-21-200424_create_organization_api_key/down.sql → migrations/sqlite/2023-06-02-200424_create_organization_api_key/down.sql


+ 2 - 0
migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql → 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;

+ 2 - 0
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);

+ 1 - 1
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
     })))
 }
 

+ 231 - 0
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))
+    }
+}

+ 4 - 0
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

+ 10 - 5
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: {

+ 1 - 1
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")
             }

+ 24 - 0
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()

+ 1 - 0
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>,
     }
 }
 

+ 1 - 0
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>,
     }
 }
 

+ 1 - 0
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>,
     }
 }