mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-06-12 13:17:43 +02:00
Org fixes (#5438)
* Security fixes for admin and sendmail Because the Vaultwarden Admin Backend endpoints did not validated the Content-Type during a request, it was possible to update settings via CSRF. But, this was only possible if there was no `ADMIN_TOKEN` set at all. To make sure these environments are also safe I added the needed content-type checks at the functions. This could cause some users who have scripts which uses cURL for example to adjust there commands to provide the correct headers. By using a crafted favicon and having access to the Admin Backend an attacker could run custom commands on the host/container where Vaultwarden is running on. The main issue here is that we allowed the sendmail binary name/path to be changed. To mitigate this we removed this configuration item and only then `sendmail` binary as a name can be used. This could cause some issues where the `sendmail` binary is not in the `$PATH` and thus not able to be started. In these cases the admins should make sure `$PATH` is set correctly or create a custom shell script or symlink at a location which is in the `$PATH`. Added an extra security header and adjusted the CSP to be more strict by setting `default-src` to `none` and added the needed missing specific policies. Also created a general email validation function which does some more checking to catch invalid email address not found by the email_address crate. Signed-off-by: BlackDex <black.dex@gmail.com> * Fix security issue with organizationId validation Because of a invalid check/validation of the OrganizationId which most of the time is located in the path but sometimes provided as a URL Parameter, the parameter overruled the path ID during the Guard checks. This resulted in someone being able to execute commands as an Admin or Owner of the OrganizationId fetched from the parameter, but the API endpoints then used the OrganizationId located in the path instead. This commit fixes the extraction of the OrganizationId in the Guard and also added some extra validations of this OrgId in several functions. Also added an extra `OrgMemberHeaders` which can be used to only allow access to organization endpoints which should only be accessible by members of that org. Signed-off-by: BlackDex <black.dex@gmail.com> * Update server version in config endpoint Updated the server version reported to the clients to `2025.1.0`. This should make Vaultwarden future proof for the newer clients released by Bitwarden. Signed-off-by: BlackDex <black.dex@gmail.com> * Fix and adjust build workflow The build workflow had an issue with some `if` checks. For one they had two `$` signs, and it is not recommended to use `always()` since canceling a workflow does not cancel those calls. Using `!cancelled()` is the preferred way. Signed-off-by: BlackDex <black.dex@gmail.com> * Update crates Signed-off-by: BlackDex <black.dex@gmail.com> * Allow sendmail to be configurable This reverts a previous change which removed the sendmail to be configurable. We now set the config to be read-only, and omit all read-only values from being stored during a save action from the admin interface. Signed-off-by: BlackDex <black.dex@gmail.com> * Add more org_id checks Added more org_id checks at all functions which use the org_id in there path. Signed-off-by: BlackDex <black.dex@gmail.com> --------- Signed-off-by: BlackDex <black.dex@gmail.com>
This commit is contained in:

committed by
GitHub

parent
c0be36a17f
commit
952992c85b
@ -171,7 +171,7 @@ struct LoginForm {
|
||||
redirect: Option<String>,
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
#[post("/", format = "application/x-www-form-urlencoded", data = "<data>")]
|
||||
fn post_admin_login(
|
||||
data: Form<LoginForm>,
|
||||
cookies: &CookieJar<'_>,
|
||||
@ -289,7 +289,7 @@ async fn get_user_or_404(user_id: &UserId, conn: &mut DbConn) -> ApiResult<User>
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/invite", data = "<data>")]
|
||||
#[post("/invite", format = "application/json", data = "<data>")]
|
||||
async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbConn) -> JsonResult {
|
||||
let data: InviteData = data.into_inner();
|
||||
if User::find_by_mail(&data.email, &mut conn).await.is_some() {
|
||||
@ -315,7 +315,7 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbCon
|
||||
Ok(Json(user.to_json(&mut conn).await))
|
||||
}
|
||||
|
||||
#[post("/test/smtp", data = "<data>")]
|
||||
#[post("/test/smtp", format = "application/json", data = "<data>")]
|
||||
async fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
||||
let data: InviteData = data.into_inner();
|
||||
|
||||
@ -393,7 +393,7 @@ async fn get_user_json(user_id: UserId, _token: AdminToken, mut conn: DbConn) ->
|
||||
Ok(Json(usr))
|
||||
}
|
||||
|
||||
#[post("/users/<user_id>/delete")]
|
||||
#[post("/users/<user_id>/delete", format = "application/json")]
|
||||
async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let user = get_user_or_404(&user_id, &mut conn).await?;
|
||||
|
||||
@ -417,7 +417,7 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em
|
||||
res
|
||||
}
|
||||
|
||||
#[post("/users/<user_id>/deauth")]
|
||||
#[post("/users/<user_id>/deauth", format = "application/json")]
|
||||
async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let mut user = get_user_or_404(&user_id, &mut conn).await?;
|
||||
|
||||
@ -438,7 +438,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt:
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/<user_id>/disable")]
|
||||
#[post("/users/<user_id>/disable", format = "application/json")]
|
||||
async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
||||
let mut user = get_user_or_404(&user_id, &mut conn).await?;
|
||||
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||
@ -452,7 +452,7 @@ async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt:
|
||||
save_result
|
||||
}
|
||||
|
||||
#[post("/users/<user_id>/enable")]
|
||||
#[post("/users/<user_id>/enable", format = "application/json")]
|
||||
async fn enable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let mut user = get_user_or_404(&user_id, &mut conn).await?;
|
||||
user.enabled = true;
|
||||
@ -460,7 +460,7 @@ async fn enable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> E
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/<user_id>/remove-2fa")]
|
||||
#[post("/users/<user_id>/remove-2fa", format = "application/json")]
|
||||
async fn remove_2fa(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let mut user = get_user_or_404(&user_id, &mut conn).await?;
|
||||
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||
@ -469,7 +469,7 @@ async fn remove_2fa(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Emp
|
||||
user.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/<user_id>/invite/resend")]
|
||||
#[post("/users/<user_id>/invite/resend", format = "application/json")]
|
||||
async fn resend_user_invite(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
if let Some(user) = User::find_by_uuid(&user_id, &mut conn).await {
|
||||
//TODO: replace this with user.status check when it will be available (PR#3397)
|
||||
@ -496,7 +496,7 @@ struct MembershipTypeData {
|
||||
org_uuid: OrganizationId,
|
||||
}
|
||||
|
||||
#[post("/users/org_type", data = "<data>")]
|
||||
#[post("/users/org_type", format = "application/json", data = "<data>")]
|
||||
async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let data: MembershipTypeData = data.into_inner();
|
||||
|
||||
@ -550,7 +550,7 @@ async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToke
|
||||
member_to_edit.save(&mut conn).await
|
||||
}
|
||||
|
||||
#[post("/users/update_revision")]
|
||||
#[post("/users/update_revision", format = "application/json")]
|
||||
async fn update_revision_users(_token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
User::update_all_revisions(&mut conn).await
|
||||
}
|
||||
@ -575,7 +575,7 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
#[post("/organizations/<org_id>/delete")]
|
||||
#[post("/organizations/<org_id>/delete", format = "application/json")]
|
||||
async fn delete_organization(org_id: OrganizationId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
|
||||
let org = Organization::find_by_uuid(&org_id, &mut conn).await.map_res("Organization doesn't exist")?;
|
||||
org.delete(&mut conn).await
|
||||
@ -733,7 +733,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
#[get("/diagnostics/config")]
|
||||
#[get("/diagnostics/config", format = "application/json")]
|
||||
fn get_diagnostics_config(_token: AdminToken) -> Json<Value> {
|
||||
let support_json = CONFIG.get_support_json();
|
||||
Json(support_json)
|
||||
@ -744,16 +744,16 @@ fn get_diagnostics_http(code: u16, _token: AdminToken) -> EmptyResult {
|
||||
err_code!(format!("Testing error {code} response"), code);
|
||||
}
|
||||
|
||||
#[post("/config", data = "<data>")]
|
||||
#[post("/config", format = "application/json", data = "<data>")]
|
||||
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
|
||||
let data: ConfigBuilder = data.into_inner();
|
||||
if let Err(e) = CONFIG.update_config(data) {
|
||||
if let Err(e) = CONFIG.update_config(data, true) {
|
||||
err!(format!("Unable to save config: {e:?}"))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/config/delete")]
|
||||
#[post("/config/delete", format = "application/json")]
|
||||
fn delete_config(_token: AdminToken) -> EmptyResult {
|
||||
if let Err(e) = CONFIG.delete_user_config() {
|
||||
err!(format!("Unable to delete config: {e:?}"))
|
||||
@ -761,7 +761,7 @@ fn delete_config(_token: AdminToken) -> EmptyResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[post("/config/backup_db")]
|
||||
#[post("/config/backup_db", format = "application/json")]
|
||||
async fn backup_db(_token: AdminToken, mut conn: DbConn) -> ApiResult<String> {
|
||||
if *CAN_BACKUP {
|
||||
match backup_database(&mut conn).await {
|
||||
|
@ -34,9 +34,13 @@ struct EventRange {
|
||||
async fn get_org_events(
|
||||
org_id: OrganizationId,
|
||||
data: EventRange,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
|
||||
// Return an empty vec when we org events are disabled.
|
||||
// This prevents client errors
|
||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||
@ -100,9 +104,12 @@ async fn get_user_events(
|
||||
org_id: OrganizationId,
|
||||
member_id: MembershipId,
|
||||
data: EventRange,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
// Return an empty vec when we org events are disabled.
|
||||
// This prevents client errors
|
||||
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
|
||||
|
@ -210,8 +210,8 @@ fn config() -> Json<Value> {
|
||||
// This means they expect a version that closely matches the Bitwarden server version
|
||||
// We should make sure that we keep this updated when we support the new server features
|
||||
// Version history:
|
||||
// - Individual cipher key encryption: 2023.9.1
|
||||
"version": "2024.2.0",
|
||||
// - Individual cipher key encryption: 2024.2.0
|
||||
"version": "2025.1.0",
|
||||
"gitHash": option_env!("GIT_REV"),
|
||||
"server": {
|
||||
"name": "Vaultwarden",
|
||||
|
@ -10,7 +10,10 @@ use crate::{
|
||||
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
|
||||
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
|
||||
},
|
||||
auth::{decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
|
||||
auth::{
|
||||
decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders,
|
||||
OwnerHeaders,
|
||||
},
|
||||
db::{models::*, DbConn},
|
||||
mail,
|
||||
util::{convert_json_key_lcase_first, NumberOrString},
|
||||
@ -213,6 +216,9 @@ async fn delete_organization(
|
||||
headers: OwnerHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: PasswordOrOtpData = data.into_inner();
|
||||
|
||||
data.validate(&headers.user, true, &mut conn).await?;
|
||||
@ -261,7 +267,10 @@ async fn leave_organization(org_id: OrganizationId, headers: Headers, mut conn:
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>")]
|
||||
async fn get_organization(org_id: OrganizationId, _headers: OwnerHeaders, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_organization(org_id: OrganizationId, headers: OwnerHeaders, mut conn: DbConn) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
match Organization::find_by_uuid(&org_id, &mut conn).await {
|
||||
Some(organization) => Ok(Json(organization.to_json())),
|
||||
None => err!("Can't find organization details"),
|
||||
@ -285,14 +294,18 @@ async fn post_organization(
|
||||
data: Json<OrganizationUpdateData>,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
|
||||
let data: OrganizationUpdateData = data.into_inner();
|
||||
|
||||
let Some(mut org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
|
||||
err!("Can't find organization details")
|
||||
err!("Organization not found")
|
||||
};
|
||||
|
||||
org.name = data.name;
|
||||
org.billing_email = data.billing_email;
|
||||
org.billing_email = data.billing_email.to_lowercase();
|
||||
|
||||
org.save(&mut conn).await?;
|
||||
|
||||
@ -325,12 +338,15 @@ async fn get_user_collections(headers: Headers, mut conn: DbConn) -> Json<Value>
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/collections")]
|
||||
async fn get_org_collections(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
|
||||
Json(json!({
|
||||
async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
||||
if org_id != headers.membership.org_uuid {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
Ok(Json(json!({
|
||||
"data": _get_org_collections(&org_id, &mut conn).await,
|
||||
"object": "list",
|
||||
"continuationToken": null,
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/collections/details")]
|
||||
@ -339,6 +355,9 @@ async fn get_org_collections_details(
|
||||
headers: ManagerHeadersLoose,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.membership.org_uuid {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
|
||||
let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await else {
|
||||
@ -413,6 +432,9 @@ async fn post_organization_collections(
|
||||
data: Json<NewCollectionData>,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.membership.org_uuid {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: NewCollectionData = data.into_inner();
|
||||
|
||||
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
|
||||
@ -485,6 +507,9 @@ async fn post_organization_collection_update(
|
||||
data: Json<NewCollectionData>,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: NewCollectionData = data.into_inner();
|
||||
|
||||
if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() {
|
||||
@ -545,9 +570,12 @@ async fn delete_organization_collection_member(
|
||||
org_id: OrganizationId,
|
||||
col_id: CollectionId,
|
||||
member_id: MembershipId,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else {
|
||||
err!("Collection not found", "Collection does not exist or does not belong to this organization")
|
||||
};
|
||||
@ -638,6 +666,9 @@ async fn bulk_delete_organization_collections(
|
||||
data: Json<BulkCollectionIds>,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.membership.org_uuid {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: BulkCollectionIds = data.into_inner();
|
||||
|
||||
let collections = data.ids;
|
||||
@ -657,6 +688,9 @@ async fn get_org_collection_detail(
|
||||
headers: ManagerHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
match Collection::find_by_uuid_and_user(&col_id, headers.user.uuid.clone(), &mut conn).await {
|
||||
None => err!("Collection not found"),
|
||||
Some(collection) => {
|
||||
@ -718,9 +752,12 @@ async fn get_org_collection_detail(
|
||||
async fn get_collection_users(
|
||||
org_id: OrganizationId,
|
||||
col_id: CollectionId,
|
||||
_headers: ManagerHeaders,
|
||||
headers: ManagerHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
// Get org and collection, check that collection is from org
|
||||
let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else {
|
||||
err!("Collection not found in Organization")
|
||||
@ -744,9 +781,12 @@ async fn put_collection_users(
|
||||
org_id: OrganizationId,
|
||||
col_id: CollectionId,
|
||||
data: Json<Vec<MembershipData>>,
|
||||
_headers: ManagerHeaders,
|
||||
headers: ManagerHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
// Get org and collection, check that collection is from org
|
||||
if Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await.is_none() {
|
||||
err!("Collection not found in Organization")
|
||||
@ -778,10 +818,9 @@ struct OrgIdData {
|
||||
}
|
||||
|
||||
#[get("/ciphers/organization-details?<data..>")]
|
||||
async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
if Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &mut conn).await.is_none()
|
||||
{
|
||||
err_code!("Resource not found.", rocket::http::Status::NotFound.code);
|
||||
async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: DbConn) -> JsonResult {
|
||||
if data.organization_id != headers.org_id {
|
||||
err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code);
|
||||
}
|
||||
|
||||
Ok(Json(json!({
|
||||
@ -814,9 +853,12 @@ struct GetOrgUserData {
|
||||
async fn get_members(
|
||||
data: GetOrgUserData,
|
||||
org_id: OrganizationId,
|
||||
_headers: ManagerHeadersLoose,
|
||||
headers: ManagerHeadersLoose,
|
||||
mut conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
if org_id != headers.membership.org_uuid {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let mut users_json = Vec::new();
|
||||
for u in Membership::find_by_org(&org_id, &mut conn).await {
|
||||
users_json.push(
|
||||
@ -829,20 +871,23 @@ async fn get_members(
|
||||
);
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"data": users_json,
|
||||
"object": "list",
|
||||
"continuationToken": null,
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[post("/organizations/<org_id>/keys", data = "<data>")]
|
||||
async fn post_org_keys(
|
||||
org_id: OrganizationId,
|
||||
data: Json<OrgKeyData>,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: OrgKeyData = data.into_inner();
|
||||
|
||||
let mut org = match Organization::find_by_uuid(&org_id, &mut conn).await {
|
||||
@ -905,6 +950,9 @@ async fn send_invite(
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let mut data: InviteData = data.into_inner();
|
||||
|
||||
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
|
||||
@ -1045,7 +1093,10 @@ async fn bulk_reinvite_members(
|
||||
data: Json<BulkMembershipIds>,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: BulkMembershipIds = data.into_inner();
|
||||
|
||||
let mut bulk_response = Vec::new();
|
||||
@ -1064,11 +1115,11 @@ async fn bulk_reinvite_members(
|
||||
))
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"data": bulk_response,
|
||||
"object": "list",
|
||||
"continuationToken": null
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[post("/organizations/<org_id>/users/<member_id>/reinvite")]
|
||||
@ -1237,7 +1288,10 @@ async fn bulk_confirm_invite(
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data = data.into_inner();
|
||||
|
||||
let mut bulk_response = Vec::new();
|
||||
@ -1263,11 +1317,11 @@ async fn bulk_confirm_invite(
|
||||
None => error!("No keys to confirm"),
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"data": bulk_response,
|
||||
"object": "list",
|
||||
"continuationToken": null
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[post("/organizations/<org_id>/users/<member_id>/confirm", data = "<data>")]
|
||||
@ -1364,19 +1418,22 @@ async fn _confirm_invite(
|
||||
#[get("/organizations/<org_id>/users/mini-details", rank = 1)]
|
||||
async fn get_org_user_mini_details(
|
||||
org_id: OrganizationId,
|
||||
_headers: ManagerHeadersLoose,
|
||||
headers: ManagerHeadersLoose,
|
||||
mut conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
if org_id != headers.membership.org_uuid {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let mut members_json = Vec::new();
|
||||
for m in Membership::find_by_org(&org_id, &mut conn).await {
|
||||
members_json.push(m.to_json_mini_details(&mut conn).await);
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"data": members_json,
|
||||
"object": "list",
|
||||
"continuationToken": null,
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/users/<member_id>?<data..>", rank = 2)]
|
||||
@ -1384,9 +1441,12 @@ async fn get_user(
|
||||
org_id: OrganizationId,
|
||||
member_id: MembershipId,
|
||||
data: GetOrgUserData,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let Some(user) = Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await else {
|
||||
err!("The specified user isn't a member of the organization")
|
||||
};
|
||||
@ -1430,6 +1490,9 @@ async fn edit_member(
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let mut data: EditUserData = data.into_inner();
|
||||
|
||||
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
|
||||
@ -1551,7 +1614,10 @@ async fn bulk_delete_member(
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: BulkMembershipIds = data.into_inner();
|
||||
|
||||
let mut bulk_response = Vec::new();
|
||||
@ -1570,11 +1636,11 @@ async fn bulk_delete_member(
|
||||
))
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"data": bulk_response,
|
||||
"object": "list",
|
||||
"continuationToken": null
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[delete("/organizations/<org_id>/users/<member_id>")]
|
||||
@ -1644,9 +1710,12 @@ async fn _delete_member(
|
||||
async fn bulk_public_keys(
|
||||
org_id: OrganizationId,
|
||||
data: Json<BulkMembershipIds>,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: BulkMembershipIds = data.into_inner();
|
||||
|
||||
let mut bulk_response = Vec::new();
|
||||
@ -1670,11 +1739,11 @@ async fn bulk_public_keys(
|
||||
}
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"data": bulk_response,
|
||||
"object": "list",
|
||||
"continuationToken": null
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
use super::ciphers::update_cipher_from_data;
|
||||
@ -1819,15 +1888,18 @@ async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/policies")]
|
||||
async fn list_policies(org_id: OrganizationId, _headers: AdminHeaders, mut conn: DbConn) -> Json<Value> {
|
||||
async fn list_policies(org_id: OrganizationId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await;
|
||||
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"data": policies_json,
|
||||
"object": "list",
|
||||
"continuationToken": null
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/policies/token?<token>")]
|
||||
@ -1855,7 +1927,10 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbCo
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/policies/<pol_type>")]
|
||||
async fn get_policy(org_id: OrganizationId, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else {
|
||||
err!("Invalid or unsupported policy type")
|
||||
};
|
||||
@ -1884,6 +1959,9 @@ async fn put_policy(
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data: PolicyData = data.into_inner();
|
||||
|
||||
let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else {
|
||||
@ -2217,7 +2295,7 @@ async fn bulk_deactivate_members(
|
||||
data: Json<BulkRevokeMembershipIds>,
|
||||
headers: AdminHeaders,
|
||||
conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
bulk_revoke_members(org_id, data, headers, conn).await
|
||||
}
|
||||
|
||||
@ -2237,7 +2315,10 @@ async fn bulk_revoke_members(
|
||||
data: Json<BulkRevokeMembershipIds>,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data = data.into_inner();
|
||||
|
||||
let mut bulk_response = Vec::new();
|
||||
@ -2261,11 +2342,11 @@ async fn bulk_revoke_members(
|
||||
None => error!("No users to revoke"),
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"data": bulk_response,
|
||||
"object": "list",
|
||||
"continuationToken": null
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
async fn _revoke_member(
|
||||
@ -2326,7 +2407,7 @@ async fn bulk_activate_members(
|
||||
data: Json<BulkMembershipIds>,
|
||||
headers: AdminHeaders,
|
||||
conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
bulk_restore_members(org_id, data, headers, conn).await
|
||||
}
|
||||
|
||||
@ -2346,7 +2427,10 @@ async fn bulk_restore_members(
|
||||
data: Json<BulkMembershipIds>,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let data = data.into_inner();
|
||||
|
||||
let mut bulk_response = Vec::new();
|
||||
@ -2365,11 +2449,11 @@ async fn bulk_restore_members(
|
||||
));
|
||||
}
|
||||
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"data": bulk_response,
|
||||
"object": "list",
|
||||
"continuationToken": null
|
||||
}))
|
||||
})))
|
||||
}
|
||||
|
||||
async fn _restore_member(
|
||||
@ -2426,7 +2510,10 @@ async fn _restore_member(
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/groups")]
|
||||
async fn get_groups(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
|
||||
if org_id != headers.membership.org_uuid {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
|
||||
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
|
||||
let groups = Group::find_by_organization(&org_id, &mut conn).await;
|
||||
@ -2614,9 +2701,12 @@ async fn add_update_group(
|
||||
async fn get_group_details(
|
||||
org_id: OrganizationId,
|
||||
group_id: GroupId,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
@ -2696,7 +2786,10 @@ async fn bulk_delete_groups(
|
||||
}
|
||||
|
||||
#[get("/organizations/<org_id>/groups/<group_id>", rank = 2)]
|
||||
async fn get_group(org_id: OrganizationId, group_id: GroupId, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
@ -2712,9 +2805,12 @@ async fn get_group(org_id: OrganizationId, group_id: GroupId, _headers: AdminHea
|
||||
async fn get_group_members(
|
||||
org_id: OrganizationId,
|
||||
group_id: GroupId,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
@ -2774,9 +2870,12 @@ async fn put_group_members(
|
||||
async fn get_user_groups(
|
||||
org_id: OrganizationId,
|
||||
member_id: MembershipId,
|
||||
_headers: AdminHeaders,
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
@ -2816,6 +2915,9 @@ async fn put_user_groups(
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
@ -2865,6 +2967,9 @@ async fn delete_group_member(
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
if !CONFIG.org_groups_enabled() {
|
||||
err!("Group support is disabled");
|
||||
}
|
||||
@ -2910,7 +3015,14 @@ struct OrganizationUserResetPasswordRequest {
|
||||
// But the clients do not seem to use this at all
|
||||
// Just add it here in case they will
|
||||
#[get("/organizations/<org_id>/public-key")]
|
||||
async fn get_organization_public_key(org_id: OrganizationId, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||
async fn get_organization_public_key(
|
||||
org_id: OrganizationId,
|
||||
headers: OrgMemberHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
|
||||
err!("Organization not found")
|
||||
};
|
||||
@ -2924,7 +3036,7 @@ async fn get_organization_public_key(org_id: OrganizationId, _headers: Headers,
|
||||
// Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients
|
||||
// https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468
|
||||
#[get("/organizations/<org_id>/keys")]
|
||||
async fn get_organization_keys(org_id: OrganizationId, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
|
||||
get_organization_public_key(org_id, headers, conn).await
|
||||
}
|
||||
|
||||
@ -2937,6 +3049,9 @@ async fn put_reset_password(
|
||||
mut conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> EmptyResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
|
||||
err!("Required organization not found")
|
||||
};
|
||||
@ -2993,6 +3108,9 @@ async fn get_reset_password_details(
|
||||
headers: AdminHeaders,
|
||||
mut conn: DbConn,
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
|
||||
err!("Required organization not found")
|
||||
};
|
||||
@ -3114,7 +3232,10 @@ async fn get_org_export(
|
||||
headers: AdminHeaders,
|
||||
client_version: Option<ClientVersion>,
|
||||
mut conn: DbConn,
|
||||
) -> Json<Value> {
|
||||
) -> JsonResult {
|
||||
if org_id != headers.org_id {
|
||||
err!("Organization not found", "Organization id's do not match");
|
||||
}
|
||||
// Since version v2023.1.0 the format of the export is different.
|
||||
// Also, this endpoint was created since v2022.9.0.
|
||||
// Therefore, we will check for any version smaller then v2023.1.0 and return a different response.
|
||||
@ -3130,7 +3251,7 @@ async fn get_org_export(
|
||||
// Also both main keys here need to be lowercase, else the export will fail.
|
||||
if use_list_response_model {
|
||||
// Backwards compatible pre v2023.1.0 response
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"collections": {
|
||||
"data": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await),
|
||||
"object": "list",
|
||||
@ -3141,13 +3262,13 @@ async fn get_org_export(
|
||||
"object": "list",
|
||||
"continuationToken": null,
|
||||
}
|
||||
}))
|
||||
})))
|
||||
} else {
|
||||
// v2023.1.0 and newer response
|
||||
Json(json!({
|
||||
Ok(Json(json!({
|
||||
"collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await),
|
||||
"ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await),
|
||||
}))
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user