mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-11 03:05:58 +03:00
When a user does not have an account yet and SMTP was disabled it would set the UserOrg status still to Accepted, though that would make it possible to verify the user by the Org Admin's. This would fail, since the user didn't actually crated his account, and therefor no PublicKey existed. This PR fixes this behaviour by checking if the password is empty and if so, puts the user to an `Invited` state instead of `Accepted`. Fixes #3737
239 lines
9.1 KiB
Rust
239 lines
9.1 KiB
Rust
use chrono::Utc;
|
|
use rocket::{
|
|
request::{self, FromRequest, Outcome},
|
|
Request, Route,
|
|
};
|
|
|
|
use std::collections::HashSet;
|
|
|
|
use crate::{
|
|
api::{EmptyResult, JsonUpcase},
|
|
auth,
|
|
db::{models::*, DbConn},
|
|
mail, CONFIG,
|
|
};
|
|
|
|
pub fn routes() -> Vec<Route> {
|
|
routes![ldap_import]
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[allow(non_snake_case)]
|
|
struct OrgImportGroupData {
|
|
Name: String,
|
|
ExternalId: String,
|
|
MemberExternalIds: Vec<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[allow(non_snake_case)]
|
|
struct OrgImportUserData {
|
|
Email: String,
|
|
ExternalId: String,
|
|
Deleted: bool,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[allow(non_snake_case)]
|
|
struct OrgImportData {
|
|
Groups: Vec<OrgImportGroupData>,
|
|
Members: Vec<OrgImportUserData>,
|
|
OverwriteExisting: bool,
|
|
// LargeImport: bool, // For now this will not be used, upstream uses this to prevent syncs of more then 2000 users or groups without the flag set.
|
|
}
|
|
|
|
#[post("/public/organization/import", data = "<data>")]
|
|
async fn ldap_import(data: JsonUpcase<OrgImportData>, token: PublicToken, mut conn: DbConn) -> EmptyResult {
|
|
// Most of the logic for this function can be found here
|
|
// https://github.com/bitwarden/server/blob/fd892b2ff4547648a276734fb2b14a8abae2c6f5/src/Core/Services/Implementations/OrganizationService.cs#L1797
|
|
|
|
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() || user.password_hash.is_empty() {
|
|
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?;
|
|
}
|
|
}
|
|
}
|
|
|
|
if CONFIG.org_groups_enabled() {
|
|
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?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
warn!("Group support is disabled, groups will not be imported!");
|
|
}
|
|
|
|
// 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 {
|
|
// Generate a HashSet to quickly verify if a member is listed or not.
|
|
let sync_members: HashSet<String> = data.Members.into_iter().map(|m| m.ExternalId).collect();
|
|
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() && !sync_members.contains(&user_external_id.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(())
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|