diff --git a/src/api/admin.rs b/src/api/admin.rs index 9a1d3417..eda6e141 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -19,6 +19,7 @@ use crate::{ unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify, }, auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp, Secure}, + config::not_readonly, config::ConfigBuilder, db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, error::{Error, MapResult}, @@ -257,6 +258,7 @@ fn render_admin_page() -> ApiResult> { let settings_json = json!({ "config": CONFIG.prepare_json(), "can_backup": *CAN_BACKUP, + "readonly": CONFIG.readonly(), }); let text = AdminTemplateData::new("admin/settings", settings_json).render()?; Ok(Html(text)) @@ -288,6 +290,8 @@ async fn get_user_or_404(uuid: &str, conn: &mut DbConn) -> ApiResult { #[post("/invite", data = "")] async fn invite_user(data: Json, _token: AdminToken, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: InviteData = data.into_inner(); if User::find_by_mail(&data.email, &mut conn).await.is_some() { err_code!("User already exists", Status::Conflict.code) @@ -363,7 +367,12 @@ async fn users_overview(_token: AdminToken, mut conn: DbConn) -> ApiResult Json #[post("/users//delete")] async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let user = get_user_or_404(uuid, &mut conn).await?; // Get the user_org records before deleting the actual user @@ -466,6 +477,8 @@ async fn remove_2fa(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRes #[post("/users//invite/resend")] async fn resend_user_invite(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + if let Some(user) = User::find_by_uuid(uuid, &mut conn).await { //TODO: replace this with user.status check when it will be available (PR#3397) if !user.password_hash.is_empty() { @@ -491,6 +504,8 @@ struct UserOrgTypeData { #[post("/users/org_type", data = "")] async fn update_user_org_type(data: Json, token: AdminToken, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let data: UserOrgTypeData = data.into_inner(); let mut user_to_edit = @@ -546,6 +561,8 @@ async fn update_user_org_type(data: Json, token: AdminToken, mu #[post("/users/update_revision")] async fn update_revision_users(_token: AdminToken, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + User::update_all_revisions(&mut conn).await } @@ -565,12 +582,19 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu organizations_json.push(org); } - let text = AdminTemplateData::new("admin/organizations", json!(organizations_json)).render()?; + let organizations_json = json!({ + "organizations": organizations_json, + "readonly": CONFIG.readonly(), + }); + + let text = AdminTemplateData::new("admin/organizations", organizations_json).render()?; Ok(Html(text)) } #[post("/organizations//delete")] async fn delete_organization(uuid: &str, _token: AdminToken, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let org = Organization::find_by_uuid(uuid, &mut conn).await.map_res("Organization doesn't exist")?; org.delete(&mut conn).await } @@ -735,6 +759,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn) "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(), "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the server date/time check as late as possible to minimize the time difference "ntp_time": get_ntp_time(has_http_access).await, // Run the ntp check as late as possible to minimize the time difference + "readonly": CONFIG.readonly(), }); let text = AdminTemplateData::new("admin/diagnostics", diagnostics_json).render()?; diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index a747f3ec..b14b61d7 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -10,6 +10,7 @@ use crate::{ PasswordOrOtpData, UpdateType, }, auth::{decode_delete, decode_invite, decode_verify_email, ClientHeaders, Headers}, + config::not_readonly, crypto, db::{models::*, DbConn}, mail, @@ -120,6 +121,8 @@ async fn is_email_2fa_required(org_user_uuid: Option, conn: &mut DbConn) #[post("/accounts/register", data = "")] async fn register(data: Json, conn: DbConn) -> JsonResult { + not_readonly()?; + _register(data, conn).await } @@ -257,11 +260,15 @@ struct ProfileData { #[put("/accounts/profile", data = "")] async fn put_profile(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + not_readonly()?; + post_profile(data, headers, conn).await } #[post("/accounts/profile", data = "")] async fn post_profile(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: ProfileData = data.into_inner(); // Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden) @@ -285,6 +292,8 @@ struct AvatarData { #[put("/accounts/avatar", data = "")] async fn put_avatar(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: AvatarData = data.into_inner(); // It looks like it only supports the 6 hex color format. @@ -320,6 +329,8 @@ async fn get_public_keys(uuid: &str, _headers: Headers, mut conn: DbConn) -> Jso #[post("/accounts/keys", data = "")] async fn post_keys(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: KeysData = data.into_inner(); let mut user = headers.user; @@ -347,6 +358,8 @@ struct ChangePassData { #[post("/accounts/password", data = "")] async fn post_password(data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + let data: ChangePassData = data.into_inner(); let mut user = headers.user; @@ -392,6 +405,8 @@ struct ChangeKdfData { #[post("/accounts/kdf", data = "")] async fn post_kdf(data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + let data: ChangeKdfData = data.into_inner(); let mut user = headers.user; @@ -479,6 +494,8 @@ struct KeyData { #[post("/accounts/key", data = "")] async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + // TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything. let data: KeyData = data.into_inner(); @@ -614,6 +631,8 @@ struct EmailTokenData { #[post("/accounts/email-token", data = "")] async fn post_email_token(data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + if !CONFIG.email_change_allowed() { err!("Email change is not allowed."); } @@ -661,6 +680,8 @@ struct ChangeEmailData { #[post("/accounts/email", data = "")] async fn post_email(data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + if !CONFIG.email_change_allowed() { err!("Email change is not allowed."); } @@ -715,6 +736,8 @@ async fn post_email(data: Json, headers: Headers, mut conn: DbC #[post("/accounts/verify-email")] async fn post_verify_email(headers: Headers) -> EmptyResult { + not_readonly()?; + let user = headers.user; if !CONFIG.mail_enabled() { @@ -737,6 +760,8 @@ struct VerifyEmailTokenData { #[post("/accounts/verify-email-token", data = "")] async fn post_verify_email_token(data: Json, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let data: VerifyEmailTokenData = data.into_inner(); let mut user = match User::find_by_uuid(&data.user_id, &mut conn).await { @@ -769,6 +794,8 @@ struct DeleteRecoverData { #[post("/accounts/delete-recover", data = "")] async fn post_delete_recover(data: Json, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let data: DeleteRecoverData = data.into_inner(); if CONFIG.mail_enabled() { @@ -796,6 +823,8 @@ struct DeleteRecoverTokenData { #[post("/accounts/delete-recover-token", data = "")] async fn post_delete_recover_token(data: Json, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let data: DeleteRecoverTokenData = data.into_inner(); let user = match User::find_by_uuid(&data.user_id, &mut conn).await { @@ -815,11 +844,15 @@ async fn post_delete_recover_token(data: Json, mut conn: #[post("/accounts/delete", data = "")] async fn post_delete_account(data: Json, headers: Headers, conn: DbConn) -> EmptyResult { + not_readonly()?; + delete_account(data, headers, conn).await } #[delete("/accounts", data = "")] async fn delete_account(data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let data: PasswordOrOtpData = data.into_inner(); let user = headers.user; @@ -952,11 +985,17 @@ async fn _api_key(data: Json, rotate: bool, headers: Headers, #[post("/accounts/api-key", data = "")] async fn api_key(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + if headers.user.api_key.is_none() { + not_readonly()?; + } + _api_key(data, false, headers, conn).await } #[post("/accounts/rotate-api-key", data = "")] async fn rotate_api_key(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + not_readonly()?; + _api_key(data, true, headers, conn).await } @@ -1017,11 +1056,15 @@ struct PushToken { #[post("/devices/identifier//token", data = "")] async fn post_device_token(uuid: &str, data: Json, headers: Headers, conn: DbConn) -> EmptyResult { + not_readonly()?; + put_device_token(uuid, data, headers, conn).await } #[put("/devices/identifier//token", data = "")] async fn put_device_token(uuid: &str, data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let data = data.into_inner(); let token = data.push_token; @@ -1055,6 +1098,8 @@ async fn put_device_token(uuid: &str, data: Json, headers: Headers, m #[put("/devices/identifier//clear-token")] async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + // This only clears push token // https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109 // https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37 @@ -1074,6 +1119,8 @@ async fn put_clear_device_token(uuid: &str, mut conn: DbConn) -> EmptyResult { // On upstream server, both PUT and POST are declared. Implementing the POST method in case it would be useful somewhere #[post("/devices/identifier//clear-token")] async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult { + not_readonly()?; + put_clear_device_token(uuid, conn).await } @@ -1095,6 +1142,8 @@ async fn post_auth_request( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let data = data.into_inner(); let user = match User::find_by_mail(&data.email, &mut conn).await { @@ -1176,6 +1225,8 @@ async fn put_auth_request( ant: AnonymousNotify<'_>, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let data = data.into_inner(); let mut auth_request: AuthRequest = match AuthRequest::find_by_uuid(uuid, &mut conn).await { Some(auth_request) => auth_request, diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 537a0c15..cd05f0e5 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -14,6 +14,7 @@ use crate::util::NumberOrString; use crate::{ api::{self, core::log_event, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType}, auth::Headers, + config::not_readonly, crypto, db::{models::*, DbConn, DbPool}, CONFIG, @@ -266,6 +267,8 @@ pub struct Attachments2Data { /// Called when an org admin clones an org cipher. #[post("/ciphers/admin", data = "")] async fn post_ciphers_admin(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + post_ciphers_create(data, headers, conn, nt).await } @@ -279,6 +282,8 @@ async fn post_ciphers_create( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let mut data: ShareCipherData = data.into_inner(); // Check if there are one more more collections selected when this cipher is part of an organization. @@ -310,6 +315,8 @@ async fn post_ciphers_create( /// Called when creating a new user-owned cipher. #[post("/ciphers", data = "")] async fn post_ciphers(data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + let mut data: CipherData = data.into_inner(); // The web/browser clients set this field to null as expected, but the @@ -554,6 +561,8 @@ async fn post_ciphers_import( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + enforce_personal_ownership_policy(None, &headers, &mut conn).await?; let data: ImportData = data.into_inner(); @@ -612,6 +621,8 @@ async fn put_cipher_admin( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + put_cipher(uuid, data, headers, conn, nt).await } @@ -623,11 +634,15 @@ async fn post_cipher_admin( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + post_cipher(uuid, data, headers, conn, nt).await } #[post("/ciphers/", data = "")] async fn post_cipher(uuid: &str, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + put_cipher(uuid, data, headers, conn, nt).await } @@ -639,6 +654,8 @@ async fn put_cipher( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let data: CipherData = data.into_inner(); let mut cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { @@ -662,6 +679,8 @@ async fn put_cipher( #[post("/ciphers//partial", data = "")] async fn post_cipher_partial(uuid: &str, data: Json, headers: Headers, conn: DbConn) -> JsonResult { + not_readonly()?; + put_cipher_partial(uuid, data, headers, conn).await } @@ -673,6 +692,8 @@ async fn put_cipher_partial( headers: Headers, mut conn: DbConn, ) -> JsonResult { + not_readonly()?; + let data: PartialCipherData = data.into_inner(); let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { @@ -713,6 +734,8 @@ async fn put_collections2_update( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + post_collections2_update(uuid, data, headers, conn, nt).await } @@ -724,6 +747,8 @@ async fn post_collections2_update( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let cipher_details = post_collections_update(uuid, data, headers, conn, nt).await?; Ok(Json(json!({ // AttachmentUploadDataResponseModel "object": "optionalCipherDetails", @@ -740,6 +765,8 @@ async fn put_collections_update( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + post_collections_update(uuid, data, headers, conn, nt).await } @@ -751,6 +778,8 @@ async fn post_collections_update( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let data: CollectionsAdminData = data.into_inner(); let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { @@ -817,6 +846,8 @@ async fn put_collections_admin( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + post_collections_admin(uuid, data, headers, conn, nt).await } @@ -828,6 +859,8 @@ async fn post_collections_admin( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + let data: CollectionsAdminData = data.into_inner(); let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { @@ -903,6 +936,8 @@ async fn post_cipher_share( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let data: ShareCipherData = data.into_inner(); share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await @@ -916,6 +951,8 @@ async fn put_cipher_share( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let data: ShareCipherData = data.into_inner(); share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await @@ -935,6 +972,8 @@ async fn put_cipher_share_selected( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + let mut data: ShareSelectedCipherData = data.into_inner(); if data.ciphers.is_empty() { @@ -973,6 +1012,8 @@ async fn share_cipher_by_uuid( conn: &mut DbConn, nt: &Notify<'_>, ) -> JsonResult { + not_readonly()?; + let mut cipher = match Cipher::find_by_uuid(uuid, conn).await { Some(cipher) => { if cipher.is_write_accessible_to_user(&headers.user.uuid, conn).await { @@ -1063,6 +1104,8 @@ async fn post_attachment_v2( headers: Headers, mut conn: DbConn, ) -> JsonResult { + not_readonly()?; + let cipher = match Cipher::find_by_uuid(uuid, &mut conn).await { Some(cipher) => cipher, None => err!("Cipher doesn't exist"), @@ -1120,6 +1163,8 @@ async fn save_attachment( mut conn: DbConn, nt: Notify<'_>, ) -> Result<(Cipher, DbConn), crate::error::Error> { + not_readonly()?; + let mut data = data.into_inner(); let Some(size) = data.data.len().to_i64() else { @@ -1295,6 +1340,8 @@ async fn post_attachment_v2_data( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + let attachment = match Attachment::find_by_id(attachment_id, &mut conn).await { Some(attachment) if uuid == attachment.cipher_uuid => Some(attachment), Some(_) => err!("Attachment doesn't belong to cipher"), @@ -1315,6 +1362,8 @@ async fn post_attachment( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + // Setting this as None signifies to save_attachment() that it should create // the attachment database record as well as saving the data to disk. let attachment = None; @@ -1332,6 +1381,8 @@ async fn post_attachment_admin( conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + post_attachment(uuid, data, headers, conn, nt).await } @@ -1344,6 +1395,8 @@ async fn post_attachment_share( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + _delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await?; post_attachment(uuid, data, headers, conn, nt).await } @@ -1356,6 +1409,8 @@ async fn delete_attachment_post_admin( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + delete_attachment(uuid, attachment_id, headers, conn, nt).await } @@ -1367,6 +1422,8 @@ async fn delete_attachment_post( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + delete_attachment(uuid, attachment_id, headers, conn, nt).await } @@ -1378,6 +1435,8 @@ async fn delete_attachment( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await } @@ -1389,40 +1448,54 @@ async fn delete_attachment_admin( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await } #[post("/ciphers//delete")] async fn delete_cipher_post(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await // permanent delete } #[post("/ciphers//delete-admin")] async fn delete_cipher_post_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await // permanent delete } #[put("/ciphers//delete")] async fn delete_cipher_put(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + _delete_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await // soft delete } #[put("/ciphers//delete-admin")] async fn delete_cipher_put_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + _delete_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await } #[delete("/ciphers/")] async fn delete_cipher(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await // permanent delete } #[delete("/ciphers//admin")] async fn delete_cipher_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await // permanent delete } @@ -1434,6 +1507,8 @@ async fn delete_cipher_selected( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete } @@ -1444,6 +1519,8 @@ async fn delete_cipher_selected_post( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete } @@ -1454,6 +1531,8 @@ async fn delete_cipher_selected_put( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_multiple_ciphers(data, headers, conn, true, nt).await // soft delete } @@ -1464,6 +1543,8 @@ async fn delete_cipher_selected_admin( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete } @@ -1474,6 +1555,8 @@ async fn delete_cipher_selected_post_admin( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_multiple_ciphers(data, headers, conn, false, nt).await // permanent delete } @@ -1484,16 +1567,22 @@ async fn delete_cipher_selected_put_admin( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_multiple_ciphers(data, headers, conn, true, nt).await // soft delete } #[put("/ciphers//restore")] async fn restore_cipher_put(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + _restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await } #[put("/ciphers//restore-admin")] async fn restore_cipher_put_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + _restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await } @@ -1504,6 +1593,8 @@ async fn restore_cipher_selected( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + _restore_multiple_ciphers(data, &headers, &mut conn, &nt).await } @@ -1521,6 +1612,8 @@ async fn move_cipher_selected( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + let data = data.into_inner(); let user_uuid = headers.user.uuid; @@ -1569,6 +1662,8 @@ async fn move_cipher_selected_put( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + move_cipher_selected(data, headers, conn, nt).await } @@ -1586,6 +1681,8 @@ async fn delete_all( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + let data: PasswordOrOtpData = data.into_inner(); let mut user = headers.user; diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 1c29b774..36ecb520 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -8,6 +8,7 @@ use crate::{ EmptyResult, JsonResult, }, auth::{decode_emergency_access_invite, Headers}, + config::not_readonly, db::{models::*, DbConn, DbPool}, mail, util::NumberOrString, @@ -123,6 +124,8 @@ async fn put_emergency_access( headers: Headers, conn: DbConn, ) -> JsonResult { + not_readonly()?; + post_emergency_access(emer_id, data, headers, conn).await } @@ -133,6 +136,8 @@ async fn post_emergency_access( headers: Headers, mut conn: DbConn, ) -> JsonResult { + not_readonly()?; + check_emergency_access_enabled()?; let data: EmergencyAccessUpdateData = data.into_inner(); @@ -164,6 +169,8 @@ async fn post_emergency_access( #[delete("/emergency-access/")] async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + check_emergency_access_enabled()?; let emergency_access = match ( @@ -187,6 +194,8 @@ async fn delete_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo #[post("/emergency-access//delete")] async fn post_delete_emergency_access(emer_id: &str, headers: Headers, conn: DbConn) -> EmptyResult { + not_readonly()?; + delete_emergency_access(emer_id, headers, conn).await } @@ -204,6 +213,8 @@ struct EmergencyAccessInviteData { #[post("/emergency-access/invite", data = "")] async fn send_invite(data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + check_emergency_access_enabled()?; let data: EmergencyAccessInviteData = data.into_inner(); @@ -282,6 +293,8 @@ async fn send_invite(data: Json, headers: Headers, mu #[post("/emergency-access//reinvite")] async fn resend_invite(emer_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + check_emergency_access_enabled()?; let mut emergency_access = @@ -334,6 +347,8 @@ struct AcceptData { #[post("/emergency-access//accept", data = "")] async fn accept_invite(emer_id: &str, data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + check_emergency_access_enabled()?; let data: AcceptData = data.into_inner(); @@ -397,6 +412,8 @@ async fn confirm_emergency_access( headers: Headers, mut conn: DbConn, ) -> JsonResult { + not_readonly()?; + check_emergency_access_enabled()?; let confirming_user = headers.user; @@ -447,6 +464,8 @@ async fn confirm_emergency_access( #[post("/emergency-access//initiate")] async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + check_emergency_access_enabled()?; let initiating_user = headers.user; @@ -486,6 +505,8 @@ async fn initiate_emergency_access(emer_id: &str, headers: Headers, mut conn: Db #[post("/emergency-access//approve")] async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + check_emergency_access_enabled()?; let mut emergency_access = @@ -523,6 +544,8 @@ async fn approve_emergency_access(emer_id: &str, headers: Headers, mut conn: DbC #[post("/emergency-access//reject")] async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + check_emergency_access_enabled()?; let mut emergency_access = @@ -561,6 +584,8 @@ async fn reject_emergency_access(emer_id: &str, headers: Headers, mut conn: DbCo #[post("/emergency-access//view")] async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + check_emergency_access_enabled()?; let emergency_access = @@ -599,6 +624,8 @@ async fn view_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn #[post("/emergency-access//takeover")] async fn takeover_emergency_access(emer_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + check_emergency_access_enabled()?; let requesting_user = headers.user; @@ -643,6 +670,8 @@ async fn password_emergency_access( headers: Headers, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + check_emergency_access_enabled()?; let data: EmergencyAccessPasswordData = data.into_inner(); diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index 9766d7a1..c32b2506 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -4,6 +4,7 @@ use serde_json::Value; use crate::{ api::{EmptyResult, JsonResult, Notify, UpdateType}, auth::Headers, + config::not_readonly, db::{models::*, DbConn}, }; @@ -46,6 +47,8 @@ pub struct FolderData { #[post("/folders", data = "")] async fn post_folders(data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + let data: FolderData = data.into_inner(); let mut folder = Folder::new(headers.user.uuid, data.name); @@ -58,6 +61,8 @@ async fn post_folders(data: Json, headers: Headers, mut conn: DbConn #[post("/folders/", data = "")] async fn post_folder(uuid: &str, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + put_folder(uuid, data, headers, conn, nt).await } @@ -69,6 +74,8 @@ async fn put_folder( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let data: FolderData = data.into_inner(); let mut folder = match Folder::find_by_uuid(uuid, &mut conn).await { @@ -90,11 +97,15 @@ async fn put_folder( #[post("/folders//delete")] async fn delete_folder_post(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + delete_folder(uuid, headers, conn, nt).await } #[delete("/folders/")] async fn delete_folder(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + let folder = match Folder::find_by_uuid(uuid, &mut conn).await { Some(folder) => folder, _ => err!("Invalid folder"), diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 41bd4d6b..b7e4eb76 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -15,6 +15,8 @@ pub use events::{event_cleanup_job, log_event, log_user_event}; use reqwest::Method; pub use sends::purge_sends; +use crate::config::not_readonly; + pub fn routes() -> Vec { let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains]; let mut hibp_routes = routes![hibp_breach]; @@ -111,6 +113,8 @@ async fn post_eq_domains( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let data: EquivDomainData = data.into_inner(); let excluded_globals = data.excluded_global_equivalent_domains.unwrap_or_default(); @@ -131,6 +135,8 @@ async fn post_eq_domains( #[put("/settings/domains", data = "")] async fn put_eq_domains(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + post_eq_domains(data, headers, conn, nt).await } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 7b7f5896..f889b876 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -9,6 +9,7 @@ use crate::{ EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, }, auth::{decode_invite, AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders}, + config::not_readonly, db::{models::*, DbConn}, error::Error, mail, @@ -150,6 +151,8 @@ struct OrgBulkIds { #[post("/organizations", data = "")] async fn create_organization(headers: Headers, data: Json, mut conn: DbConn) -> JsonResult { + not_readonly()?; + if !CONFIG.is_org_creation_allowed(&headers.user.email) { err!("User not allowed to create organizations") } @@ -190,6 +193,8 @@ async fn delete_organization( headers: OwnerHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + let data: PasswordOrOtpData = data.into_inner(); data.validate(&headers.user, true, &mut conn).await?; @@ -207,11 +212,15 @@ async fn post_delete_organization( headers: OwnerHeaders, conn: DbConn, ) -> EmptyResult { + not_readonly()?; + delete_organization(org_id, data, headers, conn).await } #[post("/organizations//leave")] async fn leave_organization(org_id: &str, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + match UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { None => err!("User not part of organization"), Some(user_org) => { @@ -252,6 +261,8 @@ async fn put_organization( data: Json, conn: DbConn, ) -> JsonResult { + not_readonly()?; + post_organization(org_id, headers, data, conn).await } @@ -262,6 +273,8 @@ async fn post_organization( data: Json, mut conn: DbConn, ) -> JsonResult { + not_readonly()?; + let data: OrganizationUpdateData = data.into_inner(); let mut org = match Organization::find_by_uuid(org_id, &mut conn).await { @@ -381,6 +394,8 @@ async fn post_organization_collections( data: Json, mut conn: DbConn, ) -> JsonResult { + not_readonly()?; + let data: NewCollectionData = data.into_inner(); let org = match Organization::find_by_uuid(org_id, &mut conn).await { @@ -437,6 +452,8 @@ async fn put_organization_collection_update( data: Json, conn: DbConn, ) -> JsonResult { + not_readonly()?; + post_organization_collection_update(org_id, col_id, headers, data, conn).await } @@ -448,6 +465,8 @@ async fn post_organization_collection_update( data: Json, mut conn: DbConn, ) -> JsonResult { + not_readonly()?; + let data: NewCollectionData = data.into_inner(); let org = match Organization::find_by_uuid(org_id, &mut conn).await { @@ -517,6 +536,8 @@ async fn delete_organization_collection_user( _headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + let collection = match Collection::find_by_uuid(col_id, &mut conn).await { None => err!("Collection not found"), Some(collection) => { @@ -547,6 +568,8 @@ async fn post_organization_collection_delete_user( headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { + not_readonly()?; + delete_organization_collection_user(org_id, col_id, org_user_id, headers, conn).await } @@ -585,6 +608,8 @@ async fn delete_organization_collection( headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + _delete_organization_collection(org_id, col_id, &headers, &mut conn).await } @@ -605,6 +630,8 @@ async fn post_organization_collection_delete( _data: Json, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + _delete_organization_collection(org_id, col_id, &headers, &mut conn).await } @@ -621,6 +648,8 @@ async fn bulk_delete_organization_collections( data: Json, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + let data: BulkCollectionIds = data.into_inner(); let collections = data.ids; @@ -717,6 +746,8 @@ async fn put_collection_users( _headers: ManagerHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + // Get org and collection, check that collection is from org if Collection::find_by_uuid_and_org(coll_id, org_id, &mut conn).await.is_none() { err!("Collection not found in Organization") @@ -805,6 +836,8 @@ async fn get_org_users( #[post("/organizations//keys", data = "")] async fn post_org_keys(org_id: &str, data: Json, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: OrgKeyData = data.into_inner(); let mut org = match Organization::find_by_uuid(org_id, &mut conn).await { @@ -849,6 +882,8 @@ struct InviteData { #[post("/organizations//users/invite", data = "")] async fn send_invite(org_id: &str, data: Json, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let data: InviteData = data.into_inner(); let new_type = match UserOrgType::from_str(&data.r#type.into_string()) { @@ -965,7 +1000,9 @@ async fn bulk_reinvite_user( data: Json, headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + not_readonly()?; + let data: OrgBulkIds = data.into_inner(); let mut bulk_response = Vec::new(); @@ -984,15 +1021,17 @@ async fn bulk_reinvite_user( )) } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } #[post("/organizations//users//reinvite")] async fn reinvite_user(org_id: &str, user_org: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + _reinvite_user(org_id, user_org, &headers.user.email, &mut conn).await } @@ -1052,6 +1091,8 @@ struct AcceptData { #[post("/organizations//users/<_org_user_id>/accept", data = "")] async fn accept_invite(org_id: &str, _org_user_id: &str, data: Json, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + // The web-vault passes org_id and org_user_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)?; @@ -1145,7 +1186,9 @@ async fn bulk_confirm_invite( headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, -) -> Json { +) -> JsonResult { + not_readonly()?; + let data = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1171,11 +1214,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//users//confirm", data = "")] @@ -1187,6 +1230,8 @@ async fn confirm_invite( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + let data = data.into_inner(); let user_key = data.key.unwrap_or_default(); _confirm_invite(org_id, org_user_id, &user_key, &headers, &mut conn, &nt).await @@ -1308,6 +1353,8 @@ async fn put_organization_user( headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { + not_readonly()?; + edit_user(org_id, org_user_id, data, headers, conn).await } @@ -1319,6 +1366,8 @@ async fn edit_user( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + let data: EditUserData = data.into_inner(); let new_type = match UserOrgType::from_str(&data.r#type.into_string()) { @@ -1425,7 +1474,9 @@ async fn bulk_delete_user( headers: AdminHeaders, mut conn: DbConn, nt: Notify<'_>, -) -> Json { +) -> JsonResult { + not_readonly()?; + let data: OrgBulkIds = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1444,11 +1495,11 @@ async fn bulk_delete_user( )) } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } #[delete("/organizations//users/")] @@ -1459,6 +1510,8 @@ async fn delete_user( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_user(org_id, org_user_id, &headers, &mut conn, &nt).await } @@ -1470,6 +1523,8 @@ async fn post_delete_user( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + _delete_user(org_id, org_user_id, &headers, &mut conn, &nt).await } @@ -1520,7 +1575,9 @@ async fn bulk_public_keys( data: Json, _headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + not_readonly()?; + let data: OrgBulkIds = data.into_inner(); let mut bulk_response = Vec::new(); @@ -1544,11 +1601,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; @@ -1579,6 +1636,8 @@ async fn post_org_import( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + let data: ImportData = data.into_inner(); let org_id = query.organization_id; @@ -1696,6 +1755,8 @@ async fn put_policy( headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + not_readonly()?; + let data: PolicyData = data.into_inner(); let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { @@ -1860,6 +1921,8 @@ struct OrgImportData { #[post("/organizations//import", data = "")] async fn import(org_id: &str, data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let data = data.into_inner(); // TODO: Currently we aren't storing the externalId's anywhere, so we also don't have a way @@ -1972,6 +2035,8 @@ async fn deactivate_organization_user( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + _revoke_organization_user(org_id, org_user_id, &headers, &mut conn).await } @@ -1982,7 +2047,9 @@ async fn bulk_deactivate_organization_user( data: Json, headers: AdminHeaders, conn: DbConn, -) -> Json { +) -> JsonResult { + not_readonly()?; + bulk_revoke_organization_user(org_id, data, headers, conn).await } @@ -1993,6 +2060,8 @@ async fn revoke_organization_user( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + _revoke_organization_user(org_id, org_user_id, &headers, &mut conn).await } @@ -2008,7 +2077,9 @@ async fn bulk_revoke_organization_user( data: Json, headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + not_readonly()?; + let data = data.into_inner(); let mut bulk_response = Vec::new(); @@ -2032,11 +2103,11 @@ async fn bulk_revoke_organization_user( None => error!("No users to revoke"), } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } async fn _revoke_organization_user( @@ -2087,6 +2158,8 @@ async fn activate_organization_user( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + _restore_organization_user(org_id, org_user_id, &headers, &mut conn).await } @@ -2097,7 +2170,9 @@ async fn bulk_activate_organization_user( data: Json, headers: AdminHeaders, conn: DbConn, -) -> Json { +) -> JsonResult { + not_readonly()?; + bulk_restore_organization_user(org_id, data, headers, conn).await } @@ -2108,6 +2183,8 @@ async fn restore_organization_user( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + _restore_organization_user(org_id, org_user_id, &headers, &mut conn).await } @@ -2117,7 +2194,9 @@ async fn bulk_restore_organization_user( data: Json, headers: AdminHeaders, mut conn: DbConn, -) -> Json { +) -> JsonResult { + not_readonly()?; + let data = data.into_inner(); let mut bulk_response = Vec::new(); @@ -2136,11 +2215,11 @@ async fn bulk_restore_organization_user( )); } - Json(json!({ + Ok(Json(json!({ "data": bulk_response, "object": "list", "continuationToken": null - })) + }))) } async fn _restore_organization_user( @@ -2291,11 +2370,15 @@ async fn post_group( headers: AdminHeaders, conn: DbConn, ) -> JsonResult { + not_readonly()?; + put_group(org_id, group_id, data, headers, conn).await } #[post("/organizations//groups", data = "")] async fn post_groups(org_id: &str, headers: AdminHeaders, data: Json, mut conn: DbConn) -> JsonResult { + not_readonly()?; + if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2325,6 +2408,8 @@ async fn put_group( headers: AdminHeaders, mut conn: DbConn, ) -> JsonResult { + not_readonly()?; + if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2410,11 +2495,15 @@ async fn get_group_details(_org_id: &str, group_id: &str, _headers: AdminHeaders #[post("/organizations//groups//delete")] async fn post_delete_group(org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + _delete_group(org_id, group_id, &headers, &mut conn).await } #[delete("/organizations//groups/")] async fn delete_group(org_id: &str, group_id: &str, headers: AdminHeaders, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + _delete_group(org_id, group_id, &headers, &mut conn).await } @@ -2449,6 +2538,8 @@ async fn bulk_delete_groups( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2503,6 +2594,8 @@ async fn put_group_users( data: Json>, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2565,6 +2658,8 @@ async fn post_user_groups( headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { + not_readonly()?; + put_user_groups(org_id, org_user_id, data, headers, conn).await } @@ -2576,6 +2671,8 @@ async fn put_user_groups( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2619,6 +2716,8 @@ async fn post_delete_group_user( headers: AdminHeaders, conn: DbConn, ) -> EmptyResult { + not_readonly()?; + delete_group_user(org_id, group_id, org_user_id, headers, conn).await } @@ -2630,6 +2729,8 @@ async fn delete_group_user( headers: AdminHeaders, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + if !CONFIG.org_groups_enabled() { err!("Group support is disabled"); } @@ -2704,6 +2805,8 @@ async fn put_reset_password( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + let org = match Organization::find_by_uuid(org_id, &mut conn).await { Some(org) => org, None => err!("Required organization not found"), @@ -2839,6 +2942,8 @@ async fn put_reset_password_enrollment( data: Json, mut conn: DbConn, ) -> EmptyResult { + not_readonly()?; + let mut org_user = match UserOrganization::find_by_user_and_org(&headers.user.uuid, org_id, &mut conn).await { Some(u) => u, None => err!("User to enroll isn't member of required organization"), @@ -2964,6 +3069,8 @@ async fn _api_key( #[post("/organizations//api-key", data = "")] async fn api_key(org_id: &str, data: Json, headers: AdminHeaders, conn: DbConn) -> JsonResult { + not_readonly()?; + _api_key(org_id, data, false, headers, conn).await } @@ -2974,5 +3081,7 @@ async fn rotate_api_key( headers: AdminHeaders, conn: DbConn, ) -> JsonResult { + not_readonly()?; + _api_key(org_id, data, true, headers, conn).await } diff --git a/src/api/core/public.rs b/src/api/core/public.rs index 0cdcbb63..70aa7c1c 100644 --- a/src/api/core/public.rs +++ b/src/api/core/public.rs @@ -10,6 +10,7 @@ use std::collections::HashSet; use crate::{ api::EmptyResult, auth, + config::not_readonly, db::{models::*, DbConn}, mail, CONFIG, }; @@ -45,6 +46,8 @@ struct OrgImportData { #[post("/public/organization/import", data = "")] async fn ldap_import(data: Json, token: PublicToken, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + // 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 diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 27aea95a..e33e38da 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -11,6 +11,7 @@ use serde_json::Value; use crate::{ api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType}, auth::{ClientIp, Headers, Host}, + config::not_readonly, db::{models::*, DbConn, DbPool}, util::{NumberOrString, SafeString}, CONFIG, @@ -173,6 +174,8 @@ async fn get_send(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult #[post("/sends", data = "")] async fn post_send(data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + enforce_disable_send_policy(&headers, &mut conn).await?; let data: SendData = data.into_inner(); @@ -212,6 +215,8 @@ struct UploadDataV2<'f> { // Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167 #[post("/sends/file", format = "multipart/form-data", data = "")] async fn post_send_file(data: Form>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + enforce_disable_send_policy(&headers, &mut conn).await?; let UploadData { @@ -289,6 +294,8 @@ async fn post_send_file(data: Form>, headers: Headers, mut conn: // Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L190 #[post("/sends/file/v2", data = "")] async fn post_send_file_v2(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + enforce_disable_send_policy(&headers, &mut conn).await?; let data = data.into_inner(); @@ -359,6 +366,8 @@ async fn post_send_file_v2_data( mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { + not_readonly()?; + enforce_disable_send_policy(&headers, &mut conn).await?; let mut data = data.into_inner(); @@ -408,6 +417,8 @@ async fn post_access( ip: ClientIp, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let mut send = match Send::find_by_access_id(access_id, &mut conn).await { Some(s) => s, None => err_code!(SEND_INACCESSIBLE_MSG, 404), @@ -469,6 +480,8 @@ async fn post_access_file( mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { + not_readonly()?; + let mut send = match Send::find_by_uuid(send_id, &mut conn).await { Some(s) => s, None => err_code!(SEND_INACCESSIBLE_MSG, 404), @@ -536,6 +549,8 @@ async fn download_send(send_id: SafeString, file_id: SafeString, t: &str) -> Opt #[put("/sends/", data = "")] async fn put_send(id: &str, data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + enforce_disable_send_policy(&headers, &mut conn).await?; let data: SendData = data.into_inner(); @@ -611,6 +626,8 @@ pub async fn update_send_from_data( #[delete("/sends/")] async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + not_readonly()?; + let send = match Send::find_by_uuid(id, &mut conn).await { Some(s) => s, None => err!("Send not found"), @@ -635,6 +652,8 @@ async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_ #[put("/sends//remove-password")] async fn put_remove_password(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + not_readonly()?; + enforce_disable_send_policy(&headers, &mut conn).await?; let mut send = match Send::find_by_uuid(id, &mut conn).await { diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 9d4bd480..9c427629 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -5,6 +5,7 @@ use rocket::Route; use crate::{ api::{core::log_user_event, core::two_factor::_generate_recover_code, EmptyResult, JsonResult, PasswordOrOtpData}, auth::{ClientIp, Headers}, + config::not_readonly, crypto, db::{ models::{EventType, TwoFactor, TwoFactorType}, @@ -52,6 +53,8 @@ struct EnableAuthenticatorData { #[post("/two-factor/authenticator", data = "")] async fn activate_authenticator(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: EnableAuthenticatorData = data.into_inner(); let key = data.key; let token = data.token.into_string(); @@ -91,6 +94,8 @@ async fn activate_authenticator(data: Json, headers: He #[put("/two-factor/authenticator", data = "")] async fn activate_authenticator_put(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + not_readonly()?; + activate_authenticator(data, headers, conn).await } diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 3993397c..487feb2b 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -9,6 +9,7 @@ use crate::{ PasswordOrOtpData, }, auth::Headers, + config::not_readonly, crypto, db::{ models::{EventType, TwoFactor, TwoFactorType, User}, @@ -156,6 +157,8 @@ fn check_duo_fields_custom(data: &EnableDuoData) -> bool { #[post("/two-factor/duo", data = "")] async fn activate_duo(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: EnableDuoData = data.into_inner(); let mut user = headers.user; @@ -194,6 +197,8 @@ async fn activate_duo(data: Json, headers: Headers, mut conn: DbC #[put("/two-factor/duo", data = "")] async fn activate_duo_put(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + not_readonly()?; + activate_duo(data, headers, conn).await } diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index aea238e5..958c2b13 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -8,6 +8,7 @@ use crate::{ EmptyResult, JsonResult, PasswordOrOtpData, }, auth::Headers, + config::not_readonly, crypto, db::{ models::{EventType, TwoFactor, TwoFactorType, User}, @@ -113,6 +114,8 @@ struct SendEmailData { /// Send a verification email to the specified email address to check whether it exists/belongs to user. #[post("/two-factor/send-email", data = "")] async fn send_email(data: Json, headers: Headers, mut conn: DbConn) -> EmptyResult { + not_readonly()?; + let data: SendEmailData = data.into_inner(); let user = headers.user; @@ -157,6 +160,8 @@ struct EmailData { /// Verify email belongs to user and can be used for 2FA email codes. #[put("/two-factor/email", data = "")] async fn email(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: EmailData = data.into_inner(); let mut user = headers.user; diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 2dd88a64..7fd9125a 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -10,6 +10,7 @@ use crate::{ EmptyResult, JsonResult, PasswordOrOtpData, }, auth::{ClientHeaders, Headers}, + config::not_readonly, crypto, db::{models::*, DbConn, DbPool}, mail, @@ -80,6 +81,8 @@ struct RecoverTwoFactor { #[post("/two-factor/recover", data = "")] async fn recover(data: Json, client_headers: ClientHeaders, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: RecoverTwoFactor = data.into_inner(); use crate::db::models::User; @@ -137,6 +140,8 @@ struct DisableTwoFactorData { #[post("/two-factor/disable", data = "")] async fn disable_twofactor(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: DisableTwoFactorData = data.into_inner(); let user = headers.user; @@ -169,6 +174,8 @@ async fn disable_twofactor(data: Json, headers: Headers, m #[put("/two-factor/disable", data = "")] async fn disable_twofactor_put(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + not_readonly()?; + disable_twofactor(data, headers, conn).await } diff --git a/src/api/core/two_factor/webauthn.rs b/src/api/core/two_factor/webauthn.rs index 52ca70c4..3bbbcb5c 100644 --- a/src/api/core/two_factor/webauthn.rs +++ b/src/api/core/two_factor/webauthn.rs @@ -10,6 +10,7 @@ use crate::{ EmptyResult, JsonResult, PasswordOrOtpData, }, auth::Headers, + config::not_readonly, db::{ models::{EventType, TwoFactor, TwoFactorType}, DbConn, @@ -126,6 +127,8 @@ async fn get_webauthn(data: Json, headers: Headers, mut conn: #[post("/two-factor/get-webauthn-challenge", data = "")] async fn generate_webauthn_challenge(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: PasswordOrOtpData = data.into_inner(); let user = headers.user; @@ -239,6 +242,8 @@ impl From for PublicKeyCredential { #[post("/two-factor/webauthn", data = "")] async fn activate_webauthn(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: EnableWebauthnData = data.into_inner(); let mut user = headers.user; @@ -292,6 +297,8 @@ async fn activate_webauthn(data: Json, headers: Headers, mut #[put("/two-factor/webauthn", data = "")] async fn activate_webauthn_put(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + not_readonly()?; + activate_webauthn(data, headers, conn).await } @@ -304,6 +311,8 @@ struct DeleteU2FData { #[delete("/two-factor/webauthn", data = "")] async fn delete_webauthn(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let id = data.id.into_i32()?; if !headers.user.check_valid_password(&data.master_password_hash) { err!("Invalid password"); diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs index 2eff3b6f..4c1901e2 100644 --- a/src/api/core/two_factor/yubikey.rs +++ b/src/api/core/two_factor/yubikey.rs @@ -9,6 +9,7 @@ use crate::{ EmptyResult, JsonResult, PasswordOrOtpData, }, auth::Headers, + config::not_readonly, db::{ models::{EventType, TwoFactor, TwoFactorType}, DbConn, @@ -117,6 +118,8 @@ async fn generate_yubikey(data: Json, headers: Headers, mut c #[post("/two-factor/yubikey", data = "")] async fn activate_yubikey(data: Json, headers: Headers, mut conn: DbConn) -> JsonResult { + not_readonly()?; + let data: EnableYubikeyData = data.into_inner(); let mut user = headers.user; @@ -178,6 +181,8 @@ async fn activate_yubikey(data: Json, headers: Headers, mut c #[put("/two-factor/yubikey", data = "")] async fn activate_yubikey_put(data: Json, headers: Headers, conn: DbConn) -> JsonResult { + not_readonly()?; + activate_yubikey(data, headers, conn).await } diff --git a/src/api/identity.rs b/src/api/identity.rs index 93ef80bc..a1b9ccbc 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -18,6 +18,7 @@ use crate::{ ApiResult, EmptyResult, JsonResult, }, auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp}, + config::not_readonly, db::{models::*, DbConn}, error::MapResult, mail, util, CONFIG, @@ -681,6 +682,8 @@ async fn prelogin(data: Json, conn: DbConn) -> Json { #[post("/accounts/register", data = "")] async fn identity_register(data: Json, conn: DbConn) -> JsonResult { + not_readonly()?; + _register(data, conn).await } diff --git a/src/config.rs b/src/config.rs index 93944131..d8ae9e4e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -507,6 +507,9 @@ make_config! { /// Events days retain |> Number of days to retain events stored in the database. If unset, events are kept indefinitely. events_days_retain: i64, false, option; + + /// Read-Only Mode |> Prevent writing of data but logins are still allowed. + readonly: bool, false, def, false; }, /// Advanced settings @@ -1008,6 +1011,10 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } + if cfg.readonly { + println!("[NOTICE] Read-Only Mode is enabled"); + } + if cfg.increase_note_size_limit { println!("[WARNING] Secure Note size limit is increased to 100_000!"); println!("[WARNING] This could cause issues with clients. Also exports will not work on Bitwarden servers!."); @@ -1279,6 +1286,16 @@ impl Config { } } +pub fn not_readonly() -> Result<(), Error> { + match CONFIG.readonly() { + false => Ok(()), + true => { + let msg = "The server is in read-only mode"; + Err(Error::new(msg, msg)) + } + } +} + use handlebars::{ Context, DirectorySourceOptions, Handlebars, Helper, HelperResult, Output, RenderContext, RenderErrorReason, Renderable, diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index 099a6740..f6883677 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -1,4 +1,9 @@
+ {{#if page_data.readonly}} +
+ Read-Only Mode is enabled. This means no password changes, new accounts, or other modifications can be made. +
+ {{/if}}
Diagnostics
diff --git a/src/static/templates/admin/organizations.hbs b/src/static/templates/admin/organizations.hbs index 654f904e..8d8e5eb2 100644 --- a/src/static/templates/admin/organizations.hbs +++ b/src/static/templates/admin/organizations.hbs @@ -1,4 +1,9 @@
+ {{#if page_data.readonly}} +
+ Read-Only Mode is enabled. This means no password changes, new accounts, or other modifications can be made. +
+ {{/if}}
Organizations
@@ -14,7 +19,7 @@ - {{#each page_data}} + {{#each page_data.organizations}} diff --git a/src/static/templates/admin/settings.hbs b/src/static/templates/admin/settings.hbs index fb066cb4..bc6356ef 100644 --- a/src/static/templates/admin/settings.hbs +++ b/src/static/templates/admin/settings.hbs @@ -5,6 +5,11 @@ Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.
See: Enabling admin page - Secure the `ADMIN_TOKEN`
+ {{#if page_data.readonly}} +
+ Read-Only Mode is enabled. This means no password changes, new accounts, or other modifications can be made. +
+ {{/if}}
Configuration
diff --git a/src/static/templates/admin/users.hbs b/src/static/templates/admin/users.hbs index 09efc113..eccd9eb3 100644 --- a/src/static/templates/admin/users.hbs +++ b/src/static/templates/admin/users.hbs @@ -1,4 +1,9 @@
+ {{#if page_data.readonly}} +
+ Read-Only Mode is enabled. This means no password changes, new accounts, or other modifications can be made. +
+ {{/if}}
Registered Users
@@ -15,7 +20,7 @@ - {{#each page_data}} + {{#each page_data.users}}