aboutsummaryrefslogtreecommitdiff
path: root/src/api/core/public.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/core/public.rs')
-rw-r--r--src/api/core/public.rs231
1 files changed, 231 insertions, 0 deletions
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))
+ }
+}