aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlackDex <[email protected]>2023-06-02 22:28:30 +0200
committerBlackDex <[email protected]>2023-06-13 08:51:07 +0200
commit8e34495e73f280e1c62f0f1b63e0219225f001d7 (patch)
tree4e75f19e03a87cebf54bad3b24e69b6e79d20ea0
parent4219249e11845bb8869c26e1182fa1d38b1a162a (diff)
downloadvaultwarden-8e34495e73f280e1c62f0f1b63e0219225f001d7.tar.gz
vaultwarden-8e34495e73f280e1c62f0f1b63e0219225f001d7.zip
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
-rw-r--r--migrations/mysql/2023-06-02-200424_create_organization_api_key/down.sql (renamed from migrations/mysql/2022-07-21-200424_create_organization_api_key/down.sql)0
-rw-r--r--migrations/mysql/2023-06-02-200424_create_organization_api_key/up.sql (renamed from migrations/mysql/2022-07-21-200424_create_organization_api_key/up.sql)2
-rw-r--r--migrations/postgresql/2023-06-02-200424_create_organization_api_key/down.sql (renamed from migrations/postgresql/2022-07-21-200424_create_organization_api_key/down.sql)0
-rw-r--r--migrations/postgresql/2023-06-02-200424_create_organization_api_key/up.sql (renamed from migrations/postgresql/2022-07-21-200424_create_organization_api_key/up.sql)2
-rw-r--r--migrations/sqlite/2023-06-02-200424_create_organization_api_key/down.sql (renamed from migrations/sqlite/2022-07-21-200424_create_organization_api_key/down.sql)0
-rw-r--r--migrations/sqlite/2023-06-02-200424_create_organization_api_key/up.sql (renamed from migrations/sqlite/2022-07-21-200424_create_organization_api_key/up.sql)2
-rw-r--r--src/api/core/mod.rs2
-rw-r--r--src/api/core/organizations.rs2
-rw-r--r--src/api/core/public.rs231
-rw-r--r--src/auth.rs4
-rw-r--r--src/db/models/group.rs15
-rw-r--r--src/db/models/organization.rs2
-rw-r--r--src/db/models/user.rs24
-rw-r--r--src/db/schemas/mysql/schema.rs1
-rw-r--r--src/db/schemas/postgresql/schema.rs1
-rw-r--r--src/db/schemas/sqlite/schema.rs1
16 files changed, 282 insertions, 7 deletions
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
index e69de29b..e69de29b 100644
--- 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/2022-07-21-200424_create_organization_api_key/up.sql b/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
index e69de29b..e69de29b 100644
--- 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/2022-07-21-200424_create_organization_api_key/up.sql b/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
index e69de29b..e69de29b 100644
--- 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/2022-07-21-200424_create_organization_api_key/up.sql b/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<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
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<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
index d96e98e1..6b01a4d4 100644
--- 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
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<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
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<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
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<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
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<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
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<Integer>,
api_key -> Nullable<Text>,
avatar_color -> Nullable<Text>,
+ external_id -> Nullable<Text>,
}
}