1
0
Fork 0
mirror of https://github.com/dani-garcia/vaultwarden.git synced 2025-06-20 18:30:08 +00:00

Merge remote-tracking branch 'dani/main' into sso-support

This commit is contained in:
Timshel 2025-01-24 17:30:31 +01:00
commit 6ee5580b03
26 changed files with 345 additions and 155 deletions

View file

@ -99,6 +99,7 @@ const DT_FMT: &str = "%Y-%m-%d %H:%M:%S %Z";
const BASE_TEMPLATE: &str = "admin/base";
const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000";
pub const FAKE_ADMIN_UUID: &str = "00000000-0000-0000-0000-000000000000";
fn admin_path() -> String {
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
@ -299,7 +300,9 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbCon
async fn _generate_invite(user: &User, conn: &mut DbConn) -> EmptyResult {
if CONFIG.mail_enabled() {
mail::send_invite(user, None, None, &CONFIG.invitation_org_name(), None).await
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
mail::send_invite(user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
} else {
let invitation = Invitation::new(&user.email);
invitation.save(conn).await
@ -475,7 +478,9 @@ async fn resend_user_invite(user_id: UserId, _token: AdminToken, mut conn: DbCon
}
if CONFIG.mail_enabled() {
mail::send_invite(&user, None, None, &CONFIG.invitation_org_name(), None).await
let org_id: OrganizationId = FAKE_ADMIN_UUID.to_string().into();
let member_id: MembershipId = FAKE_ADMIN_UUID.to_string().into();
mail::send_invite(&user, org_id, member_id, &CONFIG.invitation_org_name(), None).await
} else {
Ok(())
}

View file

@ -30,6 +30,7 @@ pub fn routes() -> Vec<rocket::Route> {
profile,
put_profile,
post_profile,
put_avatar,
get_public_keys,
post_keys,
post_password,
@ -43,9 +44,8 @@ pub fn routes() -> Vec<rocket::Route> {
post_verify_email_token,
post_delete_recover,
post_delete_recover_token,
post_device_token,
delete_account,
post_delete_account,
delete_account,
revision_date,
password_hint,
prelogin,
@ -53,7 +53,9 @@ pub fn routes() -> Vec<rocket::Route> {
api_key,
rotate_api_key,
get_known_device,
put_avatar,
get_all_devices,
get_device,
post_device_token,
put_device_token,
put_clear_device_token,
post_clear_device_token,
@ -1157,6 +1159,26 @@ impl<'r> FromRequest<'r> for KnownDevice {
}
}
#[get("/devices")]
async fn get_all_devices(headers: Headers, mut conn: DbConn) -> JsonResult {
let devices = Device::find_with_auth_request_by_user(&headers.user.uuid, &mut conn).await;
let devices = devices.iter().map(|device| device.to_json()).collect::<Vec<Value>>();
Ok(Json(json!({
"data": devices,
"continuationToken": null,
"object": "list"
})))
}
#[get("/devices/identifier/<device_id>")]
async fn get_device(device_id: DeviceId, headers: Headers, mut conn: DbConn) -> JsonResult {
let Some(device) = Device::find_by_uuid_and_user(&device_id, &headers.user.uuid, &mut conn).await else {
err!("No device found");
};
Ok(Json(device.to_json()))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct PushToken {

View file

@ -4,6 +4,7 @@ use rocket::Route;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use crate::api::admin::FAKE_ADMIN_UUID;
use crate::{
api::{
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
@ -142,6 +143,7 @@ struct NewCollectionGroupData {
hide_passwords: bool,
id: GroupId,
read_only: bool,
manage: bool,
}
#[derive(Deserialize)]
@ -150,6 +152,7 @@ struct NewCollectionMemberData {
hide_passwords: bool,
id: MembershipId,
read_only: bool,
manage: bool,
}
#[derive(Deserialize)]
@ -375,18 +378,13 @@ async fn get_org_collections_details(
|| (CONFIG.org_groups_enabled()
&& GroupUser::has_access_to_collection_by_member(&col.uuid, &member.uuid, &mut conn).await);
// Not assigned collections should not be returned
if !assigned {
continue;
}
// get the users assigned directly to the given collection
let users: Vec<Value> = col_users
.iter()
.filter(|collection_user| collection_user.collection_uuid == col.uuid)
.map(|collection_user| {
collection_user.to_json_details_for_user(
*membership_type.get(&collection_user.membership_uuid).unwrap_or(&(MembershipType::User as i32)),
.filter(|collection_member| collection_member.collection_uuid == col.uuid)
.map(|collection_member| {
collection_member.to_json_details_for_member(
*membership_type.get(&collection_member.membership_uuid).unwrap_or(&(MembershipType::User as i32)),
)
})
.collect();
@ -450,7 +448,7 @@ async fn post_organization_collections(
.await;
for group in data.groups {
CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords)
CollectionGroup::new(collection.uuid.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
.save(&mut conn)
.await?;
}
@ -464,12 +462,19 @@ async fn post_organization_collections(
continue;
}
CollectionUser::save(&member.user_uuid, &collection.uuid, user.read_only, user.hide_passwords, &mut conn)
.await?;
CollectionUser::save(
&member.user_uuid,
&collection.uuid,
user.read_only,
user.hide_passwords,
user.manage,
&mut conn,
)
.await?;
}
if headers.membership.atype == MembershipType::Manager && !headers.membership.access_all {
CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, &mut conn).await?;
CollectionUser::save(&headers.membership.user_uuid, &collection.uuid, false, false, false, &mut conn).await?;
}
Ok(Json(collection.to_json()))
@ -526,7 +531,9 @@ async fn post_organization_collection_update(
CollectionGroup::delete_all_by_collection(&col_id, &mut conn).await?;
for group in data.groups {
CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords).save(&mut conn).await?;
CollectionGroup::new(col_id.clone(), group.id, group.read_only, group.hide_passwords, group.manage)
.save(&mut conn)
.await?;
}
CollectionUser::delete_all_by_collection(&col_id, &mut conn).await?;
@ -540,7 +547,8 @@ async fn post_organization_collection_update(
continue;
}
CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, &mut conn).await?;
CollectionUser::save(&member.user_uuid, &col_id, user.read_only, user.hide_passwords, user.manage, &mut conn)
.await?;
}
Ok(Json(collection.to_json_details(&headers.user.uuid, None, &mut conn).await))
@ -698,10 +706,10 @@ async fn get_org_collection_detail(
CollectionUser::find_by_collection_swap_user_uuid_with_member_uuid(&collection.uuid, &mut conn)
.await
.iter()
.map(|collection_user| {
collection_user.to_json_details_for_user(
.map(|collection_member| {
collection_member.to_json_details_for_member(
*membership_type
.get(&collection_user.membership_uuid)
.get(&collection_member.membership_uuid)
.unwrap_or(&(MembershipType::User as i32)),
)
})
@ -771,7 +779,7 @@ async fn put_collection_users(
continue;
}
CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, &mut conn).await?;
CollectionUser::save(&user.user_uuid, &col_id, d.read_only, d.hide_passwords, d.manage, &mut conn).await?;
}
Ok(())
@ -891,6 +899,7 @@ struct CollectionData {
id: CollectionId,
read_only: bool,
hide_passwords: bool,
manage: bool,
}
#[derive(Deserialize)]
@ -899,6 +908,7 @@ struct MembershipData {
id: MembershipId,
read_only: bool,
hide_passwords: bool,
manage: bool,
}
#[derive(Deserialize)]
@ -997,8 +1007,8 @@ async fn send_invite(
if let Err(e) = mail::send_invite(
&user,
Some(org_id.clone()),
Some(new_member.uuid.clone()),
org_id.clone(),
new_member.uuid.clone(),
&org_name,
Some(headers.user.email.clone()),
)
@ -1037,6 +1047,7 @@ async fn send_invite(
&collection.uuid,
col.read_only,
col.hide_passwords,
col.manage,
&mut conn,
)
.await?;
@ -1124,14 +1135,7 @@ async fn _reinvite_member(
};
if CONFIG.mail_enabled() {
mail::send_invite(
&user,
Some(org_id.clone()),
Some(member.uuid),
&org_name,
Some(invited_by_email.to_string()),
)
.await?;
mail::send_invite(&user, org_id.clone(), member.uuid, &org_name, Some(invited_by_email.to_string())).await?;
} else if user.password_hash.is_empty() {
let invitation = Invitation::new(&user.email);
invitation.save(conn).await?;
@ -1163,21 +1167,24 @@ async fn accept_invite(
// The web-vault passes org_id and member_id in the URL, but we are just reading them from the JWT instead
let data: AcceptData = data.into_inner();
let claims = decode_invite(&data.token)?;
let user = headers.user;
// Don't allow other users from accepting an invitation.
if !claims.email.eq(&headers.user.email) {
err!("Invitation was issued to a different account", "Claim does not match user_id")
}
// If a claim does not have a member_id or it does not match the one in from the URI, something is wrong.
match &claims.member_id {
Some(ou_id) if ou_id.eq(&member_id) => {}
_ => err!("Error accepting the invitation", "Claim does not match the member_id"),
if !claims.member_id.eq(&member_id) {
err!("Error accepting the invitation", "Claim does not match the member_id")
}
if user.email != claims.email {
err!("Invitation claim does not match the user")
}
let member = &claims.member_id;
let org = &claims.org_id;
Invitation::take(&claims.email, &mut conn).await;
if let (Some(member), Some(org)) = (&claims.member_id, &claims.org_id) {
// skip invitation logic when we were invited via the /admin panel
if **member != FAKE_ADMIN_UUID {
let Some(mut member) = Membership::find_by_uuid_and_org(member, org, &mut conn).await else {
err!("Error accepting the invitation")
};
@ -1198,7 +1205,7 @@ async fn accept_invite(
Ok(_) => {}
Err(OrgPolicyErr::TwoFactorMissing) => {
if CONFIG.email_2fa_auto_fallback() {
two_factor::email::activate_email_2fa(&user, &mut conn).await?;
two_factor::email::activate_email_2fa(&headers.user, &mut conn).await?;
} else {
err!("You cannot join this organization until you enable two-step login on your user account");
}
@ -1219,18 +1226,16 @@ async fn accept_invite(
}
if CONFIG.mail_enabled() {
let mut org_name = CONFIG.invitation_org_name();
if let Some(org_id) = &claims.org_id {
org_name = match Organization::find_by_uuid(org_id, &mut conn).await {
if let Some(invited_by_email) = &claims.invited_by_email {
let org_name = match Organization::find_by_uuid(&claims.org_id, &mut conn).await {
Some(org) => org.name,
None => err!("Organization not found."),
};
};
if let Some(invited_by_email) = &claims.invited_by_email {
// User was invited to an organization, so they must be confirmed manually after acceptance
mail::send_invite_accepted(&claims.email, invited_by_email, &org_name).await?;
} else {
// User was invited from /admin, so they are automatically confirmed
let org_name = CONFIG.invitation_org_name();
mail::send_invite_confirmed(&claims.email, &org_name).await?;
}
}
@ -1535,6 +1540,7 @@ async fn edit_member(
&collection.uuid,
col.read_only,
col.hide_passwords,
col.manage,
&mut conn,
)
.await?;
@ -1852,23 +1858,17 @@ async fn list_policies(org_id: OrganizationId, _headers: AdminHeaders, mut conn:
#[get("/organizations/<org_id>/policies/token?<token>")]
async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbConn) -> JsonResult {
// web-vault 2024.6.2 seems to send these values and cause logs to output errors
// Catch this and prevent errors in the logs
// TODO: CleanUp after 2024.6.x is not used anymore.
if org_id.as_ref() == "undefined" && token == "undefined" {
return Ok(Json(json!({})));
}
let invite = decode_invite(token)?;
let Some(invite_org_id) = invite.org_id else {
err!("Invalid token")
};
if invite_org_id != org_id {
if invite.org_id != org_id {
err!("Token doesn't match request organization");
}
// exit early when we have been invited via /admin panel
if org_id.as_ref() == FAKE_ADMIN_UUID {
return Ok(Json(json!({})));
}
// TODO: We receive the invite token as ?token=<>, validate it contains the org id
let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await;
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
@ -2183,8 +2183,8 @@ async fn import(org_id: OrganizationId, data: Json<OrgImportData>, headers: Head
mail::send_invite(
&user,
Some(org_id.clone()),
Some(new_member.uuid.clone()),
org_id.clone(),
new_member.uuid.clone(),
&org_name,
Some(headers.user.email.clone()),
)
@ -2527,11 +2527,12 @@ struct SelectedCollection {
id: CollectionId,
read_only: bool,
hide_passwords: bool,
manage: bool,
}
impl SelectedCollection {
pub fn to_collection_group(&self, groups_uuid: GroupId) -> CollectionGroup {
CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords)
CollectionGroup::new(self.id.clone(), groups_uuid, self.read_only, self.hide_passwords, self.manage)
}
}

View file

@ -119,14 +119,8 @@ async fn ldap_import(data: Json<OrgImportData>, token: PublicToken, mut conn: Db
None => err!("Error looking up organization"),
};
if let Err(e) = mail::send_invite(
&user,
Some(org_id.clone()),
Some(new_member.uuid.clone()),
&org_name,
Some(org_email),
)
.await
if let Err(e) =
mail::send_invite(&user, org_id.clone(), new_member.uuid.clone(), &org_name, Some(org_email)).await
{
// Upon error delete the user, invite and org member records when needed
if user_created {

View file

@ -291,9 +291,7 @@ fn get_favicons_node(dom: Tokenizer<StringReader<'_>, FaviconEmitter>, icons: &m
TAG_HEAD if token.closing => {
break;
}
_ => {
continue;
}
_ => {}
}
}

View file

@ -157,7 +157,6 @@ fn websockets_hub<'r>(
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
yield Message::binary(INITIAL_RESPONSE);
continue;
}
}
@ -225,7 +224,6 @@ fn anonymous_websockets_hub<'r>(ws: WebSocket, token: String, ip: ClientIp) -> R
if serde_json::from_str(msg).ok() == Some(INITIAL_MESSAGE) {
yield Message::binary(INITIAL_RESPONSE);
continue;
}
}