Browse Source

Add Read-Only Mode

pull/4786/head
Manuel Thomassen 9 months ago
parent
commit
3bf51d7f42
  1. 29
      src/api/admin.rs
  2. 51
      src/api/core/accounts.rs
  3. 97
      src/api/core/ciphers.rs
  4. 29
      src/api/core/emergency_access.rs
  5. 11
      src/api/core/folders.rs
  6. 6
      src/api/core/mod.rs
  7. 149
      src/api/core/organizations.rs
  8. 3
      src/api/core/public.rs
  9. 19
      src/api/core/sends.rs
  10. 5
      src/api/core/two_factor/authenticator.rs
  11. 5
      src/api/core/two_factor/duo.rs
  12. 5
      src/api/core/two_factor/email.rs
  13. 7
      src/api/core/two_factor/mod.rs
  14. 9
      src/api/core/two_factor/webauthn.rs
  15. 5
      src/api/core/two_factor/yubikey.rs
  16. 3
      src/api/identity.rs
  17. 17
      src/config.rs
  18. 5
      src/static/templates/admin/diagnostics.hbs
  19. 7
      src/static/templates/admin/organizations.hbs
  20. 5
      src/static/templates/admin/settings.hbs
  21. 7
      src/static/templates/admin/users.hbs

29
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<Html<String>> {
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<User> {
#[post("/invite", data = "<data>")]
async fn invite_user(data: Json<InviteData>, _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<Html<
users_json.push(usr);
}
let text = AdminTemplateData::new("admin/users", json!(users_json)).render()?;
let users_json = json!({
"users": users_json,
"readonly": CONFIG.readonly(),
});
let text = AdminTemplateData::new("admin/users", users_json).render()?;
Ok(Html(text))
}
@ -390,6 +399,8 @@ async fn get_user_json(uuid: &str, _token: AdminToken, mut conn: DbConn) -> Json
#[post("/users/<uuid>/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/<uuid>/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 = "<data>")]
async fn update_user_org_type(data: Json<UserOrgTypeData>, 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<UserOrgTypeData>, 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/<uuid>/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()?;

51
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<String>, conn: &mut DbConn)
#[post("/accounts/register", data = "<data>")]
async fn register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
not_readonly()?;
_register(data, conn).await
}
@ -257,11 +260,15 @@ struct ProfileData {
#[put("/accounts/profile", data = "<data>")]
async fn put_profile(data: Json<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
not_readonly()?;
post_profile(data, headers, conn).await
}
#[post("/accounts/profile", data = "<data>")]
async fn post_profile(data: Json<ProfileData>, 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 = "<data>")]
async fn put_avatar(data: Json<AvatarData>, 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 = "<data>")]
async fn post_keys(data: Json<KeysData>, 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 = "<data>")]
async fn post_password(data: Json<ChangePassData>, 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 = "<data>")]
async fn post_kdf(data: Json<ChangeKdfData>, 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 = "<data>")]
async fn post_rotatekey(data: Json<KeyData>, 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 = "<data>")]
async fn post_email_token(data: Json<EmailTokenData>, 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 = "<data>")]
async fn post_email(data: Json<ChangeEmailData>, 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<ChangeEmailData>, 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 = "<data>")]
async fn post_verify_email_token(data: Json<VerifyEmailTokenData>, 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 = "<data>")]
async fn post_delete_recover(data: Json<DeleteRecoverData>, 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 = "<data>")]
async fn post_delete_recover_token(data: Json<DeleteRecoverTokenData>, 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<DeleteRecoverTokenData>, mut conn:
#[post("/accounts/delete", data = "<data>")]
async fn post_delete_account(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> EmptyResult {
not_readonly()?;
delete_account(data, headers, conn).await
}
#[delete("/accounts", data = "<data>")]
async fn delete_account(data: Json<PasswordOrOtpData>, 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<PasswordOrOtpData>, rotate: bool, headers: Headers,
#[post("/accounts/api-key", data = "<data>")]
async fn api_key(data: Json<PasswordOrOtpData>, 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 = "<data>")]
async fn rotate_api_key(data: Json<PasswordOrOtpData>, headers: Headers, conn: DbConn) -> JsonResult {
not_readonly()?;
_api_key(data, true, headers, conn).await
}
@ -1017,11 +1056,15 @@ struct PushToken {
#[post("/devices/identifier/<uuid>/token", data = "<data>")]
async fn post_device_token(uuid: &str, data: Json<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
not_readonly()?;
put_device_token(uuid, data, headers, conn).await
}
#[put("/devices/identifier/<uuid>/token", data = "<data>")]
async fn put_device_token(uuid: &str, data: Json<PushToken>, 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<PushToken>, headers: Headers, m
#[put("/devices/identifier/<uuid>/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/<uuid>/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,

97
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 = "<data>")]
async fn post_ciphers_admin(data: Json<ShareCipherData>, 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 = "<data>")]
async fn post_ciphers(data: Json<CipherData>, 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/<uuid>", data = "<data>")]
async fn post_cipher(uuid: &str, data: Json<CipherData>, 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/<uuid>/partial", data = "<data>")]
async fn post_cipher_partial(uuid: &str, data: Json<PartialCipherData>, 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/<uuid>/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/<uuid>/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/<uuid>/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/<uuid>/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/<uuid>")]
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/<uuid>/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/<uuid>/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/<uuid>/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;

29
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/<emer_id>")]
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/<emer_id>/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 = "<data>")]
async fn send_invite(data: Json<EmergencyAccessInviteData>, 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<EmergencyAccessInviteData>, headers: Headers, mu
#[post("/emergency-access/<emer_id>/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/<emer_id>/accept", data = "<data>")]
async fn accept_invite(emer_id: &str, data: Json<AcceptData>, 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/<emer_id>/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/<emer_id>/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/<emer_id>/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/<emer_id>/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/<emer_id>/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();

11
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 = "<data>")]
async fn post_folders(data: Json<FolderData>, 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<FolderData>, headers: Headers, mut conn: DbConn
#[post("/folders/<uuid>", data = "<data>")]
async fn post_folder(uuid: &str, data: Json<FolderData>, 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/<uuid>/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/<uuid>")]
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"),

6
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<Route> {
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 = "<data>")]
async fn put_eq_domains(data: Json<EquivDomainData>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
not_readonly()?;
post_eq_domains(data, headers, conn, nt).await
}

149
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 = "<data>")]
async fn create_organization(headers: Headers, data: Json<OrgData>, 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/<org_id>/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<OrganizationUpdateData>,
conn: DbConn,
) -> JsonResult {
not_readonly()?;
post_organization(org_id, headers, data, conn).await
}
@ -262,6 +273,8 @@ async fn post_organization(
data: Json<OrganizationUpdateData>,
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<NewCollectionData>,
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<NewCollectionData>,
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<NewCollectionData>,
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<DeleteCollectionData>,
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<BulkCollectionIds>,
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/<org_id>/keys", data = "<data>")]
async fn post_org_keys(org_id: &str, data: Json<OrgKeyData>, _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/<org_id>/users/invite", data = "<data>")]
async fn send_invite(org_id: &str, data: Json<InviteData>, 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<OrgBulkIds>,
headers: AdminHeaders,
mut conn: DbConn,
) -> Json<Value> {
) -> 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/<org_id>/users/<user_org>/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/<org_id>/users/<_org_user_id>/accept", data = "<data>")]
async fn accept_invite(org_id: &str, _org_user_id: &str, data: Json<AcceptData>, 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<Value> {
) -> 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/<org_id>/users/<org_user_id>/confirm", data = "<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<Value> {
) -> 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/<org_id>/users/<org_user_id>")]
@ -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<OrgBulkIds>,
_headers: AdminHeaders,
mut conn: DbConn,
) -> Json<Value> {
) -> 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/<org_id>/import", data = "<data>")]
async fn import(org_id: &str, data: Json<OrgImportData>, 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<OrgBulkRevokeData>,
headers: AdminHeaders,
conn: DbConn,
) -> Json<Value> {
) -> 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<OrgBulkRevokeData>,
headers: AdminHeaders,
mut conn: DbConn,
) -> Json<Value> {
) -> 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<OrgBulkIds>,
headers: AdminHeaders,
conn: DbConn,
) -> Json<Value> {
) -> 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<OrgBulkIds>,
headers: AdminHeaders,
mut conn: DbConn,
) -> Json<Value> {
) -> 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/<org_id>/groups", data = "<data>")]
async fn post_groups(org_id: &str, headers: AdminHeaders, data: Json<GroupRequest>, 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/<org_id>/groups/<group_id>/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/<org_id>/groups/<group_id>")]
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<Vec<String>>,
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<OrganizationUserResetPasswordEnrollmentRequest>,
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/<org_id>/api-key", data = "<data>")]
async fn api_key(org_id: &str, data: Json<PasswordOrOtpData>, 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
}

3
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 = "<data>")]
async fn ldap_import(data: Json<OrgImportData>, 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

19
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 = "<data>")]
async fn post_send(data: Json<SendData>, 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 = "<data>")]
async fn post_send_file(data: Form<UploadData<'_>>, 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<UploadData<'_>>, headers: Headers, mut conn:
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L190
#[post("/sends/file/v2", data = "<data>")]
async fn post_send_file_v2(data: Json<SendData>, 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/<id>", data = "<data>")]
async fn put_send(id: &str, data: Json<SendData>, 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/<id>")]
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/<id>/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 {

5
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 = "<data>")]
async fn activate_authenticator(data: Json<EnableAuthenticatorData>, 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<EnableAuthenticatorData>, headers: He
#[put("/two-factor/authenticator", data = "<data>")]
async fn activate_authenticator_put(data: Json<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
not_readonly()?;
activate_authenticator(data, headers, conn).await
}

5
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 = "<data>")]
async fn activate_duo(data: Json<EnableDuoData>, 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<EnableDuoData>, headers: Headers, mut conn: DbC
#[put("/two-factor/duo", data = "<data>")]
async fn activate_duo_put(data: Json<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
not_readonly()?;
activate_duo(data, headers, conn).await
}

5
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 = "<data>")]
async fn send_email(data: Json<SendEmailData>, 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 = "<data>")]
async fn email(data: Json<EmailData>, headers: Headers, mut conn: DbConn) -> JsonResult {
not_readonly()?;
let data: EmailData = data.into_inner();
let mut user = headers.user;

7
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 = "<data>")]
async fn recover(data: Json<RecoverTwoFactor>, 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 = "<data>")]
async fn disable_twofactor(data: Json<DisableTwoFactorData>, 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<DisableTwoFactorData>, headers: Headers, m
#[put("/two-factor/disable", data = "<data>")]
async fn disable_twofactor_put(data: Json<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
not_readonly()?;
disable_twofactor(data, headers, conn).await
}

9
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<PasswordOrOtpData>, headers: Headers, mut conn:
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
async fn generate_webauthn_challenge(data: Json<PasswordOrOtpData>, headers: Headers, mut conn: DbConn) -> JsonResult {
not_readonly()?;
let data: PasswordOrOtpData = data.into_inner();
let user = headers.user;
@ -239,6 +242,8 @@ impl From<PublicKeyCredentialCopy> for PublicKeyCredential {
#[post("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn(data: Json<EnableWebauthnData>, 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<EnableWebauthnData>, headers: Headers, mut
#[put("/two-factor/webauthn", data = "<data>")]
async fn activate_webauthn_put(data: Json<EnableWebauthnData>, headers: Headers, conn: DbConn) -> JsonResult {
not_readonly()?;
activate_webauthn(data, headers, conn).await
}
@ -304,6 +311,8 @@ struct DeleteU2FData {
#[delete("/two-factor/webauthn", data = "<data>")]
async fn delete_webauthn(data: Json<DeleteU2FData>, 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");

5
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<PasswordOrOtpData>, headers: Headers, mut c
#[post("/two-factor/yubikey", data = "<data>")]
async fn activate_yubikey(data: Json<EnableYubikeyData>, 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<EnableYubikeyData>, headers: Headers, mut c
#[put("/two-factor/yubikey", data = "<data>")]
async fn activate_yubikey_put(data: Json<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
not_readonly()?;
activate_yubikey(data, headers, conn).await
}

3
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<PreloginData>, conn: DbConn) -> Json<Value> {
#[post("/accounts/register", data = "<data>")]
async fn identity_register(data: Json<RegisterData>, conn: DbConn) -> JsonResult {
not_readonly()?;
_register(data, conn).await
}

17
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,

5
src/static/templates/admin/diagnostics.hbs

@ -1,4 +1,9 @@
<main class="container-xl">
{{#if page_data.readonly}}
<div id="readonly_mode" class="alert alert-info fade show">
Read-Only Mode is enabled. This means no password changes, new accounts, or other modifications can be made.
</div>
{{/if}}
<div id="diagnostics-block" class="my-3 p-3 rounded shadow">
<h6 class="border-bottom pb-2 mb-2">Diagnostics</h6>

7
src/static/templates/admin/organizations.hbs

@ -1,4 +1,9 @@
<main class="container-xl">
{{#if page_data.readonly}}
<div id="readonly_mode" class="alert alert-info fade show">
Read-Only Mode is enabled. This means no password changes, new accounts, or other modifications can be made.
</div>
{{/if}}
<div id="organizations-block" class="my-3 p-3 rounded shadow">
<h6 class="border-bottom pb-2 mb-3">Organizations</h6>
<div class="table-responsive-xl small">
@ -14,7 +19,7 @@
</tr>
</thead>
<tbody>
{{#each page_data}}
{{#each page_data.organizations}}
<tr>
<td>
<svg width="48" height="48" class="float-start me-2 rounded" data-jdenticon-value="{{id}}">

5
src/static/templates/admin/settings.hbs

@ -5,6 +5,11 @@
Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.<br>
See: <a href="https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token" target="_blank" rel="noopener noreferrer">Enabling admin page - Secure the `ADMIN_TOKEN`</a>
</div>
{{#if page_data.readonly}}
<div id="readonly_mode" class="alert alert-info fade show">
Read-Only Mode is enabled. This means no password changes, new accounts, or other modifications can be made.
</div>
{{/if}}
<div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow">
<div>
<h6 class="text-white mb-3">Configuration</h6>

7
src/static/templates/admin/users.hbs

@ -1,4 +1,9 @@
<main class="container-xl">
{{#if page_data.readonly}}
<div id="readonly_mode" class="alert alert-info fade show">
Read-Only Mode is enabled. This means no password changes, new accounts, or other modifications can be made.
</div>
{{/if}}
<div id="users-block" class="my-3 p-3 rounded shadow">
<h6 class="border-bottom pb-2 mb-3">Registered Users</h6>
<div class="table-responsive-xl small">
@ -15,7 +20,7 @@
</tr>
</thead>
<tbody>
{{#each page_data}}
{{#each page_data.users}}
<tr>
<td>
<svg width="48" height="48" class="float-start me-2 rounded" data-jdenticon-value="{{email}}">

Loading…
Cancel
Save